node-opcua-client
Version:
pure nodejs OPCUA SDK - module client
977 lines (976 loc) • 48.6 kB
JavaScript
"use strict";
/**
* @module node-opcua-client-private
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.OPCUAClientImpl = void 0;
const node_crypto_1 = require("node:crypto");
const node_util_1 = require("node:util");
const chalk_1 = __importDefault(require("chalk"));
const node_opcua_assert_1 = require("node-opcua-assert");
const node_opcua_buffer_utils_1 = require("node-opcua-buffer-utils");
const node_opcua_client_dynamic_extension_object_1 = require("node-opcua-client-dynamic-extension-object");
const web_1 = require("node-opcua-crypto/web");
const node_opcua_data_model_1 = require("node-opcua-data-model");
const node_opcua_debug_1 = require("node-opcua-debug");
const node_opcua_hostname_1 = require("node-opcua-hostname");
const node_opcua_pseudo_session_1 = require("node-opcua-pseudo-session");
const node_opcua_secure_channel_1 = require("node-opcua-secure-channel");
const node_opcua_service_endpoints_1 = require("node-opcua-service-endpoints");
const node_opcua_service_secure_channel_1 = require("node-opcua-service-secure-channel");
const node_opcua_service_session_1 = require("node-opcua-service-session");
const node_opcua_status_code_1 = require("node-opcua-status-code");
const node_opcua_utils_1 = require("node-opcua-utils");
const opcua_client_1 = require("../opcua_client");
const client_base_impl_1 = require("./client_base_impl");
const client_session_impl_1 = require("./client_session_impl");
const reconnection_1 = require("./reconnection/reconnection");
const doDebug = (0, node_opcua_debug_1.checkDebugFlag)(__filename);
const debugLog = (0, node_opcua_debug_1.make_debugLog)(__filename);
const errorLog = (0, node_opcua_debug_1.make_errorLog)(__filename);
const warningLog = (0, node_opcua_debug_1.make_warningLog)(__filename);
function validateServerNonce(serverNonce) {
return !(serverNonce && serverNonce.length < 32) || (serverNonce && serverNonce.length === 0);
}
function verifyEndpointDescriptionMatches(_client, _responseServerEndpoints) {
// The Server returns its EndpointDescriptions in the response. Clients use this information to
// determine whether the list of EndpointDescriptions returned from the Discovery Endpoint matches
// the Endpoints that the Server has. If there is a difference then the Client shall close the
// Session and report an error.
// The Server returns all EndpointDescriptions for the serverUri
// specified by the Client in the request. The Client only verifies EndpointDescriptions with a
// transportProfileUri that matches the profileUri specified in the original GetEndpoints request.
// A Client may skip this check if the EndpointDescriptions were provided by a trusted source
// such as the Administrator.
// serverEndpoints:
// The Client shall verify this list with the list from a Discovery Endpoint if it used a Discovery Endpoint
// fetch to the EndpointDescriptions.
// ToDo
return true;
}
const hasDeprecatedSecurityPolicy = (userIdentity) => {
return (userIdentity.securityPolicyUri === node_opcua_secure_channel_1.SecurityPolicy.Basic128Rsa15 ||
userIdentity.securityPolicyUri === node_opcua_secure_channel_1.SecurityPolicy.Basic128);
};
const ordered = [
// obsolete
node_opcua_secure_channel_1.SecurityPolicy.Basic128Rsa15,
node_opcua_secure_channel_1.SecurityPolicy.Basic192Rsa15,
node_opcua_secure_channel_1.SecurityPolicy.Basic256,
node_opcua_secure_channel_1.SecurityPolicy.None,
node_opcua_secure_channel_1.SecurityPolicy.Basic128,
node_opcua_secure_channel_1.SecurityPolicy.Basic192,
node_opcua_secure_channel_1.SecurityPolicy.Basic256Rsa15,
node_opcua_secure_channel_1.SecurityPolicy.Basic256Sha256,
node_opcua_secure_channel_1.SecurityPolicy.Aes128_Sha256_RsaOaep,
node_opcua_secure_channel_1.SecurityPolicy.Aes256_Sha256_RsaPss
];
const _compareSecurityPolicy = (a, b) => {
if (a === b) {
return 0;
}
if (!a && b)
return 1;
if (a && !b)
return -1;
const rankA = ordered.indexOf(a);
const rankB = ordered.indexOf(b);
return rankB - rankA;
};
const compareSecurityPolicy = (a, b) => {
return _compareSecurityPolicy(a.securityPolicyUri, b.securityPolicyUri);
};
function findUserTokenPolicy(endpointDescription, userTokenType) {
endpointDescription.userIdentityTokens = endpointDescription.userIdentityTokens || [];
let r = endpointDescription.userIdentityTokens.filter((userIdentity) => userIdentity.tokenType === userTokenType);
if (r.length === 0) {
return null;
}
if (r.length > 1) {
// avoid Basic128Rsa15 & Basic128 encryption algorithm
// note: some servers (S7) sometime provides multiple policyId with various encryption algorithm
// when the connection is Encrypted.
// even though there is no need to further encrypt a password.
// Further more, Basic128Rsa15 & Basic128 encryption algorithm are flawed and not working any more
// with nodejs 21.11.1 onwards
r = r.filter((userIdentity) => !hasDeprecatedSecurityPolicy(userIdentity));
}
if (r.length > 1) {
if (endpointDescription.securityMode === node_opcua_service_secure_channel_1.MessageSecurityMode.SignAndEncrypt) {
// no encryption will do if available
const unencrypted = r.find((userIdentity) => userIdentity.securityPolicyUri === node_opcua_secure_channel_1.SecurityPolicy.None || !userIdentity.securityPolicyUri);
if (unencrypted)
return unencrypted;
}
// if not then use the strongest encryption,
r = r.sort(compareSecurityPolicy);
}
return r.length === 0 ? null : r[0];
}
function createAnonymousIdentityToken(context) {
const endpoint = context.endpoint;
const userTokenPolicy = findUserTokenPolicy(endpoint, node_opcua_service_endpoints_1.UserTokenType.Anonymous);
if (!userTokenPolicy) {
throw new Error("Cannot find ANONYMOUS user token policy in end point description");
}
return new node_opcua_service_session_1.AnonymousIdentityToken({ policyId: userTokenPolicy.policyId });
}
/**
*
* @param context
* @param certificate - the user certificate
* @param privateKey - the private key associated with the user certificate
*/
function createX509IdentityToken(context, certificate, privateKey) {
const endpoint = context.endpoint;
(0, node_opcua_assert_1.assert)(endpoint instanceof node_opcua_service_endpoints_1.EndpointDescription);
const userTokenPolicy = findUserTokenPolicy(endpoint, node_opcua_service_endpoints_1.UserTokenType.Certificate);
// c8 ignore next
if (!userTokenPolicy) {
throw new Error("Cannot find Certificate (X509) user token policy in end point description");
}
let securityPolicy = (0, node_opcua_secure_channel_1.fromURI)(userTokenPolicy.securityPolicyUri);
// if the security policy is not specified we use the session security policy
if (securityPolicy === node_opcua_secure_channel_1.SecurityPolicy.Invalid) {
securityPolicy = context.securityPolicy;
}
const userIdentityToken = new node_opcua_service_session_1.X509IdentityToken({
certificateData: certificate,
policyId: userTokenPolicy.policyId
});
const serverCertificate = context.serverCertificate;
(0, node_opcua_assert_1.assert)(serverCertificate instanceof Buffer);
const serverNonce = context.serverNonce || Buffer.alloc(0);
(0, node_opcua_assert_1.assert)(serverNonce instanceof Buffer);
// see Release 1.02 155 OPC Unified Architecture, Part 4
const cryptoFactory = (0, node_opcua_secure_channel_1.getCryptoFactory)(securityPolicy);
// c8 ignore next
if (!cryptoFactory) {
throw new Error(" Unsupported security Policy");
}
/**
* OPCUA Spec 1.04 - part 4
* page 28:
* 5.6.3.1
* ...
* If the token is an X509IdentityToken then the proof is a signature generated with private key
* associated with the Certificate. The data to sign is created by appending the last serverNonce to
* the **serverCertificate** specified in the CreateSession response. If a token includes a secret then it
* should be encrypted using the public key from the serverCertificate.
*
* page 155:
* Token Encryption and Proof of Possession
* 7.36.2.1 Overview
* The Client shall always prove possession of a UserIdentityToken when it passes it to the Server.
* Some tokens include a secret such as a password which the Server will accept as proof. In order
* to protect these secrets the Token may be encrypted before it is passed to the Server. Other types
* of tokens allow the Client to create a signature with the secret associated with the Token. In these
* cases, the Client proves possession of a UserIdentityToken by creating a signature with the secret
* and passing it to the Server
*
* page 159:
* 7.36.5 X509IdentityTokens
* The X509IdentityToken is used to pass an X.509 v3 Certificate which is issued by the user.
* This token shall always be accompanied by a Signature in the userTokenSignature parameter of
* ActivateSession if required by the SecurityPolicy. The Server should specify a SecurityPolicy for
* the UserTokenPolicy if the SecureChannel has a SecurityPolicy of None.
*/
// now create the proof of possession, by creating a signature
// The data to sign is created by appending the last serverNonce to the serverCertificate
// The signature generated with private key associated with the User Certificate
const userTokenSignature = (0, node_opcua_secure_channel_1.computeSignature)(serverCertificate, serverNonce, privateKey, securityPolicy);
return { userIdentityToken, userTokenSignature };
}
function createUserNameIdentityToken(session, userName, password) {
// assert(endpoint instanceof EndpointDescription);
(0, node_opcua_assert_1.assert)(userName === null || typeof userName === "string");
(0, node_opcua_assert_1.assert)(password === null || typeof password === "string");
const endpoint = session.endpoint;
(0, node_opcua_assert_1.assert)(endpoint instanceof node_opcua_service_endpoints_1.EndpointDescription);
/**
* OPC Unified Architecture 1.0.4: Part 4 155
* Each UserIdentityToken allowed by an Endpoint shall have a UserTokenPolicy specified in the
* EndpointDescription. The UserTokenPolicy specifies what SecurityPolicy to use when encrypting
* or signing. If this SecurityPolicy is omitted then the Client uses the SecurityPolicy in the
* EndpointDescription. If the matching SecurityPolicy is set to None then no encryption or signature
* is required.
*
*/
const userTokenPolicy = findUserTokenPolicy(endpoint, node_opcua_service_endpoints_1.UserTokenType.UserName);
// c8 ignore next
if (!userTokenPolicy) {
throw new Error("Cannot find USERNAME user token policy in end point description");
}
let securityPolicy = (0, node_opcua_secure_channel_1.fromURI)(userTokenPolicy.securityPolicyUri);
// if the security policy is not specified we use the session security policy
if (securityPolicy === node_opcua_secure_channel_1.SecurityPolicy.Invalid) {
securityPolicy = session.securityPolicy;
}
let identityToken;
let serverCertificate = session.serverCertificate;
// if server does not provide certificate use unencrypted password
if (!serverCertificate || serverCertificate.length === 0) {
identityToken = new node_opcua_service_session_1.UserNameIdentityToken({
encryptionAlgorithm: null,
password: Buffer.from(password, "utf-8"),
policyId: userTokenPolicy.policyId,
userName
});
return identityToken;
}
(0, node_opcua_assert_1.assert)(serverCertificate instanceof Buffer);
serverCertificate = (0, web_1.toPem)(serverCertificate, "CERTIFICATE");
const publicKey = (0, node_crypto_1.createPublicKey)((0, web_1.extractPublicKeyFromCertificateSync)(serverCertificate));
const serverNonce = session.serverNonce || Buffer.alloc(0);
(0, node_opcua_assert_1.assert)(serverNonce instanceof Buffer);
// If None is specified for the UserTokenPolicy and SecurityPolicy is None
// then the password only contains the UTF-8 encoded password.
// note: this means that password is sent in clear text to the server
// note: OPCUA specification discourages use of unencrypted password
// but some old OPCUA server may only provide this policy and we
// still have to support in the client?
if (securityPolicy === node_opcua_secure_channel_1.SecurityPolicy.None) {
identityToken = new node_opcua_service_session_1.UserNameIdentityToken({
encryptionAlgorithm: null,
password: Buffer.from(password, "utf-8"),
policyId: userTokenPolicy.policyId,
userName
});
return identityToken;
}
// see Release 1.02 155 OPC Unified Architecture, Part 4
const cryptoFactory = (0, node_opcua_secure_channel_1.getCryptoFactory)(securityPolicy);
// c8 ignore next
if (!cryptoFactory) {
throw new Error(` Unsupported security Policy ${securityPolicy.toString()}`);
}
identityToken = new node_opcua_service_session_1.UserNameIdentityToken({
encryptionAlgorithm: cryptoFactory.asymmetricEncryptionAlgorithm,
password: Buffer.from(password, "utf-8"),
policyId: userTokenPolicy.policyId,
userName
});
// now encrypt password as requested
const lenBuf = (0, node_opcua_buffer_utils_1.createFastUninitializedBuffer)(4);
lenBuf.writeUInt32LE(identityToken.password.length + serverNonce.length, 0);
const block = Buffer.concat([lenBuf, identityToken.password, serverNonce]);
identityToken.password = cryptoFactory.asymmetricEncrypt(block, publicKey);
return identityToken;
}
function _adjustRevisedSessionTimeout(revisedSessionTimeout, requestedTimeout) {
// Some old OPCUA Servers are known to report an invalid revisedSessionTimeout
// such as Siemens SimoCode Pro V.
// we need to adjust the value here, by guessing a sensible sessionTimeout value to use instead.
if (revisedSessionTimeout < 1e-10) {
warningLog(`the revisedSessionTimeout ${revisedSessionTimeout} reported by the server is inconsistent and has been adjusted back to requestedTimeout ${requestedTimeout}`);
return requestedTimeout;
}
if (revisedSessionTimeout < OPCUAClientImpl.minimumRevisedSessionTimeout) {
warningLog(`the revisedSessionTimeout ${revisedSessionTimeout} is smaller than the minimum timeout (OPCUAClientImpl.minimumRevisedSessionTimeout = ${OPCUAClientImpl.minimumRevisedSessionTimeout}) and has been clamped to this value`);
return OPCUAClientImpl.minimumRevisedSessionTimeout;
}
return revisedSessionTimeout;
}
class OPCUAClientImpl extends client_base_impl_1.ClientBaseImpl {
static minimumRevisedSessionTimeout = 100.0;
_retryCreateSessionTimer;
static create(options) {
return new OPCUAClientImpl(options);
}
endpoint;
endpointMustExist;
requestedSessionTimeout;
___sessionName_counter;
serverUri;
clientNonce;
dataTypeExtractStrategy;
constructor(options) {
options = options || {};
super(options);
this.dataTypeExtractStrategy = options.dataTypeExtractStrategy || node_opcua_client_dynamic_extension_object_1.DataTypeExtractStrategy.Auto;
// @property endpointMustExist {Boolean}
// if set to true , create Session will only accept connection from server which endpoint_url has been reported
// by GetEndpointsRequest.
// By default, the client is strict.
if (Object.hasOwn(options, "endpoint_must_exist")) {
if (Object.hasOwn(options, "endpointMustExist")) {
throw new Error("endpoint_must_exist is deprecated! you must now use endpointMustExist instead of endpoint_must_exist ");
}
warningLog("Warning: endpoint_must_exist is now deprecated, use endpointMustExist instead");
options.endpointMustExist = options.endpoint_must_exist;
}
this.endpointMustExist = (0, node_opcua_utils_1.isNullOrUndefined)(options.endpointMustExist) ? true : !!options.endpointMustExist;
this.requestedSessionTimeout = options.requestedSessionTimeout || 60000; // 1 minute
this.___sessionName_counter = 0;
this.endpoint = undefined;
}
/**
* @internal
* @param args
*
*/
// biome-ignore lint/suspicious/noExplicitAny: overload implementation
createSession(...args) {
if (args.length === 1) {
return this.createSession({ type: node_opcua_service_endpoints_1.UserTokenType.Anonymous }, args[0]);
}
const userIdentityInfo = args[0] || { type: node_opcua_service_endpoints_1.UserTokenType.Anonymous };
const callback = args[1];
(0, node_opcua_assert_1.assert)(typeof callback === "function");
this._createSession((err, session) => {
if (err) {
callback(err);
}
else {
/* c8 ignore next */
if (!session) {
return callback(new Error("Internal Error"));
}
this._addSession(session);
this._activateSession(session, userIdentityInfo, (err1, session2) => {
if (err1) {
session
.close(true)
.then(() => {
callback(err1, null);
})
.catch((err2) => {
err2;
callback(err1, null);
});
}
else {
callback(null, session2);
}
});
}
});
}
// biome-ignore lint/suspicious/noExplicitAny: overload implementation
createSession2(...args) {
if (args.length === 1) {
return this.createSession2({ type: node_opcua_service_endpoints_1.UserTokenType.Anonymous }, args[0]);
}
const userIdentityInfo = args[0];
const callback = args[1];
if (!this._secureChannel) {
// we do not have a connection anymore
return callback(new Error("Connection is closed"));
}
if (this._internalState === "disconnected" || this._internalState === "disconnecting") {
return callback(new Error(`disconnecting`));
}
return this.createSession(args[0], (err, session) => {
if (err?.message.match(/BadTooManySessions/)) {
const delayToRetry = 5; // seconds
errorLog(`TooManySession .... we need to retry later ... in ${delayToRetry} secondes ${this._internalState}`);
this._retryCreateSessionTimer = setTimeout(() => {
errorLog(`TooManySession .... now retrying (${this._internalState})`);
this.createSession2(userIdentityInfo, callback);
}, delayToRetry * 1000);
return;
}
callback(err, session);
});
}
// biome-ignore lint/suspicious/noExplicitAny: overload implementation
changeSessionIdentity(...args) {
warningLog("[NODE-OPCUA-W34] OPCUAClient.changeSessionIdentity(session,userIdentity) is deprecated use ClientSession.changeUser(userIdentity) instead");
const session = args[0];
const userIdentityInfo = args[1];
const callback = args[2];
(0, node_opcua_assert_1.assert)(typeof callback === "function");
session.changeUser(userIdentityInfo, callback);
}
closeSession(session, deleteSubscriptions, callback) {
if (this._retryCreateSessionTimer) {
clearTimeout(this._retryCreateSessionTimer);
this._retryCreateSessionTimer = undefined;
}
super.closeSession(session, deleteSubscriptions, callback);
}
toString() {
let str = client_base_impl_1.ClientBaseImpl.prototype.toString.call(this);
str += ` requestedSessionTimeout....... ${this.requestedSessionTimeout}\n`;
str += ` endpointUrl................... ${this.endpointUrl}\n`;
str += ` serverUri..................... ${this.serverUri}\n`;
return str;
}
/**
*
* @example
*
* ```javascript
*
* const session = await OPCUAClient.createSession(endpointUrl);
* const dataValue = await session.read({ nodeId, attributeId: AttributeIds.Value });
* await session.close();
*
* ```
* @stability experimental
*
* @param endpointUrl
* @param userIdentity
* @returns session
*
*
* const create
*/
// biome-ignore lint/suspicious/useAdjacentOverloadSignatures: static vs instance method
static async createSession(endpointUrl, userIdentity, clientOptions) {
const client = opcua_client_1.OPCUAClient.create(clientOptions || {});
await client.connect(endpointUrl);
const session = await client.createSession2(userIdentity);
// biome-ignore lint/suspicious/noExplicitAny: monkey patch close
const oldClose = session.close;
// biome-ignore lint/suspicious/noExplicitAny: monkey patch close
session.close = (0, thenify_ex_1.withCallback)((...args) => {
if (args.length === 1) {
return session.close(true, args[0]);
}
const deleteSubscriptions = args[0];
const callback = args[1];
session.close = oldClose;
oldClose.call(session, deleteSubscriptions, (_err) => {
client.disconnect((err) => {
callback(err);
});
});
});
return session;
}
/**
*
* @param connectionPoint
* @param func
* @returns
*/
async withSessionAsync(connectionPoint, func) {
(0, node_opcua_assert_1.assert)(typeof func === "function");
(0, node_opcua_assert_1.assert)(func.length === 1, "expecting a single argument in func");
const endpointUrl = typeof connectionPoint === "string" ? connectionPoint : connectionPoint.endpointUrl;
const userIdentity = typeof connectionPoint === "string" ? { type: node_opcua_service_endpoints_1.UserTokenType.Anonymous } : connectionPoint.userIdentity;
this.on("backoff", (count, delay) => {
warningLog("cannot connect to ", endpointUrl, `attempt #${count}`, " retrying in ", delay);
});
await this.connect(endpointUrl);
try {
const session = await this.createSession2(userIdentity);
let result;
// always need this
await (0, node_opcua_pseudo_session_1.readNamespaceArray)(session);
try {
result = await func(session);
return result;
}
catch (err) {
errorLog(err);
throw err;
}
finally {
await session.close();
}
}
catch (err) {
errorLog(err.message);
throw err;
}
finally {
await this.disconnect();
}
}
async withSubscriptionAsync(connectionPoint, parameters, func) {
return await this.withSessionAsync(connectionPoint, async (session) => {
(0, node_opcua_assert_1.assert)(session, " session must exist");
const client1 = this;
if (client1.beforeSubscriptionRecreate) {
await client1.beforeSubscriptionRecreate(session);
}
const subscription = await session.createSubscription2(parameters);
try {
const result = await func(session, subscription);
return result;
}
catch (err) {
errorLog("withSubscriptionAsync inner function failed ", err.message);
throw err;
}
finally {
await subscription.terminate();
}
});
}
// biome-ignore lint/suspicious/noExplicitAny: overload implementation
reactivateSession(session, callback) {
const internalSession = session;
(0, node_opcua_assert_1.assert)(typeof callback === "function");
if (!this._secureChannel) {
return callback?.(new Error(" client must be connected first"));
}
// c8 ignore next
if (!this.__resolveEndPoint() || !this.endpoint) {
return callback?.(new Error(" End point must exist " +
this._secureChannel?.endpointUrl +
" securityMode = " +
node_opcua_service_secure_channel_1.MessageSecurityMode[this.securityMode] +
" securityPolicy = " +
this.securityPolicy));
}
(0, node_opcua_assert_1.assert)(!internalSession._client || (0, node_opcua_utils_1.matchUri)(internalSession._client.endpointUrl, this.endpointUrl), "cannot reactivateSession on a different endpoint");
const old_client = internalSession._client;
debugLog("OPCUAClientImpl#reactivateSession");
this._activateSession(internalSession, internalSession.userIdentityInfo, (err /*, newSession?: ClientSessionImpl*/) => {
if (!err) {
if (old_client !== this) {
// remove session from old client:
if (old_client) {
old_client._removeSession(internalSession);
(0, node_opcua_assert_1.assert)(old_client._sessions.indexOf(internalSession) === -1);
}
this._addSession(internalSession);
(0, node_opcua_assert_1.assert)(internalSession._client === this);
(0, node_opcua_assert_1.assert)(!internalSession._closed, "session should not vbe closed");
(0, node_opcua_assert_1.assert)(this._sessions.indexOf(internalSession) !== -1);
}
callback?.();
}
else {
// c8 ignore next
if (doDebug) {
debugLog(chalk_1.default.red.bgWhite("reactivateSession has failed !"), err.message);
}
callback?.(err);
}
});
}
/**
* @internal
* @private
*/
_on_connection_reestablished(callback) {
super._on_connection_reestablished(( /*err?: Error*/) => {
(0, reconnection_1.repair_client_sessions)(this, callback);
});
}
/**
*
* @internal
* @private
*/
#createSession_step3(session, callback) {
(0, node_opcua_assert_1.assert)(typeof callback === "function");
(0, node_opcua_assert_1.assert)(this.serverUri !== undefined, ` must have a valid server URI ${this.serverUri}`);
(0, node_opcua_assert_1.assert)(this.endpointUrl !== undefined, " must have a valid server endpointUrl");
(0, node_opcua_assert_1.assert)(this.endpoint);
// c8 ignore next
if (!this._secureChannel) {
callback(new Error("Invalid channel"));
return;
}
const applicationUri = this._getApplicationUri();
const applicationDescription = {
applicationName: new node_opcua_data_model_1.LocalizedText({ text: this.applicationName, locale: null }),
applicationType: node_opcua_service_endpoints_1.ApplicationType.Client,
applicationUri,
discoveryProfileUri: undefined,
discoveryUrls: [],
gatewayServerUri: undefined,
productUri: "NodeOPCUA-Client"
};
// note : do not confuse CreateSessionRequest.clientNonce with OpenSecureChannelRequest.clientNonce
// which are two different nonce, with different size (although they share the same name )
this.clientNonce = (0, node_crypto_1.randomBytes)(32);
// recycle session name if already exists
const sessionName = session.name;
const request = new node_opcua_service_session_1.CreateSessionRequest({
clientCertificate: this.getCertificate(),
clientDescription: applicationDescription,
clientNonce: this.clientNonce,
endpointUrl: this.endpointUrl,
maxResponseMessageSize: 800000,
requestedSessionTimeout: this.requestedSessionTimeout,
serverUri: this.serverUri,
sessionName
});
// a client Nonce must be provided if security mode is set
(0, node_opcua_assert_1.assert)(this._secureChannel.securityMode === node_opcua_service_secure_channel_1.MessageSecurityMode.None || request.clientNonce !== null);
this.performMessageTransaction(request, (err, response) => {
/* c8 ignore next */
if (err) {
debugLog("__createSession_step3 has failed", err.message);
return callback(err);
// // we could have an invalid state here or a connection error
// errorLog("error: ", err.message, " retrying in ... 5 secondes");
// setTimeout(() => {
// errorLog(" .... now retrying");
// this.__createSession_step3(session, callback);
// }, 5 * 1000);
// return;
}
/* c8 ignore next */
if (!response || !(response instanceof node_opcua_service_session_1.CreateSessionResponse)) {
return callback(new Error("internal error"));
}
if (response.responseHeader.serviceResult === node_opcua_status_code_1.StatusCodes.BadTooManySessions) {
return callback(new Error(response.responseHeader.serviceResult.toString()));
}
if (response.responseHeader.serviceResult !== node_opcua_status_code_1.StatusCodes.Good) {
err = new Error(`Error ${response.responseHeader.serviceResult.name} ${response.responseHeader.serviceResult.description}`);
return callback(err);
}
// c8 ignore next
if (!validateServerNonce(response.serverNonce)) {
return callback(new Error("Invalid server Nonce"));
}
// todo: verify SignedSoftwareCertificates and response.serverSignature
session.name = request.sessionName || "";
session.sessionId = response.sessionId;
session.authenticationToken = response.authenticationToken;
session.timeout = _adjustRevisedSessionTimeout(response.revisedSessionTimeout, this.requestedSessionTimeout);
session.serverNonce = response.serverNonce;
session.serverCertificate = response.serverCertificate;
session.serverSignature = response.serverSignature;
debugLog("revised session timeout = ", session.timeout, response.revisedSessionTimeout);
response.serverEndpoints = response.serverEndpoints || [];
if (!verifyEndpointDescriptionMatches(this, response.serverEndpoints)) {
errorLog("Endpoint description previously retrieved with GetEndpointsDescription");
errorLog("CreateSessionResponse.serverEndpoints= ");
errorLog(response.serverEndpoints);
return callback(new Error("Invalid endpoint descriptions Found"));
}
// this._serverEndpoints = response.serverEndpoints;
session.serverEndpoints = response.serverEndpoints;
callback(null, session);
});
}
/**
*
* @internal
* @private
*/
__createSession_step2(session, callback) {
(0, node_util_1.callbackify)(node_opcua_hostname_1.extractFullyQualifiedDomainName)(() => {
this.#createSession_step3(session, callback);
});
}
/**
* @internal
* @private
*/
_activateSession(session, userIdentityInfo, callback) {
// see OPCUA Part 4 - $7.35
(0, node_opcua_assert_1.assert)(typeof callback === "function");
// c8 ignore next
if (!this._secureChannel) {
callback(new Error(" No secure channel"));
return;
}
const serverCertificate = session.serverCertificate;
// If the securityPolicyUri is None and none of the UserTokenPolicies requires encryption,
// the Client shall ignore the ApplicationInstanceCertificate (serverCertificate)
(0, node_opcua_assert_1.assert)(serverCertificate === null || serverCertificate instanceof Buffer);
const serverNonce = session.serverNonce;
(0, node_opcua_assert_1.assert)(!serverNonce || serverNonce instanceof Buffer);
// make sure session is attached to this client
const _old_client = session._client;
session._client = this;
const context = {
endpoint: this.endpoint,
securityPolicy: this._secureChannel.securityPolicy,
serverCertificate,
serverNonce: serverNonce // please check this !
};
this.createUserIdentityToken(context, userIdentityInfo, (err, data) => {
if (err) {
session._client = _old_client;
return callback(err);
}
data = data;
const userIdentityToken = data.userIdentityToken;
const userTokenSignature = data.userTokenSignature;
// TODO. fill the ActivateSessionRequest
// see 5.6.3.2 Parameters OPC Unified Architecture, Part 4 30 Release 1.02
const request = new node_opcua_service_session_1.ActivateSessionRequest({
// This is a signature generated with the private key associated with the
// clientCertificate. The SignatureAlgorithm shall be the AsymmetricSignatureAlgorithm
// specified in the SecurityPolicy for the Endpoint. The SignatureData type is defined in 7.30.
clientSignature: this.computeClientSignature(this._secureChannel, serverCertificate, serverNonce) || undefined,
// These are the SoftwareCertificates which have been issued to the Client application.
// The productUri contained in the SoftwareCertificates shall match the productUri in the
// ApplicationDescription passed by the Client in the CreateSession requests. Certificates without
// matching productUri should be ignored. Servers may reject connections from Clients if they are
// not satisfied with the SoftwareCertificates provided by the Client.
// This parameter only needs to be specified in the first ActivateSession request
// after CreateSession.
// It shall always be omitted if the maxRequestMessageSize returned from the Server in the
// CreateSession response is less than one megabyte.
// The SignedSoftwareCertificate type is defined in 7.31.
clientSoftwareCertificates: [],
// List of locale ids in priority order for localized strings. The first LocaleId in the list
// has the highest priority. If the Server returns a localized string to the Client, the Server
// shall return the translation with the highest priority that it can. If it does not have a
// translation for any of the locales identified in this list, then it shall return the string
// value that it has and include the locale id with the string.
// See Part 3 for more detail on locale ids. If the Client fails to specify at least one locale id,
// the Server shall use any that it has.
// This parameter only needs to be specified during the first call to ActivateSession during
// a single application Session. If it is not specified the Server shall keep using the current
// localeIds for the Session.
localeIds: [],
// The credentials of the user associated with the Client application. The Server uses these
// credentials to determine whether the Client should be allowed to activate a Session and what
// resources the Client has access to during this Session. The UserIdentityToken is an extensible
// parameter type defined in 7.35.
// The EndpointDescription specifies what UserIdentityTokens the Server shall accept.
userIdentityToken,
// If the Client specified a user identity token that supports digital signatures,
// then it shall create a signature and pass it as this parameter. Otherwise the parameter
// is omitted.
// The SignatureAlgorithm depends on the identity token type.
userTokenSignature
});
request.requestHeader.authenticationToken = session.authenticationToken;
session.lastRequestSentTime = new Date();
this.performMessageTransaction(request, (err1, response) => {
if (!err1 && response && response.responseHeader.serviceResult === node_opcua_status_code_1.StatusCodes.Good) {
/* c8 ignore next */
if (!(response instanceof node_opcua_service_session_1.ActivateSessionResponse)) {
return callback(new Error("Internal Error"));
}
if (!validateServerNonce(response.serverNonce)) {
return callback(new Error("Invalid server Nonce"));
}
session._client = this;
session.serverNonce = response.serverNonce;
session.lastResponseReceivedTime = new Date();
if (this.keepSessionAlive) {
session.startKeepAliveManager(this.keepAliveInterval);
}
session.userIdentityInfo = userIdentityInfo;
return callback(null, session);
}
else {
// restore client
session._client = _old_client;
/* c8 ignore next */
if (!err1 && response) {
err1 = new Error(response.responseHeader.serviceResult.toString());
}
session._client = _old_client;
return callback(err1);
}
});
});
}
/**
*
* @private
*/
_nextSessionName() {
if (!this.___sessionName_counter) {
this.___sessionName_counter = 0;
}
this.___sessionName_counter += 1;
return this.clientName + this.___sessionName_counter;
}
/**
*
* @private
*/
_getApplicationUri() {
const certificate = this.getCertificate();
let applicationUri;
if (certificate) {
const e = (0, web_1.exploreCertificate)(certificate);
if (e.tbsCertificate.extensions?.subjectAltName?.uniformResourceIdentifier) {
applicationUri = e.tbsCertificate.extensions.subjectAltName.uniformResourceIdentifier[0];
}
else {
errorLog("Certificate has no extensions.subjectAltName.uniformResourceIdentifier, ");
errorLog((0, web_1.toPem)(certificate, "CERTIFICATE"));
applicationUri = this._getBuiltApplicationUri();
}
}
else {
applicationUri = this._getBuiltApplicationUri();
}
return applicationUri;
}
/**
*
* @private
*/
__resolveEndPoint() {
this.securityPolicy = this.securityPolicy || node_opcua_secure_channel_1.SecurityPolicy.None;
let endpoint = this.findEndpoint(this._secureChannel?.endpointUrl || "", this.securityMode, this.securityPolicy);
this.endpoint = endpoint;
// this is explained here : see OPCUA Part 4 Version 1.02 $5.4.1 page 12:
// A Client shall verify the HostName specified in the Server Certificate is the same as the HostName
// contained in the endpointUrl provided in the EndpointDescription. If there is a difference then the
// Client shall report the difference and may close the SecureChannel.
if (!this.endpoint) {
if (this.endpointMustExist) {
warningLog("OPCUAClientImpl#endpointMustExist = true and endpoint with url ", this._secureChannel?.endpointUrl, " cannot be found");
const infos = this._serverEndpoints.map((endpoint) => `${endpoint.endpointUrl} ${node_opcua_service_secure_channel_1.MessageSecurityMode[endpoint.securityMode]}, ${endpoint.securityPolicyUri} `);
warningLog("Valid endpoints are ");
warningLog(` ${infos.join("\n ")}`);
return false;
}
else {
// fallback :
// our strategy is to take the first server_end_point that match the security settings
// ( is this really OK ?)
// this will permit us to access a OPCUA Server using it's IP address instead of its hostname
endpoint = this.findEndpointForSecurity(this.securityMode, this.securityPolicy);
if (!endpoint) {
return false;
}
this.endpoint = endpoint;
}
}
return true;
}
/**
*
* @private
*/
_createSession(callback) {
(0, node_opcua_assert_1.assert)(typeof callback === "function");
(0, node_opcua_assert_1.assert)(this._secureChannel);
if (!this.__resolveEndPoint() || !this.endpoint) {
/* c8 ignore next */
if (this._serverEndpoints) {
warningLog("server endpoints =", this._serverEndpoints
.map((endpoint) => endpoint.endpointUrl +
" " +
node_opcua_service_secure_channel_1.MessageSecurityMode[endpoint.securityMode] +
" " +
endpoint.securityPolicyUri +
" " +
endpoint.userIdentityTokens?.map((u) => node_opcua_service_endpoints_1.UserTokenType[u.tokenType]).join(","))
.join("\n"));
}
return callback(new Error(" End point must exist " +
this._secureChannel?.endpointUrl +
" securityMode = " +
node_opcua_service_secure_channel_1.MessageSecurityMode[this.securityMode] +
" securityPolicy = " +
this.securityPolicy));
}
this.serverUri = this.endpoint.server.applicationUri || "invalid application uri";
this.endpointUrl = this._secureChannel?.endpointUrl || "";
const session = new client_session_impl_1.ClientSessionImpl(this);
session.name = this._nextSessionName();
this.__createSession_step2(session, callback);
}
/**
*
* @private
*/
computeClientSignature(channel, serverCertificate, serverNonce) {
return (0, node_opcua_secure_channel_1.computeSignature)(serverCertificate, serverNonce || Buffer.alloc(0), this.getPrivateKey(), channel.securityPolicy);
}
/**
*
* @private
*/
createUserIdentityToken(context, userIdentityInfo, callback) {
// biome-ignore lint/suspicious/noExplicitAny: user provided object that needs soft type coercion
function coerceUserIdentityInfo(identityInfo) {
if (!identityInfo) {
return { type: node_opcua_service_endpoints_1.UserTokenType.Anonymous };
}
if (Object.hasOwn(identityInfo, "type")) {
return identityInfo;
}
if (Object.hasOwn(identityInfo, "userName")) {
identityInfo.type = node_opcua_service_endpoints_1.UserTokenType.UserName;
return identityInfo;
}
if (Object.hasOwn(identityInfo, "certificateData")) {
identityInfo.type = node_opcua_service_endpoints_1.UserTokenType.Certificate;
return identityInfo;
}
identityInfo.type = node_opcua_service_endpoints_1.UserTokenType.Anonymous;
return identityInfo;
}
userIdentityInfo = coerceUserIdentityInfo(userIdentityInfo);
(0, node_opcua_assert_1.assert)(typeof callback === "function");
if (null === userIdentityInfo) {
return callback(null, {
userIdentityToken: null,
userTokenSignature: {}
});
}
let userIdentityToken;
let userTokenSignature = {
algorithm: undefined,
signature: undefined
};
try {
switch (userIdentityInfo.type) {
case node_opcua_service_endpoints_1.UserTokenType.Anonymous:
userIdentityToken = createAnonymousIdentityToken(context);
break;
case node_opcua_service_endpoints_1.UserTokenType.UserName: {
const userName = userIdentityInfo.userName || "";
const password = userIdentityInfo.password || "";
userIdentityToken = createUserNameIdentityToken(context, userName, password);
break;
}
case node_opcua_service_endpoints_1.UserTokenType.Certificate: {
const certificate = userIdentityInfo.certificateData;
const privateKey = (0, web_1.makePrivateKeyFromPem)(userIdentityInfo.privateKey);
({ userIdentityToken, userTokenSignature } = createX509IdentityToken(context, certificate, privateKey));
break;
}
default:
debugLog(" userIdentityInfo = ", userIdentityInfo);
return callback(new Error("CLIENT: Invalid userIdentityInfo"));
}
}
catch (err) {
if (typeof err === "string") {
return callback(new Error(`Create identity token failed ${userIdentityInfo.type} ${err}`));
}
return callback(err);
}
return callback(null, { userIdentityToken, userTokenSignature });
}
}
exports.OPCUAClientImpl = OPCUAClientImpl;
// tslint:disable:no-var-requires
// tslint:disable:max-line-length
const thenify_ex_1 = require("thenify-ex");
/**
*
* @example
* // create a anonymous session
* const session = await client.createSession();
*
* @example
* // create a session with a userName and password
* const userIdentityInfo = {
* type: UserTokenType.UserName,
* userName: "JoeDoe",
* password:"secret"
* };
* const session = client.createSession(userIdentityInfo);
*
*/
OPCUAClientImpl.prototype.createSession = (0, thenify_ex_1.withCallback)(OPCUAClientImpl.prototype.createSession);
OPCUAClientImpl.prototype.createSession2 = (0, thenify_ex_1.withCallback)(OPCUAClientImpl.prototype.createSession2);
/**
*/
OPCUAClientImpl.prototype.changeSessionIdentity = (0, thenify_ex_1.withCallback)(OPCUAClientImpl.prototype.changeSessionIdentity);
/**
* @example
* const session = await client.createSession();
* await client.closeSession(session);
*/
OPCUAClientImpl.prototype.closeSession = (0, thenify_ex_1.withCallback)(OPCUAClientImpl.prototype.closeSession);
OPCUAClientImpl.prototype.reactivateSession = (0, thenify_ex_1.withCallback)(OPCUAClientImpl.prototype.reactivateSession);
//# sourceMappingURL=opcua_client_impl.js.map