UNPKG

node-opcua-server

Version:

pure nodejs OPCUA SDK - module -server

1,245 lines (968 loc) 105 kB
"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