Skip to content

Automatically use secure WebSocket on HTTPS pages #371

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 23, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/v1/internal/ch-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
5 changes: 0 additions & 5 deletions src/v1/internal/ch-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -362,10 +361,6 @@ class NodeChannel {
}
}

isEncrypted() {
return this._encrypted;
}

/**
* Write the passed in buffer to connection
* @param {NodeBuffer} buffer - Buffer to write
Expand Down
106 changes: 89 additions & 17 deletions src/v1/internal/ch-websocket.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,27 +29,20 @@ 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 = [];
this._error = null;
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);
Expand Down Expand Up @@ -114,10 +107,6 @@ class WebSocketChannel {
}
}

isEncrypted() {
return this._config.encrypted;
}

/**
* Write the passed in buffer to connection
* @param {HeapBuffer} buffer - Buffer to write
Expand Down Expand Up @@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the information given in Security Considerations section here, it may make sense generating warning message about mixing protocols may not work - if encryption is explicitly turned off or on (i.e. https/ws or http/wss).

Copy link
Contributor Author

@lutovich lutovich May 17, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a warn print in c623d8a

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} <code>true</code> if encryption enabled in the config, <code>false</code> otherwise.
*/
function isEncryptionExplicitlyTurnedOn(config) {
return config.encrypted === true || config.encrypted === ENCRYPTION_ON;
}

/**
* @param {ChannelConfig} config - configuration for the channel.
* @return {boolean} <code>true</code> if encryption disabled in the config, <code>false</code> 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} <code>true</code> if protocol returned by the given function is secure, <code>false</code> 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
4 changes: 0 additions & 4 deletions src/v1/internal/connector.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 5 additions & 1 deletion test/internal/ch-config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
118 changes: 117 additions & 1 deletion test/internal/ch-websocket.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {

Expand All @@ -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(() => {
Expand All @@ -43,6 +49,7 @@ describe('WebSocketChannel', () => {
if (webSocketChannel) {
webSocketChannel.close();
}
console.warn = originalConsoleWarn;
});

it('should fallback to literal IPv6 when SyntaxError is thrown', () => {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}

});