Skip to content

Commit 54622da

Browse files
committed
Support IPv6 for bolt addresses
This commit makes driver use previously introduced parser that supports host names, IPv4 and IPv6 addresses. Previously used regex-based parsing is now removed. Driver is now able to connect to IPv6 addresses and supports resolution of host names to IPv6 addresses. Routing procedure responses can now safely contain IPv6 as well. Bolt URL with IPv6 always requires address in square brackets like: `bolt://[ff02::2:ff00:0]` or `bolt+routing://[ff02::2:ff00:0]:8080`. Renamed `url.js` to `url-util.js` and it's functions to better represent what they do.
1 parent 297396b commit 54622da

18 files changed

+347
-393
lines changed

src/v1/index.js

Lines changed: 9 additions & 9 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
@@ -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: 10 additions & 3 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);

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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ class WebSocketChannel {
5252
return;
5353
}
5454
}
55-
this._url = scheme + '://' + config.host + ':' + config.port;
55+
this._url = scheme + '://' + config.url.hostAndPort;
5656
this._ws = new WebSocket(this._url);
5757
this._ws.binaryType = "arraybuffer";
5858

src/v1/internal/connector.js

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {alloc} from './buf';
2525
import {Node, Path, PathSegment, Relationship, UnboundRelationship} from '../graph-types';
2626
import {newError} from './../error';
2727
import ChannelConfig from './ch-config';
28-
import {parseHost, parsePort} from './util';
28+
import urlUtil from './url-util';
2929
import StreamObserver from './stream-observer';
3030
import {ServerVersion, VERSION_3_2_0} from './server-version';
3131

@@ -586,12 +586,9 @@ class ConnectionState {
586586
*/
587587
function connect(url, config = {}, connectionErrorCode = null) {
588588
const Ch = config.channel || Channel;
589-
const host = parseHost(url);
590-
const port = parsePort(url) || 7687;
591-
const completeUrl = host + ':' + port;
592-
const channelConfig = new ChannelConfig(host, port, config, connectionErrorCode);
593-
594-
return new Connection( new Ch(channelConfig), completeUrl);
589+
const parsedUrl = urlUtil.parseBoltUrl(url);
590+
const channelConfig = new ChannelConfig(parsedUrl, config, connectionErrorCode);
591+
return new Connection(new Ch(channelConfig), parsedUrl.hostAndPort);
595592
}
596593

597594
export {

src/v1/internal/host-name-resolvers.js

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
* limitations under the License.
1818
*/
1919

20-
import {parseHost, parsePort} from './util';
20+
import urlUtil from './url-util';
2121

2222
class HostNameResolver {
2323

@@ -41,15 +41,14 @@ export class DnsHostNameResolver extends HostNameResolver {
4141
}
4242

4343
resolve(seedRouter) {
44-
const seedRouterHost = parseHost(seedRouter);
45-
const seedRouterPort = parsePort(seedRouter);
44+
const parsedAddress = urlUtil.parseBoltUrl(seedRouter);
4645

4746
return new Promise((resolve) => {
48-
this._dns.lookup(seedRouterHost, {all: true}, (error, addresses) => {
47+
this._dns.lookup(parsedAddress.host, {all: true}, (error, addresses) => {
4948
if (error) {
5049
resolve(resolveToItself(seedRouter));
5150
} else {
52-
const addressesWithPorts = addresses.map(address => addressWithPort(address, seedRouterPort));
51+
const addressesWithPorts = addresses.map(address => addressWithPort(address, parsedAddress.port));
5352
resolve(addressesWithPorts);
5453
}
5554
});
@@ -63,8 +62,11 @@ function resolveToItself(address) {
6362

6463
function addressWithPort(addressObject, port) {
6564
const address = addressObject.address;
66-
if (port) {
67-
return address + ':' + port;
65+
const addressFamily = addressObject.family;
66+
67+
if (!port) {
68+
return address;
6869
}
69-
return address;
70+
71+
return addressFamily === 6 ? urlUtil.formatIPv6Address(address, port) : urlUtil.formatIPv4Address(address, port);
7072
}

src/v1/internal/routing-util.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,9 @@ export default class RoutingUtil {
4747
}).catch(error => {
4848
if (error.code === PROCEDURE_NOT_FOUND_CODE) {
4949
// throw when getServers procedure not found because this is clearly a configuration issue
50-
throw newError('Server ' + routerAddress + ' could not perform routing. ' +
51-
'Make sure you are connecting to a causal cluster', SERVICE_UNAVAILABLE);
50+
throw newError(
51+
`Server at ${routerAddress} can't perform routing. Make sure you are connecting to a causal cluster`,
52+
SERVICE_UNAVAILABLE);
5253
} else if (error.code === UNAUTHORIZED_CODE) {
5354
// auth error is a sign of a configuration issue, rediscovery should not proceed
5455
throw error;

src/v1/internal/url.js renamed to src/v1/internal/url-util.js

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,52 +18,64 @@
1818
*/
1919

2020
import ParsedUrl from 'url-parse';
21+
import {assertString} from './util';
22+
import {DEFAULT_PORT} from './ch-config';
2123

22-
class Url {
24+
export class Url {
2325

24-
constructor(scheme, host, port, query) {
26+
constructor(scheme, host, port, hostAndPort, query) {
2527
/**
2628
* Nullable scheme (protocol) of the URL.
29+
* Example: 'bolt', 'bolt+routing', 'http', 'https', etc.
2730
* @type {string}
2831
*/
2932
this.scheme = scheme;
3033

3134
/**
32-
* Nonnull host name or IP address. IPv6 always wrapped in square brackets.
35+
* Nonnull host name or IP address. IPv6 not wrapped in square brackets.
36+
* Example: 'neo4j.com', 'localhost', '127.0.0.1', '192.168.10.15', '::1', '2001:4860:4860::8844', etc.
3337
* @type {string}
3438
*/
3539
this.host = host;
3640

3741
/**
38-
* Nullable number representing port.
42+
* Nonnull number representing port. Default port {@link DEFAULT_PORT} value is used if given URL string
43+
* does not contain port. Example: 7687, 12000, etc.
3944
* @type {number}
4045
*/
4146
this.port = port;
4247

4348
/**
44-
* Nonnull host name or IP address plus port, separated by ':'.
49+
* Nonnull host name or IP address plus port, separated by ':'. IPv6 wrapped in square brackets.
50+
* Example: 'neo4j.com', 'neo4j.com:7687', '127.0.0.1', '127.0.0.1:8080', '[2001:4860:4860::8844]',
51+
* '[2001:4860:4860::8844]:9090', etc.
4552
* @type {string}
4653
*/
47-
this.hostAndPort = port ? `${host}:${port}` : host;
54+
this.hostAndPort = hostAndPort;
4855

4956
/**
5057
* Nonnull object representing parsed query string key-value pairs. Duplicated keys not supported.
58+
* Example: '{}', '{'key1': 'value1', 'key2': 'value2'}', etc.
5159
* @type {object}
5260
*/
5361
this.query = query;
5462
}
5563
}
5664

57-
function parse(url) {
65+
function parseBoltUrl(url) {
66+
assertString(url, 'URL');
67+
5868
const sanitized = sanitizeUrl(url);
5969
const parsedUrl = new ParsedUrl(sanitized.url, {}, query => extractQuery(query, url));
6070

6171
const scheme = sanitized.schemeMissing ? null : extractScheme(parsedUrl.protocol);
62-
const host = extractHost(parsedUrl.hostname);
72+
const rawHost = extractHost(parsedUrl.hostname); // has square brackets for IPv6
73+
const host = unescapeIPv6Address(rawHost); // no square brackets for IPv6
6374
const port = extractPort(parsedUrl.port);
75+
const hostAndPort = port ? `${rawHost}:${port}` : rawHost;
6476
const query = parsedUrl.query;
6577

66-
return new Url(scheme, host, port, query);
78+
return new Url(scheme, host, port, hostAndPort, query);
6779
}
6880

6981
function sanitizeUrl(url) {
@@ -97,7 +109,7 @@ function extractHost(host, url) {
97109

98110
function extractPort(portString) {
99111
const port = parseInt(portString, 10);
100-
return port ? port : null;
112+
return (port === 0 || port) ? port : DEFAULT_PORT;
101113
}
102114

103115
function extractQuery(queryString, url) {
@@ -141,6 +153,43 @@ function trimAndVerifyQueryElement(element, name, url) {
141153
return element;
142154
}
143155

156+
function escapeIPv6Address(address) {
157+
const startsWithSquareBracket = address.charAt(0) === '[';
158+
const endsWithSquareBracket = address.charAt(address.length - 1) === ']';
159+
160+
if (!startsWithSquareBracket && !endsWithSquareBracket) {
161+
return `[${address}]`;
162+
} else if (startsWithSquareBracket && endsWithSquareBracket) {
163+
return address;
164+
} else {
165+
throw new Error(`Illegal IPv6 address ${address}`);
166+
}
167+
}
168+
169+
function unescapeIPv6Address(address) {
170+
const startsWithSquareBracket = address.charAt(0) === '[';
171+
const endsWithSquareBracket = address.charAt(address.length - 1) === ']';
172+
173+
if (!startsWithSquareBracket && !endsWithSquareBracket) {
174+
return address;
175+
} else if (startsWithSquareBracket && endsWithSquareBracket) {
176+
return address.substring(1, address.length - 1);
177+
} else {
178+
throw new Error(`Illegal IPv6 address ${address}`);
179+
}
180+
}
181+
182+
function formatIPv4Address(address, port) {
183+
return `${address}:${port}`;
184+
}
185+
186+
function formatIPv6Address(address, port) {
187+
const escapedAddress = escapeIPv6Address(address);
188+
return `${escapedAddress}:${port}`;
189+
}
190+
144191
export default {
145-
parse: parse
192+
parseBoltUrl: parseBoltUrl,
193+
formatIPv4Address: formatIPv4Address,
194+
formatIPv6Address: formatIPv6Address
146195
};

0 commit comments

Comments
 (0)