neo4j-driver-bolt-connection
Version:
Implements the connection with the Neo4j Database using the Bolt Protocol
383 lines (382 loc) • 16.6 kB
JavaScript
"use strict";
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.
*/
/* eslint-env browser */
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;
// Just to be sure that these values are with us even after WebSocket is injected
// for tests.
// eslint-disable-next-line no-unused-vars
var WS_CONNECTING = 0;
var WS_OPEN = 1;
// eslint-disable-next-line no-unused-vars
var WS_CLOSING = 2;
var WS_CLOSED = 3;
/**
* Create a new WebSocketChannel to be used in web browsers.
* @access private
*/
var WebSocketChannel = /** @class */ (function () {
/**
* Create new instance
* @param {ChannelConfig} config - configuration for this channel.
* @param {function(): string} protocolSupplier - function that detects protocol of the web page. Should only be used in tests.
*/
function WebSocketChannel(config, protocolSupplier, socketFactory) {
if (protocolSupplier === void 0) { protocolSupplier = detectWebPageProtocol; }
if (socketFactory === void 0) { socketFactory = function (url) { return new WebSocket(url); }; }
var _this = this;
this._open = true;
this._pending = [];
this._error = null;
this._handleConnectionError = this._handleConnectionError.bind(this);
this._config = config;
this._receiveTimeout = null;
this._receiveTimeoutStarted = false;
this._receiveTimeoutId = null;
this._closingPromise = null;
var _a = determineWebSocketScheme(config, protocolSupplier), scheme = _a.scheme, error = _a.error;
if (error) {
this._error = error;
return;
}
this._ws = createWebSocket(scheme, config.address, socketFactory);
this._ws.binaryType = 'arraybuffer';
var self = this;
// All connection errors are not sent to the error handler
// we must also check for dirty close calls
this._ws.onclose = function (e) {
if (e && !e.wasClean) {
self._handleConnectionError();
}
self._open = false;
};
this._ws.onopen = function () {
// Connected! Cancel the connection timeout
self._clearConnectionTimeout();
// Drain all pending messages
var pending = self._pending;
self._pending = null;
for (var i = 0; i < pending.length; i++) {
self.write(pending[i]);
}
};
this._ws.onmessage = function (event) {
_this._resetTimeout();
if (self.onmessage) {
var b = new channel_buf_1.default(event.data);
self.onmessage(b);
}
};
this._ws.onerror = this._handleConnectionError;
this._connectionTimeoutFired = false;
this._connectionTimeoutId = this._setupConnectionTimeout();
}
WebSocketChannel.prototype._handleConnectionError = function () {
if (this._connectionTimeoutFired) {
// timeout fired - not connected within configured time
this._error = (0, neo4j_driver_core_1.newError)("Failed to establish connection in ".concat(this._config.connectionTimeout, "ms"), this._config.connectionErrorCode);
if (this.onerror) {
this.onerror(this._error);
}
return;
}
// onerror triggers on websocket close as well.. don't get me started.
if (this._open && !this._timedout) {
// http://stackoverflow.com/questions/25779831/how-to-catch-websocket-connection-to-ws-xxxnn-failed-connection-closed-be
this._error = (0, neo4j_driver_core_1.newError)('WebSocket connection failure. Due to security ' +
'constraints in your web browser, the reason for the failure is not available ' +
'to this Neo4j Driver. Please use your browsers development console to determine ' +
'the root cause of the failure. Common reasons include the database being ' +
'unavailable, using the wrong connection URL or temporary network problems. ' +
'If you have enabled encryption, ensure your browser is configured to trust the ' +
'certificate Neo4j is configured to use. WebSocket `readyState` is: ' +
this._ws.readyState, this._config.connectionErrorCode);
if (this.onerror) {
this.onerror(this._error);
}
}
};
/**
* Write the passed in buffer to connection
* @param {ChannelBuffer} buffer - Buffer to write
*/
WebSocketChannel.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) {
try {
this._ws.send(buffer._buffer);
}
catch (error) {
if (this._ws.readyState !== WS_OPEN) {
// Websocket has been closed
this._handleConnectionError();
}
else {
// Some other error occured
throw error;
}
}
}
else {
throw (0, neo4j_driver_core_1.newError)("Don't know how to send buffer: " + buffer);
}
};
/**
* Close the connection
* @returns {Promise} A promise that will be resolved after channel is closed
*/
WebSocketChannel.prototype.close = function () {
var _this = this;
if (this._closingPromise === null) {
this._closingPromise = new Promise(function (resolve, reject) {
_this._clearConnectionTimeout();
if (_this._ws && _this._ws.readyState !== WS_CLOSED) {
_this._open = false;
_this.stopReceiveTimeout();
_this._ws.onclose = function () {
resolve();
};
_this._ws.close();
}
else {
resolve();
}
});
}
return this._closingPromise;
};
/**
* Setup the receive timeout for the channel.
*
* Not supported for the browser channel.
*
* @param {number} receiveTimeout The amount of time the channel will keep without receive any data before timeout (ms)
* @returns {void}
*/
WebSocketChannel.prototype.setupReceiveTimeout = function (receiveTimeout) {
this._receiveTimeout = receiveTimeout;
};
/**
* Stops the receive timeout for the channel.
*/
WebSocketChannel.prototype.stopReceiveTimeout = function () {
if (this._receiveTimeout !== null && this._receiveTimeoutStarted) {
this._receiveTimeoutStarted = false;
if (this._receiveTimeoutId != null) {
clearTimeout(this._receiveTimeoutId);
}
this._receiveTimeoutId = null;
}
};
/**
* Start the receive timeout for the channel.
*/
WebSocketChannel.prototype.startReceiveTimeout = function () {
if (this._open && this._receiveTimeout !== null && !this._receiveTimeoutStarted) {
this._receiveTimeoutStarted = true;
this._resetTimeout();
}
};
WebSocketChannel.prototype._resetTimeout = function () {
var _this = this;
if (!this._receiveTimeoutStarted) {
return;
}
if (this._receiveTimeoutId !== null) {
clearTimeout(this._receiveTimeoutId);
}
this._receiveTimeoutId = setTimeout(function () {
_this._receiveTimeoutId = null;
_this._timedout = true;
_this.stopReceiveTimeout();
_this._error = (0, neo4j_driver_core_1.newError)("Connection lost. Server didn't respond in ".concat(_this._receiveTimeout, "ms"), _this._config.connectionErrorCode);
_this.close();
if (_this.onerror) {
_this.onerror(_this._error);
}
}, this._receiveTimeout);
};
/**
* Set connection timeout on the given WebSocket, if configured.
* @return {number} the timeout id or null.
* @private
*/
WebSocketChannel.prototype._setupConnectionTimeout = function () {
var _this = this;
var timeout = this._config.connectionTimeout;
if (timeout) {
var webSocket_1 = this._ws;
return setTimeout(function () {
if (webSocket_1.readyState !== WS_OPEN) {
_this._connectionTimeoutFired = true;
webSocket_1.close();
}
}, timeout);
}
return null;
};
/**
* Remove active connection timeout, if any.
* @private
*/
WebSocketChannel.prototype._clearConnectionTimeout = function () {
var timeoutId = this._connectionTimeoutId;
if (timeoutId || timeoutId === 0) {
this._connectionTimeoutFired = false;
this._connectionTimeoutId = null;
clearTimeout(timeoutId);
}
};
return WebSocketChannel;
}());
exports.default = WebSocketChannel;
function createWebSocket(scheme, address, socketFactory) {
var url = scheme + '://' + address.asHostPort();
try {
return socketFactory(url);
}
catch (error) {
if (isIPv6AddressIssueOnWindows(error, address)) {
// WebSocket in IE and Edge browsers on Windows do not support regular IPv6 address syntax because they contain ':'.
// It's an invalid character for UNC (https://en.wikipedia.org/wiki/IPv6_address#Literal_IPv6_addresses_in_UNC_path_names)
// and Windows requires IPv6 to be changes in the following way:
// 1) replace all ':' with '-'
// 2) replace '%' with 's' for link-local address
// 3) append '.ipv6-literal.net' suffix
// only then resulting string can be considered a valid IPv6 address. Yes, this is extremely weird!
// For more details see:
// https://social.msdn.microsoft.com/Forums/ie/en-US/06cca73b-63c2-4bf9-899b-b229c50449ff/whether-ie10-websocket-support-ipv6?forum=iewebdevelopment
// https://www.itdojo.com/ipv6-addresses-and-unc-path-names-overcoming-illegal/
// Creation of WebSocket with unconverted address results in SyntaxError without message or stacktrace.
// That is why here we "catch" SyntaxError and rewrite IPv6 address if needed.
var windowsFriendlyUrl = asWindowsFriendlyIPv6Address(scheme, address);
return socketFactory(windowsFriendlyUrl);
}
else {
throw error;
}
}
}
function isIPv6AddressIssueOnWindows(error, address) {
return error.name === 'SyntaxError' && isIPv6Address(address.asHostPort());
}
function isIPv6Address(hostAndPort) {
return hostAndPort.charAt(0) === '[' && hostAndPort.indexOf(']') !== -1;
}
function asWindowsFriendlyIPv6Address(scheme, address) {
// replace all ':' with '-'
var hostWithoutColons = address.host().replace(/:/g, '-');
// replace '%' with 's' for link-local IPv6 address like 'fe80::1%lo0'
var hostWithoutPercent = hostWithoutColons.replace('%', 's');
// append magic '.ipv6-literal.net' suffix
var ipv6Host = hostWithoutPercent + '.ipv6-literal.net';
return "".concat(scheme, "://").concat(ipv6Host, ":").concat(address.port());
}
/**
* @param {ChannelConfig} config - configuration for the channel.
* @param {function(): string} protocolSupplier - function that detects protocol of the web page.
* @return {{scheme: string|null, error: Neo4jError|null}} object containing either scheme or error.
*/
function determineWebSocketScheme(config, protocolSupplier) {
var encryptionOn = isEncryptionExplicitlyTurnedOn(config);
var encryptionOff = isEncryptionExplicitlyTurnedOff(config);
var trust = config.trust;
var secureProtocol = isProtocolSecure(protocolSupplier);
verifyEncryptionSettings(encryptionOn, encryptionOff, secureProtocol);
if (encryptionOff) {
// encryption explicitly turned off in the config
return { scheme: 'ws', error: null };
}
if (secureProtocol) {
// driver is used in a secure https web page, use 'wss'
return { scheme: 'wss', error: null };
}
if (encryptionOn) {
// encryption explicitly requested in the config
if (!trust || trust === 'TRUST_SYSTEM_CA_SIGNED_CERTIFICATES') {
// trust strategy not specified or the only supported strategy is specified
return { scheme: 'wss', error: null };
}
else {
var error = (0, neo4j_driver_core_1.newError)('The browser version of this driver only supports one trust ' +
"strategy, 'TRUST_SYSTEM_CA_SIGNED_CERTIFICATES'. " +
trust +
' is not supported. Please ' +
'either use TRUST_SYSTEM_CA_SIGNED_CERTIFICATES or disable encryption by setting ' +
'`encrypted:"' +
ENCRYPTION_OFF +
'"` in the driver configuration.');
return { scheme: null, error: error };
}
}
// default to unencrypted web socket
return { scheme: 'ws', error: null };
}
/**
* @param {ChannelConfig} config - configuration for the channel.
* @return {boolean} `true` if encryption enabled in the config, `false` otherwise.
*/
function isEncryptionExplicitlyTurnedOn(config) {
return config.encrypted === true || config.encrypted === ENCRYPTION_ON;
}
/**
* @param {ChannelConfig} config - configuration for the channel.
* @return {boolean} `true` if encryption disabled in the config, `false` otherwise.
*/
function isEncryptionExplicitlyTurnedOff(config) {
return config.encrypted === false || config.encrypted === ENCRYPTION_OFF;
}
/**
* @param {function(): string} protocolSupplier - function that detects protocol of the web page.
* @return {boolean} `true` if protocol returned by the given function is secure, `false` otherwise.
*/
function isProtocolSecure(protocolSupplier) {
var protocol = typeof protocolSupplier === 'function' ? protocolSupplier() : '';
return protocol && protocol.toLowerCase().indexOf('https') >= 0;
}
function verifyEncryptionSettings(encryptionOn, encryptionOff, secureProtocol) {
if (secureProtocol === null) {
// do nothing sice the protocol could not be identified
}
else if (encryptionOn && !secureProtocol) {
// encryption explicitly turned on for a driver used on a HTTP web page
console.warn('Neo4j driver is configured to use secure WebSocket on a HTTP web page. ' +
'WebSockets might not work in a mixed content environment. ' +
'Please consider configuring driver to not use encryption.');
}
else if (encryptionOff && secureProtocol) {
// encryption explicitly turned off for a driver used on a HTTPS web page
console.warn('Neo4j driver is configured to use insecure WebSocket on a HTTPS web page. ' +
'WebSockets might not work in a mixed content environment. ' +
'Please consider configuring driver to use encryption.');
}
}
function detectWebPageProtocol() {
return typeof window !== 'undefined' && window.location
? window.location.protocol
: null;
}