Skip to content

Commit e8b4bcd

Browse files
committed
Automatically use secure WebSocket on HTTPS pages
Previously, driver used in browser used secure WebSocket with 'wss' scheme only when encryption was explicitly turned on in the driver config. On HTTP web pages it is perfectly fine to use either plane or secure WebSocket ('ws' and 'wss' respectively). On HTTPS web pages only 'wss' is allowed. So explicit `{encrypted: true}` configuration was always required for drivers used on HTTPS web pages. This commit makes driver automatically use secure WebSocket when driver is used on a HTTPS web page.
1 parent bfa41c1 commit e8b4bcd

File tree

6 files changed

+131
-30
lines changed

6 files changed

+131
-30
lines changed

src/v1/internal/ch-config.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,12 @@ export default class ChannelConfig {
4242

4343
function extractEncrypted(driverConfig) {
4444
// check if encryption was configured by the user, use explicit null check because we permit boolean value
45-
const encryptionConfigured = driverConfig.encrypted == null;
45+
const encryptionNotConfigured = driverConfig.encrypted == null;
4646
// default to using encryption if trust-all-certificates is available
47-
return encryptionConfigured ? hasFeature('trust_all_certificates') : driverConfig.encrypted;
47+
if (encryptionNotConfigured && hasFeature('trust_all_certificates')) {
48+
return true;
49+
}
50+
return driverConfig.encrypted;
4851
}
4952

5053
function extractTrust(driverConfig) {

src/v1/internal/ch-node.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,6 @@ class NodeChannel {
297297
this._handleConnectionTerminated = this._handleConnectionTerminated.bind(this);
298298
this._connectionErrorCode = config.connectionErrorCode;
299299

300-
this._encrypted = config.encrypted;
301300
this._conn = connect(config, () => {
302301
if(!self._open) {
303302
return;
@@ -362,10 +361,6 @@ class NodeChannel {
362361
}
363362
}
364363

365-
isEncrypted() {
366-
return this._encrypted;
367-
}
368-
369364
/**
370365
* Write the passed in buffer to connection
371366
* @param {NodeBuffer} buffer - Buffer to write

src/v1/internal/ch-websocket.js

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,27 +29,20 @@ class WebSocketChannel {
2929
/**
3030
* Create new instance
3131
* @param {ChannelConfig} config - configuration for this channel.
32+
* @param {function(): string} protocolSupplier - function that detects protocol of the web page. Should only be used in tests.
3233
*/
33-
constructor(config) {
34+
constructor(config, protocolSupplier = detectWebPageProtocol) {
3435

3536
this._open = true;
3637
this._pending = [];
3738
this._error = null;
3839
this._handleConnectionError = this._handleConnectionError.bind(this);
3940
this._config = config;
4041

41-
let scheme = "ws";
42-
//Allow boolean for backwards compatibility
43-
if (config.encrypted === true || config.encrypted === ENCRYPTION_ON) {
44-
if ((!config.trust) || config.trust === 'TRUST_CUSTOM_CA_SIGNED_CERTIFICATES') {
45-
scheme = "wss";
46-
} else {
47-
this._error = newError("The browser version of this driver only supports one trust " +
48-
'strategy, \'TRUST_CUSTOM_CA_SIGNED_CERTIFICATES\'. ' + config.trust + ' is not supported. Please ' +
49-
"either use TRUST_CUSTOM_CA_SIGNED_CERTIFICATES or disable encryption by setting " +
50-
"`encrypted:\"" + ENCRYPTION_OFF + "\"` in the driver configuration.");
51-
return;
52-
}
42+
const {scheme, error} = determineWebSocketScheme(config, protocolSupplier);
43+
if (error) {
44+
this._error = error;
45+
return;
5346
}
5447

5548
this._ws = createWebSocket(scheme, config.url);
@@ -114,10 +107,6 @@ class WebSocketChannel {
114107
}
115108
}
116109

117-
isEncrypted() {
118-
return this._config.encrypted;
119-
}
120-
121110
/**
122111
* Write the passed in buffer to connection
123112
* @param {HeapBuffer} buffer - Buffer to write
@@ -233,4 +222,46 @@ function asWindowsFriendlyIPv6Address(scheme, parsedUrl) {
233222
return `${scheme}://${ipv6Host}:${parsedUrl.port}`;
234223
}
235224

225+
/**
226+
* @param {ChannelConfig} config - configuration for the channel.
227+
* @param {function(): string} protocolSupplier - function that detects protocol of the web page.
228+
* @return {{scheme: string|null, error: Neo4jError|null}} object containing either scheme or error.
229+
*/
230+
function determineWebSocketScheme(config, protocolSupplier) {
231+
const encrypted = config.encrypted;
232+
const trust = config.trust;
233+
234+
if (encrypted === false || encrypted === ENCRYPTION_OFF) {
235+
// encryption explicitly turned off in the config
236+
return {scheme: 'ws', error: null};
237+
}
238+
239+
const protocol = typeof protocolSupplier === 'function' ? protocolSupplier() : '';
240+
if (protocol && protocol.toLowerCase().indexOf('https') >= 0) {
241+
// driver is used in a secure https web page, use 'wss'
242+
return {scheme: 'wss', error: null};
243+
}
244+
245+
if (encrypted === true || encrypted === ENCRYPTION_ON) {
246+
// encryption explicitly requested in the config
247+
if (!trust || trust === 'TRUST_CUSTOM_CA_SIGNED_CERTIFICATES') {
248+
// trust strategy not specified or the only supported strategy is specified
249+
return {scheme: 'wss', error: null};
250+
} else {
251+
const error = newError('The browser version of this driver only supports one trust ' +
252+
'strategy, \'TRUST_CUSTOM_CA_SIGNED_CERTIFICATES\'. ' + trust + ' is not supported. Please ' +
253+
'either use TRUST_CUSTOM_CA_SIGNED_CERTIFICATES or disable encryption by setting ' +
254+
'`encrypted:"' + ENCRYPTION_OFF + '"` in the driver configuration.');
255+
return {scheme: null, error: error};
256+
}
257+
}
258+
259+
// default to unencrypted web socket
260+
return {scheme: 'ws', error: null};
261+
}
262+
263+
function detectWebPageProtocol() {
264+
return window && window.location ? window.location.protocol : null;
265+
}
266+
236267
export default _websocketChannelModule

src/v1/internal/connector.js

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -397,10 +397,6 @@ class Connection {
397397
return !this._isBroken && this._ch._open;
398398
}
399399

400-
isEncrypted() {
401-
return this._ch.isEncrypted();
402-
}
403-
404400
/**
405401
* Call close on the channel.
406402
* @param {function} cb - Function to call on close.

test/internal/ch-config.test.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,11 @@ describe('ChannelConfig', () => {
7676
it('should use encryption if available but not configured', () => {
7777
const config = new ChannelConfig(null, {}, '');
7878

79-
expect(config.encrypted).toEqual(hasFeature('trust_all_certificates'));
79+
if (hasFeature('trust_all_certificates')) {
80+
expect(config.encrypted).toBeTruthy();
81+
} else {
82+
expect(config.encrypted).toBeFalsy();
83+
}
8084
});
8185

8286
it('should use available trust conf when nothing configured', () => {

test/internal/ch-websocket.test.js

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@
1919
import wsChannel from '../../src/v1/internal/ch-websocket';
2020
import ChannelConfig from '../../src/v1/internal/ch-config';
2121
import urlUtil from '../../src/v1/internal/url-util';
22-
import {SERVICE_UNAVAILABLE} from '../../src/v1/error';
22+
import {Neo4jError, SERVICE_UNAVAILABLE} from '../../src/v1/error';
2323
import {setTimeoutMock} from './timers-util';
24+
import {ENCRYPTION_OFF, ENCRYPTION_ON} from '../../src/v1/internal/util';
2425

2526
describe('WebSocketChannel', () => {
2627

@@ -94,6 +95,54 @@ describe('WebSocketChannel', () => {
9495
}
9596
});
9697

98+
it('should select wss when running on https page', () => {
99+
testWebSocketScheme('https:', {}, 'wss');
100+
});
101+
102+
it('should select ws when running on http page', () => {
103+
testWebSocketScheme('http:', {}, 'ws');
104+
});
105+
106+
it('should select ws when running on https page but encryption turned off with boolean', () => {
107+
testWebSocketScheme('https:', {encrypted: false}, 'ws');
108+
});
109+
110+
it('should select ws when running on https page but encryption turned off with string', () => {
111+
testWebSocketScheme('https:', {encrypted: ENCRYPTION_OFF}, 'ws');
112+
});
113+
114+
it('should select wss when running on http page but encryption configured with boolean', () => {
115+
testWebSocketScheme('http:', {encrypted: true}, 'wss');
116+
});
117+
118+
it('should select wss when running on http page but encryption configured with string', () => {
119+
testWebSocketScheme('http:', {encrypted: ENCRYPTION_ON}, 'wss');
120+
});
121+
122+
it('should fail when encryption configured with unsupported trust strategy', () => {
123+
if (!webSocketChannelAvailable) {
124+
return;
125+
}
126+
127+
const protocolSupplier = () => 'http:';
128+
129+
WebSocket = () => {
130+
return {
131+
close: () => {
132+
}
133+
};
134+
};
135+
136+
const url = urlUtil.parseDatabaseUrl('bolt://localhost:8989');
137+
const driverConfig = {encrypted: true, trust: 'TRUST_ON_FIRST_USE'};
138+
const channelConfig = new ChannelConfig(url, driverConfig, SERVICE_UNAVAILABLE);
139+
140+
const channel = new WebSocketChannel(channelConfig, protocolSupplier);
141+
142+
expect(channel._error).toBeDefined();
143+
expect(channel._error.name).toEqual('Neo4jError');
144+
});
145+
97146
function testFallbackToLiteralIPv6(boltAddress, expectedWsAddress) {
98147
if (!webSocketChannelAvailable) {
99148
return;
@@ -121,4 +170,27 @@ describe('WebSocketChannel', () => {
121170
expect(webSocketChannel._ws.url).toEqual(expectedWsAddress);
122171
}
123172

173+
function testWebSocketScheme(windowLocationProtocol, driverConfig, expectedScheme) {
174+
if (!webSocketChannelAvailable) {
175+
return;
176+
}
177+
178+
const protocolSupplier = () => windowLocationProtocol;
179+
180+
// replace real WebSocket with a function that memorizes the url
181+
WebSocket = url => {
182+
return {
183+
url: url,
184+
close: () => {
185+
}
186+
};
187+
};
188+
189+
const url = urlUtil.parseDatabaseUrl('bolt://localhost:8989');
190+
const channelConfig = new ChannelConfig(url, driverConfig, SERVICE_UNAVAILABLE);
191+
const channel = new WebSocketChannel(channelConfig, protocolSupplier);
192+
193+
expect(channel._ws.url).toEqual(expectedScheme + '://localhost:8989');
194+
}
195+
124196
});

0 commit comments

Comments
 (0)