diff --git a/src/v1/internal/ch-config.js b/src/v1/internal/ch-config.js index 5bb98d1cb..d79b22abb 100644 --- a/src/v1/internal/ch-config.js +++ b/src/v1/internal/ch-config.js @@ -42,9 +42,12 @@ export default class ChannelConfig { function extractEncrypted(driverConfig) { // check if encryption was configured by the user, use explicit null check because we permit boolean value - const encryptionConfigured = driverConfig.encrypted == null; + const encryptionNotConfigured = driverConfig.encrypted == null; // default to using encryption if trust-all-certificates is available - return encryptionConfigured ? hasFeature('trust_all_certificates') : driverConfig.encrypted; + if (encryptionNotConfigured && hasFeature('trust_all_certificates')) { + return true; + } + return driverConfig.encrypted; } function extractTrust(driverConfig) { diff --git a/src/v1/internal/ch-node.js b/src/v1/internal/ch-node.js index a8e07b29d..c88311d11 100644 --- a/src/v1/internal/ch-node.js +++ b/src/v1/internal/ch-node.js @@ -297,7 +297,6 @@ class NodeChannel { this._handleConnectionTerminated = this._handleConnectionTerminated.bind(this); this._connectionErrorCode = config.connectionErrorCode; - this._encrypted = config.encrypted; this._conn = connect(config, () => { if(!self._open) { return; @@ -362,10 +361,6 @@ class NodeChannel { } } - isEncrypted() { - return this._encrypted; - } - /** * Write the passed in buffer to connection * @param {NodeBuffer} buffer - Buffer to write diff --git a/src/v1/internal/ch-websocket.js b/src/v1/internal/ch-websocket.js index b61080341..9080d0acd 100644 --- a/src/v1/internal/ch-websocket.js +++ b/src/v1/internal/ch-websocket.js @@ -29,8 +29,9 @@ class WebSocketChannel { /** * 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. */ - constructor(config) { + constructor(config, protocolSupplier = detectWebPageProtocol) { this._open = true; this._pending = []; @@ -38,18 +39,10 @@ class WebSocketChannel { this._handleConnectionError = this._handleConnectionError.bind(this); this._config = config; - let scheme = "ws"; - //Allow boolean for backwards compatibility - if (config.encrypted === true || config.encrypted === ENCRYPTION_ON) { - if ((!config.trust) || config.trust === 'TRUST_CUSTOM_CA_SIGNED_CERTIFICATES') { - scheme = "wss"; - } else { - this._error = newError("The browser version of this driver only supports one trust " + - 'strategy, \'TRUST_CUSTOM_CA_SIGNED_CERTIFICATES\'. ' + config.trust + ' is not supported. Please ' + - "either use TRUST_CUSTOM_CA_SIGNED_CERTIFICATES or disable encryption by setting " + - "`encrypted:\"" + ENCRYPTION_OFF + "\"` in the driver configuration."); - return; - } + const {scheme, error} = determineWebSocketScheme(config, protocolSupplier); + if (error) { + this._error = error; + return; } this._ws = createWebSocket(scheme, config.url); @@ -114,10 +107,6 @@ class WebSocketChannel { } } - isEncrypted() { - return this._config.encrypted; - } - /** * Write the passed in buffer to connection * @param {HeapBuffer} buffer - Buffer to write @@ -233,4 +222,87 @@ function asWindowsFriendlyIPv6Address(scheme, parsedUrl) { return `${scheme}://${ipv6Host}:${parsedUrl.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) { + const encryptionOn = isEncryptionExplicitlyTurnedOn(config); + const encryptionOff = isEncryptionExplicitlyTurnedOff(config); + const trust = config.trust; + const 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_CUSTOM_CA_SIGNED_CERTIFICATES') { + // trust strategy not specified or the only supported strategy is specified + return {scheme: 'wss', error: null}; + } else { + const error = newError('The browser version of this driver only supports one trust ' + + 'strategy, \'TRUST_CUSTOM_CA_SIGNED_CERTIFICATES\'. ' + trust + ' is not supported. Please ' + + 'either use TRUST_CUSTOM_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) { + const protocol = typeof protocolSupplier === 'function' ? protocolSupplier() : ''; + return protocol && protocol.toLowerCase().indexOf('https') >= 0; +} + +function verifyEncryptionSettings(encryptionOn, encryptionOff, secureProtocol) { + 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 window && window.location ? window.location.protocol : null; +} + export default _websocketChannelModule diff --git a/src/v1/internal/connector.js b/src/v1/internal/connector.js index 6898165a3..15e9b44f4 100644 --- a/src/v1/internal/connector.js +++ b/src/v1/internal/connector.js @@ -397,10 +397,6 @@ class Connection { return !this._isBroken && this._ch._open; } - isEncrypted() { - return this._ch.isEncrypted(); - } - /** * Call close on the channel. * @param {function} cb - Function to call on close. diff --git a/test/internal/ch-config.test.js b/test/internal/ch-config.test.js index 48a274858..4543b8afa 100644 --- a/test/internal/ch-config.test.js +++ b/test/internal/ch-config.test.js @@ -76,7 +76,11 @@ describe('ChannelConfig', () => { it('should use encryption if available but not configured', () => { const config = new ChannelConfig(null, {}, ''); - expect(config.encrypted).toEqual(hasFeature('trust_all_certificates')); + if (hasFeature('trust_all_certificates')) { + expect(config.encrypted).toBeTruthy(); + } else { + expect(config.encrypted).toBeFalsy(); + } }); it('should use available trust conf when nothing configured', () => { diff --git a/test/internal/ch-websocket.test.js b/test/internal/ch-websocket.test.js index b544090ca..e5ec48944 100644 --- a/test/internal/ch-websocket.test.js +++ b/test/internal/ch-websocket.test.js @@ -19,8 +19,9 @@ import wsChannel from '../../src/v1/internal/ch-websocket'; import ChannelConfig from '../../src/v1/internal/ch-config'; import urlUtil from '../../src/v1/internal/url-util'; -import {SERVICE_UNAVAILABLE} from '../../src/v1/error'; +import {Neo4jError, SERVICE_UNAVAILABLE} from '../../src/v1/error'; import {setTimeoutMock} from './timers-util'; +import {ENCRYPTION_OFF, ENCRYPTION_ON} from '../../src/v1/internal/util'; describe('WebSocketChannel', () => { @@ -29,11 +30,16 @@ describe('WebSocketChannel', () => { let OriginalWebSocket; let webSocketChannel; + let originalConsoleWarn; beforeEach(() => { if (webSocketChannelAvailable) { OriginalWebSocket = WebSocket; } + originalConsoleWarn = console.warn; + console.warn = () => { + // mute by default + }; }); afterEach(() => { @@ -43,6 +49,7 @@ describe('WebSocketChannel', () => { if (webSocketChannel) { webSocketChannel.close(); } + console.warn = originalConsoleWarn; }); it('should fallback to literal IPv6 when SyntaxError is thrown', () => { @@ -94,6 +101,64 @@ describe('WebSocketChannel', () => { } }); + it('should select wss when running on https page', () => { + testWebSocketScheme('https:', {}, 'wss'); + }); + + it('should select ws when running on http page', () => { + testWebSocketScheme('http:', {}, 'ws'); + }); + + it('should select ws when running on https page but encryption turned off with boolean', () => { + testWebSocketScheme('https:', {encrypted: false}, 'ws'); + }); + + it('should select ws when running on https page but encryption turned off with string', () => { + testWebSocketScheme('https:', {encrypted: ENCRYPTION_OFF}, 'ws'); + }); + + it('should select wss when running on http page but encryption configured with boolean', () => { + testWebSocketScheme('http:', {encrypted: true}, 'wss'); + }); + + it('should select wss when running on http page but encryption configured with string', () => { + testWebSocketScheme('http:', {encrypted: ENCRYPTION_ON}, 'wss'); + }); + + it('should fail when encryption configured with unsupported trust strategy', () => { + if (!webSocketChannelAvailable) { + return; + } + + const protocolSupplier = () => 'http:'; + + WebSocket = () => { + return { + close: () => { + } + }; + }; + + const url = urlUtil.parseDatabaseUrl('bolt://localhost:8989'); + const driverConfig = {encrypted: true, trust: 'TRUST_ON_FIRST_USE'}; + const channelConfig = new ChannelConfig(url, driverConfig, SERVICE_UNAVAILABLE); + + const channel = new WebSocketChannel(channelConfig, protocolSupplier); + + expect(channel._error).toBeDefined(); + expect(channel._error.name).toEqual('Neo4jError'); + }); + + it('should generate a warning when encryption turned on for HTTP web page', () => { + testWarningInMixedEnvironment(true, 'http'); + testWarningInMixedEnvironment(ENCRYPTION_ON, 'http'); + }); + + it('should generate a warning when encryption turned off for HTTPS web page', () => { + testWarningInMixedEnvironment(false, 'https'); + testWarningInMixedEnvironment(ENCRYPTION_OFF, 'https'); + }); + function testFallbackToLiteralIPv6(boltAddress, expectedWsAddress) { if (!webSocketChannelAvailable) { return; @@ -121,4 +186,55 @@ describe('WebSocketChannel', () => { expect(webSocketChannel._ws.url).toEqual(expectedWsAddress); } + function testWebSocketScheme(windowLocationProtocol, driverConfig, expectedScheme) { + if (!webSocketChannelAvailable) { + return; + } + + const protocolSupplier = () => windowLocationProtocol; + + // replace real WebSocket with a function that memorizes the url + WebSocket = url => { + return { + url: url, + close: () => { + } + }; + }; + + const url = urlUtil.parseDatabaseUrl('bolt://localhost:8989'); + const channelConfig = new ChannelConfig(url, driverConfig, SERVICE_UNAVAILABLE); + const channel = new WebSocketChannel(channelConfig, protocolSupplier); + + expect(channel._ws.url).toEqual(expectedScheme + '://localhost:8989'); + } + + function testWarningInMixedEnvironment(encrypted, scheme) { + if (!webSocketChannelAvailable) { + return; + } + + // replace real WebSocket with a function that memorizes the url + WebSocket = url => { + return { + url: url, + close: () => { + } + }; + }; + + // replace console.warn with a function that memorizes the message + const warnMessages = []; + console.warn = message => warnMessages.push(message); + + const url = urlUtil.parseDatabaseUrl('bolt://localhost:8989'); + const config = new ChannelConfig(url, {encrypted: encrypted}, SERVICE_UNAVAILABLE); + const protocolSupplier = () => scheme + ':'; + + const channel = new WebSocketChannel(config, protocolSupplier); + + expect(channel).toBeDefined(); + expect(warnMessages.length).toEqual(1); + } + });