UNPKG

neo4j-driver-bolt-connection

Version:

Implements the connection with the Neo4j Database using the Bolt Protocol

383 lines (382 loc) 16.7 kB
"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 (0, neo4j_driver_core_1.newError)('unexpected failure to write.', undefined, 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; }