Skip to content

IPv6 support #317

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 14 commits into from
Jan 11, 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
6 changes: 6 additions & 0 deletions gulpfile.babel.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,12 @@ gulp.task('run-browser-test-edge', function(cb){
}, cb).start();
});

gulp.task('run-browser-test-ie', function (cb) {
new karmaServer({
configFile: __dirname + '/test/browser/karma-ie.conf.js',
}, cb).start();
});

gulp.task('watch', function () {
return watch('src/**/*.js', batch(function (events, done) {
gulp.start('all', done);
Expand Down
37 changes: 25 additions & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
"vinyl-source-stream": "^1.1.0"
},
"dependencies": {
"babel-runtime": "^6.18.0"
"babel-runtime": "^6.18.0",
"url-parse": "^1.2.0"
}
}
24 changes: 12 additions & 12 deletions src/v1/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ import Record from './record';
import {Driver, READ, WRITE} from './driver';
import RoutingDriver from './routing-driver';
import VERSION from '../version';
import {assertString, isEmptyObjectOrNull, parseRoutingContext, parseScheme, parseUrl} from './internal/util';
import {assertString, isEmptyObjectOrNull} from './internal/util';
import urlUtil from './internal/url-util';

/**
* @property {function(username: string, password: string, realm: ?string)} basic the function to create a
Expand Down Expand Up @@ -152,9 +153,9 @@ const USER_AGENT = "neo4j-javascript/" + VERSION;
* // version.
* loadBalancingStrategy: "least_connected" | "round_robin",
*
* // Specify socket connection timeout in milliseconds. Non-numeric, negative and zero values are treated as an
* // infinite timeout. Connection will be then bound by the timeout configured on the operating system level.
* // Timeout value should be numeric and greater or equal to zero. Default value is 5000 which is 5 seconds.
* // Specify socket connection timeout in milliseconds. Numeric values are expected. Negative and zero values
* // result in no timeout being applied. Connection establishment will be then bound by the timeout configured
* // on the operating system level. Default value is 5000, which is 5 seconds.
* connectionTimeout: 5000, // 5 seconds
* }
*
Expand All @@ -165,17 +166,16 @@ const USER_AGENT = "neo4j-javascript/" + VERSION;
*/
function driver(url, authToken, config = {}) {
assertString(url, 'Bolt URL');
const scheme = parseScheme(url);
const routingContext = parseRoutingContext(url);
if (scheme === 'bolt+routing://') {
return new RoutingDriver(parseUrl(url), routingContext, USER_AGENT, authToken, config);
} else if (scheme === 'bolt://') {
if (!isEmptyObjectOrNull(routingContext)) {
const parsedUrl = urlUtil.parseBoltUrl(url);
if (parsedUrl.scheme === 'bolt+routing') {
return new RoutingDriver(parsedUrl.hostAndPort, parsedUrl.query, USER_AGENT, authToken, config);
} else if (parsedUrl.scheme === 'bolt') {
if (!isEmptyObjectOrNull(parsedUrl.query)) {
throw new Error(`Parameters are not supported with scheme 'bolt'. Given URL: '${url}'`);
}
return new Driver(parseUrl(url), USER_AGENT, authToken, config);
return new Driver(parsedUrl.hostAndPort, USER_AGENT, authToken, config);
} else {
throw new Error(`Unknown scheme: ${scheme}`);
throw new Error(`Unknown scheme: ${parsedUrl.scheme}`);
}
}

Expand Down
26 changes: 21 additions & 5 deletions src/v1/internal/ch-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,18 @@ import {SERVICE_UNAVAILABLE} from '../error';

const DEFAULT_CONNECTION_TIMEOUT_MILLIS = 5000; // 5 seconds by default

export const DEFAULT_PORT = 7687;

export default class ChannelConfig {

constructor(host, port, driverConfig, connectionErrorCode) {
this.host = host;
this.port = port;
/**
* @constructor
* @param {Url} url the URL for the channel to connect to.
* @param {object} driverConfig the driver config provided by the user when driver is created.
* @param {string} connectionErrorCode the default error code to use on connection errors.
*/
constructor(url, driverConfig, connectionErrorCode) {
this.url = url;
this.encrypted = extractEncrypted(driverConfig);
this.trust = extractTrust(driverConfig);
this.trustedCertificates = extractTrustedCertificates(driverConfig);
Expand Down Expand Up @@ -61,8 +68,17 @@ function extractKnownHostsPath(driverConfig) {

function extractConnectionTimeout(driverConfig) {
const configuredTimeout = parseInt(driverConfig.connectionTimeout, 10);
if (!configuredTimeout || configuredTimeout < 0) {
if (configuredTimeout === 0) {
// timeout explicitly configured to 0
return null;
} else if (configuredTimeout && configuredTimeout < 0) {
// timeout explicitly configured to a negative value
return null;
} else if (!configuredTimeout) {
// timeout not configured, use default value
return DEFAULT_CONNECTION_TIMEOUT_MILLIS;
} else {
// timeout configured, use the provided value
return configuredTimeout;
}
return configuredTimeout;
}
12 changes: 6 additions & 6 deletions src/v1/internal/ch-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ const TrustStrategy = {
rejectUnauthorized: false
};

let socket = tls.connect(config.port, config.host, tlsOpts, function () {
let socket = tls.connect(config.url.port, config.url.host, tlsOpts, function () {
if (!socket.authorized) {
onFailure(newError("Server certificate is not trusted. If you trust the database you are connecting to, add" +
" the signing certificate, or the server certificate, to the list of certificates trusted by this driver" +
Expand All @@ -152,7 +152,7 @@ const TrustStrategy = {
// a more helpful error to the user
rejectUnauthorized: false
};
let socket = tls.connect(config.port, config.host, tlsOpts, function () {
let socket = tls.connect(config.url.port, config.url.host, tlsOpts, function () {
if (!socket.authorized) {
onFailure(newError("Server certificate is not trusted. If you trust the database you are connecting to, use " +
"TRUST_CUSTOM_CA_SIGNED_CERTIFICATES and add" +
Expand Down Expand Up @@ -180,7 +180,7 @@ const TrustStrategy = {
rejectUnauthorized: false
};

let socket = tls.connect(config.port, config.host, tlsOpts, function () {
let socket = tls.connect(config.url.port, config.url.host, tlsOpts, function () {
var serverCert = socket.getPeerCertificate(/*raw=*/true);

if( !serverCert.raw ) {
Expand All @@ -197,7 +197,7 @@ const TrustStrategy = {

const serverFingerprint = require('crypto').createHash('sha512').update(serverCert.raw).digest("hex");
const knownHostsPath = config.knownHostsPath || path.join(userHome(), ".neo4j", "known_hosts");
const serverId = config.host + ":" + config.port;
const serverId = config.url.hostAndPort;

loadFingerprint(serverId, knownHostsPath, (knownFingerprint) => {
if( knownFingerprint === serverFingerprint ) {
Expand Down Expand Up @@ -232,7 +232,7 @@ const TrustStrategy = {
const tlsOpts = {
rejectUnauthorized: false
};
const socket = tls.connect(config.port, config.host, tlsOpts, function () {
const socket = tls.connect(config.url.port, config.url.host, tlsOpts, function () {
const certificate = socket.getPeerCertificate();
if (isEmptyObjectOrNull(certificate)) {
onFailure(newError("Secure connection was successful but server did not return any valid " +
Expand All @@ -259,7 +259,7 @@ const TrustStrategy = {
function connect( config, onSuccess, onFailure=(()=>null) ) {
//still allow boolean for backwards compatibility
if (config.encrypted === false || config.encrypted === ENCRYPTION_OFF) {
var conn = net.connect(config.port, config.host, onSuccess);
var conn = net.connect(config.url.port, config.url.host, onSuccess);
conn.on('error', onFailure);
return conn;
} else if( TrustStrategy[config.trust]) {
Expand Down
75 changes: 70 additions & 5 deletions src/v1/internal/ch-websocket.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ class WebSocketChannel {
return;
}
}
this._url = scheme + '://' + config.host + ':' + config.port;
this._ws = new WebSocket(this._url);

this._ws = createWebSocket(scheme, config.url);
this._ws.binaryType = "arraybuffer";

let self = this;
Expand All @@ -65,8 +65,8 @@ class WebSocketChannel {
}
};
this._ws.onopen = function() {
// Connected! Cancel connection timeout
clearTimeout(self._connectionTimeoutId);
// Connected! Cancel the connection timeout
self._clearConnectionTimeout();

// Drain all pending messages
let pending = self._pending;
Expand All @@ -85,7 +85,7 @@ class WebSocketChannel {
this._ws.onerror = this._handleConnectionError;

this._connectionTimeoutFired = false;
this._connectionTimeoutId = this._setupConnectionTimeout(config);
this._connectionTimeoutId = this._setupConnectionTimeout();
}

_handleConnectionError() {
Expand Down Expand Up @@ -141,6 +141,7 @@ class WebSocketChannel {
*/
close ( cb = ( () => null )) {
this._open = false;
this._clearConnectionTimeout();
this._ws.close();
this._ws.onclose = cb;
}
Expand All @@ -164,9 +165,73 @@ class WebSocketChannel {
}
return null;
}

/**
* Remove active connection timeout, if any.
* @private
*/
_clearConnectionTimeout() {
const timeoutId = this._connectionTimeoutId;
if (timeoutId || timeoutId === 0) {
this._connectionTimeoutFired = false;
this._connectionTimeoutId = null;
clearTimeout(timeoutId);
}
}
}

let available = typeof WebSocket !== 'undefined';
let _websocketChannelModule = {channel: WebSocketChannel, available: available};

function createWebSocket(scheme, parsedUrl) {
const url = scheme + '://' + parsedUrl.hostAndPort;

try {
return new WebSocket(url);
} catch (error) {
if (isIPv6AddressIssueOnWindows(error, parsedUrl)) {

// WebSocket in IE and Edge browsers on Windows do not support regular IPv6 address syntax because they contain ':'.
// It's an invalid character for UNC (https://en.wikipedia.org/wiki/IPv6_address#Literal_IPv6_addresses_in_UNC_path_names)
// and Windows requires IPv6 to be changes in the following way:
// 1) replace all ':' with '-'
// 2) replace '%' with 's' for link-local address
// 3) append '.ipv6-literal.net' suffix
// only then resulting string can be considered a valid IPv6 address. Yes, this is extremely weird!
// For more details see:
// https://social.msdn.microsoft.com/Forums/ie/en-US/06cca73b-63c2-4bf9-899b-b229c50449ff/whether-ie10-websocket-support-ipv6?forum=iewebdevelopment
// https://www.itdojo.com/ipv6-addresses-and-unc-path-names-overcoming-illegal/
// Creation of WebSocket with unconverted address results in SyntaxError without message or stacktrace.
// That is why here we "catch" SyntaxError and rewrite IPv6 address if needed.

const windowsFriendlyUrl = asWindowsFriendlyIPv6Address(scheme, parsedUrl);
return new WebSocket(windowsFriendlyUrl);
} else {
throw error;
}
}
}

function isIPv6AddressIssueOnWindows(error, parsedUrl) {
return error.name === 'SyntaxError' && isIPv6Address(parsedUrl);
}

function isIPv6Address(parsedUrl) {
const hostAndPort = parsedUrl.hostAndPort;
return hostAndPort.charAt(0) === '[' && hostAndPort.indexOf(']') !== -1;
}

function asWindowsFriendlyIPv6Address(scheme, parsedUrl) {
// replace all ':' with '-'
const hostWithoutColons = parsedUrl.host.replace(new RegExp(':', 'g'), '-');

// replace '%' with 's' for link-local IPv6 address like 'fe80::1%lo0'
const hostWithoutPercent = hostWithoutColons.replace('%', 's');

// append magic '.ipv6-literal.net' suffix
const ipv6Host = hostWithoutPercent + '.ipv6-literal.net';

return `${scheme}://${ipv6Host}:${parsedUrl.port}`;
}

export default _websocketChannelModule
Loading