Skip to content

Commit f55184a

Browse files
authored
Merge pull request #371 from lutovich/1.7-wss-in-https
Automatically use secure WebSocket on HTTPS pages
2 parents 5012b7f + c623d8a commit f55184a

File tree

6 files changed

+216
-30
lines changed

6 files changed

+216
-30
lines changed

src/v1/internal/ch-config.js

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

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

5154
function extractTrust(driverConfig) {

src/v1/internal/ch-node.js

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

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

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

src/v1/internal/ch-websocket.js

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

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

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

5649
this._ws = createWebSocket(scheme, config.url);
@@ -115,10 +108,6 @@ class WebSocketChannel {
115108
}
116109
}
117110

118-
isEncrypted() {
119-
return this._config.encrypted;
120-
}
121-
122111
/**
123112
* Write the passed in buffer to connection
124113
* @param {HeapBuffer} buffer - Buffer to write
@@ -234,4 +223,87 @@ function asWindowsFriendlyIPv6Address(scheme, parsedUrl) {
234223
return `${scheme}://${ipv6Host}:${parsedUrl.port}`;
235224
}
236225

226+
/**
227+
* @param {ChannelConfig} config - configuration for the channel.
228+
* @param {function(): string} protocolSupplier - function that detects protocol of the web page.
229+
* @return {{scheme: string|null, error: Neo4jError|null}} object containing either scheme or error.
230+
*/
231+
function determineWebSocketScheme(config, protocolSupplier) {
232+
const encryptionOn = isEncryptionExplicitlyTurnedOn(config);
233+
const encryptionOff = isEncryptionExplicitlyTurnedOff(config);
234+
const trust = config.trust;
235+
const secureProtocol = isProtocolSecure(protocolSupplier);
236+
verifyEncryptionSettings(encryptionOn, encryptionOff, secureProtocol);
237+
238+
if (encryptionOff) {
239+
// encryption explicitly turned off in the config
240+
return {scheme: 'ws', error: null};
241+
}
242+
243+
if (secureProtocol) {
244+
// driver is used in a secure https web page, use 'wss'
245+
return {scheme: 'wss', error: null};
246+
}
247+
248+
if (encryptionOn) {
249+
// encryption explicitly requested in the config
250+
if (!trust || trust === 'TRUST_CUSTOM_CA_SIGNED_CERTIFICATES') {
251+
// trust strategy not specified or the only supported strategy is specified
252+
return {scheme: 'wss', error: null};
253+
} else {
254+
const error = newError('The browser version of this driver only supports one trust ' +
255+
'strategy, \'TRUST_CUSTOM_CA_SIGNED_CERTIFICATES\'. ' + trust + ' is not supported. Please ' +
256+
'either use TRUST_CUSTOM_CA_SIGNED_CERTIFICATES or disable encryption by setting ' +
257+
'`encrypted:"' + ENCRYPTION_OFF + '"` in the driver configuration.');
258+
return {scheme: null, error: error};
259+
}
260+
}
261+
262+
// default to unencrypted web socket
263+
return {scheme: 'ws', error: null};
264+
}
265+
266+
/**
267+
* @param {ChannelConfig} config - configuration for the channel.
268+
* @return {boolean} <code>true</code> if encryption enabled in the config, <code>false</code> otherwise.
269+
*/
270+
function isEncryptionExplicitlyTurnedOn(config) {
271+
return config.encrypted === true || config.encrypted === ENCRYPTION_ON;
272+
}
273+
274+
/**
275+
* @param {ChannelConfig} config - configuration for the channel.
276+
* @return {boolean} <code>true</code> if encryption disabled in the config, <code>false</code> otherwise.
277+
*/
278+
function isEncryptionExplicitlyTurnedOff(config) {
279+
return config.encrypted === false || config.encrypted === ENCRYPTION_OFF;
280+
}
281+
282+
/**
283+
* @param {function(): string} protocolSupplier - function that detects protocol of the web page.
284+
* @return {boolean} <code>true</code> if protocol returned by the given function is secure, <code>false</code> otherwise.
285+
*/
286+
function isProtocolSecure(protocolSupplier) {
287+
const protocol = typeof protocolSupplier === 'function' ? protocolSupplier() : '';
288+
return protocol && protocol.toLowerCase().indexOf('https') >= 0;
289+
}
290+
291+
function verifyEncryptionSettings(encryptionOn, encryptionOff, secureProtocol) {
292+
if (encryptionOn && !secureProtocol) {
293+
// encryption explicitly turned on for a driver used on a HTTP web page
294+
console.warn('Neo4j driver is configured to use secure WebSocket on a HTTP web page. ' +
295+
'WebSockets might not work in a mixed content environment. ' +
296+
'Please consider configuring driver to not use encryption.');
297+
} else if (encryptionOff && secureProtocol) {
298+
// encryption explicitly turned off for a driver used on a HTTPS web page
299+
console.warn('Neo4j driver is configured to use insecure WebSocket on a HTTPS web page. ' +
300+
'WebSockets might not work in a mixed content environment. ' +
301+
'Please consider configuring driver to use encryption.');
302+
}
303+
}
304+
305+
function detectWebPageProtocol() {
306+
return window && window.location ? window.location.protocol : null;
307+
}
308+
237309
export default _websocketChannelModule

src/v1/internal/connector.js

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

401-
isEncrypted() {
402-
return this._ch.isEncrypted();
403-
}
404-
405401
/**
406402
* Call close on the channel.
407403
* @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
@@ -77,7 +77,11 @@ describe('ChannelConfig', () => {
7777
it('should use encryption if available but not configured', () => {
7878
const config = new ChannelConfig(null, {}, '');
7979

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

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

test/internal/ch-websocket.test.js

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@
2020
import wsChannel from '../../src/v1/internal/ch-websocket';
2121
import ChannelConfig from '../../src/v1/internal/ch-config';
2222
import urlUtil from '../../src/v1/internal/url-util';
23-
import {SERVICE_UNAVAILABLE} from '../../src/v1/error';
23+
import {Neo4jError, SERVICE_UNAVAILABLE} from '../../src/v1/error';
2424
import {setTimeoutMock} from './timers-util';
25+
import {ENCRYPTION_OFF, ENCRYPTION_ON} from '../../src/v1/internal/util';
2526

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

@@ -30,11 +31,16 @@ describe('WebSocketChannel', () => {
3031

3132
let OriginalWebSocket;
3233
let webSocketChannel;
34+
let originalConsoleWarn;
3335

3436
beforeEach(() => {
3537
if (webSocketChannelAvailable) {
3638
OriginalWebSocket = WebSocket;
3739
}
40+
originalConsoleWarn = console.warn;
41+
console.warn = () => {
42+
// mute by default
43+
};
3844
});
3945

4046
afterEach(() => {
@@ -44,6 +50,7 @@ describe('WebSocketChannel', () => {
4450
if (webSocketChannel) {
4551
webSocketChannel.close();
4652
}
53+
console.warn = originalConsoleWarn;
4754
});
4855

4956
it('should fallback to literal IPv6 when SyntaxError is thrown', () => {
@@ -95,6 +102,64 @@ describe('WebSocketChannel', () => {
95102
}
96103
});
97104

105+
it('should select wss when running on https page', () => {
106+
testWebSocketScheme('https:', {}, 'wss');
107+
});
108+
109+
it('should select ws when running on http page', () => {
110+
testWebSocketScheme('http:', {}, 'ws');
111+
});
112+
113+
it('should select ws when running on https page but encryption turned off with boolean', () => {
114+
testWebSocketScheme('https:', {encrypted: false}, 'ws');
115+
});
116+
117+
it('should select ws when running on https page but encryption turned off with string', () => {
118+
testWebSocketScheme('https:', {encrypted: ENCRYPTION_OFF}, 'ws');
119+
});
120+
121+
it('should select wss when running on http page but encryption configured with boolean', () => {
122+
testWebSocketScheme('http:', {encrypted: true}, 'wss');
123+
});
124+
125+
it('should select wss when running on http page but encryption configured with string', () => {
126+
testWebSocketScheme('http:', {encrypted: ENCRYPTION_ON}, 'wss');
127+
});
128+
129+
it('should fail when encryption configured with unsupported trust strategy', () => {
130+
if (!webSocketChannelAvailable) {
131+
return;
132+
}
133+
134+
const protocolSupplier = () => 'http:';
135+
136+
WebSocket = () => {
137+
return {
138+
close: () => {
139+
}
140+
};
141+
};
142+
143+
const url = urlUtil.parseDatabaseUrl('bolt://localhost:8989');
144+
const driverConfig = {encrypted: true, trust: 'TRUST_ON_FIRST_USE'};
145+
const channelConfig = new ChannelConfig(url, driverConfig, SERVICE_UNAVAILABLE);
146+
147+
const channel = new WebSocketChannel(channelConfig, protocolSupplier);
148+
149+
expect(channel._error).toBeDefined();
150+
expect(channel._error.name).toEqual('Neo4jError');
151+
});
152+
153+
it('should generate a warning when encryption turned on for HTTP web page', () => {
154+
testWarningInMixedEnvironment(true, 'http');
155+
testWarningInMixedEnvironment(ENCRYPTION_ON, 'http');
156+
});
157+
158+
it('should generate a warning when encryption turned off for HTTPS web page', () => {
159+
testWarningInMixedEnvironment(false, 'https');
160+
testWarningInMixedEnvironment(ENCRYPTION_OFF, 'https');
161+
});
162+
98163
function testFallbackToLiteralIPv6(boltAddress, expectedWsAddress) {
99164
if (!webSocketChannelAvailable) {
100165
return;
@@ -122,4 +187,55 @@ describe('WebSocketChannel', () => {
122187
expect(webSocketChannel._ws.url).toEqual(expectedWsAddress);
123188
}
124189

190+
function testWebSocketScheme(windowLocationProtocol, driverConfig, expectedScheme) {
191+
if (!webSocketChannelAvailable) {
192+
return;
193+
}
194+
195+
const protocolSupplier = () => windowLocationProtocol;
196+
197+
// replace real WebSocket with a function that memorizes the url
198+
WebSocket = url => {
199+
return {
200+
url: url,
201+
close: () => {
202+
}
203+
};
204+
};
205+
206+
const url = urlUtil.parseDatabaseUrl('bolt://localhost:8989');
207+
const channelConfig = new ChannelConfig(url, driverConfig, SERVICE_UNAVAILABLE);
208+
const channel = new WebSocketChannel(channelConfig, protocolSupplier);
209+
210+
expect(channel._ws.url).toEqual(expectedScheme + '://localhost:8989');
211+
}
212+
213+
function testWarningInMixedEnvironment(encrypted, scheme) {
214+
if (!webSocketChannelAvailable) {
215+
return;
216+
}
217+
218+
// replace real WebSocket with a function that memorizes the url
219+
WebSocket = url => {
220+
return {
221+
url: url,
222+
close: () => {
223+
}
224+
};
225+
};
226+
227+
// replace console.warn with a function that memorizes the message
228+
const warnMessages = [];
229+
console.warn = message => warnMessages.push(message);
230+
231+
const url = urlUtil.parseDatabaseUrl('bolt://localhost:8989');
232+
const config = new ChannelConfig(url, {encrypted: encrypted}, SERVICE_UNAVAILABLE);
233+
const protocolSupplier = () => scheme + ':';
234+
235+
const channel = new WebSocketChannel(config, protocolSupplier);
236+
237+
expect(channel).toBeDefined();
238+
expect(warnMessages.length).toEqual(1);
239+
}
240+
125241
});

0 commit comments

Comments
 (0)