neo4j-driver-bolt-connection
Version:
Implements the connection with the Neo4j Database using the Bolt Protocol
359 lines (358 loc) • 16.4 kB
JavaScript
"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;