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 1 commit
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
65 changes: 48 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,46 @@ 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 encrypted = config.encrypted;
const trust = config.trust;

if (encrypted === false || encrypted === ENCRYPTION_OFF) {
// encryption explicitly turned off in the config
return {scheme: 'ws', error: null};
}

const protocol = typeof protocolSupplier === 'function' ? protocolSupplier() : '';
if (protocol && protocol.toLowerCase().indexOf('https') >= 0) {
// driver is used in a secure https web page, use 'wss'
return {scheme: 'wss', error: null};
}

if (encrypted === true || encrypted === ENCRYPTION_ON) {
// encryption explicitly requested in the config
if (!trust || trust === 'TRUST_CUSTOM_CA_SIGNED_CERTIFICATES') {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is totally out of scope of this PR, but I find TRUST_CUSTOM_CA_SIGNED_CERTIFICATES setting to be confusing and would prefer TRUST_SYSTEM_CA_SIGNED_CERTIFICATES as the default. We may consider revising naming / behaviour in upcoming releases?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll create a card for this

// 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};
}

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
74 changes: 73 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 Down Expand Up @@ -94,6 +95,54 @@ 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');
});

function testFallbackToLiteralIPv6(boltAddress, expectedWsAddress) {
if (!webSocketChannelAvailable) {
return;
Expand Down Expand Up @@ -121,4 +170,27 @@ 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');
}

});