Skip to content

Commit 25dc55b

Browse files
authored
Merge pull request #317 from lutovich/1.5-ipv6
IPv6 support
2 parents 8769596 + 28b61b8 commit 25dc55b

28 files changed

+1501
-320
lines changed

gulpfile.babel.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,12 @@ gulp.task('run-browser-test-edge', function(cb){
197197
}, cb).start();
198198
});
199199

200+
gulp.task('run-browser-test-ie', function (cb) {
201+
new karmaServer({
202+
configFile: __dirname + '/test/browser/karma-ie.conf.js',
203+
}, cb).start();
204+
});
205+
200206
gulp.task('watch', function () {
201207
return watch('src/**/*.js', batch(function (events, done) {
202208
gulp.start('all', done);

package-lock.json

Lines changed: 25 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
"vinyl-source-stream": "^1.1.0"
7070
},
7171
"dependencies": {
72-
"babel-runtime": "^6.18.0"
72+
"babel-runtime": "^6.18.0",
73+
"url-parse": "^1.2.0"
7374
}
7475
}

src/v1/index.js

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ import Record from './record';
2626
import {Driver, READ, WRITE} from './driver';
2727
import RoutingDriver from './routing-driver';
2828
import VERSION from '../version';
29-
import {assertString, isEmptyObjectOrNull, parseRoutingContext, parseScheme, parseUrl} from './internal/util';
29+
import {assertString, isEmptyObjectOrNull} from './internal/util';
30+
import urlUtil from './internal/url-util';
3031

3132
/**
3233
* @property {function(username: string, password: string, realm: ?string)} basic the function to create a
@@ -152,9 +153,9 @@ const USER_AGENT = "neo4j-javascript/" + VERSION;
152153
* // version.
153154
* loadBalancingStrategy: "least_connected" | "round_robin",
154155
*
155-
* // Specify socket connection timeout in milliseconds. Non-numeric, negative and zero values are treated as an
156-
* // infinite timeout. Connection will be then bound by the timeout configured on the operating system level.
157-
* // Timeout value should be numeric and greater or equal to zero. Default value is 5000 which is 5 seconds.
156+
* // Specify socket connection timeout in milliseconds. Numeric values are expected. Negative and zero values
157+
* // result in no timeout being applied. Connection establishment will be then bound by the timeout configured
158+
* // on the operating system level. Default value is 5000, which is 5 seconds.
158159
* connectionTimeout: 5000, // 5 seconds
159160
* }
160161
*
@@ -165,17 +166,16 @@ const USER_AGENT = "neo4j-javascript/" + VERSION;
165166
*/
166167
function driver(url, authToken, config = {}) {
167168
assertString(url, 'Bolt URL');
168-
const scheme = parseScheme(url);
169-
const routingContext = parseRoutingContext(url);
170-
if (scheme === 'bolt+routing://') {
171-
return new RoutingDriver(parseUrl(url), routingContext, USER_AGENT, authToken, config);
172-
} else if (scheme === 'bolt://') {
173-
if (!isEmptyObjectOrNull(routingContext)) {
169+
const parsedUrl = urlUtil.parseBoltUrl(url);
170+
if (parsedUrl.scheme === 'bolt+routing') {
171+
return new RoutingDriver(parsedUrl.hostAndPort, parsedUrl.query, USER_AGENT, authToken, config);
172+
} else if (parsedUrl.scheme === 'bolt') {
173+
if (!isEmptyObjectOrNull(parsedUrl.query)) {
174174
throw new Error(`Parameters are not supported with scheme 'bolt'. Given URL: '${url}'`);
175175
}
176-
return new Driver(parseUrl(url), USER_AGENT, authToken, config);
176+
return new Driver(parsedUrl.hostAndPort, USER_AGENT, authToken, config);
177177
} else {
178-
throw new Error(`Unknown scheme: ${scheme}`);
178+
throw new Error(`Unknown scheme: ${parsedUrl.scheme}`);
179179
}
180180
}
181181

src/v1/internal/ch-config.js

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,18 @@ import {SERVICE_UNAVAILABLE} from '../error';
2222

2323
const DEFAULT_CONNECTION_TIMEOUT_MILLIS = 5000; // 5 seconds by default
2424

25+
export const DEFAULT_PORT = 7687;
26+
2527
export default class ChannelConfig {
2628

27-
constructor(host, port, driverConfig, connectionErrorCode) {
28-
this.host = host;
29-
this.port = port;
29+
/**
30+
* @constructor
31+
* @param {Url} url the URL for the channel to connect to.
32+
* @param {object} driverConfig the driver config provided by the user when driver is created.
33+
* @param {string} connectionErrorCode the default error code to use on connection errors.
34+
*/
35+
constructor(url, driverConfig, connectionErrorCode) {
36+
this.url = url;
3037
this.encrypted = extractEncrypted(driverConfig);
3138
this.trust = extractTrust(driverConfig);
3239
this.trustedCertificates = extractTrustedCertificates(driverConfig);
@@ -61,8 +68,17 @@ function extractKnownHostsPath(driverConfig) {
6168

6269
function extractConnectionTimeout(driverConfig) {
6370
const configuredTimeout = parseInt(driverConfig.connectionTimeout, 10);
64-
if (!configuredTimeout || configuredTimeout < 0) {
71+
if (configuredTimeout === 0) {
72+
// timeout explicitly configured to 0
73+
return null;
74+
} else if (configuredTimeout && configuredTimeout < 0) {
75+
// timeout explicitly configured to a negative value
76+
return null;
77+
} else if (!configuredTimeout) {
78+
// timeout not configured, use default value
6579
return DEFAULT_CONNECTION_TIMEOUT_MILLIS;
80+
} else {
81+
// timeout configured, use the provided value
82+
return configuredTimeout;
6683
}
67-
return configuredTimeout;
6884
}

src/v1/internal/ch-node.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ const TrustStrategy = {
130130
rejectUnauthorized: false
131131
};
132132

133-
let socket = tls.connect(config.port, config.host, tlsOpts, function () {
133+
let socket = tls.connect(config.url.port, config.url.host, tlsOpts, function () {
134134
if (!socket.authorized) {
135135
onFailure(newError("Server certificate is not trusted. If you trust the database you are connecting to, add" +
136136
" the signing certificate, or the server certificate, to the list of certificates trusted by this driver" +
@@ -152,7 +152,7 @@ const TrustStrategy = {
152152
// a more helpful error to the user
153153
rejectUnauthorized: false
154154
};
155-
let socket = tls.connect(config.port, config.host, tlsOpts, function () {
155+
let socket = tls.connect(config.url.port, config.url.host, tlsOpts, function () {
156156
if (!socket.authorized) {
157157
onFailure(newError("Server certificate is not trusted. If you trust the database you are connecting to, use " +
158158
"TRUST_CUSTOM_CA_SIGNED_CERTIFICATES and add" +
@@ -180,7 +180,7 @@ const TrustStrategy = {
180180
rejectUnauthorized: false
181181
};
182182

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

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

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

202202
loadFingerprint(serverId, knownHostsPath, (knownFingerprint) => {
203203
if( knownFingerprint === serverFingerprint ) {
@@ -232,7 +232,7 @@ const TrustStrategy = {
232232
const tlsOpts = {
233233
rejectUnauthorized: false
234234
};
235-
const socket = tls.connect(config.port, config.host, tlsOpts, function () {
235+
const socket = tls.connect(config.url.port, config.url.host, tlsOpts, function () {
236236
const certificate = socket.getPeerCertificate();
237237
if (isEmptyObjectOrNull(certificate)) {
238238
onFailure(newError("Secure connection was successful but server did not return any valid " +
@@ -259,7 +259,7 @@ const TrustStrategy = {
259259
function connect( config, onSuccess, onFailure=(()=>null) ) {
260260
//still allow boolean for backwards compatibility
261261
if (config.encrypted === false || config.encrypted === ENCRYPTION_OFF) {
262-
var conn = net.connect(config.port, config.host, onSuccess);
262+
var conn = net.connect(config.url.port, config.url.host, onSuccess);
263263
conn.on('error', onFailure);
264264
return conn;
265265
} else if( TrustStrategy[config.trust]) {

src/v1/internal/ch-websocket.js

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ class WebSocketChannel {
5252
return;
5353
}
5454
}
55-
this._url = scheme + '://' + config.host + ':' + config.port;
56-
this._ws = new WebSocket(this._url);
55+
56+
this._ws = createWebSocket(scheme, config.url);
5757
this._ws.binaryType = "arraybuffer";
5858

5959
let self = this;
@@ -65,8 +65,8 @@ class WebSocketChannel {
6565
}
6666
};
6767
this._ws.onopen = function() {
68-
// Connected! Cancel connection timeout
69-
clearTimeout(self._connectionTimeoutId);
68+
// Connected! Cancel the connection timeout
69+
self._clearConnectionTimeout();
7070

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

8787
this._connectionTimeoutFired = false;
88-
this._connectionTimeoutId = this._setupConnectionTimeout(config);
88+
this._connectionTimeoutId = this._setupConnectionTimeout();
8989
}
9090

9191
_handleConnectionError() {
@@ -141,6 +141,7 @@ class WebSocketChannel {
141141
*/
142142
close ( cb = ( () => null )) {
143143
this._open = false;
144+
this._clearConnectionTimeout();
144145
this._ws.close();
145146
this._ws.onclose = cb;
146147
}
@@ -164,9 +165,73 @@ class WebSocketChannel {
164165
}
165166
return null;
166167
}
168+
169+
/**
170+
* Remove active connection timeout, if any.
171+
* @private
172+
*/
173+
_clearConnectionTimeout() {
174+
const timeoutId = this._connectionTimeoutId;
175+
if (timeoutId || timeoutId === 0) {
176+
this._connectionTimeoutFired = false;
177+
this._connectionTimeoutId = null;
178+
clearTimeout(timeoutId);
179+
}
180+
}
167181
}
168182

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

186+
function createWebSocket(scheme, parsedUrl) {
187+
const url = scheme + '://' + parsedUrl.hostAndPort;
188+
189+
try {
190+
return new WebSocket(url);
191+
} catch (error) {
192+
if (isIPv6AddressIssueOnWindows(error, parsedUrl)) {
193+
194+
// WebSocket in IE and Edge browsers on Windows do not support regular IPv6 address syntax because they contain ':'.
195+
// It's an invalid character for UNC (https://en.wikipedia.org/wiki/IPv6_address#Literal_IPv6_addresses_in_UNC_path_names)
196+
// and Windows requires IPv6 to be changes in the following way:
197+
// 1) replace all ':' with '-'
198+
// 2) replace '%' with 's' for link-local address
199+
// 3) append '.ipv6-literal.net' suffix
200+
// only then resulting string can be considered a valid IPv6 address. Yes, this is extremely weird!
201+
// For more details see:
202+
// https://social.msdn.microsoft.com/Forums/ie/en-US/06cca73b-63c2-4bf9-899b-b229c50449ff/whether-ie10-websocket-support-ipv6?forum=iewebdevelopment
203+
// https://www.itdojo.com/ipv6-addresses-and-unc-path-names-overcoming-illegal/
204+
// Creation of WebSocket with unconverted address results in SyntaxError without message or stacktrace.
205+
// That is why here we "catch" SyntaxError and rewrite IPv6 address if needed.
206+
207+
const windowsFriendlyUrl = asWindowsFriendlyIPv6Address(scheme, parsedUrl);
208+
return new WebSocket(windowsFriendlyUrl);
209+
} else {
210+
throw error;
211+
}
212+
}
213+
}
214+
215+
function isIPv6AddressIssueOnWindows(error, parsedUrl) {
216+
return error.name === 'SyntaxError' && isIPv6Address(parsedUrl);
217+
}
218+
219+
function isIPv6Address(parsedUrl) {
220+
const hostAndPort = parsedUrl.hostAndPort;
221+
return hostAndPort.charAt(0) === '[' && hostAndPort.indexOf(']') !== -1;
222+
}
223+
224+
function asWindowsFriendlyIPv6Address(scheme, parsedUrl) {
225+
// replace all ':' with '-'
226+
const hostWithoutColons = parsedUrl.host.replace(new RegExp(':', 'g'), '-');
227+
228+
// replace '%' with 's' for link-local IPv6 address like 'fe80::1%lo0'
229+
const hostWithoutPercent = hostWithoutColons.replace('%', 's');
230+
231+
// append magic '.ipv6-literal.net' suffix
232+
const ipv6Host = hostWithoutPercent + '.ipv6-literal.net';
233+
234+
return `${scheme}://${ipv6Host}:${parsedUrl.port}`;
235+
}
236+
172237
export default _websocketChannelModule

0 commit comments

Comments
 (0)