node-opcua-server
Version:
pure nodejs OPCUA SDK - module -server
1,245 lines (968 loc) • 105 kB
JavaScript
"use strict";
/*global: require Buffer*/
/**
* @module opcua.server
*/
var assert = require("node-opcua-assert");
var async = require("async");
var util = require("util");
var fs = require("fs");
var _ = require("underscore");
var ApplicationType = require("node-opcua-service-endpoints").ApplicationType;
var StatusCodes = require("node-opcua-status-code").StatusCodes;
var SessionContext = require("node-opcua-address-space").SessionContext;
var fromURI = require("node-opcua-secure-channel").fromURI;
var SecurityPolicy = require("node-opcua-secure-channel").SecurityPolicy;
var MessageSecurityMode = require("node-opcua-service-secure-channel").MessageSecurityMode;
var utils = require("node-opcua-utils");
var debugLog = require("node-opcua-debug").make_debugLog(__filename);
var ServerEngine = require("./server_engine").ServerEngine;
var browse_service = require("node-opcua-service-browse");
var read_service = require("node-opcua-service-read");
var write_service = require("node-opcua-service-write");
var historizing_service = require("node-opcua-service-history");
var subscription_service = require("node-opcua-service-subscription");
var register_server_service = require("node-opcua-service-register-server");
var translate_service = require("node-opcua-service-translate-browse-path");
var session_service = require("node-opcua-service-session");
var register_node_service = require("node-opcua-service-register-node");
var call_service = require("node-opcua-service-call");
var endpoints_service = require("node-opcua-service-endpoints");
var query_service = require("node-opcua-service-query");
var ServerState = require("node-opcua-common").ServerState;
var EndpointDescription = endpoints_service.EndpointDescription;
var TimestampsToReturn = read_service.TimestampsToReturn;
var ActivateSessionRequest = session_service.ActivateSessionRequest;
var ActivateSessionResponse = session_service.ActivateSessionResponse;
var CreateSessionRequest = session_service.CreateSessionRequest;
var CreateSessionResponse = session_service.CreateSessionResponse;
var CloseSessionRequest = session_service.CloseSessionRequest;
var CloseSessionResponse = session_service.CloseSessionResponse;
var DeleteMonitoredItemsRequest = subscription_service.DeleteMonitoredItemsRequest;
var DeleteMonitoredItemsResponse = subscription_service.DeleteMonitoredItemsResponse;
var RepublishRequest = subscription_service.RepublishRequest;
var RepublishResponse = subscription_service.RepublishResponse;
var PublishRequest = subscription_service.PublishRequest;
var PublishResponse = subscription_service.PublishResponse;
var CreateSubscriptionRequest = subscription_service.CreateSubscriptionRequest;
var CreateSubscriptionResponse = subscription_service.CreateSubscriptionResponse;
var DeleteSubscriptionsRequest = subscription_service.DeleteSubscriptionsRequest;
var DeleteSubscriptionsResponse = subscription_service.DeleteSubscriptionsResponse;
var TransferSubscriptionsRequest = subscription_service.TransferSubscriptionsRequest;
var TransferSubscriptionsResponse = subscription_service.TransferSubscriptionsResponse;
var CreateMonitoredItemsRequest = subscription_service.CreateMonitoredItemsRequest;
var CreateMonitoredItemsResponse = subscription_service.CreateMonitoredItemsResponse;
var ModifyMonitoredItemsRequest = subscription_service.ModifyMonitoredItemsRequest;
var ModifyMonitoredItemsResponse = subscription_service.ModifyMonitoredItemsResponse;
var MonitoredItemModifyResult = subscription_service.MonitoredItemModifyResult;
var MonitoredItemCreateResult = subscription_service.MonitoredItemCreateResult;
var SetPublishingModeRequest = subscription_service.SetPublishingModeRequest;
var SetPublishingModeResponse = subscription_service.SetPublishingModeResponse;
var CallRequest = call_service.CallRequest;
var CallResponse = call_service.CallResponse;
var ReadRequest = read_service.ReadRequest;
var ReadResponse = read_service.ReadResponse;
var WriteRequest = write_service.WriteRequest;
var WriteResponse = write_service.WriteResponse;
var ReadValueId = read_service.ReadValueId;
var HistoryReadRequest = historizing_service.HistoryReadRequest;
var HistoryReadResponse = historizing_service.HistoryReadResponse;
var BrowseRequest = browse_service.BrowseRequest;
var BrowseResponse = browse_service.BrowseResponse;
var BrowseNextRequest = browse_service.BrowseNextRequest;
var BrowseNextResponse = browse_service.BrowseNextResponse;
var RegisterNodesRequest = register_node_service.RegisterNodesRequest;
var RegisterNodesResponse = register_node_service.RegisterNodesResponse;
var UnregisterNodesRequest = register_node_service.UnregisterNodesRequest;
var UnregisterNodesResponse = register_node_service.UnregisterNodesResponse;
var TranslateBrowsePathsToNodeIdsRequest = translate_service.TranslateBrowsePathsToNodeIdsRequest;
var TranslateBrowsePathsToNodeIdsResponse = translate_service.TranslateBrowsePathsToNodeIdsResponse;
var RegisterServerRequest = register_server_service.RegisterServerRequest;
var RegisterServerResponse = register_server_service.RegisterServerResponse;
var NodeId = require("node-opcua-nodeid").NodeId;
var DataValue = require("node-opcua-data-value").DataValue;
var DataType = require("node-opcua-variant").DataType;
var AttributeIds = require("node-opcua-data-model").AttributeIds;
var MonitoredItem = require("./monitored_item").MonitoredItem;
var View = require("node-opcua-address-space").View;
var crypto = require("crypto");
var dump = require("node-opcua-debug").dump;
var OPCUAServerEndPoint = require("./server_end_point").OPCUAServerEndPoint;
var OPCUABaseServer = require("./base_server").OPCUABaseServer;
var OPCUAClientBase = require("node-opcua-client").OPCUAClientBase;
var exploreCertificate = require("node-opcua-crypto").crypto_explore_certificate.exploreCertificate;
var Factory = function Factory(engine) {
assert(_.isObject(engine));
this.engine = engine;
};
var factories = require("node-opcua-factory");
Factory.prototype.constructObject = function (id) {
return factories.constructObject(id);
};
var default_maxAllowedSessionNumber = 10;
var default_maxConnectionsPerEndpoint = 10;
function g_sendError(channel, message, ResponseClass, statusCode) {
var response = new ResponseClass({
responseHeader: {serviceResult: statusCode}
});
return channel.send_response("MSG", response, message);
}
var package_info = require("../package.json");
//xx var package_info = JSON.parse(fs.readFileSync(package_json_file));
var 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
};
/**
* @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
* @constructor
*/
function OPCUAServer(options) {
options = options || {};
OPCUABaseServer.apply(this, arguments);
var self = this;
self.options = options;
self.maxAllowedSessionNumber = options.maxAllowedSessionNumber || default_maxAllowedSessionNumber;
self.maxConnectionsPerEndpoint = options.maxConnectionsPerEndpoint || default_maxConnectionsPerEndpoint;
// build Info
var 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;
var port = options.port || 26543;
assert(_.isFinite(port));
self.objectFactory = new Factory(self.engine);
// todo should self.serverInfo.productUri match self.engine.buildInfo.productUri ?
options.allowAnonymous = ( options.allowAnonymous === undefined) ? true : options.allowAnonymous;
//xx console.log(" maxConnectionsPerEndpoint = ",self.maxConnectionsPerEndpoint);
// add the tcp/ip endpoint with no security
var 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;
};
}
}
util.inherits(OPCUAServer, OPCUABaseServer);
var 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
var 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 () {
var self = this;
return self.engine.currentSubscriptionCount;
});
/**
* @type {number}
*/
OPCUAServer.prototype.__defineGetter__("rejectedSessionCount", function () {
return this.engine.rejectedSessionCount;
});
OPCUAServer.prototype.__defineGetter__("rejectedRequestsCount", function () {
return this.engine.rejectedRequestsCount;
});
OPCUAServer.prototype.__defineGetter__("sessionAbortCount", function () {
return this.engine.sessionAbortCount;
});
OPCUAServer.prototype.__defineGetter__("publishingIntervalCount", function () {
return this.engine.publishingIntervalCount;
});
/**
* create and register a new session
* @method createSession
* @return {ServerSession}
*/
OPCUAServer.prototype.createSession = function (options) {
var 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) {
var self = this;
return self.engine.getSession(authenticationToken, activeOnly);
};
/**
* true if the server has been initialized
* @property initialized
* @type {Boolean}
*
*/
OPCUAServer.prototype.__defineGetter__("initialized", function () {
var 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) {
var 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) {
var self = this;
var 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 {
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;
}
assert(_.isFunction(callback));
var self = this;
debugLog("OPCUAServer#shutdown (timeout = ",timeout,")");
self.engine.setServerState(ServerState.Shutdown);
setTimeout(function () {
self.engine.shutdown();
debugLog("OPCUAServer#shutdown: started");
OPCUABaseServer.prototype.shutdown.call(self, function (err) {
debugLog("OPCUAServer#shutdown: completed");
OPCUAServer.registry.unregister(self);
callback(err);
});
}, timeout);
};
var computeSignature = require("node-opcua-secure-channel").computeSignature;
var verifySignature = require("node-opcua-secure-channel").verifySignature;
OPCUAServer.prototype.computeServerSignature = function (channel, clientCertificate, clientNonce) {
var self = this;
return computeSignature(clientCertificate, clientNonce, self.getPrivateKey(), channel.messageBuilder.securityPolicy);
};
var split_der = require("node-opcua-crypto").crypto_explore_certificate.split_der;
OPCUAServer.prototype.verifyClientSignature = function (session, channel, clientSignature) {
var self = this;
var clientCertificate = channel.receiverCertificate;
var securityPolicy = channel.messageBuilder.securityPolicy;
var serverCertificateChain = self.getCertificateChain();
var result = verifySignature(serverCertificateChain, session.nonce, clientSignature, clientCertificate, securityPolicy);
return result;
};
var minSessionTimeout = 100; // 100 milliseconds
var defaultSessionTimeout = 1000 * 30 ; // 30 seconds
var maxSessionTimeout = 1000 * 60 * 50; // 50 minutes
function _adjust_session_timeout(sessionTimeout) {
var 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 channel_unregisterSession(channel,session) {
assert(session.nonce && session.nonce instanceof Buffer);
var key = session.authenticationToken.toString("hex");
assert(channel.sessionTokens.hasOwnProperty(key));
assert(session.channel);
delete channel.sessionTokens[key];
session.channel = null;
session.secureChannelId = null;
}
function channel_registerSession(channel,session) {
assert(session.nonce && session.nonce instanceof Buffer);
session.channel = channel;
session.secureChannelId = channel.secureChannelId;
var key = session.authenticationToken.toString("hex");
assert(!channel.sessionTokens.hasOwnProperty(key),"channel has already a session");
channel.sessionTokens[key] = session;
}
function moveSessionToChannel(session,channel) {
if (session.publishEngine) {
session.publishEngine.cancelPendingPublishRequestBeforeChannelChange();
}
// unregister all session
channel_unregisterSession(session.channel,session);
//
channel_registerSession(channel,session);
assert(session.channel.secureChannelId === channel.secureChannelId);
}
function _attempt_to_close_some_old_unactivated_session(server) {
var session = server.engine.getOldestUnactivatedSession();
if (session) {
server.engine.closeSession(session.authenticationToken, false,"Forcing");
}
}
// session services
OPCUAServer.prototype._on_CreateSessionRequest = function (message, channel) {
var server = this;
var request = message.request;
assert(request instanceof CreateSessionRequest);
function rejectConnection(statusCode) {
server.engine._rejectedSessionCount += 1;
var 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.
var 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
}
var e = exploreCertificate(clientCertificate);
var 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) {
var endpoints = server._get_endpoints();
// ignore restricted endpoints
endpoints = endpoints.filter(function (endpoint) {
return !endpoint.restricted;
});
var endpoints_matching_security_mode = endpoints.filter(function (e) {
return e.securityMode === channel.securityMode;
});
if (endpoints_matching_security_mode.length === 0) {
return StatusCodes.BadSecurityModeRejected;
}
var 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;
}
var errStatus = validate_security_endpoint(channel);
if (errStatus !== StatusCodes.Good) {
return rejectConnection(errStatus);
}
// see Release 1.02 27 OPC Unified Architecture, Part 4
var 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;
channel_registerSession(channel,session);
var serverCertificateChain = server.getCertificateChain();
var 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: serverCertificateChain,
// 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: server._get_endpoints(),
//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");
var 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);
};
var UserNameIdentityToken = session_service.UserNameIdentityToken;
var AnonymousIdentityToken = session_service.AnonymousIdentityToken;
var SecurityPolicy = require("node-opcua-secure-channel").SecurityPolicy;
var fromURI = require("node-opcua-secure-channel").fromURI;
var getCryptoFactory = require("node-opcua-secure-channel").getCryptoFactory;
function adjustSecurityPolicy(channel, userTokenPolicy_securityPolicyUri) {
// check that userIdentityToken
var 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) {
var self = this;
assert(userIdentityToken instanceof UserNameIdentityToken);
var securityPolicy = adjustSecurityPolicy(channel, userTokenPolicy.securityPolicyUri);
var 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;
}
var userName = userIdentityToken.userName;
var 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) {
var self = this;
assert(userIdentityToken instanceof UserNameIdentityToken);
assert(self.isValidUserNameIdentityToken(channel, session, userTokenPolicy, userIdentityToken));
var securityPolicy = adjustSecurityPolicy(channel, userTokenPolicy.securityPolicyUri);
var serverPrivateKey = self.getPrivateKey();
var serverNonce = session.nonce;
assert(serverNonce instanceof Buffer);
var cryptoFactory = getCryptoFactory(securityPolicy);
if (!cryptoFactory) {
return done(new Error(" Unsupported security Policy"));
}
var userName = userIdentityToken.userName;
var password = userIdentityToken.password;
var buff = cryptoFactory.asymmetricDecrypt(password, serverPrivateKey);
var 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 {
var authorized=self.userManager.isValidUser.call(session, userName, password);
async.setImmediate(function() { done(null, authorized) });
}
};
function findUserTokenByPolicy(endpoint_description, policyId) {
assert(endpoint_description instanceof EndpointDescription);
var 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];
}
OPCUAServer.prototype.isValidUserIdentityToken = function (channel, session, userIdentityToken) {
var self = this;
assert(userIdentityToken);
var endpoint_desc = channel.endpoint;
assert(endpoint_desc instanceof EndpointDescription);
var 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) {
var self = this;
assert(userIdentityToken);
assert(_.isFunction(done));
var endpoint_desc = channel.endpoint;
assert(endpoint_desc instanceof EndpointDescription);
var 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) {
var server = this;
var request = message.request;
assert(request instanceof ActivateSessionRequest);
// get session from authenticationToken
var authenticationToken = request.requestHeader.authenticationToken;
var session = server.getSession(authenticationToken);
function rejectConnection(statusCode) {
server.engine._rejectedSessionCount += 1;
var response = new ActivateSessionResponse({responseHeader: {serviceResult: statusCode}});
channel.send_response("MSG", response, message);
// and close !
}
var response;
/* istanbul ignore next */
if (!session) {
console.log(" 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.
var old_channel_cert_thumbprint = thumbprint(session.channel.clientCertificate);
var 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);
}
// 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);
var userIdentityTokenPasswordRemoved=function(userIdentityToken) {
var 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
var 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/ActivateSes