Skip to content

Commit c1a5bd1

Browse files
committed
Make IPv6 WebSocket work in Edge
WebSocket in IE and Edge does not understand IPv6 addresses because they contain ':'. Which is an invalid symbol for UNC https://en.wikipedia.org/wiki/IPv6_address#Literal_IPv6_addresses_in_UNC_path_names. This caused `new WebSocket()` call to throw `SyntaxError` for all given IPv6 addresses. Following URL rewriting is needed to make WebSocket work with IPv6: 1) replace all ':' with '-' 2) replace '%' with 's' for link-local addresses 3) append magic '.ipv6-literal.net' suffix After such changes WebSocket in IE and Edge can connect to an IPv6 address. This commit makes `WebSocketChannel` catch `SyntaxError` and rewrite IPv6 address. It chooses not to detect browser/OS and things like this upfront because it seems hard. User-agent string changes often and differs for different versions of IE. See following links for more details: 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/ https://en.wikipedia.org/wiki/IPv6_address#Literal_IPv6_addresses_in_UNC_path_names
1 parent 6ce6039 commit c1a5bd1

File tree

2 files changed

+127
-2
lines changed

2 files changed

+127
-2
lines changed

src/v1/internal/ch-websocket.js

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ class WebSocketChannel {
5252
return;
5353
}
5454
}
55-
this._url = scheme + '://' + config.url.hostAndPort;
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;
@@ -169,4 +169,55 @@ class WebSocketChannel {
169169
let available = typeof WebSocket !== 'undefined';
170170
let _websocketChannelModule = {channel: WebSocketChannel, available: available};
171171

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

test/internal/ch-websocket.test.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* Copyright (c) 2002-2017 "Neo Technology,","
3+
* Network Engine for Objects in Lund AB [http://neotechnology.com]
4+
*
5+
* This file is part of Neo4j.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
20+
import wsChannel from '../../src/v1/internal/ch-websocket';
21+
import ChannelConfig from '../../src/v1/internal/ch-config';
22+
import urlUtil from '../../src/v1/internal/url-util';
23+
import {SERVICE_UNAVAILABLE} from '../../src/v1/error';
24+
25+
describe('WebSocketChannel', () => {
26+
27+
const WebSocketChannel = wsChannel.channel;
28+
const webSocketChannelAvailable = wsChannel.available;
29+
30+
let OriginalWebSocket;
31+
32+
beforeEach(() => {
33+
if (webSocketChannelAvailable) {
34+
OriginalWebSocket = WebSocket;
35+
}
36+
});
37+
38+
afterEach(() => {
39+
if (webSocketChannelAvailable) {
40+
WebSocket = OriginalWebSocket;
41+
}
42+
});
43+
44+
it('should fallback to literal IPv6 when SyntaxError is thrown', () => {
45+
testFallbackToLiteralIPv6('bolt://[::1]:7687', 'ws://--1.ipv6-literal.net:7687');
46+
});
47+
48+
it('should fallback to literal link-local IPv6 when SyntaxError is thrown', () => {
49+
testFallbackToLiteralIPv6('bolt://[fe80::1%lo0]:8888', 'ws://fe80--1slo0.ipv6-literal.net:8888');
50+
});
51+
52+
function testFallbackToLiteralIPv6(boltAddress, expectedWsAddress) {
53+
if (!webSocketChannelAvailable) {
54+
return;
55+
}
56+
57+
// replace real WebSocket with a function that throws when IPv6 address is used
58+
WebSocket = url => {
59+
if (url.indexOf('[') !== -1) {
60+
throw new SyntaxError();
61+
}
62+
return {url: url};
63+
};
64+
65+
const url = urlUtil.parseBoltUrl(boltAddress);
66+
// disable connection timeout, so that WebSocketChannel does not set any timeouts
67+
const driverConfig = {connectionTimeout: 0};
68+
const channelConfig = new ChannelConfig(url, driverConfig, SERVICE_UNAVAILABLE);
69+
const webSocketChannel = new WebSocketChannel(channelConfig);
70+
71+
expect(webSocketChannel._ws.url).toEqual(expectedWsAddress);
72+
}
73+
74+
});

0 commit comments

Comments
 (0)