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);
+ }
+
});