APIs

Show:
"use strict";
/**
 * @module opcua.client
 */

const util = require("util");
const EventEmitter = require("events").EventEmitter;
const fs = require("fs");
const path = require("path");
const async = require("async");
const _ = require("underscore");
const assert = require("node-opcua-assert").assert;
const once = require("once");
const delayed = require("delayed");


const endpoints_service = require("node-opcua-service-endpoints");

const GetEndpointsRequest = endpoints_service.GetEndpointsRequest;
const GetEndpointsResponse = endpoints_service.GetEndpointsResponse;
const MessageSecurityMode = require("node-opcua-service-secure-channel").MessageSecurityMode;
const toURI = require("node-opcua-secure-channel").toURI;
const SecurityPolicy = require("node-opcua-secure-channel").SecurityPolicy;

const debugLog = require("node-opcua-debug").make_debugLog(__filename);
const doDebug = require("node-opcua-debug").checkDebugFlag(__filename);

const OPCUASecureObject = require("node-opcua-common").OPCUASecureObject;

const ClientSecureChannelLayer = require("node-opcua-secure-channel/src/client/client_secure_channel_layer").ClientSecureChannelLayer;
const ClientSession = require("./client_session").ClientSession;


const defaultConnectionStrategy = {
    maxRetry: 10000000, // almost infinite
    initialDelay: 1000,
    maxDelay: 20*1000,// 20 seconds
    randomisationFactor: 0.1
};

/**
 * @class OPCUAClientBase
 * @extends EventEmitter
 * @param options
 * @param options.defaultSecureTokenLifetime {Number} default secure token lifetime in ms
 * @param [options.securityMode=MessageSecurityMode.None] {MessageSecurityMode} the default security mode.
 * @param [options.securityPolicy =SecurityPolicy.NONE] {SecurityPolicy} the security mode.
 * @param [options.serverCertificate=null] {Certificate} the server certificate.
 * @param [options.certificateFile="certificates/client_selfsigned_cert_1024.pem"] {String} client certificate pem file.
 * @param [options.privateKeyFile="certificates/client_key_1024.pem"] {String} client private key pem file.
 * @param [options.connectionStrategy] {Object}
 * @param [options.keepSessionAlive=false]{Boolean}
 * @param [options.tokenRenewalInterval =0] {Number} if not specify or set to 0 , token  renewal will happen around 75% of the defaultSecureTokenLifetime
 * @param [options.keepPendingSessionsOnDisconnect=false] if set to true, pending session will not be automatically closed *
 *                                                        when disconnect is called
 * @param [options.clientName=""] the client Name
 * @constructor
 */
function OPCUAClientBase(options) {


    options = options || {};

    EventEmitter.call(this);

    this.clientName = options.clientName || "Session";

    options.certificateFile = options.certificateFile || path.join(__dirname, "../certificates/client_selfsigned_cert_1024.pem");

    options.privateKeyFile = options.privateKeyFile || path.join(__dirname, "../certificates/PKI/own/private/private_key.pem");

    // istanbul ignore next
    if (!fs.existsSync(options.certificateFile)) {
        throw new Error(" cannot locate certificate file " + options.certificateFile);
    }

    // istanbul ignore next
    if (!fs.existsSync(options.privateKeyFile)) {
        throw new Error(" cannot locate private key file " + options.privateKeyFile);
    }

    OPCUASecureObject.call(this, options);

    // must be ZERO with Spec 1.0.2
    this.protocolVersion = 0;

    this._sessions = [];


    this._server_endpoints = [];
    this._secureChannel = null;

    this.defaultSecureTokenLifetime = options.defaultSecureTokenLifetime || 600000;
    this.tokenRenewalInterval = options.tokenRenewalInterval || 0;
    assert(_.isFinite(this.tokenRenewalInterval) && this.tokenRenewalInterval >= 0);
    /**
     * @property securityMode
     * @type MessageSecurityMode
     */
    this.securityMode = options.securityMode || MessageSecurityMode.NONE;
    this.securityMode = MessageSecurityMode.get(this.securityMode);

    /**
     * @property securityPolicy
     * @type {SecurityPolicy}
     */
    this.securityPolicy = options.securityPolicy || toURI("None");
    this.securityPolicy = SecurityPolicy.get(this.securityPolicy);

    /**
     * @property serverCertificate
     * @type {Certificate}
     */
    this.serverCertificate = options.serverCertificate || null;

    /**
     * true if session shall periodically probe the server to keep the session alive and prevent timeout
     * @property keepSessionAlive
     * @type {boolean}
     */
    this.keepSessionAlive = _.isBoolean(options.keepSessionAlive) ? options.keepSessionAlive : false;

    // statistics...
    this._byteRead = 0;
    this._byteWritten = 0;
    this._transactionsPerformed = 0;
    this._timedOutRequestCount = 0;

    /**
     * @property connectionStrategy
     * @type {options.connectionStrategy|{maxRetry, initialDelay, maxDelay, randomisationFactor}|*|{maxRetry: number, initialDelay: number, maxDelay: number, randomisationFactor: number}}
     */
    this.connectionStrategy = options.connectionStrategy || defaultConnectionStrategy;

    /***
     * @property keepPendingSessionsOnDisconnect²
     * @type {boolean}
     */
    this.keepPendingSessionsOnDisconnect = options.keepPendingSessionsOnDisconnect || false;
}

util.inherits(OPCUAClientBase, EventEmitter);

OPCUAClientBase.prototype.getPrivateKey = OPCUASecureObject.prototype.getPrivateKey;
OPCUAClientBase.prototype.getCertificate = OPCUASecureObject.prototype.getCertificate;
OPCUAClientBase.prototype.getCertificateChain = OPCUASecureObject.prototype.getCertificateChain;

const ObjectRegistry = require("node-opcua-object-registry").ObjectRegistry;
OPCUAClientBase.registry = new ObjectRegistry();

/**
 * is true when the client has already requested the server end points.
 * @property knowsServerEndpoint
 * @type boolean
 */
OPCUAClientBase.prototype.__defineGetter__("knowsServerEndpoint", function () {
    const self = this;
    return (self._server_endpoints && self._server_endpoints.length > 0);
});

OPCUAClientBase.prototype._destroy_secure_channel = function () {


    const self = this;
    if (self._secureChannel) {


        if (doDebug) {
            debugLog(" DESTROYING SECURE CHANNEL ", self._secureChannel.isTransactionInProgress());
        }
        // keep accumulated statistics
        self._byteWritten += self._secureChannel.bytesWritten;
        self._byteRead += self._secureChannel.bytesRead;
        self._transactionsPerformed += self._secureChannel.transactionsPerformed;
        self._timedOutRequestCount += self._secureChannel.timedOutRequestCount;


        self._secureChannel.dispose();

        self._secureChannel.removeAllListeners();
        self._secureChannel = null;

        if (doDebug) {
            debugLog("byteWritten  = ",self._byteWritten );
            debugLog("byteRead     = ",self._byteRead);
        }
    }
};


function __findEndpoint(endpointUrl, params, callback) {

    const securityMode = params.securityMode;
    const securityPolicy = params.securityPolicy;

    const options = {
        connectionStrategy: params.connectionStrategy,
        endpoint_must_exist: false,
        certificateFile: params.certificateFile,
        privateKeyFile: params.privateKeyFile,
		applicationName: params.applicationName
    };

    const client = new OPCUAClientBase(options);

    let selected_endpoint = null;
    const all_endpoints = null;
    const tasks = [
        function (callback) {
            client.on("backoff", function () {
                console.log("finding Endpoint => reconnecting ");
            });
            client.connect(endpointUrl, function (err) {
                if (err) {
                    console.log("Fail to connect to server ", endpointUrl, " to collect certificate server");
                }
                return callback(err);
            });
        },
        function (callback) {
            client.getEndpoints(function (err, endpoints) {

                if (!err) {
                    endpoints.forEach(function (endpoint, i) {
                        if (endpoint.securityMode === securityMode && endpoint.securityPolicyUri === securityPolicy.value) {
                            selected_endpoint = endpoint; // found it
                        }
                    });
                }
                callback(err);
            });
        },
        function (callback) {
            client.disconnect(callback);
        }
    ];

    async.series(tasks, function (err) {

        if (err) {
            return callback(err);
        }

        if (!selected_endpoint) {
            callback(new Error(" Cannot find an Endpoint matching " +
                " security mode: " + securityMode.toString() +
                " policy: " + securityPolicy.toString()));
        }

        const result = {
            selectedEndpoint: selected_endpoint,
            endpoints: all_endpoints
        };
        callback(null, result);
    });
}


/**
 * @property isReconnecting
 * @type {Boolean} true if the client is trying to reconnect to the server after a connection break.
 */
OPCUAClientBase.prototype.__defineGetter__("isReconnecting", function () {

    const self = this;
    return !!(self._secureChannel && self._secureChannel.isConnecting);
});

OPCUAClientBase.prototype._cancel_reconnection = function (callback) {

    const self = this;

    // istanbul ignore next
    if (!self._secureChannel) {
        return callback(null); // nothing to do
    }
    self._secureChannel.abortConnection(function (err) {
        self._secureChannel = null;
        callback();
    });
};

OPCUAClientBase.prototype._recreate_secure_channel = function (callback) {

    debugLog("_recreate_secure_channel...");

    const self = this;
    assert(_.isFunction(callback));

    if (!self.knowsServerEndpoint) {
        console.log("Cannot reconnect , server endpoint is unknown");
        return callback(new Error("Cannot reconnect, server endpoint is unknown"));
    }
    assert(self.knowsServerEndpoint);

    assert(!self.isReconnecting);

    /**
     * notifies the observer that the OPCUA is now trying to reestablish the connection
     * after having received a connection break...
     * @event start_reconnection
     *
     */
    self.emit("start_reconnection"); // send after callback

    // create a secure channel
    // a new secure channel must be established
    setImmediate(function () {

        self._destroy_secure_channel();

        assert(!self._secureChannel);

        self._internal_create_secure_channel(function (err) {

            if (err) {
                debugLog("OPCUAClientBase: cannot reconnect ..".bgWhite.red);
            } else {
                assert(self._secureChannel, "expecting a secureChannel here ");
                // a new channel has be created and a new connection is established
                debugLog("OPCUAClientBase:  RECONNECTED                                       !!!".bgWhite.red)
            }

            callback(err);

            /**
             * notify the observers that the reconnection process has been completed
             * @event after_reconnection
             * @param err
             */
            self.emit("after_reconnection", err); // send after callback

        });
    });
};

function _verify_serverCertificate(serverCertificate, callback) {
    // check if certificate is trusted or untrusted
    const crypto_utils = require("node-opcua-crypto");

    const pki_folder = process.cwd() + "/pki";

    // istanbul ignore next
    if (!fs.existsSync(pki_folder)) {
        fs.mkdirSync(pki_folder);
    }
    const pki_untrusted_folder = path.join(pki_folder, "untrusted");

    // istanbul ignore next
    if (!fs.existsSync(pki_untrusted_folder)) {
        fs.mkdirSync(pki_untrusted_folder);
    }
    const thumbprint = crypto_utils.makeSHA1Thumbprint(serverCertificate);

    const certificate_filename = path.join(pki_untrusted_folder, thumbprint.toString("hex") + ".pem");
    fs.writeFile(certificate_filename, crypto_utils.toPem(serverCertificate, "CERTIFICATE"), function () {
        setImmediate(callback);
    });

}


OPCUAClientBase.prototype._internal_create_secure_channel = function (callback) {

    const self = this;
    let secureChannel;
    assert(self._secureChannel === null);
    assert(_.isString(self.endpointUrl));

    async.series([

        //------------------------------------------------- STEP 2 : OpenSecureChannel
        function (_inner_callback) {

            secureChannel = new ClientSecureChannelLayer({
                defaultSecureTokenLifetime: self.defaultSecureTokenLifetime,
                securityMode: self.securityMode,
                securityPolicy: self.securityPolicy,
                serverCertificate: self.serverCertificate,
                parent: self,
                objectFactory: self.objectFactory,
                connectionStrategy: self.connectionStrategy,
                tokenRenewalInterval: self.tokenRenewalInterval,
            });

            self._secureChannel = secureChannel;

            secureChannel.protocolVersion = self.protocolVersion;

            secureChannel.create(self.endpointUrl, function (err) {
                if (err) {
                    debugLog("Cannot create secureChannel".yellow, (err.message ? err.message.cyan : ""));
                    self._destroy_secure_channel();
                } else {
                    if (!self._secureChannel) {
                        console.log("_secureChannel has been closed during the transaction !");
                        self._destroy_secure_channel();
                        return _inner_callback(new Error("Secure Channel Closed"));
                    }
                    assert(self._secureChannel !== null);
                    _install_secure_channel_event_handlers(self, secureChannel);
                }
                assert(err || self._secureChannel !== null);
                _inner_callback(err);
            });

            secureChannel.on("backoff", function (number, delay) {
                self.emit("backoff", number, delay);
            });

            secureChannel.on("abort", function () {
                self.emit("abort");
            });


        },
        //------------------------------------------------- STEP 3 : GetEndpointsRequest
        function (_inner_callback) {

            if (!self.knowsServerEndpoint) {
                assert(self._secureChannel !== null);
                self.getEndpoints(function (err/*, endpoints*/) {
                    assert(self._secureChannel !== null);
                    _inner_callback(err);
                });
            } else {
                // end points are already known
                assert(self._secureChannel !== null);
                _inner_callback(null);
            }
        }

    ], function (err) {

        if (err) {
            //xx self.disconnect(function () {
            //xx });
            self._secureChannel = null;
            callback(err);
        } else {
            assert(self._secureChannel !== null);
            callback(err, secureChannel);
        }
    });

};

/**
 * true if the connection strategy is set to automatically try to reconnect in case of failure
 * @property reconnectOnFailure
 * @type {Boolean}
 */
OPCUAClientBase.prototype.__defineGetter__("reconnectOnFailure", function () {
    const self = this;
    return self.connectionStrategy.maxRetry > 0 || self.connectionStrategy.maxRetry === -1;
});


function _install_secure_channel_event_handlers(self, secureChannel) {

    assert(self instanceof OPCUAClientBase);

    secureChannel.on("send_chunk", function (message_chunk) {
        /**
         * notify the observer that a message_chunk has been sent
         * @event send_chunk
         * @param message_chunk
         */
        self.emit("send_chunk", message_chunk);
    });

    secureChannel.on("receive_chunk", function (message_chunk) {
        /**
         * notify the observer that a message_chunk has been received
         * @event receive_chunk
         * @param message_chunk
         */
        self.emit("receive_chunk", message_chunk);
    });

    secureChannel.on("send_request", function (message) {
        /**
         * notify the observer that a request has been sent to the server.
         * @event send_request
         * @param message
         */
        self.emit("send_request", message);
    });

    secureChannel.on("receive_response", function (message) {
        /**
         * notify the observer that a response has been received from the server.
         * @event receive_response
         * @param message
         */
        self.emit("receive_response", message);
    });

    secureChannel.on("lifetime_75", function (token) {
        // secureChannel requests a new token
        debugLog("SecureChannel Security Token ", token.tokenId, " is about to expired , it's time to request a new token");
        // forward message to upper level
        self.emit("lifetime_75", token);
    });

    secureChannel.on("security_token_renewed", function () {
        // forward message to upper level
        self.emit("security_token_renewed");
    });

    secureChannel.on("close", function (err) {

        debugLog(" OPCUAClientBase emitting close".yellow.bold, err);

        if (!err || !self.reconnectOnFailure) {

            // this is a normal close operation initiated byu

            /**
             * @event close
             * @param error {Error}
             */
            self.emit("close", err);

            setImmediate(function () {
                self._destroy_secure_channel();
            });
            return;

        } else {

            self.emit("connection_lost");

            setImmediate(function () {

                debugLog("recreating new secure channel ");

                self._recreate_secure_channel(function (err) {

                    debugLog("secureChannel#on(close) => _recreate_secure_channel returns ", err ? err.message : "OK");

                    if (err) {
                        //xx assert(!self._secureChannel);
                        self.emit("close", err);
                        return;
                    } else {
                        /**
                         * @event connection_reestablished
                         *        send when the connection is reestablished after a connection break
                         */
                        self.emit("connection_reestablished");

                        // now delegate to upper class the
                        if (self._on_connection_reestablished) {
                            assert(_.isFunction(self._on_connection_reestablished));
                            self._on_connection_reestablished(function (err) {

                                if (err) {
                                    debugLog("connection_reestablished has failed");
                                    self.disconnect(function () {
                                        //xx callback(err);
                                    });
                                }
                            });
                        }
                    }
                });
            });
        }
        //xx console.log("xxxx OPCUAClientBase emitting close".yellow.bold,err);
    });

    secureChannel.on("timed_out_request", function (request) {
        /**
         * send when a request has timed out without receiving a response
         * @event timed_out_request
         * @param request
         */
        self.emit("timed_out_request", request);
    });
//            self._secureChannel.on("end", function (err) {
//                console.log("xxx OPCUAClientBase emitting end".yellow.bold,err);
//                self.emit("close", err);
//            });
}

/**
 *
 * connect the OPC-UA client to a server end point.
 * @method connect
 * @async
 * @param endpointUrl {string}
 * @param callback {Function}
 */
OPCUAClientBase.prototype.connect = function (endpointUrl, callback) {

    assert(_.isFunction(callback), "expecting a callback");
    const self = this;

    self.endpointUrl = endpointUrl;

    debugLog("OPCUAClientBase#connect ", endpointUrl);

    // prevent illegal call to connect
    if (self._secureChannel !== null) {
        setImmediate(function () {
            callback(new Error("connect already called"), null);
        });
        return;
    }

    if (!self.serverCertificate && self.securityMode !== MessageSecurityMode.NONE) {

        debugLog("OPCUAClient : getting serverCertificate");
        // we have not been given the serverCertificate but this certificate
        // is required as the connection is to be secured.
        //
        // Let's explore the server endpoint that matches our security settings
        // This will give us the missing Certificate as well from the server itself.
        // todo :
        // Once we have the certificate, we cannot trust it straight away
        // we have to verify that the certificate is valid and not outdated and not revoked.
        // if the certificate is self-signed the certificate must appear in the trust certificate
        // list.
        // if the certificate has been certified by an Certificate Authority we have to
        // verify that the certificates in the chain are valid and not revoked.
        //
		const cert = self.certificateFile || 'certificates/client_selfsigned_cert_1024.pem';
		const key = self.privateKeyFile || 'certificates/client_key_1024.pem';
		const appName = self.applicationName || 'NodeOPCUA-Client';
        const params = {
            securityMode: this.securityMode,
            securityPolicy: this.securityPolicy,
            connectionStrategy: this.connectionStrategy,
            endpoint_must_exist: false,
			certificateFile: cert, 
            privateKeyFile: key,
			applicationName: appName
        };
        return __findEndpoint(endpointUrl,params, function (err, result) {
            if (err) {
                return callback(err);
            }

            const endpoint = result.selectedEndpoint;
            if (!endpoint) {
                // no matching end point can be found ...
                return callback(new Error("cannot find endpoint"));
            }
            assert(endpoint);
            _verify_serverCertificate(endpoint.serverCertificate, function (err) {
                if (err) {
                    return callback(err);
                }
                self.serverCertificate = endpoint.serverCertificate;
                return self.connect(endpointUrl, callback);
            });
        });
    }

    //todo: make sure endpointUrl exists in the list of endpoints send by the server
    // [...]

    // make sure callback will only be call once regardless of outcome, and will be also deferred.
    const callback_od = once(delayed.deferred(callback));
    callback = null;

    OPCUAClientBase.registry.register(self);

    self._internal_create_secure_channel(function (err, secureChannel) {
        callback_od(err);
    });

};

OPCUAClientBase.prototype.getClientNonce = function () {
    return this._secureChannel.clientNonce;
};

OPCUAClientBase.prototype.performMessageTransaction = function (request, callback) {

    const self = this;
    if (!self._secureChannel) {
        // this may happen if the Server has closed the connection abruptly for some unknown reason
        // or if the tcp connection has been broken.
        return callback(new Error("No SecureChannel , connection may have been canceled abruptly by server"));
    }
    assert(self._secureChannel);
    assert(request);
    assert(request.requestHeader);
    assert(typeof callback === "function");
    self._secureChannel.performMessageTransaction(request, callback);
};


/**
 *
 * return the endpoint information matching  security mode and security policy.
 * @method findEndpoint
 * @return {EndPoint}
 */
OPCUAClientBase.prototype.findEndpointForSecurity = function (securityMode, securityPolicy) {
    securityMode = MessageSecurityMode.get(securityMode);
    securityPolicy = SecurityPolicy.get(securityPolicy);
    assert(this.knowsServerEndpoint, "Server end point are not known yet");
    return _.find(this._server_endpoints, function (endpoint) {
        return endpoint.securityMode === securityMode &&
            endpoint.securityPolicyUri === securityPolicy.value;
    });
};

/**
 *
 * return the endpoint information matching the specified url , security mode and security policy.
 * @method findEndpoint
 * @return {EndPoint}
 */
OPCUAClientBase.prototype.findEndpoint = function (endpointUrl, securityMode, securityPolicy) {
    assert(this.knowsServerEndpoint, "Server end point are not known yet");
    return _.find(this._server_endpoints, function (endpoint) {
        return endpoint.endpointUrl === endpointUrl &&
            endpoint.securityMode === securityMode &&
            endpoint.securityPolicyUri === securityPolicy.value;
    });
};


/**
 * @method getEndpoints
 * @async
 * @async
 *
 * @param [options]
 * @param [options.endpointUrl] {String} the network address that the Client used to access the Discovery Endpoint .
 * @param [options.localeIds} {Array<LocaleId>}  List of locales to use.
 * @param [options.profileUris} {Array<String>}  List of transport profiles that the returned Endpoints shall support.
 * @param callback {Function}
 * @param callback.err {Error|null}
 * @param callback.serverEndpoints {Array<EndpointDescription>} the array of endpoint descriptions
 *
 */
OPCUAClientBase.prototype.getEndpoints = function (options, callback) {

    const self = this;

    if (!callback) {
        callback = options;
        options = {};
    }
    assert(_.isFunction(callback));

  //  options.endpointUrl = options.hasOwnProperty("endpointUrl") ? options.endpointUrl : self.endpointUrl;
    options.localeIds = options.localeIds || [];
    options.profileUris = options.profileUris || [];

    const request = new GetEndpointsRequest({
        endpointUrl: options.endpointUrl || self.endpointUrl,
        localeIds: options.localeIds,
        profileUris: options.profileUris,
        requestHeader: {
            auditEntryId: null
        }
    });

    self.performMessageTransaction(request, function (err, response) {
        self._server_endpoints = null;
        if (!err) {
            assert(response instanceof GetEndpointsResponse);
            self._server_endpoints = response.endpoints;
        }
        callback(err, self._server_endpoints);
    });
};

OPCUAClientBase.prototype.getEndpointsRequest = function(options,callback) {
    console.log("note: OPCUAClientBase#getEndpointsRequest is deprecated, use OPCUAClientBase#getEndpoints instead");
    return this.getEndpoints(options,callback);
};

/**
 *
 * send a FindServers request to a discovery server
 * @method findServers
 * @async
 * @param callback [Function}
 */

const discovery_service = require("node-opcua-service-discovery");
const FindServersRequest = discovery_service.FindServersRequest;
const FindServersResponse = discovery_service.FindServersResponse;
const FindServersOnNetworkRequest = discovery_service.FindServersOnNetworkRequest;
const FindServersOnNetworkResponse = discovery_service.FindServersOnNetworkResponse;

/**
 * @method findServers
 * @param options
 * @param [options.endpointUrl]
 * @param [options.localeIds] Array
 * @param [options.serverUris] Array
 * @param callback
 */
OPCUAClientBase.prototype.findServers = function (options, callback) {

    const self = this;

    if (!self._secureChannel) {
        setImmediate(function () {
            callback(new Error("Invalid Secure Channel"));
        });
        return;
    }

    if (!callback) {
        callback = options;
        options = {};
    }

    const request = new FindServersRequest({
        endpointUrl: options.endpointUrl || this.endpointUrl,
        localeIds: options.localeIds || [],
        serverUris: options.serverUris || []
    });


    self.performMessageTransaction(request, function (err, response) {
        if (err) {
            return callback(err);
        }
        assert(response instanceof FindServersResponse);
        callback(null, response.servers);
    });
};


OPCUAClientBase.prototype.findServersOnNetwork = function (options, callback) {

    const self = this;

    if (!self._secureChannel) {
        setImmediate(function () {
            callback(new Error("Invalid Secure Channel"));
        });
        return;
    }

    if (!callback) {
        callback = options;
        options = {};
    }

    const request = new FindServersOnNetworkRequest(options);

    self.performMessageTransaction(request, function (err, response) {
        if (err) {
            return callback(err);
        }
        assert(response instanceof FindServersOnNetworkResponse);
        callback(null, response.servers);
    });
};


OPCUAClientBase.prototype._close_pending_sessions = function (callback) {

    assert(_.isFunction(callback));
    const self = this;

    const sessions = _.clone(self._sessions);
    async.map(sessions, function (session, next) {
        assert(session instanceof ClientSession);
        assert(session._client === self);
        session.close(function (err) {
            // We should not bother if we have an error here
            // Session may fail to close , if they haven't been activate and forcefully closed by server
            // in a attempt to preserve resources in the case of a DOS attack for instance.
            if (err) {
                debugLog(" failing to close session " + session.authenticationToken.toString());
            }
            next();
        });

    }, function (err) {

        // istanbul ignore next
        if (self._sessions.length > 0) {
            debugLog(self._sessions.map(function (s) {
                return s.authenticationToken.toString()
            }).join(" "));
        }

        assert(self._sessions.length === 0, " failed to disconnect exiting sessions ");
        callback(err);
    });

};

OPCUAClientBase.prototype._addSession = function (session) {
    const self = this;
    assert(!session._client || session._client === self);
    assert(!_.contains(self._sessions, session), "session already added");
    session._client = self;
    self._sessions.push(session);

    if (self.keepSessionAlive) {
        session.startKeepAliveManager();
    }

};

OPCUAClientBase.prototype._removeSession = function (session) {
    const self = this;
    const index = self._sessions.indexOf(session);
    if (index >= 0) {
        const s = self._sessions.splice(index, 1)[0];
        assert(s === session);
        assert(!_.contains(self._sessions, session));
        assert(session._client === self)
        session._client = null;
    }
    assert(!_.contains(self._sessions, session));
};


/**
 * disconnect client from server
 * @method disconnect
 * @async
 * @param callback [Function}
 */
OPCUAClientBase.prototype.disconnect = function (callback) {

    assert(_.isFunction(callback), "expecting a callback function here");
    const self = this;

    debugLog("OPCUAClientBase#disconnect",self.endpointUrl);

    if (self.isReconnecting) {
        debugLog("OPCUAClientBase#disconnect called while reconnection is in progress");
        // let's abort the reconnection process
        return self._cancel_reconnection(function (err) {
            assert(!err, " why would this fail ?");
            assert(!self.isReconnecting);
            // sessions cannot be cancelled properly and must be discarded.
            self.disconnect(callback);
        });
    }

    if (self._sessions.length && !self.keepPendingSessionsOnDisconnect) {
        debugLog("warning : disconnection : closing pending sessions");
        // disconnect has been called whereas living session exists
        // we need to close them first ....
        self._close_pending_sessions(function (/*err*/) {
            self.disconnect(callback);
        });
        return;
    }

    if (self._sessions.length ) {
        // transfer active session to  orphan and detach them from channel
        _.forEach(self._sessions,function(session) {
            self._removeSession(session)
        });
        self._sessions = [];
    }
    assert(self._sessions.length === 0, " attempt to disconnect a client with live sessions ");

    OPCUAClientBase.registry.unregister(self);

    if (self._secureChannel) {

        const tmp_channel = self._secureChannel;

        self._destroy_secure_channel();

        tmp_channel.close(function () {

            debugLog(" EMIT NORMAL CLOSE");
            /**
             * @event close
             */
            self.emit("close", null);
            setImmediate(callback);
        });
    } else {
        self.emit("close", null);
        callback();
    }
};

/**
 * total number of bytes read by the client
 * @property bytesRead
 * @type {Number}
 */
OPCUAClientBase.prototype.__defineGetter__("bytesRead", function () {
    const self = this;
    return self._byteRead + (self._secureChannel ? self._secureChannel.bytesRead : 0);
});

/**
 * total number of bytes written by the client
 * @property bytesWritten
 * @type {Number}
 */
OPCUAClientBase.prototype.__defineGetter__("bytesWritten", function () {
    const self = this;
    return self._byteWritten + (self._secureChannel ? self._secureChannel.bytesWritten : 0);
});

/**
 * total number of transactions performed by the client
 * @property transactionsPerformed
 * @type {Number}
 */
OPCUAClientBase.prototype.__defineGetter__("transactionsPerformed", function () {
    const self = this;
    return self._transactionsPerformed + (self._secureChannel ? self._secureChannel.transactionsPerformed : 0);
});

OPCUAClientBase.prototype.__defineGetter__("timedOutRequestCount", function () {
    const self = this;
    return self._timedOutRequestCount + (self._secureChannel ? self._secureChannel.timedOutRequestCount : 0);
});

// override me !
OPCUAClientBase.prototype._on_connection_reestablished = function (callback) {
    callback();
};


OPCUAClientBase.prototype.toString = function () {

    console.log("  defaultSecureTokenLifetime.... ", this.defaultSecureTokenLifetime);
    console.log("  securityMode.................. ", this.securityMode.toString());
    console.log("  securityPolicy................ ", this.securityPolicy.toString());
    //xx this.serverCertificate = options.serverCertificate || null;
    console.log("  keepSessionAlive.............. ", this.keepSessionAlive);
    console.log("  bytesRead..................... ", this.bytesRead);
    console.log("  bytesWritten.................. ", this.bytesWritten);
    console.log("  transactionsPerformed......... ", this.transactionsPerformed);
    console.log("  timedOutRequestCount.......... ", this.timedOutRequestCount);
    console.log("  connectionStrategy.");
    console.log("        .maxRetry............... ", this.connectionStrategy.maxRetry);
    console.log("        .initialDelay........... ", this.connectionStrategy.initialDelay);
    console.log("        .maxDelay............... ", this.connectionStrategy.maxDelay);
    console.log("        .randomisationFactor.... ", this.connectionStrategy.randomisationFactor);
    console.log("  keepSessionAlive.............. ", this.keepSessionAlive);
};

exports.OPCUAClientBase = OPCUAClientBase;

const thenify = require("thenify");
/**
 * @method connect
 * @param endpointUrl {string}
 * @async
 * @return {Promise}
 */
OPCUAClientBase.prototype.connect = thenify.withCallback(OPCUAClientBase.prototype.connect);
OPCUAClientBase.prototype.disconnect = thenify.withCallback(OPCUAClientBase.prototype.disconnect);
OPCUAClientBase.prototype.getEndpoints = thenify.withCallback(OPCUAClientBase.prototype.getEndpoints);
OPCUAClientBase.prototype.findServers = thenify.withCallback(OPCUAClientBase.prototype.findServers);
OPCUAClientBase.prototype.findServersOnNetwork = thenify.withCallback(OPCUAClientBase.prototype.findServersOnNetwork);
// deprecated:
OPCUAClientBase.prototype.getEndpointsRequest = thenify.withCallback(OPCUAClientBase.prototype.getEndpointsRequest);