"use strict";
var __assign = (this && this.__assign) || function () {
    __assign = Object.assign || function(t) {
        for (var s, i = 1, n = arguments.length; i < n; i++) {
            s = arguments[i];
            for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
                t[p] = s[p];
        }
        return t;
    };
    return __assign.apply(this, arguments);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
/**
 * Copyright (c) "Neo4j"
 * Neo4j Sweden AB [https://neo4j.com]
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
var net_1 = __importDefault(require("net"));
var tls_1 = __importDefault(require("tls"));
var fs_1 = __importDefault(require("fs"));
var channel_buf_1 = __importDefault(require("../channel-buf"));
var neo4j_driver_core_1 = require("neo4j-driver-core");
var _a = neo4j_driver_core_1.internal.util, ENCRYPTION_OFF = _a.ENCRYPTION_OFF, ENCRYPTION_ON = _a.ENCRYPTION_ON, isEmptyObjectOrNull = _a.isEmptyObjectOrNull;
var _CONNECTION_IDGEN = 0;
var TrustStrategy = {
    TRUST_CUSTOM_CA_SIGNED_CERTIFICATES: function (config, onSuccess, onFailure) {
        if (!config.trustedCertificates ||
            config.trustedCertificates.length === 0) {
            onFailure((0, neo4j_driver_core_1.newError)('You are using TRUST_CUSTOM_CA_SIGNED_CERTIFICATES as the method ' +
                'to verify trust for encrypted  connections, but have not configured any ' +
                'trustedCertificates. You  must specify the path to at least one trusted ' +
                'X.509 certificate for this to work. Two other alternatives is to use ' +
                'TRUST_ALL_CERTIFICATES or to disable encryption by setting encrypted="' +
                ENCRYPTION_OFF +
                '"' +
                'in your driver configuration.'));
            return;
        }
        var tlsOpts = newTlsOptions(config.address.host(), config.trustedCertificates.map(function (f) { return fs_1.default.readFileSync(f); }), config.clientCertificate);
        var socket = tls_1.default.connect(config.address.port(), config.address.resolvedHost(), tlsOpts, function () {
            if (!socket.authorized) {
                onFailure((0, neo4j_driver_core_1.newError)('Server certificate is not trusted. If you trust the database you are connecting to, add' +
                    ' the signing certificate, or the server certificate, to the list of certificates trusted by this driver' +
                    " using `neo4j.driver(.., { trustedCertificates:['path/to/certificate.crt']}). This " +
                    ' is a security measure to protect against man-in-the-middle attacks. If you are just trying ' +
                    ' Neo4j out and are not concerned about encryption, simply disable it using `encrypted="' +
                    ENCRYPTION_OFF +
                    '"`' +
                    ' in the driver options. Socket responded with: ' +
                    socket.authorizationError));
            }
            else {
                onSuccess();
            }
        });
        socket.on('error', onFailure);
        return configureSocket(socket);
    },
    TRUST_SYSTEM_CA_SIGNED_CERTIFICATES: function (config, onSuccess, onFailure) {
        var tlsOpts = newTlsOptions(config.address.host(), undefined, config.clientCertificate);
        var socket = tls_1.default.connect(config.address.port(), config.address.resolvedHost(), tlsOpts, function () {
            if (!socket.authorized) {
                onFailure((0, neo4j_driver_core_1.newError)('Server certificate is not trusted. If you trust the database you are connecting to, use ' +
                    'TRUST_CUSTOM_CA_SIGNED_CERTIFICATES and add' +
                    ' the signing certificate, or the server certificate, to the list of certificates trusted by this driver' +
                    " using `neo4j.driver(.., { trustedCertificates:['path/to/certificate.crt']}). This " +
                    ' is a security measure to protect against man-in-the-middle attacks. If you are just trying ' +
                    ' Neo4j out and are not concerned about encryption, simply disable it using `encrypted="' +
                    ENCRYPTION_OFF +
                    '"`' +
                    ' in the driver options. Socket responded with: ' +
                    socket.authorizationError));
            }
            else {
                onSuccess();
            }
        });
        socket.on('error', onFailure);
        return configureSocket(socket);
    },
    TRUST_ALL_CERTIFICATES: function (config, onSuccess, onFailure) {
        var tlsOpts = newTlsOptions(config.address.host(), undefined, config.clientCertificate);
        var socket = tls_1.default.connect(config.address.port(), config.address.resolvedHost(), tlsOpts, function () {
            var certificate = socket.getPeerCertificate();
            if (isEmptyObjectOrNull(certificate)) {
                onFailure((0, neo4j_driver_core_1.newError)('Secure connection was successful but server did not return any valid ' +
                    'certificates. Such connection can not be trusted. If you are just trying ' +
                    ' Neo4j out and are not concerned about encryption, simply disable it using ' +
                    '`encrypted="' +
                    ENCRYPTION_OFF +
                    '"` in the driver options. ' +
                    'Socket responded with: ' +
                    socket.authorizationError));
            }
            else {
                onSuccess();
            }
        });
        socket.on('error', onFailure);
        return configureSocket(socket);
    }
};
/**
 * Connect using node socket.
 * @param {ChannelConfig} config - configuration of this channel.
 * @param {function} onSuccess - callback to execute on connection success.
 * @param {function} onFailure - callback to execute on connection failure.
 * @return {*} socket connection.
 */
function _connect(config, onSuccess, onFailure) {
    if (onFailure === void 0) { onFailure = function () { return null; }; }
    var trustStrategy = trustStrategyName(config);
    if (!isEncrypted(config)) {
        var socket = net_1.default.connect(config.address.port(), config.address.resolvedHost(), onSuccess);
        socket.on('error', onFailure);
        return configureSocket(socket);
    }
    else if (TrustStrategy[trustStrategy]) {
        return TrustStrategy[trustStrategy](config, onSuccess, onFailure);
    }
    else {
        onFailure((0, neo4j_driver_core_1.newError)('Unknown trust strategy: ' +
            config.trust +
            '. Please use either ' +
            "trust:'TRUST_CUSTOM_CA_SIGNED_CERTIFICATES' or trust:'TRUST_ALL_CERTIFICATES' in your driver " +
            'configuration. Alternatively, you can disable encryption by setting ' +
            '`encrypted:"' +
            ENCRYPTION_OFF +
            '"`. There is no mechanism to use encryption without trust verification, ' +
            'because this incurs the overhead of encryption without improving security. If ' +
            'the driver does not verify that the peer it is connected to is really Neo4j, it ' +
            'is very easy for an attacker to bypass the encryption by pretending to be Neo4j.'));
    }
}
function isEncrypted(config) {
    var encryptionNotConfigured = config.encrypted == null || config.encrypted === undefined;
    if (encryptionNotConfigured) {
        // default to using encryption if trust-all-certificates is available
        return false;
    }
    return config.encrypted === true || config.encrypted === ENCRYPTION_ON;
}
function trustStrategyName(config) {
    if (config.trust) {
        return config.trust;
    }
    return 'TRUST_SYSTEM_CA_SIGNED_CERTIFICATES';
}
/**
 * Create a new configuration options object for the {@code tls.connect()} call.
 * @param {string} hostname the target hostname.
 * @param {string|undefined} ca an optional CA.
 * @param {string|undefined} cert an optional client cert.
 * @param {string|undefined} key an optional client cert key.
 * @param {string|undefined} passphrase an optional client cert passphrase
 * @return {Object} a new options object.
 */
function newTlsOptions(hostname, ca, clientCertificate) {
    if (ca === void 0) { ca = undefined; }
    if (clientCertificate === void 0) { clientCertificate = undefined; }
    return __assign({ rejectUnauthorized: false, servername: (net_1.default.isIPv4(hostname) || net_1.default.isIPv6(hostname)) ? undefined : hostname, // server name for the SNI (Server Name Indication) TLS extension
        ca: ca }, clientCertificate);
}
/**
 * Update socket options for the newly created socket. Accepts either `net.Socket` or its subclass `tls.TLSSocket`.
 * @param {net.Socket} socket the socket to configure.
 * @return {net.Socket} the given socket.
 */
function configureSocket(socket) {
    socket.setKeepAlive(true);
    return socket;
}
/**
 * In a Node.js environment the 'net' module is used
 * as transport.
 * @access private
 */
var NodeChannel = /** @class */ (function () {
    /**
     * Create new instance
     * @param {ChannelConfig} config - configuration for this channel.
     */
    function NodeChannel(config, connect) {
        if (connect === void 0) { connect = _connect; }
        var self = this;
        this.id = _CONNECTION_IDGEN++;
        this._pending = [];
        this._open = true;
        this._error = null;
        this._handleConnectionError = this._handleConnectionError.bind(this);
        this._handleConnectionTerminated = this._handleConnectionTerminated.bind(this);
        this._connectionErrorCode = config.connectionErrorCode;
        this._receiveTimeout = null;
        this._receiveTimeoutStarted = false;
        this._conn = connect(config, function () {
            if (!self._open) {
                return;
            }
            self._conn.on('data', function (buffer) {
                if (self.onmessage) {
                    self.onmessage(new channel_buf_1.default(buffer));
                }
            });
            self._conn.on('end', self._handleConnectionTerminated);
            // Drain all pending messages
            var pending = self._pending;
            self._pending = null;
            for (var i = 0; i < pending.length; i++) {
                self.write(pending[i]);
            }
        }, this._handleConnectionError);
        this._setupConnectionTimeout(config, this._conn);
    }
    NodeChannel.prototype._handleConnectionError = function (err) {
        var msg = 'Failed to connect to server. ' +
            'Please ensure that your database is listening on the correct host and port ' +
            'and that you have compatible encryption settings both on Neo4j server and driver. ' +
            'Note that the default encryption setting has changed in Neo4j 4.0.';
        if (err.message)
            msg += ' Caused by: ' + err.message;
        if (this._conn.destroyed) {
            this._open = false;
        }
        this._error = (0, neo4j_driver_core_1.newError)(msg, this._connectionErrorCode);
        if (this.onerror) {
            this.onerror(this._error);
        }
    };
    NodeChannel.prototype._handleConnectionTerminated = function () {
        this._open = false;
        this._error = (0, neo4j_driver_core_1.newError)('Connection was closed by server', this._connectionErrorCode);
        if (this.onerror) {
            this.onerror(this._error);
        }
    };
    /**
     * Setup connection timeout on the socket, if configured.
     * @param {ChannelConfig} config - configuration of this channel.
     * @param {Object} socket - `net.Socket` or `tls.TLSSocket` object.
     * @private
     */
    NodeChannel.prototype._setupConnectionTimeout = function (config, socket) {
        var _this = this;
        var timeout = config.connectionTimeout;
        if (timeout) {
            var connectListener_1 = function () {
                // connected - clear connection timeout
                socket.setTimeout(0);
            };
            var timeoutListener_1 = function () {
                // timeout fired - not connected within configured time. cancel timeout and destroy socket
                socket.setTimeout(0);
                socket.destroy((0, neo4j_driver_core_1.newError)("Failed to establish connection in ".concat(timeout, "ms"), config.connectionErrorCode));
            };
            socket.on('connect', connectListener_1);
            socket.on('timeout', timeoutListener_1);
            this._removeConnectionTimeoutListeners = function () {
                _this._conn.off('connect', connectListener_1);
                _this._conn.off('timeout', timeoutListener_1);
            };
            socket.setTimeout(timeout);
        }
    };
    /**
     * Setup the receive timeout for the channel.
     *
     * @param {number} receiveTimeout How long the channel will wait for receiving data before timing out (ms)
     * @returns {void}
     */
    NodeChannel.prototype.setupReceiveTimeout = function (receiveTimeout) {
        var _this = this;
        if (this._removeConnectionTimeoutListeners) {
            this._removeConnectionTimeoutListeners();
        }
        this._conn.on('timeout', function () {
            _this._conn.destroy((0, neo4j_driver_core_1.newError)("Connection lost. Server didn't respond in ".concat(receiveTimeout, "ms"), _this._connectionErrorCode));
        });
        this._receiveTimeout = receiveTimeout;
    };
    /**
     * Stops the receive timeout for the channel.
     */
    NodeChannel.prototype.stopReceiveTimeout = function () {
        if (this._receiveTimeout !== null && this._receiveTimeoutStarted) {
            this._receiveTimeoutStarted = false;
            this._conn.setTimeout(0);
        }
    };
    /**
     * Start the receive timeout for the channel.
     */
    NodeChannel.prototype.startReceiveTimeout = function () {
        if (this._receiveTimeout !== null && !this._receiveTimeoutStarted) {
            this._receiveTimeoutStarted = true;
            this._conn.setTimeout(this._receiveTimeout);
        }
    };
    /**
     * Write the passed in buffer to connection
     * @param {ChannelBuffer} buffer - Buffer to write
     */
    NodeChannel.prototype.write = function (buffer) {
        // If there is a pending queue, push this on that queue. This means
        // we are not yet connected, so we queue things locally.
        if (this._pending !== null) {
            this._pending.push(buffer);
        }
        else if (buffer instanceof channel_buf_1.default) {
            this._conn.write(buffer._buffer);
        }
        else {
            throw (0, neo4j_driver_core_1.newError)("Don't know how to write: " + buffer);
        }
    };
    /**
     * Close the connection
     * @returns {Promise} A promise that will be resolved after channel is closed
     */
    NodeChannel.prototype.close = function () {
        var _this = this;
        return new Promise(function (resolve, reject) {
            var cleanup = function () {
                if (!_this._conn.destroyed) {
                    _this._conn.destroy();
                }
                resolve();
            };
            if (_this._open) {
                _this._open = false;
                _this._conn.removeListener('end', _this._handleConnectionTerminated);
                _this._conn.on('end', function () { return cleanup(); });
                _this._conn.on('close', function () { return cleanup(); });
                _this._conn.end();
                _this._conn.destroy();
            }
            else {
                cleanup();
            }
        });
    };
    return NodeChannel;
}());
exports.default = NodeChannel;
