"use strict";
/*global: require Buffer*/
/**
* @module opcua.server
*/
const assert = require("node-opcua-assert").assert;
const async = require("async");
const util = require("util");
const fs = require("fs");
const _ = require("underscore");
const ApplicationType = require("node-opcua-service-endpoints").ApplicationType;
const StatusCodes = require("node-opcua-status-code").StatusCodes;
const SessionContext = require("node-opcua-address-space").SessionContext;
const fromURI = require("node-opcua-secure-channel").fromURI;
const SecurityPolicy = require("node-opcua-secure-channel").SecurityPolicy;
const MessageSecurityMode = require("node-opcua-service-secure-channel").MessageSecurityMode;
const utils = require("node-opcua-utils");
const debugLog = require("node-opcua-debug").make_debugLog(__filename);
const forceGarbageCollectionOnSessionClose = false;
const ServerEngine = require("./server_engine").ServerEngine;
const browse_service = require("node-opcua-service-browse");
const read_service = require("node-opcua-service-read");
const write_service = require("node-opcua-service-write");
const historizing_service = require("node-opcua-service-history");
const subscription_service = require("node-opcua-service-subscription");
const translate_service = require("node-opcua-service-translate-browse-path");
const session_service = require("node-opcua-service-session");
const register_node_service = require("node-opcua-service-register-node");
const call_service = require("node-opcua-service-call");
const endpoints_service = require("node-opcua-service-endpoints");
const query_service = require("node-opcua-service-query");
const ServerState = require("node-opcua-common").ServerState;
const EndpointDescription = endpoints_service.EndpointDescription;
const TimestampsToReturn = read_service.TimestampsToReturn;
const ActivateSessionRequest = session_service.ActivateSessionRequest;
const ActivateSessionResponse = session_service.ActivateSessionResponse;
const CreateSessionRequest = session_service.CreateSessionRequest;
const CreateSessionResponse = session_service.CreateSessionResponse;
const CloseSessionRequest = session_service.CloseSessionRequest;
const CloseSessionResponse = session_service.CloseSessionResponse;
const DeleteMonitoredItemsRequest = subscription_service.DeleteMonitoredItemsRequest;
const DeleteMonitoredItemsResponse = subscription_service.DeleteMonitoredItemsResponse;
const RepublishRequest = subscription_service.RepublishRequest;
const RepublishResponse = subscription_service.RepublishResponse;
const PublishRequest = subscription_service.PublishRequest;
const PublishResponse = subscription_service.PublishResponse;
const CreateSubscriptionRequest = subscription_service.CreateSubscriptionRequest;
const CreateSubscriptionResponse = subscription_service.CreateSubscriptionResponse;
const DeleteSubscriptionsRequest = subscription_service.DeleteSubscriptionsRequest;
const DeleteSubscriptionsResponse = subscription_service.DeleteSubscriptionsResponse;
const TransferSubscriptionsRequest = subscription_service.TransferSubscriptionsRequest;
const TransferSubscriptionsResponse = subscription_service.TransferSubscriptionsResponse;
const CreateMonitoredItemsRequest = subscription_service.CreateMonitoredItemsRequest;
const CreateMonitoredItemsResponse = subscription_service.CreateMonitoredItemsResponse;
const ModifyMonitoredItemsRequest = subscription_service.ModifyMonitoredItemsRequest;
const ModifyMonitoredItemsResponse = subscription_service.ModifyMonitoredItemsResponse;
const MonitoredItemModifyResult = subscription_service.MonitoredItemModifyResult;
const MonitoredItemCreateResult = subscription_service.MonitoredItemCreateResult;
const SetPublishingModeRequest = subscription_service.SetPublishingModeRequest;
const SetPublishingModeResponse = subscription_service.SetPublishingModeResponse;
const CallRequest = call_service.CallRequest;
const CallResponse = call_service.CallResponse;
const ReadRequest = read_service.ReadRequest;
const ReadResponse = read_service.ReadResponse;
const WriteRequest = write_service.WriteRequest;
const WriteResponse = write_service.WriteResponse;
const ReadValueId = read_service.ReadValueId;
const HistoryReadRequest = historizing_service.HistoryReadRequest;
const HistoryReadResponse = historizing_service.HistoryReadResponse;
const BrowseRequest = browse_service.BrowseRequest;
const BrowseResponse = browse_service.BrowseResponse;
const BrowseNextRequest = browse_service.BrowseNextRequest;
const BrowseNextResponse = browse_service.BrowseNextResponse;
const RegisterNodesRequest = register_node_service.RegisterNodesRequest;
const RegisterNodesResponse = register_node_service.RegisterNodesResponse;
const UnregisterNodesRequest = register_node_service.UnregisterNodesRequest;
const UnregisterNodesResponse = register_node_service.UnregisterNodesResponse;
const TranslateBrowsePathsToNodeIdsRequest = translate_service.TranslateBrowsePathsToNodeIdsRequest;
const TranslateBrowsePathsToNodeIdsResponse = translate_service.TranslateBrowsePathsToNodeIdsResponse;
const NodeId = require("node-opcua-nodeid").NodeId;
const DataValue = require("node-opcua-data-value").DataValue;
const DataType = require("node-opcua-variant").DataType;
const AttributeIds = require("node-opcua-data-model").AttributeIds;
const MonitoredItem = require("./monitored_item").MonitoredItem;
const View = require("node-opcua-address-space").View;
const crypto = require("crypto");
const dump = require("node-opcua-debug").dump;
const OPCUAServerEndPoint = require("./server_end_point").OPCUAServerEndPoint;
const OPCUABaseServer = require("./base_server").OPCUABaseServer;
const exploreCertificate = require("node-opcua-crypto").exploreCertificate;
const Factory = function Factory(engine) {
assert(_.isObject(engine));
this.engine = engine;
};
const factories = require("node-opcua-factory");
Factory.prototype.constructObject = function (id) {
return factories.constructObject(id);
};
const default_maxAllowedSessionNumber = 10;
const default_maxConnectionsPerEndpoint = 10;
function g_sendError(channel, message, ResponseClass, statusCode) {
const response = new ResponseClass({
responseHeader: {serviceResult: statusCode}
});
return channel.send_response("MSG", response, message);
}
const package_info = require("../package.json");
const default_build_info = {
productName: "NODEOPCUA-SERVER",
productUri: null, // << should be same as default_server_info.productUri?
manufacturerName: "Node-OPCUA : MIT Licence ( see http://node-opcua.github.io/)",
softwareVersion: package_info.version,
//xx buildDate: fs.statSync(package_json_file).mtime
};
const RegisterServerManager = require("./register_server_manager").RegisterServerManager;
const EventEmitter = require("events").EventEmitter;
function RegisterServerManagerHidden(options) {
}
util.inherits(RegisterServerManagerHidden,EventEmitter);
RegisterServerManagerHidden.prototype.stop = function(callback)
{
setImmediate(callback);
};
RegisterServerManagerHidden.prototype.start = function(callback)
{
setImmediate(callback);
};
RegisterServerManagerHidden.prototype.dispose = function(){
};
const _announcedOnMulticastSubnet = require("node-opcua-service-discovery")._announcedOnMulticastSubnet;
const _stop_announcedOnMulticastSubnet = require("node-opcua-service-discovery")._stop_announcedOnMulticastSubnet;
/**
*
* @param options
* @constructor
*/
function RegisterServerManagerMDNSONLY(options) {
const self = this;
self.server = options.server;
assert(self.server);
assert(self.server instanceof OPCUABaseServer);
self.bonjour = null;
}
util.inherits(RegisterServerManagerMDNSONLY,EventEmitter);
RegisterServerManagerMDNSONLY.prototype.stop = function(callback)
{
const self = this;
_stop_announcedOnMulticastSubnet.call(self);
setImmediate(function() {
self.emit("serverUnregistered");
setImmediate(callback);
});
};
RegisterServerManagerMDNSONLY.prototype.start = function(callback)
{
const self = this;
assert(self.server);
assert(self.server instanceof OPCUABaseServer);
_announcedOnMulticastSubnet.call(self,{
applicationUri: self.server.serverInfo.applicationUri,
port: self.server.endpoints[0].port,
path: "/", //<- to do
capabilities: self.server.capabilitiesForMDNS
});
setImmediate(function() {
self.emit("serverRegistered");
setImmediate(callback);
});
};
RegisterServerManagerMDNSONLY.prototype.dispose = function(){
this.server = null;
};
function _installRegisterServerManager(self)
{
assert(self instanceof OPCUAServer);
assert(!self.registerServerManager);
assert(self.registerServerMethod);
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({
server: self,
discoveryServerEndpointUrl: self.discoveryServerEndpointUrl
});
break;
default:
assert(false && "Invalid switch");
}
self.registerServerManager.on("serverRegistrationPending", function () {
/**
* 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", function () {
/**
* emitted when the server is successfully registered to the LDS
* @event serverRegistered
*/
debugLog("serverRegistered");
self.emit("serverRegistered");
});
self.registerServerManager.on("serverRegistrationRenewed", function () {
/**
* emitted when the server has successfully renewed its registration to the LDS
* @event serverRegistrationRenewed
*/
debugLog("serverRegistrationRenewed");
self.emit("serverRegistrationRenewed");
});
self.registerServerManager.on("serverUnregistered", function () {
debugLog("serverUnregistered");
/**
* emitted when the server is successfully unregistered to the LDS
* ( for instance during shutdown)
* @event serverUnregistered
*/
self.emit("serverUnregistered");
});
}
const Enum = require("node-opcua-enum");
const RegisterServerMethod = new Enum({
"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)
});
/**
* @class OPCUAServer
* @extends OPCUABaseServer
* @uses ServerEngine
* @param options
* @param [options.defaultSecureTokenLifetime = 60000] {Number} the default secure token life time in ms.
* @param [options.timeout=10000] {Number} the HEL/ACK transaction timeout in ms. Use a large value
* ( i.e 15000 ms) for slow connections or embedded devices.
* @param [options.port= 26543] {Number} the TCP port to listen to.
* @param [options.maxAllowedSessionNumber = 10 ] the maximum number of concurrent sessions allowed.
*
* @param [options.nodeset_filename]{Array<String>|String} the nodeset.xml files to load
* @param [options.serverInfo = null] the information used in the end point description
* @param [options.serverInfo.applicationUri = "urn:NodeOPCUA-Server"] {String}
* @param [options.serverInfo.productUri = "NodeOPCUA-Server"]{String}
* @param [options.serverInfo.applicationName = {text: "applicationName"}]{LocalizedText}
* @param [options.serverInfo.gatewayServerUri = null]{String}
* @param [options.serverInfo.discoveryProfileUri= null]{String}
* @param [options.serverInfo.discoveryUrls = []]{Array<String>}
* @param [options.securityPolicies= [SecurityPolicy.None,SecurityPolicy.Basic128Rsa15,SecurityPolicy.Basic256]]
* @param [options.securityModes= [MessageSecurityMode.NONE,MessageSecurityMode.SIGN,MessageSecurityMode.SIGNANDENCRYPT]]
* @param [options.disableDiscovery = false] true if Discovery Service on unsecure channel shall be disabled
* @param [options.allowAnonymous = true] tells if the server default endpoints should allow anonymous connection.
* @param [options.userManager = null ] a object that implements user authentication methods
* @param [options.userManager.isValidUser ] synchronous function to check the credentials - can be overruled by isValidUserAsync
* @param [options.userManager.isValidUserAsync ] asynchronous function to check if the credentials - overrules isValidUser
* @param [options.userManager.getUserRole ] synchronous function to return the role of the given user
* @param [options.resourcePath=null] {String} resource Path is a string added at the end of the url such as "/UA/Server"
* @param [options.alternateHostname=null] {String} alternate hostname to use
* @param [options.maxConnectionsPerEndpoint=null]
* @param [options.serverCapabilities]
* UserNameIdentityToken is valid.
* @param [options.isAuditing = false] {Boolean} true if server shall raise AuditingEvent
*
* @param [options.registerServerMethod= RegisterServerMethod.HIDDEN] {RegisterServerMethod}
* @param [options.discoveryServerEndpointUrl = "opc.tcp://localhost:4840"]
* @param [options.capabilitiesForMDNS = ["NA"] ] {Array<String>} supported server capabilities for the Mutlicast (mDNS)
* (any of node-opcua-discovery.serverCapabilities)
*
* @constructor
*/
function OPCUAServer(options) {
options = options || {};
OPCUABaseServer.apply(this, arguments);
const self = this;
self.options = options;
/**
* @property maxAllowedSessionNumber
* @type {number}
*/
self.maxAllowedSessionNumber = options.maxAllowedSessionNumber || default_maxAllowedSessionNumber;
/**
* @property maxConnectionsPerEndpoint
* @type {number}
*/
self.maxConnectionsPerEndpoint = options.maxConnectionsPerEndpoint || default_maxConnectionsPerEndpoint;
// build Info
let buildInfo = _.clone(default_build_info);
buildInfo = _.extend(buildInfo, options.buildInfo);
// repair product name
buildInfo.productUri = buildInfo.productUri || self.serverInfo.productUri;
self.serverInfo.productUri = self.serverInfo.productUri || buildInfo.productUri;
self.serverInfo.productName = self.serverInfo.productName || buildInfo.productName;
self.engine = new ServerEngine({
buildInfo: buildInfo,
serverCapabilities: options.serverCapabilities,
applicationUri: self.serverInfo.applicationUri,
isAuditing: options.isAuditing
});
self.nonce = self.makeServerNonce();
self.protocolVersion = 0;
const port = options.port || 26543;
assert(_.isFinite(port));
self.objectFactory = new Factory(self.engine);
// todo should self.serverInfo.productUri match self.engine.buildInfo.productUri ?
/**
* @property allowAnonymous
* @type {boolean}
*/
options.allowAnonymous = (options.allowAnonymous === undefined) ? true : options.allowAnonymous;
//xx console.log(" maxConnectionsPerEndpoint = ",self.maxConnectionsPerEndpoint);
// add the tcp/ip endpoint with no security
const endPoint = new OPCUAServerEndPoint({
port: port,
defaultSecureTokenLifetime: options.defaultSecureTokenLifetime || 600000,
timeout: options.timeout || 10000,
certificateChain: self.getCertificateChain(),
privateKey: self.getPrivateKey(),
objectFactory: self.objectFactory,
serverInfo: self.serverInfo,
maxConnections: self.maxConnectionsPerEndpoint
});
endPoint.addStandardEndpointDescriptions({
securityPolicies: options.securityPolicies,
securityModes: options.securityModes,
allowAnonymous: !!options.allowAnonymous,
disableDiscovery: !!options.disableDiscovery,
resourcePath: options.resourcePath || "",
hostname: options.alternateHostname
});
self.endpoints.push(endPoint);
endPoint.on("message", function (message, channel) {
self.on_request(message, channel);
});
endPoint.on("error", function (err) {
console.log("OPCUAServer endpoint error", err);
// set serverState to ServerState.Failed;
self.engine.setServerState(ServerState.Failed);
self.shutdown(function () {
});
});
self.serverInfo.applicationType = ApplicationType.SERVER;
self.userManager = options.userManager || {};
if (!_.isFunction(self.userManager.isValidUser)) {
self.userManager.isValidUser = function (/*userName,password*/) {
return false;
};
}
self.discoveryServerEndpointUrl = options.discoveryServerEndpointUrl || "opc.tcp://localhost:4840";
assert(typeof self.discoveryServerEndpointUrl === "string");
self.capabilitiesForMDNS = options.capabilitiesForMDNS || [ "NA" ];
self.registerServerMethod = options.registerServerMethod || RegisterServerMethod.HIDDEN;
_installRegisterServerManager(self);
}
util.inherits(OPCUAServer, OPCUABaseServer);
const ObjectRegistry = require("node-opcua-object-registry").ObjectRegistry;
OPCUAServer.registry = new ObjectRegistry();
/**
* total number of bytes written by the server since startup
* @property bytesWritten
* @type {Number}
*/
OPCUAServer.prototype.__defineGetter__("bytesWritten", function () {
return this.endpoints.reduce(function (accumulated, endpoint) {
return accumulated + endpoint.bytesWritten;
}, 0);
});
/**
* total number of bytes read by the server since startup
* @property bytesRead
* @type {Number}
*/
OPCUAServer.prototype.__defineGetter__("bytesRead", function () {
return this.endpoints.reduce(function (accumulated, endpoint) {
return accumulated + endpoint.bytesRead;
}, 0);
});
/**
* Number of transactions processed by the server since startup
* @property transactionsCount
* @type {Number}
*/
OPCUAServer.prototype.__defineGetter__("transactionsCount", function () {
return this.endpoints.reduce(function (accumulated, endpoint) {
return accumulated + endpoint.transactionsCount;
}, 0);
});
/**
* The server build info
* @property buildInfo
* @type {BuildInfo}
*/
OPCUAServer.prototype.__defineGetter__("buildInfo", function () {
return this.engine.buildInfo;
});
/**
*
* the number of connected channel on all existing end points
* @property currentChannelCount
* @type {Number}
*/
OPCUAServer.prototype.__defineGetter__("currentChannelCount", function () {
// TODO : move to base
const self = this;
return self.endpoints.reduce(function (currentValue, endPoint) {
return currentValue + endPoint.currentChannelCount;
}, 0);
});
/**
* The number of active subscriptions from all sessions
* @property currentSubscriptionCount
* @type {Number}
*/
OPCUAServer.prototype.__defineGetter__("currentSubscriptionCount", function () {
const self = this;
return self.engine.currentSubscriptionCount;
});
/**
* @property rejectedSessionCount
* @type {number}
*/
OPCUAServer.prototype.__defineGetter__("rejectedSessionCount", function () {
return this.engine.rejectedSessionCount;
});
/**
* @property rejectedSessionCount
* @type {number}
*/
OPCUAServer.prototype.__defineGetter__("rejectedRequestsCount", function () {
return this.engine.rejectedRequestsCount;
});
/**
* @property sessionAbortCount
* @type {number}
*/
OPCUAServer.prototype.__defineGetter__("sessionAbortCount", function () {
return this.engine.sessionAbortCount;
});
/**
* @property publishingIntervalCount
* @type {number}
*/
OPCUAServer.prototype.__defineGetter__("publishingIntervalCount", function () {
return this.engine.publishingIntervalCount;
});
/**
* create and register a new session
* @method createSession
* @return {ServerSession}
*/
OPCUAServer.prototype.createSession = function (options) {
const self = this;
return self.engine.createSession(options);
};
/**
* the number of sessions currently active
* @property currentSessionCount
* @type {Number}
*/
OPCUAServer.prototype.__defineGetter__("currentSessionCount", function () {
return this.engine.currentSessionCount;
});
/**
* retrieve a session by authentication token
* @method getSession
*
* @param authenticationToken
* @param activeOnly search only within sessions that are not closed
*/
OPCUAServer.prototype.getSession = function (authenticationToken, activeOnly) {
const self = this;
return self.engine.getSession(authenticationToken, activeOnly);
};
/**
* true if the server has been initialized
* @property initialized
* @type {Boolean}
*
*/
OPCUAServer.prototype.__defineGetter__("initialized", function () {
const self = this;
return self.engine.addressSpace !== null;
});
/**
* Initialize the server by installing default node set.
*
* @method initialize
* @async
*
* This is a asynchronous function that requires a callback function.
* The callback function typically completes the creation of custom node
* and instruct the server to listen to its endpoints.
*
* @param {Function} done
*/
OPCUAServer.prototype.initialize = function (done) {
const self = this;
assert(!self.initialized);// already initialized ?
OPCUAServer.registry.register(self);
self.engine.initialize(self.options, function () {
self.emit("post_initialize");
done();
});
};
/**
* Initiate the server by starting all its endpoints
* @method start
* @async
* @param done {Function}
*/
OPCUAServer.prototype.start = function (done) {
const self = this;
const tasks = [];
if (!self.initialized) {
tasks.push(function (callback) {
self.initialize(callback);
});
}
tasks.push(function (callback) {
OPCUABaseServer.prototype.start.call(self, function (err) {
if (err) {
self.shutdown(function (/*err2*/) {
callback(err);
});
}
else {
// we start the registration process asynchronously
// as we want to make server immediately available
self.registerServerManager.start(function() {});
setImmediate(callback);
}
});
});
async.series(tasks, done);
};
OPCUAServer.fallbackSessionName = "Client didn't provide a meaningful sessionName ...";
/**
* shutdown all server endpoints
* @method shutdown
* @async
* @param [timeout=0] {Number} the timeout before the server is actually shutdown
* @param callback {Callback}
* @param callback.err {Error|null}
*
*
* @example
*
* // shutdown immediately
* server.shutdown(function(err) {
* });
*
* // shutdown within 10 seconds
* server.shutdown(10000,function(err) {
* });
*/
OPCUAServer.prototype.shutdown = function (timeout, callback) {
if (!callback) {
callback = timeout;
timeout = 10; // 1 second
}
assert(_.isFunction(callback));
const self = this;
debugLog("OPCUAServer#shutdown (timeout = ", timeout, ")");
assert(self.engine);
if (!self.engine.serverStatus) {
// 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");
return callback(err);
}
self.engine.setServerState(ServerState.Shutdown);
self.registerServerManager.stop(function (err) {
debugLog("OPCUServer unregistered from discovery server",err);
setTimeout(function () {
self.engine.shutdown();
debugLog("OPCUAServer#shutdown: started");
OPCUABaseServer.prototype.shutdown.call(self, function (err) {
debugLog("OPCUAServer#shutdown: completed");
self.dispose();
callback(err);
});
}, timeout);
});
};
OPCUAServer.prototype.dispose = function () {
const self = this;
self.endpoints.forEach(function (endpoint) {
endpoint.dispose();
});
self.endpoints = [];
self.removeAllListeners();
if (self.registerServerManager) {
self.registerServerManager.dispose();
self.registerServerManager = null;
}
OPCUAServer.registry.unregister(self);
};
const computeSignature = require("node-opcua-secure-channel").computeSignature;
const verifySignature = require("node-opcua-secure-channel").verifySignature;
OPCUAServer.prototype.computeServerSignature = function (channel, clientCertificate, clientNonce) {
const self = this;
return computeSignature(clientCertificate, clientNonce, self.getPrivateKey(), channel.messageBuilder.securityPolicy);
};
OPCUAServer.prototype.verifyClientSignature = function (session, channel, clientSignature) {
const self = this;
const clientCertificate = channel.receiverCertificate;
const securityPolicy = channel.messageBuilder.securityPolicy;
const serverCertificateChain = self.getCertificateChain();
const result = verifySignature(serverCertificateChain, session.nonce, clientSignature, clientCertificate, securityPolicy);
return result;
};
const minSessionTimeout = 100; // 100 milliseconds
const defaultSessionTimeout = 1000 * 30; // 30 seconds
const maxSessionTimeout = 1000 * 60 * 50; // 50 minutes
function _adjust_session_timeout(sessionTimeout) {
let revisedSessionTimeout = sessionTimeout || defaultSessionTimeout;
revisedSessionTimeout = Math.min(revisedSessionTimeout, maxSessionTimeout);
revisedSessionTimeout = Math.max(revisedSessionTimeout, minSessionTimeout);
return revisedSessionTimeout;
}
function channel_has_session(channel, session) {
if (session.channel === channel) {
assert(channel.sessionTokens.hasOwnProperty(session.authenticationToken.toString("hex")));
return true;
}
return false;
}
function moveSessionToChannel(session, channel) {
debugLog("moveSessionToChannel sessionId", session.sessionId, " channelId=", channel.secureChannelId);
if (session.publishEngine) {
session.publishEngine.cancelPendingPublishRequestBeforeChannelChange();
}
session._detach_channel();
session._attach_channel(channel);
assert(session.channel.secureChannelId === channel.secureChannelId);
}
function _attempt_to_close_some_old_unactivated_session(server) {
const session = server.engine.getOldestUnactivatedSession();
if (session) {
server.engine.closeSession(session.authenticationToken, false, "Forcing");
}
}
function onlyforUri(serverUri, endpoint) {
assert(_.isString(serverUri));
assert(endpoint instanceof EndpointDescription);
// to do ...
return serverUri === endpoint.endpointUri;
}
function getRequiredEndpointInfo(endpoint) {
assert(endpoint instanceof EndpointDescription);
// It is recommended that Servers only include the endpointUrl, securityMode,
// securityPolicyUri, userIdentityTokens, transportProfileUri and securityLevel with all
// other parameters set to null. Only the recommended parameters shall be verified by
// the client.
const e = new EndpointDescription({
endpointUrl: endpoint.endpointUrl,
securityMode: endpoint.securityMode,
securityPolicyUri: endpoint.securityPolicyUri,
userIdentityTokens: endpoint.userIdentityTokens,
transportProfileUri: endpoint.transportProfileUri,
securityLevel: endpoint.securityLevel,
});
// reduce even further by explicitly setting unwanted members to null
e.server = null;
e.serverCertificate = null;
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, serverUri) {
serverUri; // unused then
// The Server shall return a set of EndpointDescriptions available for the serverUri specified in the request.
// It is recommended that Servers only include the endpointUrl, securityMode,
// securityPolicyUri, userIdentityTokens, transportProfileUri and securityLevel with all other parameters
// set to null. Only the recommended parameters shall be verified by the client.
return server._get_endpoints()
//xx .filter(onlyforUri.bind(null,serverUri)
.map(getRequiredEndpointInfo);
}
// session services
OPCUAServer.prototype._on_CreateSessionRequest = function (message, channel) {
const server = this;
const request = message.request;
assert(request instanceof CreateSessionRequest);
function rejectConnection(statusCode) {
server.engine._rejectedSessionCount += 1;
const response = new CreateSessionResponse({responseHeader: {serviceResult: statusCode}});
channel.send_response("MSG", response, message);
// and close !
}
// From OPCUA V1.03 Part 4 5.6.2 CreateSession
// A Server application should limit the number of Sessions. To protect against misbehaving Clients and denial
// of service attacks, the Server shall close the oldest Session that is not activated before reaching the
// maximum number of supported Sessions
if (server.currentSessionCount >= server.maxAllowedSessionNumber) {
_attempt_to_close_some_old_unactivated_session(server);
}
// check if session count hasn't reach the maximum allowed sessions
if (server.currentSessionCount >= server.maxAllowedSessionNumber) {
return rejectConnection(StatusCodes.BadTooManySessions);
}
// Release 1.03 OPC Unified Architecture, Part 4 page 24 - CreateSession Parameters
// client should prove a sessionName
// Session name is a Human readable string that identifies the Session. The Server makes this name and the sessionId
// visible in its AddressSpace for diagnostic purposes. The Client should provide a name that is unique for the
// instance of the Client.
// If this parameter is not specified the Server shall assign a value.
if (utils.isNullOrUndefined(request.sessionName)) {
// see also #198
// let's the server assign a sessionName for this lazy client.
debugLog("assigning OPCUAServer.fallbackSessionName because client's sessionName is null ", OPCUAServer.fallbackSessionName);
request.sessionName = OPCUAServer.fallbackSessionName;
}
// Duration Requested maximum number of milliseconds that a Session should remain open without activity.
// If the Client fails to issue a Service request within this interval, then the Server shall automatically
// terminate the Client Session.
const revisedSessionTimeout = _adjust_session_timeout(request.requestedSessionTimeout);
// Release 1.02 page 27 OPC Unified Architecture, Part 4: CreateSession.clientNonce
// A random number that should never be used in any other request. This number shall have a minimum length of 32
// bytes. Profiles may increase the required length. The Server shall use this value to prove possession of
// its application instance Certificate in the response.
if (!request.clientNonce || request.clientNonce.length < 32) {
if (channel.securityMode !== MessageSecurityMode.NONE) {
console.log("SERVER with secure connection: Missing or invalid client Nonce ".red, request.clientNonce && request.clientNonce.toString("hex"));
return rejectConnection(StatusCodes.BadNonceInvalid);
}
}
function validate_applicationUri(applicationUri, 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 applicationUriFromCert = e.tbsCertificate.extensions.subjectAltName.uniformResourceIdentifier[0];
return applicationUriFromCert === applicationUri;
}
// check application spoofing
// check if applicationUri in createSessionRequest matches applicationUri in client Certificate
if (!validate_applicationUri(request.clientDescription.applicationUri, request.clientCertificate)) {
return rejectConnection(StatusCodes.BadCertificateUriInvalid);
}
function validate_security_endpoint(channel) {
let endpoints = server._get_endpoints();
// ignore restricted endpoints
endpoints = endpoints.filter(function (endpoint) {
return !endpoint.restricted;
});
const endpoints_matching_security_mode = endpoints.filter(function (e) {
return e.securityMode === channel.securityMode;
});
if (endpoints_matching_security_mode.length === 0) {
return StatusCodes.BadSecurityModeRejected;
}
const endpoints_matching_security_policy = endpoints_matching_security_mode.filter(function (e) {
return e.securityPolicyUri === channel.securityHeader.securityPolicyUri;
});
if (endpoints_matching_security_policy.length === 0) {
return StatusCodes.BadSecurityPolicyRejected;
}
return StatusCodes.Good;
}
const errStatus = validate_security_endpoint(channel);
if (errStatus !== StatusCodes.Good) {
return rejectConnection(errStatus);
}
//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.
function validate_endpointUri() {
// ToDo: check endpointUrl validity and emit an AuditUrlMismatchEventType event if not
}
validate_endpointUri();
// see Release 1.02 27 OPC Unified Architecture, Part 4
const session = server.createSession({
sessionTimeout: revisedSessionTimeout,
clientDescription: request.clientDescription
});
assert(session);
assert(session.sessionTimeout === revisedSessionTimeout);
session.clientDescription = request.clientDescription;
session.sessionName = request.sessionName;
// Depending upon on the SecurityPolicy and the SecurityMode of the SecureChannel, the exchange of
// ApplicationInstanceCertificates and Nonces may be optional and the signatures may be empty. See
// Part 7 for the definition of SecurityPolicies and the handling of these parameters
// serverNonce:
// A random number that should never be used in any other request.
// This number shall have a minimum length of 32 bytes.
// The Client shall use this value to prove possession of its application instance
// Certificate in the ActivateSession request.
// This value may also be used to prove possession of the userIdentityToken it
// specified in the ActivateSession request.
//
// ( this serverNonce will only be used up to the _on_ActivateSessionRequest
// where a new nonce will be created)
session.nonce = server.makeServerNonce();
session.secureChannelId = channel.secureChannelId;
session._attach_channel(channel);
const serverCertificateChain = server.getCertificateChain();
const hasEncryption = true;
// If the securityPolicyUri is NONE and none of the UserTokenPolicies requires encryption
if (session.channel.securityMode === MessageSecurityMode.NONE) {
// ToDo: Check that none of our unsecure endpoint has a a UserTokenPolicy that require encryption
// and set hasEncryption = false under this condition
}
const response = new CreateSessionResponse({
// A identifier which uniquely identifies the session.
sessionId: session.nodeId,
// A unique identifier assigned by the Server to the Session.
// The token used to authenticate the client in subsequent requests.
authenticationToken: session.authenticationToken,
revisedSessionTimeout: revisedSessionTimeout,
serverNonce: session.nonce,
// serverCertificate: type ApplicationServerCertificate
// The application instance Certificate issued to the Server.
// A Server shall prove possession by using the private key to sign the Nonce provided
// by the Client in the request. The Client shall verify that this Certificate is the same as
// the one it used to create the SecureChannel.
// The ApplicationInstanceCertificate type is defined in OpCUA 1.03 part 4 - $7.2 page 108
// If the securityPolicyUri is NONE and none of the UserTokenPolicies requires
// encryption, the Server shall not send an ApplicationInstanceCertificate and the Client
// shall ignore the ApplicationInstanceCertificate.
serverCertificate: hasEncryption ? serverCertificateChain : null,
// The endpoints provided by the server.
// The Server shall return a set of EndpointDescriptions available for the serverUri
// specified in the request.[...]
// The Client shall verify this list with the list from a Discovery Endpoint if it used a Discovery
// Endpoint to fetch the EndpointDescriptions.
// It is recommended that Servers only include the endpointUrl, securityMode,
// securityPolicyUri, userIdentityTokens, transportProfileUri and securityLevel with all
// other parameters set to null. Only the recommended parameters shall be verified by
// the client.
serverEndpoints: _serverEndpointsForCreateSessionResponse(server, request.serverUri),
//This parameter is deprecated and the array shall be empty.
serverSoftwareCertificates: null,
// This is a signature generated with the private key associated with the
// serverCertificate. This parameter is calculated by appending the clientNonce to the
// clientCertificate and signing the resulting sequence of bytes.
// The SignatureAlgorithm shall be the AsymmetricSignatureAlgorithm specified in the
// SecurityPolicy for the Endpoint.
// The SignatureData type is defined in 7.30.
serverSignature: server.computeServerSignature(channel, request.clientCertificate, request.clientNonce),
// The maximum message size accepted by the server
// The Client Communication Stack should return a Bad_RequestTooLarge error to the
// application if a request message exceeds this limit.
// The value zero indicates that this parameter is not used.
maxRequestMessageSize: 0x4000000
});
server.emit("create_session", session);
session.on("session_closed", function (session, deleteSubscriptions, reason) {
assert(_.isString(reason));
if (server.isAuditing) {
assert(reason === "Timeout" || reason === "Terminated" || reason === "CloseSession" || reason === "Forcing");
const sourceName = "Session/" + reason;
server.raiseEvent("AuditSessionEventType", {
/* part 5 - 6.4.3 AuditEventType */
actionTimeStamp: {dataType: "DateTime", value: new Date()},
status: {dataType: "Boolean", value: true},
serverId: {dataType: "String", value: ""},
// ClientAuditEntryId contains the human-readable AuditEntryId defined in Part 3.
clientAuditEntryId: {dataType: "String", value: ""},
// The ClientUserId identifies the user of the client requesting an action. The ClientUserId can be
// obtained from the UserIdentityToken passed in the ActivateSession call.
clientUserId: {dataType: "String", value: ""},
sourceName: {dataType: "String", value: sourceName},
/* part 5 - 6.4.7 AuditSessionEventType */
sessionId: {dataType: "NodeId", value: session.nodeId}
});
}
server.emit("session_closed", session, deleteSubscriptions);
});
if (server.isAuditing) {
// ----------------------------------------------------------------------------------------------------------------
server.raiseEvent("AuditCreateSessionEventType", {
/* part 5 - 6.4.3 AuditEventType */
actionTimeStamp: {dataType: "DateTime", value: new Date()},
status: {dataType: "Boolean", value: true},
serverId: {dataType: "String", value: ""},
// ClientAuditEntryId contains the human-readable AuditEntryId defined in Part 3.
clientAuditEntryId: {dataType: "String", value: ""},
// The ClientUserId identifies the user of the client requesting an action. The ClientUserId can be
// obtained from the UserIdentityToken passed in the ActivateSession call.
clientUserId: {dataType: "String", value: ""},
sourceName: {dataType: "String", value: "Session/CreateSession"},
/* part 5 - 6.4.7 AuditSessionEventType */
sessionId: {dataType: "NodeId", value: session.nodeId},
/* part 5 - 6.4.8 AuditCreateSessionEventType */
// SecureChannelId shall uniquely identify the SecureChannel. The application shall use the same identifier in
// all AuditEvents related to the Session Service Set (AuditCreateSessionEventType, AuditActivateSessionEventType
// and their subtypes) and the SecureChannel Service Set (AuditChannelEventType and its subtypes
secureChannelId: {dataType: "String", value: session.channel.secureChannelId.toString()},
// Duration
revisedSessionTimeout: {dataType: "Duration", value: session.sessionTimeout},
// clientCertificate
clientCertificate: {dataType: "ByteString", value: session.channel.clientCertificate},
// clientCertificateThumbprint
clientCertificateThumbprint: {dataType: "ByteString", value: thumbprint(session.channel.clientCertificate)}
});
}
// ----------------------------------------------------------------------------------------------------------------
assert(response.authenticationToken);
channel.send_response("MSG", response, message);
};
const UserNameIdentityToken = session_service.UserNameIdentityToken;
const AnonymousIdentityToken = session_service.AnonymousIdentityToken;
const getCryptoFactory = require("node-opcua-secure-channel").getCryptoFactory;
function adjustSecurityPolicy(channel, userTokenPolicy_securityPolicyUri) {
// 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.clientSecurityHeader.securityPolicyUri);
assert(securityPolicy);
}
return securityPolicy;
}
OPCUAServer.prototype.isValidUserNameIdentityToken = function (channel, session, userTokenPolicy, userIdentityToken) {
const self = this;
assert(userIdentityToken instanceof UserNameIdentityToken);
const securityPolicy = adjustSecurityPolicy(channel, userTokenPolicy.securityPolicyUri);
const cryptoFactory = getCryptoFactory(securityPolicy);
if (!cryptoFactory) {
throw new Error(" Unsupported security Policy");
}
if (userIdentityToken.encryptionAlgorithm !== cryptoFactory.asymmetricEncryptionAlgorithm) {
console.log("invalid encryptionAlgorithm");
console.log("userTokenPolicy", userTokenPolicy.toString());
console.log("userTokenPolicy", userIdentityToken.toString());
return false;
}
const userName = userIdentityToken.userName;
const password = userIdentityToken.password;
if (!userName || !password) {
return false;
}
return true;
};
/**
* @method userNameIdentityTokenAuthenticateUser
* @param channel
* @param session
* @param userTokenPolicy
* @param userIdentityToken
* @param done {Function}
* @param done.err {Error}
* @param done.isAuthorized {Boolean}
* @return {*}
*/
OPCUAServer.prototype.userNameIdentityTokenAuthenticateUser = function (channel, session, userTokenPolicy, userIdentityToken, done) {
const self = this;
assert(userIdentityToken instanceof UserNameIdentityToken);
assert(self.isValidUserNameIdentityToken(channel, session, userTokenPolicy, userIdentityToken));
const securityPolicy = adjustSecurityPolicy(channel, userTokenPolicy.securityPolicyUri);
const serverPrivateKey = self.getPrivateKey();
const serverNonce = session.nonce;
assert(serverNonce instanceof Buffer);
const cryptoFactory = getCryptoFactory(securityPolicy);
if (!cryptoFactory) {
return done(new Error(" Unsupported security Policy"));
}
const userName = userIdentityToken.userName;
let password = userIdentityToken.password;
const buff = cryptoFactory.asymmetricDecrypt(password, serverPrivateKey);
const length = buff.readUInt32LE(0) - serverNonce.length;
password = buff.slice(4, 4 + length).toString("utf-8");
if (_.isFunction(self.userManager.isValidUserAsync)) {
self.userManager.isValidUserAsync.call(session, userName, password, done);
} else {
const authorized = self.userManager.isValidUser.call(session, userName, password);
async.setImmediate(function () {
done(null, authorized)
});
}
};
function findUserTokenByPolicy(endpoint_description, policyId) {
assert(endpoint_description instanceof EndpointDescription);
const r = _.filter(endpoint_description.userIdentityTokens, function (userIdentity) {
// assert(userIdentity instanceof UserTokenPolicy)
assert(userIdentity.tokenType);
return userIdentity.policyId === policyId;
});
return r.length === 0 ? null : r[0];
}
const UserIdentityTokenType = require("node-opcua-service-endpoints").UserIdentityTokenType;
function findUserTokenPolicy(endpoint_description, userTokenType) {
assert(endpoint_description instanceof EndpointDescription);
const r = _.filter(endpoint_description.userIdentityTokens, function (userIdentity) {
// assert(userIdentity instanceof UserTokenPolicy)
assert(userIdentity.tokenType);
return userIdentity.tokenType === userTokenType;
});
return r.length === 0 ? null : r[0];
}
function createAnonymousIdentityToken(endpoint_desc) {
assert(endpoint_desc instanceof EndpointDescription);
const userTokenPolicy = findUserTokenPolicy(endpoint_desc, UserIdentityTokenType.ANONYMOUS);
if (!userTokenPolicy) {
throw new Error("Cannot find ANONYMOUS user token policy in end point description");
}
return new AnonymousIdentityToken({policyId: userTokenPolicy.policyId});
}
OPCUAServer.prototype.isValidUserIdentityToken = function (channel, session, userIdentityToken) {
const self = this;
assert(userIdentityToken);
const endpoint_desc = channel.endpoint;
assert(endpoint_desc instanceof EndpointDescription);
const userTokenPolicy = findUserTokenByPolicy(endpoint_desc, userIdentityToken.policyId);
if (!userTokenPolicy) {
// cannot find token with this policyId
return false;
}
//
if (userIdentityToken instanceof UserNameIdentityToken) {
return self.isValidUserNameIdentityToken(channel, session, userTokenPolicy, userIdentityToken);
}
return true;
};
OPCUAServer.prototype.isUserAuthorized = function (channel, session, userIdentityToken, done) {
const self = this;
assert(userIdentityToken);
assert(_.isFunction(done));
const endpoint_desc = channel.endpoint;
assert(endpoint_desc instanceof EndpointDescription);
const userTokenPolicy = findUserTokenByPolicy(endpoint_desc, userIdentityToken.policyId);
assert(userTokenPolicy);
// find if a userToken exists
if (userIdentityToken instanceof UserNameIdentityToken) {
return self.userNameIdentityTokenAuthenticateUser(channel, session, userTokenPolicy, userIdentityToken, done);
}
async.setImmediate(done.bind(null, null, true));
};
OPCUAServer.prototype.makeServerNonce = function () {
return crypto.randomBytes(32);
};
function sameIdentityToken(token1, token2) {
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")) {
return false;
}
} else if (token1 instanceof AnonymousIdentityToken) {
if (!(token2 instanceof AnonymousIdentityToken)) {
return false;
}
if (token1.policyId !== token2.policyId) {
return false;
}
return true;
}
assert(0, " Not implemented yet");
return false;
}
function thumbprint(certificate) {
return certificate ? certificate.toString("base64") : "";
}
// TODO : implement this:
//
// When the ActivateSession Service is called for the first time then the Server shall reject the request
// if the SecureChannel is not same as the one associated with the CreateSession request.
// Subsequent calls to ActivateSession may be associated with different SecureChannels. If this is the
// case then the Server shall verify that the Certificate the Client used to create the new
// SecureChannel is the same as the Certificate used to create the original SecureChannel. In addition,
// the Server shall verify that the Client supplied a UserIdentityToken that is identical to the token
// currently associated with the Session. Once the Server accepts the new SecureChannel it shall
// reject requests sent via the old SecureChannel.
/**
*
* @method _on_ActivateSessionRequest
* @param message {Buffer}
* @param channel {ServerSecureChannelLayer}
* @private
*
*
*/
OPCUAServer.prototype._on_ActivateSessionRequest = function (message, channel) {
const server = this;
const request = message.request;
assert(request instanceof ActivateSessionRequest);
// get session from authenticationToken
const authenticationToken = request.requestHeader.authenticationToken;
const session = server.getSession(authenticationToken);
function rejectConnection(statusCode) {
server.engine._rejectedSessionCount += 1;
const response = new ActivateSessionResponse({responseHeader: {serviceResult: statusCode}});
channel.send_response("MSG", response, message);
// and close !
}
let response;
/* istanbul ignore next */
if (!session) {
// this may happen when the server has been restarted and a client tries to reconnect, thinking
// that the previous session may still be active
debugLog(" Bad Session in _on_ActivateSessionRequest".yellow.bold, authenticationToken.value.toString("hex"));
return rejectConnection(StatusCodes.BadSessionNotActivated);
}
// OpcUA 1.02 part 3 $5.6.3.1 ActiveSession Set page 29
// When the ActivateSession Service is called f or the first time then the Server shall reject the request
// if the SecureChannel is not same as the one associated with the CreateSession request.
if (session.status === "new") {
//xx if (channel.session_nonce !== session.nonce) {
if (!channel_has_session(channel, session)) {
// it looks like session activation is being using a channel that is not the
// one that have been used to create the session
console.log(" channel.sessionTokens === " + Object.keys(channel.sessionTokens).join(" "));
return rejectConnection(StatusCodes.BadSessionNotActivated);
}
}
// OpcUA 1.02 part 3 $5.6.3.1 ActiveSession Set page 29
// ... Subsequent calls to ActivateSession may be associated with different SecureChannels. If this is the
// case then the Server shall verify that the Certificate the Client used to create the new
// SecureChannel is the same as the Certificate used to create the original SecureChannel.
if (session.status === "active") {
if (session.channel.secureChannelId !== channel.secureChannelId) {
console.log(" Session is being transferred from channel",
session.channel.secureChannelId.toString().cyan,
" to channel ", channel.secureChannelId.toString().cyan);
// session is being reassigned to a new Channel,
// we shall verify that the certificate used to create the Session is the same as the current channel certificate.
const old_channel_cert_thumbprint = thumbprint(session.channel.clientCertificate);
const new_channel_cert_thumbprint = thumbprint(channel.clientCertificate);
if (old_channel_cert_thumbprint !== new_channel_cert_thumbprint) {
return rejectConnection(StatusCodes.BadNoValidCertificates); // not sure about this code !
}
// ... In addition the Server shall verify that the Client supplied a UserIdentityToken that is identical to
// the token currently associated with the Session reassign session to new channel.
if (!sameIdentityToken(session.userIdentityToken, request.userIdentityToken)) {
return rejectConnection(StatusCodes.BadIdentityChangeNotSupported); // not sure about this code !
}
}
moveSessionToChannel(session, channel);
} else if (session.status === "screwed") {
// session has been used before being activated => this should be detected and session should be dismissed.
return rejectConnection(StatusCodes.BadSessionClosed);
} else if (session.status === "closed") {
console.log(" Bad Session Closed in _on_ActivateSessionRequest".yellow.bold, authenticationToken.value.toString("hex"));
return rejectConnection(StatusCodes.BadSessionClosed);
}
// verify clientSignature provided by the client
if (!server.verifyClientSignature(session, channel, request.clientSignature, session.clientCertificate)) {
return rejectConnection(StatusCodes.BadApplicationSignatureInvalid);
}
// userIdentityToken may be missing , assume anonymous access then
request.userIdentityToken = request.userIdentityToken || createAnonymousIdentityToken(channel.endpoint);
// check request.userIdentityToken is correct ( expected type and correctly formed)
if (!server.isValidUserIdentityToken(channel, session, request.userIdentityToken)) {
return rejectConnection(StatusCodes.BadIdentityTokenInvalid);
}
session.userIdentityToken = request.userIdentityToken;
// check if user access is granted
server.isUserAuthorized(channel, session, request.userIdentityToken, function (err, authorized) {
if (err) {
return rejectConnection(StatusCodes.BadInternalError);
}
if (!authorized) {
return rejectConnection(StatusCodes.BadUserAccessDenied);
} else {
// extract : OPC UA part 4 - 5.6.3
// Once used, a serverNonce cannot be used again. For that reason, the Server returns a new
// serverNonce each time the ActivateSession Service is called.
session.nonce = server.makeServerNonce();
session.status = "active";
response = new ActivateSessionResponse({serverNonce: session.nonce});
channel.send_response("MSG", response, message);
const userIdentityTokenPasswordRemoved = function (userIdentityToken) {
const a = userIdentityToken;
// to do remove password
return a;
};
// send OPCUA Event Notification
// see part 5 : 6.4.3 AuditEventType
// 6.4.7 AuditSessionEventType
// 6.4.10 AuditActivateSessionEventType
const VariantArrayType = require("node-opcua-variant").VariantArrayType;
assert(session.nodeId); // sessionId
//xx assert(session.channel.clientCertificate instanceof Buffer);
assert(session.sessionTimeout > 0);
if (server.isAuditing) {
server.raiseEvent("AuditActivateSessionEventType", {
/* part 5 - 6.4.3 AuditEventType */
actionTimeStamp: {dataType: "DateTime", value: new Date()},
status: {dataType: "Boolean", value: true},
serverId: {dataType: "String", value: ""},
// ClientAuditEntryId contains the human-readable AuditEntryId defined in Part 3.
clientAuditEntryId: {dataType: "String", value: ""},
// The ClientUserId identifies the user of the client requesting an action. The ClientUserId can be
// obtained from the UserIdentityToken passed in the ActivateSession call.
clientUserId: {dataType: "String", value: "cc"},
sourceName: {dataType: "String", value: "Session/ActivateSession"},
/* part 5 - 6.4.7 AuditSessionEventType */
sessionId: {dataType: "NodeId", value: session.nodeId},
/* part 5 - 6.4.10 AuditActivateSessionEventType */
clientSoftwareCertificates: {
dataType: "ExtensionObject" /* SignedSoftwareCertificate */,
arrayType: VariantArrayType.Array,
value: []
},
// UserIdentityToken reflects the userIdentityToken parameter of the ActivateSession Service call.
// For Username/Password tokens the password should NOT be included.
userIdentityToken: {
dataType: "ExtensionObject" /* UserIdentityToken */,
value: userIdentityTokenPasswordRemoved(session.userIdentityToken)
},
// SecureChannelId shall uniquely identify the SecureChannel. The application shall use the same identifier
// in all AuditEvents related to the Session Service Set (AuditCreateSessionEventType,
// AuditActivateSessionEventType and their subtypes) and the SecureChannel Service Set
// (AuditChannelEventType and its subtypes).
secureChannelId: {dataType: "String", value: session.channel.secureChannelId.toString()}
});
}
}
});
};
OPCUAServer.prototype.raiseEvent = function (eventType, options) {
const self = this;
if (!self.engine.addressSpace) {
console.log("addressSpace missing");
return;
}
const server = self.engine.addressSpace.findNode("Server");
if (!server) {
//xx throw new Error("OPCUAServer#raiseEvent : cannot find Server object");
return;
}
let eventTypeNode = eventType;
if (typeof(eventType) === "string") {
eventTypeNode = self.engine.addressSpace.findEventType(eventType);
}
if (eventTypeNode) {
return server.raiseEvent(eventTypeNode, options);
} else {
console.warn(" cannot find event type ", eventType);
}
};
/**
* ensure that action is performed on a valid session object,
* @method _apply_on_SessionObject
* @param ResponseClass {Constructor} the constructor of the response Class
* @param message
* @param channel
* @param action_to_perform {Function}
* @param action_to_perform.session {ServerSession}
* @param action_to_perform.sendResponse {Function}
* @param action_to_perform.sendResponse.response {ResponseClass}
* @param action_to_perform.sendError {Function}
* @param action_to_perform.sendError.statusCode {StatusCode}
* @param action_to_perform.sendError.diagnostics {DiagnosticsInfo}
*
* @private
*/
OPCUAServer.prototype._apply_on_SessionObject = function (ResponseClass, message, channel, action_to_perform) {
assert(_.isFunction(action_to_perform));
function sendResponse(response) {
assert(response instanceof ResponseClass);
if (message.session) {
message.session.incrementRequestTotalCounter(ResponseClass.name.replace("Response", ""));
}
return channel.send_response("MSG", response, message);
}
function sendError(statusCode, diagnostics) {
if (message.session) {
message.session.incrementRequestErrorCounter(ResponseClass.name.replace("Response", ""));
}
return g_sendError(channel, message, ResponseClass, statusCode, diagnostics);
}
let response;
/* istanbul ignore next */
if (!message.session || message.session_statusCode !== StatusCodes.Good) {
const errMessage = "INVALID SESSION !! ";
response = new ResponseClass({responseHeader: {serviceResult: message.session_statusCode}});
debugLog(errMessage.red.bold, message.session_statusCode.toString().yellow, response.constructor.name);
return sendResponse(response);
}
assert(message.session_statusCode === StatusCodes.Good);
// OPC UA Specification 1.02 part 4 page 26
// When a Session is terminated, all outstanding requests on the Session are aborted and
// Bad_SessionClosed StatusCodes are returned to the Client. In addition, the Server deletes the entry
// for the Client from its SessionDiagnostics Array Variable and notifies any other Clients who were
// subscribed to this entry.
if (message.session.status === "closed") {
//note : use StatusCodes.BadSessionClosed , for pending message for this session
//xx console.log("xxxxxxxxxxxxxxxxxxxxxxxxxx message.session.status ".red.bold,message.session.status.toString().cyan);
return sendError(StatusCodes.BadSessionIdInvalid);
}
if (message.session.status !== "active") {
// mark session as being screwed ! so it cannot be activated anymore
message.session.status = "screwed";
//note : use StatusCodes.BadSessionClosed , for pending message for this session
return sendError(StatusCodes.BadSessionNotActivated);
}
// lets also reset the session watchdog so it doesn't
// (Sessions are terminated by the Server automatically if the Client fails to issue a Service request on the Session
// within the timeout period negotiated by the Server in the CreateSession Service response. )
assert(_.isFunction(message.session.keepAlive));
message.session.keepAlive();
message.session.incrementTotalRequestCount();
action_to_perform(message.session, sendResponse, sendError);
};
/**
* @method _apply_on_Subscription
* @param ResponseClass
* @param message
* @param channel
* @param action_to_perform
* @private
*/
OPCUAServer.prototype._apply_on_Subscription = function (ResponseClass, message, channel, action_to_perform) {
assert(_.isFunction(action_to_perform));
const request = message.request;
assert(request.hasOwnProperty("subscriptionId"));
this._apply_on_SessionObject(ResponseClass, message, channel, function (session, sendResponse, sendError) {
const subscription = session.getSubscription(request.subscriptionId);
if (!subscription) {
return sendError(StatusCodes.BadSubscriptionIdInvalid);
}
subscription.resetLifeTimeAndKeepAliveCounters();
action_to_perform(session, subscription, sendResponse, sendError);
});
};
/**
* @method _apply_on_SubscriptionIds
* @param ResponseClass
* @param message
* @param channel
* @param action_to_perform
* @private
*/
OPCUAServer.prototype._apply_on_SubscriptionIds = function (ResponseClass, message, channel, action_to_perform) {
assert(_.isFunction(action_to_perform));
const request = message.request;
assert(request.hasOwnProperty("subscriptionIds"));
this._apply_on_SessionObject(ResponseClass, message, channel, function (session, sendResponse, sendError) {
const subscriptionIds = request.subscriptionIds;
if (!request.subscriptionIds || request.subscriptionIds.length === 0) {
return sendError(StatusCodes.BadNothingToDo);
}
const results = subscriptionIds.map(function (subscriptionId) {
return action_to_perform(session, subscriptionId)
});
const response = new ResponseClass({
results: results
});
sendResponse(response);
});
};
/**
* @method _apply_on_Subscriptions
* @param ResponseClass
* @param message
* @param channel
* @param action_to_perform
* @private
*/
OPCUAServer.prototype._apply_on_Subscriptions = function (ResponseClass, message, channel, action_to_perform) {
this._apply_on_SubscriptionIds(ResponseClass, message, channel, function (session, subscriptionId) {
if (subscriptionId <= 0) {
return StatusCodes.BadSubscriptionIdInvalid;
}
const subscription = session.getSubscription(subscriptionId);
if (!subscription) {
return StatusCodes.BadSubscriptionIdInvalid;
}
return action_to_perform(session, subscription);
});
};
/**
* @method _on_CloseSessionRequest
* @param message
* @param channel
* @private
*/
OPCUAServer.prototype._on_CloseSessionRequest = function (message, channel) {
const server = this;
const request = message.request;
assert(request instanceof CloseSessionRequest);
let response;
message.session_statusCode = StatusCodes.Good;
function sendError(statusCode) {
return g_sendError(channel, message, CloseSessionResponse, statusCode);
}
function sendResponse(response) {
channel.send_response("MSG", response, message);
}
// do not use _apply_on_SessionObject
//this._apply_on_SessionObject(CloseSessionResponse, message, channel, function (session) {
//});
const session = message.session;
if (!session) {
return sendError(StatusCodes.BadSessionIdInvalid);
}
// session has been created but not activated !
const wasNotActivated = (session.status === "new");
server.engine.closeSession(request.requestHeader.authenticationToken, request.deleteSubscriptions, "CloseSession");
if (wasNotActivated) {
return sendError(StatusCodes.BadSessionNotActivated);
}
response = new CloseSessionResponse({});
sendResponse(response);
};
// browse services
/**
* @method _on_BrowseRequest
* @param message
* @param channel
* @private
*/
OPCUAServer.prototype._on_BrowseRequest = function (message, channel) {
const server = this;
const request = message.request;
assert(request instanceof BrowseRequest);
const diagnostic = {};
this._apply_on_SessionObject(BrowseResponse, message, channel, function (session, sendResponse, sendError) {
let response;
// test view
if (request.view && !request.view.viewId.isEmpty()) {
//xx console.log("xxxx ",request.view.toString());
//xx console.log("xxxx NodeClas",View.prototype.nodeClass);
let theView = server.engine.addressSpace.findNode(request.view.viewId);
if (theView && theView.constructor.nodeClass !== View.prototype.nodeClass) {
// Error: theView is not a View
diagnostic.localizedText = {text: "blah"};
theView = null;
}
if (!theView) {
return sendError(StatusCodes.BadViewIdUnknown, diagnostic);
}
}
if (!request.nodesToBrowse || request.nodesToBrowse.length === 0) {
return sendError(StatusCodes.BadNothingToDo);
}
if (server.engine.serverCapabilities.operationLimits.maxNodesPerBrowse > 0) {
if (request.nodesToBrowse.length > server.engine.serverCapabilities.operationLimits.maxNodesPerBrowse) {
return sendError(StatusCodes.BadTooManyOperations);
}
}
// ToDo: limit results to requestedMaxReferencesPerNode
const requestedMaxReferencesPerNode = request.requestedMaxReferencesPerNode;
let results = [];
assert(request.nodesToBrowse[0]._schema.name === "BrowseDescription");
results = server.engine.browse(request.nodesToBrowse, session);
assert(results[0]._schema.name === "BrowseResult");
// handle continuation point and requestedMaxReferencesPerNode
results = results.map(function (result) {
assert(!result.continuationPoint);
const r = session.continuationPointManager.register(requestedMaxReferencesPerNode, result.references);
assert(r.statusCode === StatusCodes.Good);
r.statusCode = result.statusCode;
return r;
});
response = new BrowseResponse({
results: results,
diagnosticInfos: null
});
sendResponse(response);
});
};
/**
* @method _on_BrowseNextRequest
* @param message
* @param channel
* @private
*/
OPCUAServer.prototype._on_BrowseNextRequest = function (message, channel) {
const request = message.request;
assert(request instanceof BrowseNextRequest);
this._apply_on_SessionObject(BrowseNextResponse, message, channel, function (session, sendResponse, sendError) {
let response;
if (!request.continuationPoints || request.continuationPoints.length === 0) {
return sendError(StatusCodes.BadNothingToDo);
}
// A Boolean parameter with the following values:
let results;
if (request.releaseContinuationPoints) {
//releaseContinuationPoints = TRUE
// passed continuationPoints shall be reset to free resources in
// the Server. The continuation points are released and the results
// and diagnosticInfos arrays are empty.
results = request.continuationPoints.map(function (continuationPoint) {
return session.continuationPointManager.cancel(continuationPoint);
});
} else {
// let extract data from continuation points
// releaseContinuationPoints = FALSE
// passed continuationPoints shall be used to get the next set of
// browse information.
results = request.continuationPoints.map(function (continuationPoint) {
return session.continuationPointManager.getNext(continuationPoint);
});
}
response = new BrowseNextResponse({
results: results,
diagnosticInfos: null
});
sendResponse(response);
});
};
// read services
OPCUAServer.prototype._on_ReadRequest = function (message, channel) {
const server = this;
const request = message.request;
assert(request instanceof ReadRequest);
this._apply_on_SessionObject(ReadResponse, message, channel, function (session, sendResponse, sendError) {
const context = new SessionContext({session, server});
let response;
let results = [];
const timestampsToReturn = request.timestampsToReturn;
if (timestampsToReturn === TimestampsToReturn.Invalid) {
return sendError(StatusCodes.BadTimestampsToReturnInvalid);
}
if (request.maxAge < 0) {
return sendError(StatusCodes.BadMaxAgeInvalid);
}
request.nodesToRead = request.nodesToRead || [];
if (!request.nodesToRead || request.nodesToRead.length <= 0) {
return sendError(StatusCodes.BadNothingToDo);
}
assert(request.nodesToRead[0]._schema.name === "ReadValueId");
assert(request.timestampsToReturn);
// limit size of nodesToRead array to maxNodesPerRead
if (server.engine.serverCapabilities.operationLimits.maxNodesPerRead > 0) {
if (request.nodesToRead.length > server.engine.serverCapabilities.operationLimits.maxNodesPerRead) {
return sendError(StatusCodes.BadTooManyOperations);
}
}
// proceed with registered nodes alias resolution
for (let i = 0; i < request.nodesToRead.length; i++) {
request.nodesToRead[i].nodeId = session.resolveRegisteredNode(request.nodesToRead[i].nodeId);
}
// ask for a refresh of asynchronous variables
server.engine.refreshValues(request.nodesToRead, function (err) {
assert(!err, " error not handled here , fix me");
results = server.engine.read(context, request);
assert(results[0]._schema.name === "DataValue");
assert(results.length === request.nodesToRead.length);
response = new ReadResponse({
results: null,
diagnosticInfos: null
});
// set it here for performance
response.results = results;
assert(response.diagnosticInfos.length === 0);
sendResponse(response);
});
});
};
// read services
OPCUAServer.prototype._on_HistoryReadRequest = function (message, channel) {
const server = this;
const request = message.request;
assert(request instanceof HistoryReadRequest);
this._apply_on_SessionObject(HistoryReadResponse, message, channel, function (session, sendResponse, sendError) {
let response;
const timestampsToReturn = request.timestampsToReturn;
if (timestampsToReturn === TimestampsToReturn.Invalid) {
return sendError(StatusCodes.BadTimestampsToReturnInvalid);
}
if (request.maxAge < 0) {
return sendError(StatusCodes.BadMaxAgeInvalid);
}
request.nodesToRead = request.nodesToRead || [];
if (!request.nodesToRead || request.nodesToRead.length <= 0) {
return sendError(StatusCodes.BadNothingToDo);
}
assert(request.nodesToRead[0]._schema.name === "HistoryReadValueId");
assert(request.timestampsToReturn);
// limit size of nodesToRead array to maxNodesPerRead
if (server.engine.serverCapabilities.operationLimits.maxNodesPerRead > 0) {
if (request.nodesToRead.length > server.engine.serverCapabilities.operationLimits.maxNodesPerRead) {
return sendError(StatusCodes.BadTooManyOperations);
}
}
// todo : handle
if (server.engine.serverCapabilities.operationLimits.maxNodesPerHistoryReadData > 0) {
if (request.nodesToRead.length > server.engine.serverCapabilities.operationLimits.maxNodesPerHistoryReadData) {
return sendError(StatusCodes.BadTooManyOperations);
}
}
if (server.engine.serverCapabilities.operationLimits.maxNodesPerHistoryReadEvents > 0) {
if (request.nodesToRead.length > server.engine.serverCapabilities.operationLimits.maxNodesPerHistoryReadEvents) {
return sendError(StatusCodes.BadTooManyOperations);
}
}
const context = new SessionContext({session, server});
// ask for a refresh of asynchronous variables
server.engine.refreshValues(request.nodesToRead, function (err) {
assert(!err, " error not handled here , fix me"); //TODO
server.engine.historyRead(context, request, function (err, results) {
assert(results[0]._schema.name === "HistoryReadResult");
assert(results.length === request.nodesToRead.length);
response = new HistoryReadResponse({
results: results,
diagnosticInfos: null
});
assert(response.diagnosticInfos.length === 0);
sendResponse(response);
});
});
});
};
/*
// write services
// OPCUA Specification 1.02 Part 3 : 5.10.4 Write
// This Service is used to write values to one or more Attributes of one or more Nodes. For constructed
// Attribute values whose elements are indexed, such as an array, this Service allows Clients to write
// the entire set of indexed values as a composite, to write individual elements or to write ranges of
// elements of the composite.
// The values are written to the data source, such as a device, and the Service does not return until it writes
// the values or determines that the value cannot be written. In certain cases, the Server will successfully
// to an intermediate system or Server, and will not know if the data source was updated properly. In these cases,
// the Server should report a success code that indicates that the write was not verified.
// In the cases where the Server is able to verify that it has successfully written to the data source,
// it reports an unconditional success.
*/
OPCUAServer.prototype._on_WriteRequest = function (message, channel) {
const server = this;
const request = message.request;
assert(request instanceof WriteRequest);
assert(!request.nodesToWrite || _.isArray(request.nodesToWrite));
this._apply_on_SessionObject(WriteResponse, message, channel, function (session, sendResponse, sendError) {
let response;
if (!request.nodesToWrite || request.nodesToWrite.length === 0) {
return sendError(StatusCodes.BadNothingToDo);
}
if (server.engine.serverCapabilities.operationLimits.maxNodesPerWrite > 0) {
if (request.nodesToWrite.length > server.engine.serverCapabilities.operationLimits.maxNodesPerWrite) {
return sendError(StatusCodes.BadTooManyOperations);
}
}
// proceed with registered nodes alias resolution
for (let i = 0; i < request.nodesToWrite.length; i++) {
request.nodesToWrite[i].nodeId = session.resolveRegisteredNode(request.nodesToWrite[i].nodeId);
}
const context = new SessionContext({session, server});
assert(request.nodesToWrite[0]._schema.name === "WriteValue");
server.engine.write(context, request.nodesToWrite, function (err, results) {
assert(!err);
assert(_.isArray(results));
assert(results.length === request.nodesToWrite.length);
response = new WriteResponse({
results: results,
diagnosticInfos: null
});
sendResponse(response);
});
});
};
/*=== 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, oldValue, node, itemToMonitor, callback) {
assert(self instanceof MonitoredItem);
assert(oldValue instanceof DataValue);
assert(itemToMonitor.attributeId === AttributeIds.Value);
const dataValue = node.readAttribute(itemToMonitor.attributeId, itemToMonitor.indexRange, itemToMonitor.dataEncoding);
callback(null, dataValue);
}
/*== private
* @method monitoredItem_read_and_record_value_async
* this method applies to Variable Values attribute
* @param self
* @param oldValue
* @param node
* @param itemToMonitor
* @private
*/
function monitoredItem_read_and_record_value_async(self, context, oldValue, node, itemToMonitor, callback) {
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, function (err, dataValue) {
callback(err, dataValue);
});
}
function build_scanning_node_function(context, addressSpace, monitoredItem, itemToMonitor) {
//assert(addressSpace instanceof AddressSpace);
assert(context instanceof SessionContext);
assert(addressSpace.constructor.name === "AddressSpace");
assert(itemToMonitor instanceof ReadValueId);
const node = addressSpace.findNode(itemToMonitor.nodeId);
/* istanbul ignore next */
if (!node) {
console.log(" INVALID NODE ID , ", itemToMonitor.nodeId.toString());
dump(itemToMonitor);
return function (oldData, callback) {
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 && _.isFunction(node.readValueAsync)) ?
monitoredItem_read_and_record_value_async :
monitoredItem_read_and_record_value;
return function (oldDataValue, callback) {
assert(this instanceof MonitoredItem);
assert(oldDataValue instanceof DataValue);
assert(_.isFunction(callback));
monitoredItem_read_and_record_value_func(this, context, 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 (oldDataValue, callback) {
const self = this;
assert(self instanceof MonitoredItem);
assert(oldDataValue instanceof DataValue);
assert(_.isFunction(callback));
const newDataValue = node.readAttribute(itemToMonitor.attributeId);
callback(null, newDataValue);
};
}
}
function prepareMonitoredItem(context, addressSpace, monitoredItem) {
const itemToMonitor = monitoredItem.itemToMonitor;
const readNodeFunc = build_scanning_node_function(context, addressSpace, monitoredItem, itemToMonitor);
monitoredItem.samplingFunc = readNodeFunc;
}
OPCUAServer.MAX_SUBSCRIPTION = 50;
// subscription services
OPCUAServer.prototype._on_CreateSubscriptionRequest = function (message, channel) {
const server = this;
const engine = server.engine;
const addressSpace = engine.addressSpace;
const request = message.request;
assert(request instanceof CreateSubscriptionRequest);
this._apply_on_SessionObject(CreateSubscriptionResponse, message, channel, function (session, sendResponse, sendError) {
const context = new SessionContext({session, server});
if (session.currentSubscriptionCount >= OPCUAServer.MAX_SUBSCRIPTION) {
return sendError(StatusCodes.BadTooManySubscriptions);
}
const subscription = session.createSubscription(request);
subscription.on("monitoredItem", function (monitoredItem) {
prepareMonitoredItem(context, addressSpace, monitoredItem);
});
const response = new CreateSubscriptionResponse({
subscriptionId: subscription.id,
revisedPublishingInterval: subscription.publishingInterval,
revisedLifetimeCount: subscription.lifeTimeCount,
revisedMaxKeepAliveCount: subscription.maxKeepAliveCount
});
sendResponse(response);
});
};
OPCUAServer.prototype._on_DeleteSubscriptionsRequest = function (message, channel) {
const server = this;
const request = message.request;
assert(request instanceof DeleteSubscriptionsRequest);
this._apply_on_SubscriptionIds(DeleteSubscriptionsResponse, message, channel, function (session, subscriptionId) {
const subscription = server.engine.findOrphanSubscription(subscriptionId);
if (subscription) {
return server.engine.deleteOrphanSubscription(subscription);
}
return session.deleteSubscription(subscriptionId);
});
};
OPCUAServer.prototype._on_TransferSubscriptionsRequest = function (message, channel) {
//
// sendInitialValue Boolean
// A Boolean parameter with the following values:
// TRUE the first Publish response(s) after the TransferSubscriptions call shall
// contain the current values of all Monitored Items in the Subscription where
// the Monitoring Mode is set to Reporting.
// FALSE the first Publish response after the TransferSubscriptions call shall contain only the value
// changes since the last Publish response was sent.
// This parameter only applies to MonitoredItems used for monitoring Attribute changes.
//
const server = this;
const engine = server.engine;
const request = message.request;
assert(request instanceof TransferSubscriptionsRequest);
this._apply_on_SubscriptionIds(TransferSubscriptionsResponse, message, channel, function (session, subscriptionId) {
return engine.transferSubscription(session, subscriptionId, request.sendInitialValues);
});
};
OPCUAServer.prototype.prepare = function (message, channel) {
const server = this;
const request = message.request;
// --- check that session is correct
const authenticationToken = request.requestHeader.authenticationToken;
const session = server.getSession(authenticationToken, /*activeOnly*/true);
message.session = session;
if (!session) {
message.session_statusCode = StatusCodes.BadSessionIdInvalid;
return;
}
//xx console.log("xxxx channel ",channel.secureChannelId,session.secureChannelId);
// --- check that provided session matches session attached to channel
if (channel.secureChannelId !== session.secureChannelId) {
if (!(request instanceof ActivateSessionRequest)) {
console.log("ERROR: channel.secureChannelId !== session.secureChannelId".red.bgWhite, channel.secureChannelId, session.secureChannelId);
//xx console.log("trace",(new Error()).stack);
}
message.session_statusCode = StatusCodes.BadSecureChannelIdInvalid;
} else if (channel_has_session(channel, session)) {
message.session_statusCode = StatusCodes.Good;
} else {
// session ma y have been moved to a different channel
message.session_statusCode = StatusCodes.BadSecureChannelIdInvalid;
}
};
OPCUAServer.prototype._on_CreateMonitoredItemsRequest = function (message, channel) {
const server = this;
const engine = server.engine;
const addressSpace = engine.addressSpace;
const request = message.request;
assert(request instanceof CreateMonitoredItemsRequest);
this._apply_on_Subscription(CreateMonitoredItemsResponse, message, channel, function (session, subscription, sendResponse, sendError) {
const timestampsToReturn = request.timestampsToReturn;
if (timestampsToReturn === TimestampsToReturn.Invalid) {
return sendError(StatusCodes.BadTimestampsToReturnInvalid);
}
if (!request.itemsToCreate || request.itemsToCreate.length === 0) {
return sendError(StatusCodes.BadNothingToDo);
}
if (server.engine.serverCapabilities.operationLimits.maxMonitoredItemsPerCall > 0) {
if (request.itemsToCreate.length > server.engine.serverCapabilities.operationLimits.maxMonitoredItemsPerCall) {
return sendError(StatusCodes.BadTooManyOperations);
}
}
const results = request.itemsToCreate.map(
subscription.createMonitoredItem.bind(subscription, addressSpace, timestampsToReturn));
const response = new CreateMonitoredItemsResponse({
responseHeader: {serviceResult: StatusCodes.Good},
results: results
//,diagnosticInfos: []
});
sendResponse(response);
});
};
const ModifySubscriptionRequest = subscription_service.ModifySubscriptionRequest;
const ModifySubscriptionResponse = subscription_service.ModifySubscriptionResponse;
OPCUAServer.prototype._on_ModifySubscriptionRequest = function (message, channel) {
const request = message.request;
assert(request instanceof ModifySubscriptionRequest);
this._apply_on_Subscription(ModifySubscriptionResponse, message, channel, function (session, subscription, sendResponse, sendError) {
subscription.modify(request);
const response = new ModifySubscriptionResponse({
revisedPublishingInterval: subscription.publishingInterval,
revisedLifetimeCount: subscription.lifeTimeCount,
revisedMaxKeepAliveCount: subscription.maxKeepAliveCount
});
sendResponse(response);
});
};
OPCUAServer.prototype._on_ModifyMonitoredItemsRequest = function (message, channel) {
const server = this;
const request = message.request;
assert(request instanceof ModifyMonitoredItemsRequest);
this._apply_on_Subscription(ModifyMonitoredItemsResponse, message, channel, function (session, subscription, sendResponse, sendError) {
const timestampsToReturn = request.timestampsToReturn;
if (timestampsToReturn === TimestampsToReturn.Invalid) {
return sendError(StatusCodes.BadTimestampsToReturnInvalid);
}
if (!request.itemsToModify || request.itemsToModify.length === 0) {
return sendError(StatusCodes.BadNothingToDo);
}
if (server.engine.serverCapabilities.operationLimits.maxMonitoredItemsPerCall > 0) {
if (request.itemsToModify.length > server.engine.serverCapabilities.operationLimits.maxMonitoredItemsPerCall) {
return sendError(StatusCodes.BadTooManyOperations);
}
}
const itemsToModify = request.itemsToModify; // MonitoredItemModifyRequest
function modifyMonitoredItem(item) {
const monitoredItemId = item.monitoredItemId;
const monitoredItem = subscription.getMonitoredItem(monitoredItemId);
if (!monitoredItem) {
return new MonitoredItemModifyResult({statusCode: StatusCodes.BadMonitoredItemIdInvalid});
}
// adjust samplingInterval if === -1
if (item.requestedParameters.samplingInterval === -1) {
item.requestedParameters.samplingInterval = subscription.publishingInterval;
}
return monitoredItem.modify(timestampsToReturn, item.requestedParameters);
}
const results = itemsToModify.map(modifyMonitoredItem);
const response = new ModifyMonitoredItemsResponse({
results: results
});
sendResponse(response);
});
};
OPCUAServer.prototype._on_PublishRequest = function (message, channel) {
const request = message.request;
assert(request instanceof PublishRequest);
this._apply_on_SessionObject(PublishResponse, message, channel, function (session, sendResponse, sendError) {
assert(session);
assert(session.publishEngine); // server.publishEngine doesn't exists, OPCUAServer has probably shut down already
session.publishEngine._on_PublishRequest(request, function (request, response) {
sendResponse(response);
});
});
};
OPCUAServer.prototype._on_SetPublishingModeRequest = function (message, channel) {
const request = message.request;
assert(request instanceof SetPublishingModeRequest);
const publishingEnabled = request.publishingEnabled;
this._apply_on_Subscriptions(SetPublishingModeResponse, message, channel, function (session, subscription) {
return subscription.setPublishingMode(publishingEnabled);
});
};
OPCUAServer.prototype._on_DeleteMonitoredItemsRequest = function (message, channel) {
const server = this;
const request = message.request;
assert(request instanceof DeleteMonitoredItemsRequest);
this._apply_on_Subscription(DeleteMonitoredItemsResponse, message, channel, function (session, subscription, sendResponse, sendError) {
if (!request.monitoredItemIds || request.monitoredItemIds.length === 0) {
return sendError(StatusCodes.BadNothingToDo);
}
if (server.engine.serverCapabilities.operationLimits.maxMonitoredItemsPerCall > 0) {
if (request.monitoredItemIds.length > server.engine.serverCapabilities.operationLimits.maxMonitoredItemsPerCall) {
return sendError(StatusCodes.BadTooManyOperations);
}
}
const results = request.monitoredItemIds.map(function (monitoredItemId) {
return subscription.removeMonitoredItem(monitoredItemId);
});
const response = new DeleteMonitoredItemsResponse({
results: results,
diagnosticInfos: null
});
sendResponse(response);
});
};
OPCUAServer.prototype._on_RepublishRequest = function (message, channel) {
const request = message.request;
assert(request instanceof RepublishRequest);
this._apply_on_Subscription(RepublishResponse, message, channel, function (session, subscription, sendResponse, sendError) {
// update diagnostic counter
subscription.subscriptionDiagnostics.republishRequestCount += 1;
const retransmitSequenceNumber = request.retransmitSequenceNumber;
const msgSequence = subscription.getMessageForSequenceNumber(retransmitSequenceNumber);
if (!msgSequence) {
return sendError(StatusCodes.BadMessageNotAvailable);
}
const response = new RepublishResponse({
responseHeader: {
serviceResult: StatusCodes.Good
},
notificationMessage: msgSequence.notification
});
sendResponse(response);
});
};
const SetMonitoringModeRequest = subscription_service.SetMonitoringModeRequest;
const SetMonitoringModeResponse = subscription_service.SetMonitoringModeResponse;
// Bad_NothingToDo
// Bad_TooManyOperations
// Bad_SubscriptionIdInvalid
// Bad_MonitoringModeInvalid
OPCUAServer.prototype._on_SetMonitoringModeRequest = function (message, channel) {
const server = this;
const request = message.request;
assert(request instanceof SetMonitoringModeRequest);
this._apply_on_Subscription(SetMonitoringModeResponse, message, channel, function (session, subscription, sendResponse, sendError) {
if (!request.monitoredItemIds || request.monitoredItemIds.length === 0) {
return sendError(StatusCodes.BadNothingToDo);
}
if (server.engine.serverCapabilities.operationLimits.maxMonitoredItemsPerCall > 0) {
if (request.monitoredItemIds.length > server.engine.serverCapabilities.operationLimits.maxMonitoredItemsPerCall) {
return sendError(StatusCodes.BadTooManyOperations);
}
}
const monitoringMode = request.monitoringMode;
if (monitoringMode === subscription_service.MonitoringMode.Invalid) {
return sendError(StatusCodes.BadMonitoringModeInvalid);
}
const results = request.monitoredItemIds.map(function (monitoredItemId) {
const monitoredItem = subscription.getMonitoredItem(monitoredItemId);
if (!monitoredItem) {
return StatusCodes.BadMonitoredItemIdInvalid;
}
monitoredItem.setMonitoringMode(monitoringMode);
return StatusCodes.Good;
});
const response = new SetMonitoringModeResponse({
results: results
});
sendResponse(response);
});
};
// _on_TranslateBrowsePathsToNodeIds service
OPCUAServer.prototype._on_TranslateBrowsePathsToNodeIdsRequest = function (message, channel) {
const request = message.request;
assert(request instanceof TranslateBrowsePathsToNodeIdsRequest);
const server = this;
this._apply_on_SessionObject(TranslateBrowsePathsToNodeIdsResponse, message, channel, function (session, sendResponse, sendError) {
if (!request.browsePath || request.browsePath.length === 0) {
return sendError(StatusCodes.BadNothingToDo);
}
if (server.engine.serverCapabilities.operationLimits.maxNodesPerTranslateBrowsePathsToNodeIds > 0) {
if (request.browsePath.length > server.engine.serverCapabilities.operationLimits.maxNodesPerTranslateBrowsePathsToNodeIds) {
return sendError(StatusCodes.BadTooManyOperations);
}
}
const browsePathResults = request.browsePath.map(function (browsePath) {
return server.engine.browsePath(browsePath);
});
const response = new TranslateBrowsePathsToNodeIdsResponse({
results: browsePathResults,
diagnosticInfos: null
});
sendResponse(response);
});
};
// Symbolic Id Description
//---------------------------- ----------------------------------------------------------------------------------------
// Bad_NodeIdInvalid Used to indicate that the specified object is not valid.
//
// Bad_NodeIdUnknown Used to indicate that the specified object is not valid.
//
// Bad_ArgumentsMissing The client did not specify all of the input arguments for the method.
// Bad_UserAccessDenied
//
// Bad_MethodInvalid The method id does not refer to a method for the specified object.
// Bad_OutOfRange Used to indicate that an input argument is outside the acceptable range.
// Bad_TypeMismatch Used to indicate that an input argument does not have the correct data type.
// A ByteString is structurally the same as a one dimensional array of Byte.
// A server shall accept a ByteString if an array of Byte is expected.
// Bad_NoCommunication
const getMethodDeclaration_ArgumentList = require("node-opcua-address-space").getMethodDeclaration_ArgumentList;
const verifyArguments_ArgumentList = require("node-opcua-address-space").verifyArguments_ArgumentList;
function callMethod(session, callMethodRequest, callback) {
/* jshint validthis: true */
const server = this;
const addressSpace = server.engine.addressSpace;
const objectId = callMethodRequest.objectId;
const methodId = callMethodRequest.methodId;
const inputArguments = callMethodRequest.inputArguments;
assert(objectId instanceof NodeId);
assert(methodId instanceof NodeId);
let response = getMethodDeclaration_ArgumentList(addressSpace, objectId, methodId);
if (response.statusCode !== StatusCodes.Good) {
return callback(null, {statusCode: response.statusCode});
}
const methodDeclaration = response.methodDeclaration;
// verify input Parameters
const methodInputArguments = methodDeclaration.getInputArguments();
response = verifyArguments_ArgumentList(addressSpace, methodInputArguments, inputArguments);
if (response.statusCode !== StatusCodes.Good) {
return callback(null, response);
}
const methodObj = addressSpace.findNode(methodId);
// invoke method on object
const context = new SessionContext({
session: session,
object: addressSpace.findNode(objectId),
server: server
});
methodObj.execute(inputArguments, context, function (err, callMethodResponse) {
/* istanbul ignore next */
if (err) {
return callback(err);
}
callMethodResponse.inputArgumentResults = response.inputArgumentResults || [];
assert(callMethodResponse.statusCode);
if (callMethodResponse.statusCode === StatusCodes.Good) {
assert(_.isArray(callMethodResponse.outputArguments));
}
assert(_.isArray(callMethodResponse.inputArgumentResults));
assert(callMethodResponse.inputArgumentResults.length === methodInputArguments.length);
return callback(null, callMethodResponse);
});
}
// Call Service Result Codes
// Symbolic Id Description
// Bad_NothingToDo See Table 165 for the description of this result code.
// Bad_TooManyOperations See Table 165 for the description of this result code.
//
OPCUAServer.prototype._on_CallRequest = function (message, channel) {
const server = this;
const request = message.request;
assert(request instanceof CallRequest);
this._apply_on_SessionObject(CallResponse, message, channel, function (session, sendResponse, sendError) {
let response;
if (!request.methodsToCall || request.methodsToCall.length === 0) {
return sendError(StatusCodes.BadNothingToDo);
}
// the MaxNodesPerMethodCall Property indicates the maximum size of the methodsToCall array when
// a Client calls the Call Service.
let maxNodesPerMethodCall = server.engine.serverCapabilities.operationLimits.maxNodesPerMethodCall;
maxNodesPerMethodCall = maxNodesPerMethodCall <= 0 ? 1000 : maxNodesPerMethodCall;
if (request.methodsToCall.length >= maxNodesPerMethodCall) {
return sendError(StatusCodes.BadTooManyOperations);
}
async.map(request.methodsToCall, callMethod.bind(server, session), function (err, results) {
if (err) {
console.log("ERROR in method Call !! ", err);
}
assert(_.isArray(results));
response = new CallResponse({results: results});
sendResponse(response);
}, function (err) {
/* istanbul ignore next */
if (err) {
channel.send_error_and_abort(StatusCodes.BadInternalError, err.message, "", function () {
});
}
});
});
};
OPCUAServer.prototype._on_RegisterNodesRequest = function (message, channel) {
const server = this;
const request = message.request;
assert(request instanceof RegisterNodesRequest);
this._apply_on_SessionObject(RegisterNodesResponse, message, channel, function (session, sendResponse, sendError) {
let response;
if (!request.nodesToRegister || request.nodesToRegister.length === 0) {
response = new RegisterNodesResponse({responseHeader: {serviceResult: StatusCodes.BadNothingToDo}});
return sendResponse(response);
}
if (server.engine.serverCapabilities.operationLimits.maxNodesPerRegisterNodes > 0) {
if (request.nodesToRegister.length > server.engine.serverCapabilities.operationLimits.maxNodesPerRegisterNodes) {
return sendError(StatusCodes.BadTooManyOperations);
}
}
// A list of NodeIds which the Client shall use for subsequent access operations. The
// size and order of this list matches the size and order of the nodesToRegister
// request parameter.
// The Server may return the NodeId from the request or a new (an alias) NodeId. It
// is recommended that the Server return a numeric NodeIds for aliasing.
// In case no optimization is supported for a Node, the Server shall return the
// NodeId from the request.
const registeredNodeIds = request.nodesToRegister.map(nodeId => session.registerNode(nodeId));
response = new RegisterNodesResponse({
registeredNodeIds: registeredNodeIds
});
sendResponse(response);
});
};
OPCUAServer.prototype._on_UnregisterNodesRequest = function (message, channel) {
const server = this;
const request = message.request;
assert(request instanceof UnregisterNodesRequest);
this._apply_on_SessionObject(UnregisterNodesResponse, message, channel, function (session, sendResponse, sendError) {
let response;
if (!request.nodesToUnregister || request.nodesToUnregister.length === 0) {
response = new UnregisterNodesResponse({responseHeader: {serviceResult: StatusCodes.BadNothingToDo}});
return sendResponse(response);
}
if (server.engine.serverCapabilities.operationLimits.maxNodesPerRegisterNodes > 0) {
if (request.nodesToRegister.length > server.engine.serverCapabilities.operationLimits.maxNodesPerRegisterNodes) {
return sendError(StatusCodes.BadTooManyOperations);
}
}
request.nodesToUnregister.map(nodeId => session.unRegisterNode(nodeId));
response = new UnregisterNodesResponse({});
sendResponse(response);
});
};
OPCUAServer.prototype._on_Cancel = function (message, channel) {
return g_sendError(channel, message, session_service.CancelResponse, StatusCodes.BadNotImplemented);
};
// NodeManagement Service Set Overview
// This Service Set defines Services to add and delete AddressSpace Nodes and References between them. All added
// Nodes continue to exist in the AddressSpace even if the Client that created them disconnects from the Server.
//
const node_managment_service = require("node-opcua-service-node-management");
OPCUAServer.prototype._on_AddNodes = function (message, channel) {
return g_sendError(channel, message, node_managment_service.AddNodesResponse, StatusCodes.BadNotImplemented);
};
OPCUAServer.prototype._on_AddReferences = function (message, channel) {
return g_sendError(channel, message, node_managment_service.AddReferencesResponse, StatusCodes.BadNotImplemented);
};
OPCUAServer.prototype._on_DeleteNodes = function (message, channel) {
return g_sendError(channel, message, node_managment_service.DeleteNodesResponse, StatusCodes.BadNotImplemented);
};
OPCUAServer.prototype._on_DeleteReferences = function (message, channel) {
return g_sendError(channel, message, node_managment_service.DeleteReferencesResponse, StatusCodes.BadNotImplemented);
};
// Query Service
OPCUAServer.prototype._on_QueryFirst = function (message, channel) {
return g_sendError(channel, message, query_service.QueryFirstResponse, StatusCodes.BadNotImplemented);
};
OPCUAServer.prototype._on_QueryNext = function (message, channel) {
return g_sendError(channel, message, query_service.QueryNextResponse, StatusCodes.BadNotImplemented);
};
OPCUAServer.prototype._on_HistoryUpdate = function (message, channel) {
return g_sendError(channel, message, historizing_service.HistoryUpdateResponse, StatusCodes.BadNotImplemented);
};
/**
* @method registerServer
* @async
* @param discoveryServerEndpointUrl
* @param isOnline
* @param outer_callback
*/
function _registerServer(discoveryServerEndpointUrl, isOnline, outer_callback) {
assert(typeof discoveryServerEndpointUrl === "string");
assert(_.isBoolean(isOnline));
const self = this;
self.registerServerManager.discoveryServerEndpointUrl = discoveryServerEndpointUrl;
if (isOnline) {
self.registerServerManager.start(outer_callback);
} else {
self.registerServerManager.stop(outer_callback);
}
}
OPCUAServer.prototype.registerServer = function (discoveryServerEndpointUrl, callback) {
assert(!"registerServer is DEPRECATED - please use registerServerMethod when creating the OPCUAServer");
_registerServer.call(this,discoveryServerEndpointUrl, true, callback);
};
OPCUAServer.prototype.unregisterServer = function (discoveryServerEndpointUrl, callback) {
assert(!"unregisterServer is DEPRECATED - please use registerServerMethod when creating the OPCUAServer");
_registerServer.call(this,discoveryServerEndpointUrl, false, callback);
};
OPCUAServer.prototype.__defineGetter__("isAuditing", function () {
return this.engine.isAuditing;
});
exports.OPCUAServerEndPoint = OPCUAServerEndPoint;
exports.OPCUAServer = OPCUAServer;
exports.RegisterServerMethod = RegisterServerMethod;