430 lines
17 KiB
JavaScript
430 lines
17 KiB
JavaScript
'use strict';
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
|
|
var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck');
|
|
|
|
var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);
|
|
|
|
var _createClass2 = require('babel-runtime/helpers/createClass');
|
|
|
|
var _createClass3 = _interopRequireDefault(_createClass2);
|
|
|
|
var _net = require('net');
|
|
|
|
var _net2 = _interopRequireDefault(_net);
|
|
|
|
var _tls = require('tls');
|
|
|
|
var _tls2 = _interopRequireDefault(_tls);
|
|
|
|
var _fs = require('fs');
|
|
|
|
var _fs2 = _interopRequireDefault(_fs);
|
|
|
|
var _path = require('path');
|
|
|
|
var _path2 = _interopRequireDefault(_path);
|
|
|
|
var _os = require('os');
|
|
|
|
var _buf = require('./buf');
|
|
|
|
var _util = require('./util');
|
|
|
|
var _error = require('./../error');
|
|
|
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
|
|
/**
|
|
* Copyright (c) 2002-2018 "Neo4j,"
|
|
* Neo4j Sweden AB [http://neo4j.com]
|
|
*
|
|
* This file is part of Neo4j.
|
|
*
|
|
* 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 _CONNECTION_IDGEN = 0;
|
|
|
|
function userHome() {
|
|
// For some reason, Browserify chokes on shimming `process`. This code
|
|
// will never get executed on the browser anyway, to just hack around it
|
|
var getOutOfHereBrowserifyYoureDrunk = require;
|
|
var process = getOutOfHereBrowserifyYoureDrunk('process');
|
|
|
|
return process.env[process.platform == 'win32' ? 'USERPROFILE' : 'HOME'];
|
|
}
|
|
|
|
function mkFullPath(pathToCreate) {
|
|
try {
|
|
_fs2.default.mkdirSync(pathToCreate);
|
|
} catch (e) {
|
|
if (e.code === 'ENOENT') {
|
|
// Create parent dir
|
|
mkFullPath(_path2.default.dirname(pathToCreate));
|
|
// And now try again
|
|
mkFullPath(pathToCreate);
|
|
return;
|
|
}
|
|
if (e.code === 'EEXIST') {
|
|
return;
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
function loadFingerprint(serverId, knownHostsPath, cb) {
|
|
try {
|
|
_fs2.default.accessSync(knownHostsPath);
|
|
} catch (e) {
|
|
return cb(null);
|
|
}
|
|
var found = false;
|
|
require('readline').createInterface({
|
|
input: _fs2.default.createReadStream(knownHostsPath)
|
|
}).on('line', function (line) {
|
|
if (!found && line.startsWith(serverId)) {
|
|
found = true;
|
|
cb(line.split(" ")[1]);
|
|
}
|
|
}).on('close', function () {
|
|
if (!found) {
|
|
cb(null);
|
|
}
|
|
});
|
|
}
|
|
|
|
var _lockFingerprintFromAppending = {};
|
|
function storeFingerprint(serverId, knownHostsPath, fingerprint, cb) {
|
|
// we check if the serverId has been appended
|
|
if (!!_lockFingerprintFromAppending[serverId]) {
|
|
// if it has, we ignore it
|
|
return cb(null);
|
|
}
|
|
|
|
// we make the line as appended
|
|
// ( 1 is more efficient to store than true because true is an oddball )
|
|
_lockFingerprintFromAppending[serverId] = 1;
|
|
|
|
// If file doesn't exist, create full path to it
|
|
try {
|
|
_fs2.default.accessSync(knownHostsPath);
|
|
} catch (_) {
|
|
mkFullPath(_path2.default.dirname(knownHostsPath));
|
|
}
|
|
|
|
_fs2.default.appendFile(knownHostsPath, serverId + " " + fingerprint + _os.EOL, "utf8", function (err) {
|
|
delete _lockFingerprintFromAppending[serverId];
|
|
if (err) {
|
|
console.log(err);
|
|
}
|
|
return cb(err);
|
|
});
|
|
}
|
|
|
|
var TrustStrategy = {
|
|
/**
|
|
* @deprecated Since version 1.0. Will be deleted in a future version. {@link #TRUST_CUSTOM_CA_SIGNED_CERTIFICATES}.
|
|
*/
|
|
TRUST_SIGNED_CERTIFICATES: function TRUST_SIGNED_CERTIFICATES(config, onSuccess, onFailure) {
|
|
console.warn('`TRUST_SIGNED_CERTIFICATES` has been deprecated as option and will be removed in a future version of ' + "the driver. Please use `TRUST_CUSTOM_CA_SIGNED_CERTIFICATES` instead.");
|
|
return TrustStrategy.TRUST_CUSTOM_CA_SIGNED_CERTIFICATES(config, onSuccess, onFailure);
|
|
},
|
|
TRUST_CUSTOM_CA_SIGNED_CERTIFICATES: function TRUST_CUSTOM_CA_SIGNED_CERTIFICATES(config, onSuccess, onFailure) {
|
|
if (!config.trustedCertificates || config.trustedCertificates.length === 0) {
|
|
onFailure((0, _error.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=\"" + _util.ENCRYPTION_OFF + "\"" + "in your driver configuration."));
|
|
return;
|
|
}
|
|
|
|
var tlsOpts = {
|
|
ca: config.trustedCertificates.map(function (f) {
|
|
return _fs2.default.readFileSync(f);
|
|
}),
|
|
// Because we manually check for this in the connect callback, to give
|
|
// a more helpful error to the user
|
|
rejectUnauthorized: false
|
|
};
|
|
|
|
var socket = _tls2.default.connect(config.url.port, config.url.host, tlsOpts, function () {
|
|
if (!socket.authorized) {
|
|
onFailure((0, _error.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.v1.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=\"" + _util.ENCRYPTION_OFF + "\"`" + " in the driver options. Socket responded with: " + socket.authorizationError));
|
|
} else {
|
|
onSuccess();
|
|
}
|
|
});
|
|
socket.on('error', onFailure);
|
|
return socket;
|
|
},
|
|
TRUST_SYSTEM_CA_SIGNED_CERTIFICATES: function TRUST_SYSTEM_CA_SIGNED_CERTIFICATES(config, onSuccess, onFailure) {
|
|
|
|
var tlsOpts = {
|
|
// Because we manually check for this in the connect callback, to give
|
|
// a more helpful error to the user
|
|
rejectUnauthorized: false
|
|
};
|
|
var socket = _tls2.default.connect(config.url.port, config.url.host, tlsOpts, function () {
|
|
if (!socket.authorized) {
|
|
onFailure((0, _error.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.v1.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=\"" + _util.ENCRYPTION_OFF + "\"`" + " in the driver options. Socket responded with: " + socket.authorizationError));
|
|
} else {
|
|
onSuccess();
|
|
}
|
|
});
|
|
socket.on('error', onFailure);
|
|
return socket;
|
|
},
|
|
/**
|
|
* @deprecated in 1.1 in favour of {@link #TRUST_ALL_CERTIFICATES}. Will be deleted in a future version.
|
|
*/
|
|
TRUST_ON_FIRST_USE: function TRUST_ON_FIRST_USE(config, onSuccess, onFailure) {
|
|
console.warn('`TRUST_ON_FIRST_USE` has been deprecated as option and will be removed in a future version of ' + "the driver. Please use `TRUST_ALL_CERTIFICATES` instead.");
|
|
|
|
var tlsOpts = {
|
|
// Because we manually verify the certificate against known_hosts
|
|
rejectUnauthorized: false
|
|
};
|
|
|
|
var socket = _tls2.default.connect(config.url.port, config.url.host, tlsOpts, function () {
|
|
var serverCert = socket.getPeerCertificate( /*raw=*/true);
|
|
|
|
if (!serverCert.raw) {
|
|
// If `raw` is not available, we're on an old version of NodeJS, and
|
|
// the raw cert cannot be accessed (or, at least I couldn't find a way to)
|
|
// therefore, we can't generate a SHA512 fingerprint, meaning we can't
|
|
// do TOFU, and the safe approach is to fail.
|
|
onFailure((0, _error.newError)("You are using a version of NodeJS that does not " + "support trust-on-first use encryption. You can either upgrade NodeJS to " + "a newer version, use `trust:TRUST_CUSTOM_CA_SIGNED_CERTIFICATES` in your driver " + "config instead, or disable encryption using `encrypted:\"" + _util.ENCRYPTION_OFF + "\"`."));
|
|
return;
|
|
}
|
|
|
|
var serverFingerprint = require('crypto').createHash('sha512').update(serverCert.raw).digest("hex");
|
|
var knownHostsPath = config.knownHostsPath || _path2.default.join(userHome(), ".neo4j", "known_hosts");
|
|
var serverId = config.url.hostAndPort;
|
|
|
|
loadFingerprint(serverId, knownHostsPath, function (knownFingerprint) {
|
|
if (knownFingerprint === serverFingerprint) {
|
|
onSuccess();
|
|
} else if (knownFingerprint == null) {
|
|
storeFingerprint(serverId, knownHostsPath, serverFingerprint, function (err) {
|
|
if (err) {
|
|
return onFailure(err);
|
|
}
|
|
return onSuccess();
|
|
});
|
|
} else {
|
|
onFailure((0, _error.newError)("Database encryption certificate has changed, and no longer " + "matches the certificate stored for " + serverId + " in `" + knownHostsPath + "`. As a security precaution, this driver will not automatically trust the new " + "certificate, because doing so would allow an attacker to pretend to be the Neo4j " + "instance we want to connect to. The certificate provided by the server looks like: " + serverCert + ". If you trust that this certificate is valid, simply remove the line " + "starting with " + serverId + " in `" + knownHostsPath + "`, and the driver will " + "update the file with the new certificate. You can configure which file the driver " + "should use to store this information by setting `knownHosts` to another path in " + "your driver configuration - and you can disable encryption there as well using " + "`encrypted:\"" + _util.ENCRYPTION_OFF + "\"`."));
|
|
}
|
|
});
|
|
});
|
|
socket.on('error', onFailure);
|
|
return socket;
|
|
},
|
|
|
|
TRUST_ALL_CERTIFICATES: function TRUST_ALL_CERTIFICATES(config, onSuccess, onFailure) {
|
|
var tlsOpts = {
|
|
rejectUnauthorized: false
|
|
};
|
|
var socket = _tls2.default.connect(config.url.port, config.url.host, tlsOpts, function () {
|
|
var certificate = socket.getPeerCertificate();
|
|
if ((0, _util.isEmptyObjectOrNull)(certificate)) {
|
|
onFailure((0, _error.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=\"" + _util.ENCRYPTION_OFF + "\"` in the driver options. " + "Socket responded with: " + socket.authorizationError));
|
|
} else {
|
|
onSuccess();
|
|
}
|
|
});
|
|
socket.on('error', onFailure);
|
|
return 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) {
|
|
var onFailure = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : function () {
|
|
return null;
|
|
};
|
|
|
|
//still allow boolean for backwards compatibility
|
|
if (config.encrypted === false || config.encrypted === _util.ENCRYPTION_OFF) {
|
|
var conn = _net2.default.connect(config.url.port, config.url.host, onSuccess);
|
|
conn.on('error', onFailure);
|
|
return conn;
|
|
} else if (TrustStrategy[config.trust]) {
|
|
return TrustStrategy[config.trust](config, onSuccess, onFailure);
|
|
} else {
|
|
onFailure((0, _error.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:\"" + _util.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."));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* In a Node.js environment the 'net' module is used
|
|
* as transport.
|
|
* @access private
|
|
*/
|
|
|
|
var NodeChannel = function () {
|
|
|
|
/**
|
|
* Create new instance
|
|
* @param {ChannelConfig} config - configuration for this channel.
|
|
*/
|
|
function NodeChannel(config) {
|
|
(0, _classCallCheck3.default)(this, NodeChannel);
|
|
|
|
var self = this;
|
|
|
|
this.id = _CONNECTION_IDGEN++;
|
|
this.available = true;
|
|
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._encrypted = config.encrypted;
|
|
this._conn = connect(config, function () {
|
|
if (!self._open) {
|
|
return;
|
|
}
|
|
|
|
self._conn.on('data', function (buffer) {
|
|
if (self.onmessage) {
|
|
self.onmessage(new _buf.NodeBuffer(buffer));
|
|
}
|
|
});
|
|
|
|
self._conn.on('error', self._handleConnectionError);
|
|
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);
|
|
}
|
|
|
|
(0, _createClass3.default)(NodeChannel, [{
|
|
key: '_handleConnectionError',
|
|
value: function _handleConnectionError(err) {
|
|
var msg = err.message || 'Failed to connect to server';
|
|
this._error = (0, _error.newError)(msg, this._connectionErrorCode);
|
|
if (this.onerror) {
|
|
this.onerror(this._error);
|
|
}
|
|
}
|
|
}, {
|
|
key: '_handleConnectionTerminated',
|
|
value: function _handleConnectionTerminated() {
|
|
this._error = (0, _error.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
|
|
*/
|
|
|
|
}, {
|
|
key: '_setupConnectionTimeout',
|
|
value: function _setupConnectionTimeout(config, socket) {
|
|
var timeout = config.connectionTimeout;
|
|
if (timeout) {
|
|
socket.on('connect', function () {
|
|
// connected - clear connection timeout
|
|
socket.setTimeout(0);
|
|
});
|
|
|
|
socket.on('timeout', function () {
|
|
// timeout fired - not connected within configured time. cancel timeout and destroy socket
|
|
socket.setTimeout(0);
|
|
socket.destroy((0, _error.newError)('Failed to establish connection in ' + timeout + 'ms', config.connectionErrorCode));
|
|
});
|
|
|
|
socket.setTimeout(timeout);
|
|
}
|
|
}
|
|
}, {
|
|
key: 'isEncrypted',
|
|
value: function isEncrypted() {
|
|
return this._encrypted;
|
|
}
|
|
|
|
/**
|
|
* Write the passed in buffer to connection
|
|
* @param {NodeBuffer} buffer - Buffer to write
|
|
*/
|
|
|
|
}, {
|
|
key: 'write',
|
|
value: function write(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 _buf.NodeBuffer) {
|
|
// console.log( "[Conn#"+this.id+"] SEND: ", buffer.toString() );
|
|
this._conn.write(buffer._buffer);
|
|
} else {
|
|
throw (0, _error.newError)("Don't know how to write: " + buffer);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Close the connection
|
|
* @param {function} cb - Function to call on close.
|
|
*/
|
|
|
|
}, {
|
|
key: 'close',
|
|
value: function close() {
|
|
var cb = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : function () {
|
|
return null;
|
|
};
|
|
|
|
this._open = false;
|
|
if (this._conn) {
|
|
this._conn.end();
|
|
this._conn.removeListener('end', this._handleConnectionTerminated);
|
|
this._conn.on('end', cb);
|
|
} else {
|
|
cb();
|
|
}
|
|
}
|
|
}]);
|
|
return NodeChannel;
|
|
}();
|
|
|
|
var _nodeChannelModule = { channel: NodeChannel, available: true };
|
|
|
|
try {
|
|
// Only define this module if 'net' is available
|
|
require.resolve("net");
|
|
} catch (e) {
|
|
_nodeChannelModule = { available: false };
|
|
}
|
|
|
|
exports.default = _nodeChannelModule; |