diff --git a/gulpfile.babel.js b/gulpfile.babel.js index d42e4840b..24223a2b1 100644 --- a/gulpfile.babel.js +++ b/gulpfile.babel.js @@ -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); diff --git a/package-lock.json b/package-lock.json index e84aa7a0d..a6fca3852 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3695,7 +3695,7 @@ "requires": { "detect-file": "1.0.0", "is-glob": "3.1.0", - "micromatch": "3.1.4", + "micromatch": "3.1.5", "resolve-dir": "1.0.1" } }, @@ -5028,7 +5028,7 @@ }, "gulp-batch": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/gulp-batch/-/gulp-batch-1.0.5.tgz", + "resolved": "http://registry.npmjs.org/gulp-batch/-/gulp-batch-1.0.5.tgz", "integrity": "sha1-xA/JsjA2dIl7EhbYLhUYtzIX2lk=", "dev": true, "requires": { @@ -7358,9 +7358,9 @@ } }, "micromatch": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.4.tgz", - "integrity": "sha512-kFRtviKYoAJT+t7HggMl0tBFGNAKLw/S7N+CO9qfEQyisob1Oy4pao+geRbkyeEd+V9aOkvZ4mhuyPvI/q9Sfg==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.5.tgz", + "integrity": "sha512-ykttrLPQrz1PUJcXjwsTUjGoPJ64StIGNE2lGVD1c9CuguJ+L7/navsE8IcDNndOoCMvYV0qc/exfVbMHkUhvA==", "dev": true, "requires": { "arr-diff": "4.0.0", @@ -7371,7 +7371,7 @@ "extglob": "2.0.3", "fragment-cache": "0.2.1", "kind-of": "6.0.2", - "nanomatch": "1.2.6", + "nanomatch": "1.2.7", "object.pick": "1.3.0", "regex-not": "1.0.0", "snapdragon": "0.8.1", @@ -7555,9 +7555,9 @@ "optional": true }, "nanomatch": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.6.tgz", - "integrity": "sha512-WJ6XTCbvWXUFPbi/bDwKcYkCeOGUHzaJj72KbuPqGn78Ba/F5Vu26Zlo6SuMQbCIst1RGKL1zfWBCOGAlbRLAg==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.7.tgz", + "integrity": "sha512-/5ldsnyurvEw7wNpxLFgjVvBLMta43niEYOy0CJ4ntcYSbx6bugRUTQeFb4BR/WanEL1o3aQgHuVLHQaB6tOqg==", "dev": true, "requires": { "arr-diff": "4.0.0", @@ -8233,6 +8233,11 @@ "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", "dev": true }, + "querystringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-1.0.0.tgz", + "integrity": "sha1-YoYkIRLFtxL6ZU5SZlK/ahP/Bcs=" + }, "randomatic": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", @@ -8293,7 +8298,7 @@ }, "read-all-stream": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/read-all-stream/-/read-all-stream-3.1.0.tgz", + "resolved": "http://registry.npmjs.org/read-all-stream/-/read-all-stream-3.1.0.tgz", "integrity": "sha1-NcPhd/IHjveJ7kv6+kNzB06u9Po=", "dev": true, "requires": { @@ -8549,8 +8554,7 @@ "requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", - "dev": true + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" }, "resolve": { "version": "1.5.0", @@ -9952,6 +9956,15 @@ } } }, + "url-parse": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.2.0.tgz", + "integrity": "sha512-DT1XbYAfmQP65M/mE6OALxmXzZ/z1+e5zk2TcSKe/KiYbNGZxgtttzC0mR/sjopbpOXcbniq7eIKmocJnUWlEw==", + "requires": { + "querystringify": "1.0.0", + "requires-port": "1.0.0" + } + }, "use": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/use/-/use-2.0.2.tgz", diff --git a/package.json b/package.json index f4e9f7b09..3245edae2 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/src/v1/index.js b/src/v1/index.js index e86c6c20e..b42314d44 100644 --- a/src/v1/index.js +++ b/src/v1/index.js @@ -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 @@ -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 * } * @@ -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}`); } } diff --git a/src/v1/internal/ch-config.js b/src/v1/internal/ch-config.js index a0c0fc52e..318fc073f 100644 --- a/src/v1/internal/ch-config.js +++ b/src/v1/internal/ch-config.js @@ -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); @@ -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; } diff --git a/src/v1/internal/ch-node.js b/src/v1/internal/ch-node.js index c56d38054..ec250a194 100644 --- a/src/v1/internal/ch-node.js +++ b/src/v1/internal/ch-node.js @@ -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" + @@ -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" + @@ -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 ) { @@ -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 ) { @@ -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 " + @@ -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]) { diff --git a/src/v1/internal/ch-websocket.js b/src/v1/internal/ch-websocket.js index e7a2b1e67..3b185befd 100644 --- a/src/v1/internal/ch-websocket.js +++ b/src/v1/internal/ch-websocket.js @@ -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; @@ -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; @@ -85,7 +85,7 @@ class WebSocketChannel { this._ws.onerror = this._handleConnectionError; this._connectionTimeoutFired = false; - this._connectionTimeoutId = this._setupConnectionTimeout(config); + this._connectionTimeoutId = this._setupConnectionTimeout(); } _handleConnectionError() { @@ -141,6 +141,7 @@ class WebSocketChannel { */ close ( cb = ( () => null )) { this._open = false; + this._clearConnectionTimeout(); this._ws.close(); this._ws.onclose = cb; } @@ -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 diff --git a/src/v1/internal/connector.js b/src/v1/internal/connector.js index 9304a18cf..e3d53f008 100644 --- a/src/v1/internal/connector.js +++ b/src/v1/internal/connector.js @@ -25,7 +25,7 @@ import {alloc} from './buf'; import {Node, Path, PathSegment, Relationship, UnboundRelationship} from '../graph-types'; import {newError} from './../error'; import ChannelConfig from './ch-config'; -import {parseHost, parsePort} from './util'; +import urlUtil from './url-util'; import StreamObserver from './stream-observer'; import {ServerVersion, VERSION_3_2_0} from './server-version'; @@ -586,12 +586,9 @@ class ConnectionState { */ function connect(url, config = {}, connectionErrorCode = null) { const Ch = config.channel || Channel; - const host = parseHost(url); - const port = parsePort(url) || 7687; - const completeUrl = host + ':' + port; - const channelConfig = new ChannelConfig(host, port, config, connectionErrorCode); - - return new Connection( new Ch(channelConfig), completeUrl); + const parsedUrl = urlUtil.parseBoltUrl(url); + const channelConfig = new ChannelConfig(parsedUrl, config, connectionErrorCode); + return new Connection(new Ch(channelConfig), parsedUrl.hostAndPort); } export { diff --git a/src/v1/internal/host-name-resolvers.js b/src/v1/internal/host-name-resolvers.js index c4fa36348..90fd5b496 100644 --- a/src/v1/internal/host-name-resolvers.js +++ b/src/v1/internal/host-name-resolvers.js @@ -17,7 +17,7 @@ * limitations under the License. */ -import {parseHost, parsePort} from './util'; +import urlUtil from './url-util'; class HostNameResolver { @@ -41,15 +41,14 @@ export class DnsHostNameResolver extends HostNameResolver { } resolve(seedRouter) { - const seedRouterHost = parseHost(seedRouter); - const seedRouterPort = parsePort(seedRouter); + const parsedAddress = urlUtil.parseBoltUrl(seedRouter); return new Promise((resolve) => { - this._dns.lookup(seedRouterHost, {all: true}, (error, addresses) => { + this._dns.lookup(parsedAddress.host, {all: true}, (error, addresses) => { if (error) { resolve(resolveToItself(seedRouter)); } else { - const addressesWithPorts = addresses.map(address => addressWithPort(address, seedRouterPort)); + const addressesWithPorts = addresses.map(address => addressWithPort(address, parsedAddress.port)); resolve(addressesWithPorts); } }); @@ -63,8 +62,11 @@ function resolveToItself(address) { function addressWithPort(addressObject, port) { const address = addressObject.address; - if (port) { - return address + ':' + port; + const addressFamily = addressObject.family; + + if (!port) { + return address; } - return address; + + return addressFamily === 6 ? urlUtil.formatIPv6Address(address, port) : urlUtil.formatIPv4Address(address, port); } diff --git a/src/v1/internal/routing-util.js b/src/v1/internal/routing-util.js index 991ecbb13..caf450bdd 100644 --- a/src/v1/internal/routing-util.js +++ b/src/v1/internal/routing-util.js @@ -47,8 +47,9 @@ export default class RoutingUtil { }).catch(error => { if (error.code === PROCEDURE_NOT_FOUND_CODE) { // throw when getServers procedure not found because this is clearly a configuration issue - throw newError('Server ' + routerAddress + ' could not perform routing. ' + - 'Make sure you are connecting to a causal cluster', SERVICE_UNAVAILABLE); + throw newError( + `Server at ${routerAddress} can't perform routing. Make sure you are connecting to a causal cluster`, + SERVICE_UNAVAILABLE); } else if (error.code === UNAUTHORIZED_CODE) { // auth error is a sign of a configuration issue, rediscovery should not proceed throw error; diff --git a/src/v1/internal/server-version.js b/src/v1/internal/server-version.js index e7e59c034..018ed4aa4 100644 --- a/src/v1/internal/server-version.js +++ b/src/v1/internal/server-version.js @@ -36,9 +36,22 @@ class ServerVersion { this.patch = patch; } + /** + * Fetch server version using the given driver. + * @param {Driver} driver the driver to use. + * @return {Promise} promise resolved with a {@link ServerVersion} object or rejected with error. + */ + static fromDriver(driver) { + const session = driver.session(); + return session.run('RETURN 1').then(result => { + session.close(); + return ServerVersion.fromString(result.summary.server.version); + }); + } + /** * Parse given string to a {@link ServerVersion} object. - * @param versionStr the string to parse. + * @param {string} versionStr the string to parse. * @return {ServerVersion} version for the given string. * @throws Error if given string can't be parsed. */ diff --git a/src/v1/internal/url-util.js b/src/v1/internal/url-util.js new file mode 100644 index 000000000..9a414e71c --- /dev/null +++ b/src/v1/internal/url-util.js @@ -0,0 +1,195 @@ +/** + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import ParsedUrl from 'url-parse'; +import {assertString} from './util'; +import {DEFAULT_PORT} from './ch-config'; + +class Url { + + constructor(scheme, host, port, hostAndPort, query) { + /** + * Nullable scheme (protocol) of the URL. + * Example: 'bolt', 'bolt+routing', 'http', 'https', etc. + * @type {string} + */ + this.scheme = scheme; + + /** + * Nonnull host name or IP address. IPv6 not wrapped in square brackets. + * Example: 'neo4j.com', 'localhost', '127.0.0.1', '192.168.10.15', '::1', '2001:4860:4860::8844', etc. + * @type {string} + */ + this.host = host; + + /** + * Nonnull number representing port. Default port {@link DEFAULT_PORT} value is used if given URL string + * does not contain port. Example: 7687, 12000, etc. + * @type {number} + */ + this.port = port; + + /** + * Nonnull host name or IP address plus port, separated by ':'. IPv6 wrapped in square brackets. + * Example: 'neo4j.com', 'neo4j.com:7687', '127.0.0.1', '127.0.0.1:8080', '[2001:4860:4860::8844]', + * '[2001:4860:4860::8844]:9090', etc. + * @type {string} + */ + this.hostAndPort = hostAndPort; + + /** + * Nonnull object representing parsed query string key-value pairs. Duplicated keys not supported. + * Example: '{}', '{'key1': 'value1', 'key2': 'value2'}', etc. + * @type {object} + */ + this.query = query; + } +} + +function parseBoltUrl(url) { + assertString(url, 'URL'); + + const sanitized = sanitizeUrl(url); + const parsedUrl = new ParsedUrl(sanitized.url, {}, query => extractQuery(query, url)); + + const scheme = sanitized.schemeMissing ? null : extractScheme(parsedUrl.protocol); + const rawHost = extractHost(parsedUrl.hostname); // has square brackets for IPv6 + const host = unescapeIPv6Address(rawHost); // no square brackets for IPv6 + const port = extractPort(parsedUrl.port); + const hostAndPort = `${rawHost}:${port}`; + const query = parsedUrl.query; + + return new Url(scheme, host, port, hostAndPort, query); +} + +function sanitizeUrl(url) { + url = url.trim(); + + if (url.indexOf('://') === -1) { + // url does not contain scheme, add dummy 'http://' to make parser work correctly + return {schemeMissing: true, url: `http://${url}`}; + } + + return {schemeMissing: false, url: url}; +} + +function extractScheme(scheme) { + if (scheme) { + scheme = scheme.trim(); + if (scheme.charAt(scheme.length - 1) === ':') { + scheme = scheme.substring(0, scheme.length - 1); + } + return scheme; + } + return null; +} + +function extractHost(host, url) { + if (!host) { + throw new Error(`Unable to extract host from ${url}`); + } + return host.trim(); +} + +function extractPort(portString) { + const port = parseInt(portString, 10); + return (port === 0 || port) ? port : DEFAULT_PORT; +} + +function extractQuery(queryString, url) { + const query = trimAndSanitizeQuery(queryString); + const context = {}; + + if (query) { + query.split('&').forEach(pair => { + const keyValue = pair.split('='); + if (keyValue.length !== 2) { + throw new Error(`Invalid parameters: '${keyValue}' in URL '${url}'.`); + } + + const key = trimAndVerifyQueryElement(keyValue[0], 'key', url); + const value = trimAndVerifyQueryElement(keyValue[1], 'value', url); + + if (context[key]) { + throw new Error(`Duplicated query parameters with key '${key}' in URL '${url}'`); + } + + context[key] = value; + }); + } + + return context; +} + +function trimAndSanitizeQuery(query) { + query = (query || '').trim(); + if (query && query.charAt(0) === '?') { + query = query.substring(1, query.length); + } + return query; +} + +function trimAndVerifyQueryElement(element, name, url) { + element = (element || '').trim(); + if (!element) { + throw new Error(`Illegal empty ${name} in URL query '${url}'`); + } + return element; +} + +function escapeIPv6Address(address) { + const startsWithSquareBracket = address.charAt(0) === '['; + const endsWithSquareBracket = address.charAt(address.length - 1) === ']'; + + if (!startsWithSquareBracket && !endsWithSquareBracket) { + return `[${address}]`; + } else if (startsWithSquareBracket && endsWithSquareBracket) { + return address; + } else { + throw new Error(`Illegal IPv6 address ${address}`); + } +} + +function unescapeIPv6Address(address) { + const startsWithSquareBracket = address.charAt(0) === '['; + const endsWithSquareBracket = address.charAt(address.length - 1) === ']'; + + if (!startsWithSquareBracket && !endsWithSquareBracket) { + return address; + } else if (startsWithSquareBracket && endsWithSquareBracket) { + return address.substring(1, address.length - 1); + } else { + throw new Error(`Illegal IPv6 address ${address}`); + } +} + +function formatIPv4Address(address, port) { + return `${address}:${port}`; +} + +function formatIPv6Address(address, port) { + const escapedAddress = escapeIPv6Address(address); + return `${escapedAddress}:${port}`; +} + +export default { + parseBoltUrl: parseBoltUrl, + formatIPv4Address: formatIPv4Address, + formatIPv6Address: formatIPv6Address +}; diff --git a/src/v1/internal/util.js b/src/v1/internal/util.js index d943869eb..43fca35b6 100644 --- a/src/v1/internal/util.js +++ b/src/v1/internal/util.js @@ -22,14 +22,6 @@ import {newError} from '../error'; const ENCRYPTION_ON = "ENCRYPTION_ON"; const ENCRYPTION_OFF = "ENCRYPTION_OFF"; -const URL_REGEX = new RegExp([ - '([^/]+//)?', // scheme - '(([^:/?#]*)', // hostname - '(?::([0-9]+))?)', // port (optional) - '([^?]*)?', // everything else - '(\\?(.+))?' // query -].join('')); - function isEmptyObjectOrNull(obj) { if (obj === null) { return true; @@ -72,58 +64,6 @@ function isString(str) { return Object.prototype.toString.call(str) === '[object String]'; } -function parseScheme(url) { - assertString(url, 'URL'); - const scheme = url.match(URL_REGEX)[1] || ''; - return scheme.toLowerCase(); -} - -function parseUrl(url) { - assertString(url, 'URL'); - return url.match(URL_REGEX)[2]; -} - -function parseHost(url) { - assertString(url, 'URL'); - return url.match(URL_REGEX)[3]; -} - -function parsePort(url) { - assertString(url, 'URL'); - return url.match(URL_REGEX)[4]; -} - -function parseRoutingContext(url) { - const query = url.match(URL_REGEX)[7] || ''; - const context = {}; - if (query) { - query.split('&').forEach(pair => { - const keyValue = pair.split('='); - if (keyValue.length !== 2) { - throw new Error('Invalid parameters: \'' + keyValue + '\' in URL \'' + url + '\'.'); - } - - const key = trimAndVerify(keyValue[0], 'key', url); - const value = trimAndVerify(keyValue[1], 'value', url); - - if (context[key]) { - throw new Error(`Duplicated query parameters with key '${key}' in URL '${url}'`); - } - - context[key] = value; - }); - } - return context; -} - -function trimAndVerify(string, name, url) { - const result = string.trim(); - if (!result) { - throw new Error(`Illegal empty ${name} in URL query '${url}'`); - } - return result; -} - function promiseOrTimeout(timeout, otherPromise, onTimeout) { let resultPromise = null; @@ -159,11 +99,6 @@ export { isString, assertString, assertCypherStatement, - parseScheme, - parseUrl, - parseHost, - parsePort, - parseRoutingContext, promiseOrTimeout, ENCRYPTION_ON, ENCRYPTION_OFF diff --git a/test/browser/karma-ie.conf.js b/test/browser/karma-ie.conf.js new file mode 100644 index 000000000..d620df25a --- /dev/null +++ b/test/browser/karma-ie.conf.js @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module.exports = function (config) { + config.set({ + frameworks: ['jasmine'], + basePath: '../../', + files: ['lib/browser/neo4j-web.test.js'], + reporters: ['spec'], + port: 9876, // karma web server port + colors: true, + logLevel: config.LOG_DEBUG, + browsers: ['IE'], + autoWatch: false, + singleRun: true, + concurrency: 1, + browserNoActivityTimeout: 30 * 60 * 1000, + }); +}; diff --git a/test/internal/ch-config.test.js b/test/internal/ch-config.test.js index 30cf53d13..deabf6d21 100644 --- a/test/internal/ch-config.test.js +++ b/test/internal/ch-config.test.js @@ -18,31 +18,26 @@ */ import ChannelConfig from '../../src/v1/internal/ch-config'; +import urlUtil from '../../src/v1/internal/url-util'; import hasFeature from '../../src/v1/internal/features'; import {SERVICE_UNAVAILABLE} from '../../src/v1/error'; describe('ChannelConfig', () => { - it('should respect given host', () => { - const host = 'neo4j.com'; + it('should respect given Url', () => { + const url = urlUtil.parseBoltUrl('bolt://neo4j.com:4242'); - const config = new ChannelConfig(host, 42, {}, ''); + const config = new ChannelConfig(url, {}, ''); - expect(config.host).toEqual(host); - }); - - it('should respect given port', () => { - const port = 4242; - - const config = new ChannelConfig('', port, {}, ''); - - expect(config.port).toEqual(port); + expect(config.url.scheme).toEqual('bolt'); + expect(config.url.host).toEqual('neo4j.com'); + expect(config.url.port).toEqual(4242); }); it('should respect given encrypted conf', () => { const encrypted = 'ENCRYPTION_ON'; - const config = new ChannelConfig('', 42, {encrypted: encrypted}, ''); + const config = new ChannelConfig(null, {encrypted: encrypted}, ''); expect(config.encrypted).toEqual(encrypted); }); @@ -50,7 +45,7 @@ describe('ChannelConfig', () => { it('should respect given trust conf', () => { const trust = 'TRUST_ALL_CERTIFICATES'; - const config = new ChannelConfig('', 42, {trust: trust}, ''); + const config = new ChannelConfig(null, {trust: trust}, ''); expect(config.trust).toEqual(trust); }); @@ -58,7 +53,7 @@ describe('ChannelConfig', () => { it('should respect given trusted certificates conf', () => { const trustedCertificates = ['./foo.pem', './bar.pem', './baz.pem']; - const config = new ChannelConfig('', 42, {trustedCertificates: trustedCertificates}, ''); + const config = new ChannelConfig(null, {trustedCertificates: trustedCertificates}, ''); expect(config.trustedCertificates).toEqual(trustedCertificates); }); @@ -66,7 +61,7 @@ describe('ChannelConfig', () => { it('should respect given known hosts', () => { const knownHostsPath = '~/.neo4j/known_hosts'; - const config = new ChannelConfig('', 42, {knownHosts: knownHostsPath}, ''); + const config = new ChannelConfig(null, {knownHosts: knownHostsPath}, ''); expect(config.knownHostsPath).toEqual(knownHostsPath); }); @@ -74,52 +69,64 @@ describe('ChannelConfig', () => { it('should respect given connection error code', () => { const connectionErrorCode = 'ConnectionFailed'; - const config = new ChannelConfig('', 42, {}, connectionErrorCode); + const config = new ChannelConfig(null, {}, connectionErrorCode); expect(config.connectionErrorCode).toEqual(connectionErrorCode); }); it('should use encryption if available but not configured', () => { - const config = new ChannelConfig('', 42, {}, ''); + const config = new ChannelConfig(null, {}, ''); expect(config.encrypted).toEqual(hasFeature('trust_all_certificates')); }); it('should use available trust conf when nothing configured', () => { - const config = new ChannelConfig('', 42, {}, ''); + const config = new ChannelConfig(null, {}, ''); const availableTrust = hasFeature('trust_all_certificates') ? 'TRUST_ALL_CERTIFICATES' : 'TRUST_CUSTOM_CA_SIGNED_CERTIFICATES'; expect(config.trust).toEqual(availableTrust); }); it('should have no trusted certificates when not configured', () => { - const config = new ChannelConfig('', 42, {}, ''); + const config = new ChannelConfig(null, {}, ''); expect(config.trustedCertificates).toEqual([]); }); it('should have null known hosts path when not configured', () => { - const config = new ChannelConfig('', 42, {}, ''); + const config = new ChannelConfig(null, {}, ''); expect(config.knownHostsPath).toBeNull(); }); it('should have service unavailable as default error code', () => { - const config = new ChannelConfig('', 42, {}, ''); + const config = new ChannelConfig(null, {}, ''); expect(config.connectionErrorCode).toEqual(SERVICE_UNAVAILABLE); }); it('should have connection timeout by default', () => { - const config = new ChannelConfig('', 42, {}, ''); + const config = new ChannelConfig(null, {}, ''); expect(config.connectionTimeout).toEqual(5000); }); it('should respect configured connection timeout', () => { - const config = new ChannelConfig('', 42, {connectionTimeout: 424242}, ''); + const config = new ChannelConfig(null, {connectionTimeout: 424242}, ''); expect(config.connectionTimeout).toEqual(424242); }); + it('should respect disabled connection timeout with value zero', () => { + const config = new ChannelConfig(null, {connectionTimeout: 0}, ''); + + expect(config.connectionTimeout).toBeNull(); + }); + + it('should respect disabled connection timeout with negative value', () => { + const config = new ChannelConfig(null, {connectionTimeout: -42}, ''); + + expect(config.connectionTimeout).toBeNull(); + }); + }); diff --git a/test/internal/ch-websocket.test.js b/test/internal/ch-websocket.test.js new file mode 100644 index 000000000..204db69a4 --- /dev/null +++ b/test/internal/ch-websocket.test.js @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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 {setTimeoutMock} from './timers-util'; + +describe('WebSocketChannel', () => { + + const WebSocketChannel = wsChannel.channel; + const webSocketChannelAvailable = wsChannel.available; + + let OriginalWebSocket; + let webSocketChannel; + + beforeEach(() => { + if (webSocketChannelAvailable) { + OriginalWebSocket = WebSocket; + } + }); + + afterEach(() => { + if (webSocketChannelAvailable) { + WebSocket = OriginalWebSocket; + } + if (webSocketChannel) { + webSocketChannel.close(); + } + }); + + it('should fallback to literal IPv6 when SyntaxError is thrown', () => { + testFallbackToLiteralIPv6('bolt://[::1]:7687', 'ws://--1.ipv6-literal.net:7687'); + }); + + it('should fallback to literal link-local IPv6 when SyntaxError is thrown', () => { + testFallbackToLiteralIPv6('bolt://[fe80::1%lo0]:8888', 'ws://fe80--1slo0.ipv6-literal.net:8888'); + }); + + it('should clear connection timeout when closed', () => { + if (!webSocketChannelAvailable) { + return; + } + + const fakeSetTimeout = setTimeoutMock.install(); + try { + // do not execute setTimeout callbacks + fakeSetTimeout.pause(); + + let fakeWebSocketClosed = false; + + // replace real WebSocket with a function that does nothing + WebSocket = () => { + return { + close: () => { + fakeWebSocketClosed = true; + } + }; + }; + + const url = urlUtil.parseBoltUrl('bolt://localhost:7687'); + const driverConfig = {connectionTimeout: 4242}; + const channelConfig = new ChannelConfig(url, driverConfig, SERVICE_UNAVAILABLE); + + webSocketChannel = new WebSocketChannel(channelConfig); + + expect(fakeWebSocketClosed).toBeFalsy(); + expect(fakeSetTimeout.invocationDelays).toEqual([]); + expect(fakeSetTimeout.clearedTimeouts).toEqual([]); + + webSocketChannel.close(); + + expect(fakeWebSocketClosed).toBeTruthy(); + expect(fakeSetTimeout.invocationDelays).toEqual([]); + expect(fakeSetTimeout.clearedTimeouts).toEqual([0]); // cleared one timeout with id 0 + } finally { + fakeSetTimeout.uninstall(); + } + }); + + function testFallbackToLiteralIPv6(boltAddress, expectedWsAddress) { + if (!webSocketChannelAvailable) { + return; + } + + // replace real WebSocket with a function that throws when IPv6 address is used + WebSocket = url => { + if (url.indexOf('[') !== -1) { + throw new SyntaxError(); + } + return { + url: url, + close: () => { + } + }; + }; + + const url = urlUtil.parseBoltUrl(boltAddress); + // disable connection timeout, so that WebSocketChannel does not set any timeouts + const driverConfig = {connectionTimeout: 0}; + const channelConfig = new ChannelConfig(url, driverConfig, SERVICE_UNAVAILABLE); + + webSocketChannel = new WebSocketChannel(channelConfig); + + expect(webSocketChannel._ws.url).toEqual(expectedWsAddress); + } + +}); diff --git a/test/internal/host-name-resolvers.test.js b/test/internal/host-name-resolvers.test.js index 83820755a..a9e698f2c 100644 --- a/test/internal/host-name-resolvers.test.js +++ b/test/internal/host-name-resolvers.test.js @@ -19,7 +19,7 @@ import {DnsHostNameResolver, DummyHostNameResolver} from '../../src/v1/internal/host-name-resolvers'; import hasFeature from '../../src/v1/internal/features'; -import {parseHost, parsePort, parseScheme} from '../../src/v1/internal/util'; +import urlUtil from '../../src/v1/internal/url-util'; describe('DummyHostNameResolver', () => { @@ -72,9 +72,10 @@ describe('DnsHostNameResolver', () => { addresses.forEach(address => { expectToBeDefined(address); - expect(parseScheme(address)).toEqual(''); - expectToBeDefined(parseHost(address)); - expect(parsePort(address)).not.toBeDefined(); + const parsedUrl = urlUtil.parseBoltUrl(address); + expect(parsedUrl.scheme).toBeNull(); + expectToBeDefined(parsedUrl.host); + expect(parsedUrl.port).toEqual(7687); // default port should be appended }); done(); @@ -90,36 +91,47 @@ describe('DnsHostNameResolver', () => { addresses.forEach(address => { expectToBeDefined(address); - expect(parseScheme(address)).toEqual(''); - expectToBeDefined(parseHost(address)); - expect(parsePort(address)).toEqual('7474'); + const parsedUrl = urlUtil.parseBoltUrl(address); + expect(parsedUrl.scheme).toBeNull(); + expectToBeDefined(parsedUrl.host); + expect(parsedUrl.port).toEqual(7474); }); done(); }); }); - it('should resolve unresolvable address to itself', done => { - const seedRouter = '127.0.0.1'; // IP can't be resolved - const resolver = new DnsHostNameResolver(); + it('should resolve IPv4 address to itself', done => { + const addressToResolve = '127.0.0.1'; + const expectedResolvedAddress = '127.0.0.1:7687'; // includes default port + testIpAddressResolution(addressToResolve, expectedResolvedAddress, done); + }); - resolver.resolve(seedRouter).then(addresses => { - expect(addresses.length).toEqual(1); - expect(addresses[0]).toEqual(seedRouter); - done(); - }); + it('should resolve IPv4 address with port to itself', done => { + const address = '127.0.0.1:7474'; + testIpAddressResolution(address, address, done); }); - it('should resolve unresolvable address with port to itself', done => { - const seedRouter = '127.0.0.1:7474'; // IP can't be resolved + it('should resolve IPv6 address to itself', done => { + const addressToResolve = '[2001:4860:4860::8888]'; + const expectedResolvedAddress = '[2001:4860:4860::8888]:7687'; // includes default port + testIpAddressResolution(addressToResolve, expectedResolvedAddress, done); + }); + + it('should resolve IPv6 address with port to itself', done => { + const address = '[2001:4860:4860::8888]:7474'; + testIpAddressResolution(address, address, done); + }); + + function testIpAddressResolution(address, expectedResolvedAddress, done) { const resolver = new DnsHostNameResolver(); - resolver.resolve(seedRouter).then(addresses => { + resolver.resolve(address).then(addresses => { expect(addresses.length).toEqual(1); - expect(addresses[0]).toEqual(seedRouter); + expect(addresses[0]).toEqual(expectedResolvedAddress); done(); }); - }); + } } }); diff --git a/test/internal/routing-util.test.js b/test/internal/routing-util.test.js index 9cfef4b77..24aa42a55 100644 --- a/test/internal/routing-util.test.js +++ b/test/internal/routing-util.test.js @@ -24,7 +24,7 @@ import {newError, PROTOCOL_ERROR, SERVICE_UNAVAILABLE, SESSION_EXPIRED} from '.. import lolex from 'lolex'; import FakeConnection from './fake-connection'; -const ROUTER_ADDRESS = 'bolt+routing://test.router.com'; +const ROUTER_ADDRESS = 'test.router.com:4242'; describe('RoutingUtil', () => { @@ -78,8 +78,8 @@ describe('RoutingUtil', () => { callRoutingProcedure(session).catch(error => { expect(error.code).toBe(SERVICE_UNAVAILABLE); - expect(error.message).toBe('Server ' + ROUTER_ADDRESS + ' could not perform routing. ' + - 'Make sure you are connecting to a causal cluster'); + expect(error.message) + .toBe(`Server at ${ROUTER_ADDRESS} can't perform routing. Make sure you are connecting to a causal cluster`); done(); }); }); diff --git a/test/internal/server-version.test.js b/test/internal/server-version.test.js index f86d517b6..099b0fdb0 100644 --- a/test/internal/server-version.test.js +++ b/test/internal/server-version.test.js @@ -17,6 +17,8 @@ * limitations under the License. */ +import neo4j from '../../src/v1'; +import sharedNeo4j from '../internal/shared-neo4j'; import {ServerVersion, VERSION_3_2_0, VERSION_IN_DEV} from '../../src/v1/internal/server-version'; describe('ServerVersion', () => { @@ -80,6 +82,33 @@ describe('ServerVersion', () => { verifyVersion(parse('Neo4j/Dev'), 0, 0, 0); }); + it('should fetch version using driver', done => { + const driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken); + ServerVersion.fromDriver(driver).then(version => { + driver.close(); + expect(version).not.toBeNull(); + expect(version).toBeDefined(); + expect(version instanceof ServerVersion).toBeTruthy(); + done(); + }).catch(error => { + driver.close(); + done.fail(error); + }); + }); + + it('should fail to fetch version using incorrect driver', done => { + const driver = neo4j.driver('bolt://localhost:4242', sharedNeo4j.authToken); // use wrong port + ServerVersion.fromDriver(driver).then(version => { + driver.close(); + done.fail('Should not be able to fetch version: ' + JSON.stringify(version)); + }).catch(error => { + expect(error).not.toBeNull(); + expect(error).toBeDefined(); + driver.close(); + done(); + }); + }); + it('should compare equal versions', () => { expect(new ServerVersion(3, 1, 0).compareTo(new ServerVersion(3, 1, 0))).toEqual(0); expect(new ServerVersion(1, 9, 12).compareTo(new ServerVersion(1, 9, 12))).toEqual(0); diff --git a/test/internal/shared-neo4j.js b/test/internal/shared-neo4j.js index be350f7ae..67f1e9eb1 100644 --- a/test/internal/shared-neo4j.js +++ b/test/internal/shared-neo4j.js @@ -64,7 +64,13 @@ class SupportedPlatform extends UnsupportedPlatform { } spawn(command, args) { - return this._childProcess.spawnSync(command, args); + const options = { + // ignore stdin, use default values for stdout and stderr + // otherwise spawned java process does not see IPv6 address of the local interface and Neo4j fails to start + // https://github.com/nodejs/node-v0.x-archive/issues/7406 + stdio: ['ignore', null, null] + }; + return this._childProcess.spawnSync(command, args, options); } listDir(path) { @@ -94,6 +100,11 @@ const username = 'neo4j'; const password = 'password'; const authToken = neo4j.auth.basic(username, password); +const additionalConfig = { + // tell neo4j to listen for IPv6 connections, only supported by 3.1+ + 'dbms.connectors.default_listen_address': '::' +}; + const neoCtrlVersionParam = '-e'; const defaultNeo4jVersion = '3.2.5'; const defaultNeoCtrlArgs = `${neoCtrlVersionParam} ${defaultNeo4jVersion}`; @@ -113,6 +124,7 @@ function start(dir, givenNeoCtrlArgs) { if (boltKitCheckResult.successful) { const neo4jDir = installNeo4j(dir, givenNeoCtrlArgs); + configureNeo4j(neo4jDir); createDefaultUser(neo4jDir); startNeo4j(neo4jDir); } else { @@ -158,6 +170,18 @@ function installNeo4j(dir, givenNeoCtrlArgs) { } } +function configureNeo4j(neo4jDir) { + console.log('Configuring Neo4j at: \'' + neo4jDir + '\' with ' + JSON.stringify(additionalConfig)); + + const configEntries = Object.keys(additionalConfig).map(key => `${key}=${additionalConfig[key]}`); + const configureResult = runCommand('neoctrl-configure', [neo4jDir, ...configEntries]); + if (!configureResult.successful) { + throw new Error('Unable to configure Neo4j.\n' + configureResult.fullOutput); + } + + console.log('Configured Neo4j at: \'' + neo4jDir + '\''); +} + function createDefaultUser(neo4jDir) { console.log('Creating user \'' + username + '\' for Neo4j at: \'' + neo4jDir + '\''); const result = runCommand('neoctrl-create-user', [neo4jDir, username, password]); diff --git a/test/internal/tls.test.js b/test/internal/tls.test.js index afbb42938..b0d1947c1 100644 --- a/test/internal/tls.test.js +++ b/test/internal/tls.test.js @@ -16,12 +16,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -var NodeChannel = require('../../lib/v1/internal/ch-node.js').default; -var neo4j = require("../../lib/v1"); -var fs = require("fs"); -var path = require('path'); -var hasFeature = require("../../lib/v1/internal/features").default; -var sharedNeo4j = require("../internal/shared-neo4j").default; + +import NodeChannel from '../../src/v1/internal/ch-node'; +import neo4j from '../../src/v1'; +import fs from 'fs'; +import path from 'path'; +import hasFeature from '../../src/v1/internal/features'; +import sharedNeo4j from '../internal/shared-neo4j'; describe('trust-signed-certificates', function() { @@ -51,7 +52,7 @@ describe('trust-signed-certificates', function() { }); }); - it('should accept known certificates', function(done) { + it('should accept known certificates', function (done) { // Assuming we only run this test on NodeJS with TOFU support if( !NodeChannel.available ) { done(); diff --git a/test/internal/url-util.test.js b/test/internal/url-util.test.js new file mode 100644 index 000000000..a54b2f15b --- /dev/null +++ b/test/internal/url-util.test.js @@ -0,0 +1,761 @@ +/** + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import urlUtil from '../../src/v1/internal/url-util'; +import {DEFAULT_PORT} from '../../src/v1/internal/ch-config'; + +describe('url-util', () => { + + it('should parse URL with just host name', () => { + verifyUrl('localhost', { + host: 'localhost' + }); + + verifyUrl('neo4j.com', { + host: 'neo4j.com' + }); + + verifyUrl('some-neo4j-server.com', { + host: 'some-neo4j-server.com' + }); + + verifyUrl('ec2-34-242-76-91.eu-west-1.compute.aws.com', { + host: 'ec2-34-242-76-91.eu-west-1.compute.aws.com' + }); + }); + + it('should parse URL with just IPv4 address', () => { + verifyUrl('127.0.0.1', { + host: '127.0.0.1' + }); + + verifyUrl('10.10.192.0', { + host: '10.10.192.0' + }); + + verifyUrl('172.10.5.1', { + host: '172.10.5.1' + }); + + verifyUrl('34.242.76.91', { + host: '34.242.76.91' + }); + }); + + it('should parse URL with just IPv6 address', () => { + verifyUrl('[::1]', { + host: '::1', + ipv6: true + }); + + verifyUrl('[ff02::2:ff00:0]', { + host: 'ff02::2:ff00:0', + ipv6: true + }); + + verifyUrl('[1afc:0:a33:85a3::ff2f]', { + host: '1afc:0:a33:85a3::ff2f', + ipv6: true + }); + + verifyUrl('[ff0a::101]', { + host: 'ff0a::101', + ipv6: true + }); + + verifyUrl('[2a05:d018:270:f400:6d8c:d425:c5f:97f3]', { + host: '2a05:d018:270:f400:6d8c:d425:c5f:97f3', + ipv6: true + }); + + verifyUrl('[fe80::1%lo0]', { + host: 'fe80::1%lo0', + ipv6: true + }); + }); + + it('should parse URL with host name and query', () => { + verifyUrl('localhost?key1=value1&key2=value2', { + host: 'localhost', + query: {key1: 'value1', key2: 'value2'} + }); + + verifyUrl('neo4j.com/?key1=1&key2=2', { + host: 'neo4j.com', + query: {key1: '1', key2: '2'} + }); + + verifyUrl('some-neo4j-server.com?a=value1&b=value2&c=value3', { + host: 'some-neo4j-server.com', + query: {a: 'value1', b: 'value2', c: 'value3'} + }); + + verifyUrl('ec2-34-242-76-91.eu-west-1.compute.aws.com/?foo=1&bar=2&baz=3&qux=4', { + host: 'ec2-34-242-76-91.eu-west-1.compute.aws.com', + query: {foo: '1', bar: '2', baz: '3', qux: '4'} + }); + }); + + it('should parse URL with IPv4 address and query', () => { + verifyUrl('127.0.0.1?key1=value1&key2=value2', { + host: '127.0.0.1', + query: {key1: 'value1', key2: 'value2'} + }); + + verifyUrl('10.10.192.0?key1=1&key2=2', { + host: '10.10.192.0', + query: {key1: '1', key2: '2'} + }); + + verifyUrl('172.10.5.1?a=value1&b=value2&c=value3', { + host: '172.10.5.1', + query: {a: 'value1', b: 'value2', c: 'value3'} + }); + + verifyUrl('34.242.76.91/?foo=1&bar=2&baz=3&qux=4', { + host: '34.242.76.91', + query: {foo: '1', bar: '2', baz: '3', qux: '4'} + }); + }); + + it('should parse URL with IPv6 address and query', () => { + verifyUrl('[::1]?key1=value1&key2=value2', { + host: '::1', + query: {key1: 'value1', key2: 'value2'}, + ipv6: true + }); + + verifyUrl('[ff02::2:ff00:0]?key1=1&key2=2', { + host: 'ff02::2:ff00:0', + query: {key1: '1', key2: '2'}, + ipv6: true + }); + + verifyUrl('[1afc:0:a33:85a3::ff2f]/?a=value1&b=value2&c=value3', { + host: '1afc:0:a33:85a3::ff2f', + query: {a: 'value1', b: 'value2', c: 'value3'}, + ipv6: true + }); + + verifyUrl('[ff0a::101]/?foo=1&bar=2&baz=3&qux=4', { + host: 'ff0a::101', + query: {foo: '1', bar: '2', baz: '3', qux: '4'}, + ipv6: true + }); + + verifyUrl('[2a05:d018:270:f400:6d8c:d425:c5f:97f3]?animal=apa', { + host: '2a05:d018:270:f400:6d8c:d425:c5f:97f3', + query: {animal: 'apa'}, + ipv6: true + }); + + verifyUrl('[fe80::1%lo0]?animal=apa', { + host: 'fe80::1%lo0', + query: {animal: 'apa'}, + ipv6: true + }); + }); + + it('should parse URL with scheme, host name and query', () => { + verifyUrl('http://localhost?key1=value1&key2=value2', { + scheme: 'http', + host: 'localhost', + query: {key1: 'value1', key2: 'value2'} + }); + + verifyUrl('https://neo4j.com/?key1=1&key2=2', { + scheme: 'https', + host: 'neo4j.com', + query: {key1: '1', key2: '2'} + }); + + verifyUrl('bolt://some-neo4j-server.com/?a=value1&b=value2&c=value3', { + scheme: 'bolt', + host: 'some-neo4j-server.com', + query: {a: 'value1', b: 'value2', c: 'value3'} + }); + + verifyUrl('bolt+routing://ec2-34-242-76-91.eu-west-1.compute.aws.com?foo=1&bar=2&baz=3&qux=4', { + scheme: 'bolt+routing', + host: 'ec2-34-242-76-91.eu-west-1.compute.aws.com', + query: {foo: '1', bar: '2', baz: '3', qux: '4'} + }); + }); + + it('should parse URL with scheme, IPv4 address and query', () => { + verifyUrl('ftp://127.0.0.1/?key1=value1&key2=value2', { + scheme: 'ftp', + host: '127.0.0.1', + query: {key1: 'value1', key2: 'value2'} + }); + + verifyUrl('bolt+routing://10.10.192.0?key1=1&key2=2', { + scheme: 'bolt+routing', + host: '10.10.192.0', + query: {key1: '1', key2: '2'} + }); + + verifyUrl('bolt://172.10.5.1?a=value1&b=value2&c=value3', { + scheme: 'bolt', + host: '172.10.5.1', + query: {a: 'value1', b: 'value2', c: 'value3'} + }); + + verifyUrl('https://34.242.76.91/?foo=1&bar=2&baz=3&qux=4', { + scheme: 'https', + host: '34.242.76.91', + query: {foo: '1', bar: '2', baz: '3', qux: '4'} + }); + }); + + it('should parse URL with scheme, IPv6 address and query', () => { + verifyUrl('bolt+routing://[::1]?key1=value1&key2=value2', { + scheme: 'bolt+routing', + host: '::1', + query: {key1: 'value1', key2: 'value2'}, + ipv6: true + }); + + verifyUrl('http://[ff02::2:ff00:0]?key1=1&key2=2', { + scheme: 'http', + host: 'ff02::2:ff00:0', + query: {key1: '1', key2: '2'}, + ipv6: true + }); + + verifyUrl('https://[1afc:0:a33:85a3::ff2f]/?a=value1&b=value2&c=value3', { + scheme: 'https', + host: '1afc:0:a33:85a3::ff2f', + query: {a: 'value1', b: 'value2', c: 'value3'}, + ipv6: true + }); + + verifyUrl('bolt://[ff0a::101]/?foo=1&bar=2&baz=3&qux=4', { + scheme: 'bolt', + host: 'ff0a::101', + query: {foo: '1', bar: '2', baz: '3', qux: '4'}, + ipv6: true + }); + + verifyUrl('bolt+routing://[2a05:d018:270:f400:6d8c:d425:c5f:97f3]?animal=apa', { + scheme: 'bolt+routing', + host: '2a05:d018:270:f400:6d8c:d425:c5f:97f3', + query: {animal: 'apa'}, + ipv6: true + }); + + verifyUrl('bolt+routing://[fe80::1%lo0]?animal=apa', { + scheme: 'bolt+routing', + host: 'fe80::1%lo0', + query: {animal: 'apa'}, + ipv6: true + }); + }); + + it('should parse URL with host name and port', () => { + verifyUrl('localhost:1212', { + host: 'localhost', + port: 1212 + }); + + verifyUrl('neo4j.com:8888', { + host: 'neo4j.com', + port: 8888 + }); + + verifyUrl('some-neo4j-server.com:42', { + host: 'some-neo4j-server.com', + port: 42 + }); + + verifyUrl('ec2-34-242-76-91.eu-west-1.compute.aws.com:62220', { + host: 'ec2-34-242-76-91.eu-west-1.compute.aws.com', + port: 62220 + }); + }); + + it('should parse URL with IPv4 address and port', () => { + verifyUrl('127.0.0.1:9090', { + host: '127.0.0.1', + port: 9090 + }); + + verifyUrl('10.10.192.0:22000', { + host: '10.10.192.0', + port: 22000 + }); + + verifyUrl('172.10.5.1:42', { + host: '172.10.5.1', + port: 42 + }); + + verifyUrl('34.242.76.91:7687', { + host: '34.242.76.91', + port: 7687 + }); + }); + + it('should parse URL with IPv6 address and port', () => { + verifyUrl('[::1]:36000', { + host: '::1', + port: 36000, + ipv6: true + }); + + verifyUrl('[ff02::2:ff00:0]:8080', { + host: 'ff02::2:ff00:0', + port: 8080, + ipv6: true + }); + + verifyUrl('[1afc:0:a33:85a3::ff2f]:7474', { + host: '1afc:0:a33:85a3::ff2f', + port: 7474, + ipv6: true + }); + + verifyUrl('[ff0a::101]:1000', { + host: 'ff0a::101', + port: 1000, + ipv6: true + }); + + verifyUrl('[2a05:d018:270:f400:6d8c:d425:c5f:97f3]:7475', { + host: '2a05:d018:270:f400:6d8c:d425:c5f:97f3', + port: 7475, + ipv6: true + }); + + verifyUrl('[fe80::1%lo0]:7475', { + host: 'fe80::1%lo0', + port: 7475, + ipv6: true + }); + }); + + it('should parse URL with scheme and host name', () => { + verifyUrl('ftp://localhost', { + scheme: 'ftp', + host: 'localhost' + }); + + verifyUrl('https://neo4j.com', { + scheme: 'https', + host: 'neo4j.com' + }); + + verifyUrl('wss://some-neo4j-server.com', { + scheme: 'wss', + host: 'some-neo4j-server.com' + }); + + verifyUrl('bolt://ec2-34-242-76-91.eu-west-1.compute.aws.com', { + scheme: 'bolt', + host: 'ec2-34-242-76-91.eu-west-1.compute.aws.com' + }); + }); + + it('should parse URL with scheme and IPv4 address', () => { + verifyUrl('bolt+routing://127.0.0.1', { + scheme: 'bolt+routing', + host: '127.0.0.1' + }); + + verifyUrl('http://10.10.192.0', { + scheme: 'http', + host: '10.10.192.0' + }); + + verifyUrl('ws://172.10.5.1', { + scheme: 'ws', + host: '172.10.5.1' + }); + + verifyUrl('bolt://34.242.76.91', { + scheme: 'bolt', + host: '34.242.76.91' + }); + }); + + it('should parse URL with scheme and IPv6 address', () => { + verifyUrl('https://[::1]', { + scheme: 'https', + host: '::1', + ipv6: true + }); + + verifyUrl('http://[ff02::2:ff00:0]', { + scheme: 'http', + host: 'ff02::2:ff00:0', + ipv6: true + }); + + verifyUrl('bolt+routing://[1afc:0:a33:85a3::ff2f]', { + scheme: 'bolt+routing', + host: '1afc:0:a33:85a3::ff2f', + ipv6: true + }); + + verifyUrl('bolt://[ff0a::101]', { + scheme: 'bolt', + host: 'ff0a::101', + ipv6: true + }); + + verifyUrl('bolt+routing://[2a05:d018:270:f400:6d8c:d425:c5f:97f3]', { + scheme: 'bolt+routing', + host: '2a05:d018:270:f400:6d8c:d425:c5f:97f3', + ipv6: true + }); + + verifyUrl('bolt+routing://[fe80::1%lo0]', { + scheme: 'bolt+routing', + host: 'fe80::1%lo0', + ipv6: true + }); + }); + + it('should parse URL with scheme, host name and port', () => { + verifyUrl('http://localhost:8080', { + scheme: 'http', + host: 'localhost', + port: 8080 + }); + + verifyUrl('bolt://neo4j.com:42', { + scheme: 'bolt', + host: 'neo4j.com', + port: 42 + }); + + verifyUrl('bolt+routing://some-neo4j-server.com:12000', { + scheme: 'bolt+routing', + host: 'some-neo4j-server.com', + port: 12000 + }); + + verifyUrl('wss://ec2-34-242-76-91.eu-west-1.compute.aws.com:2626', { + scheme: 'wss', + host: 'ec2-34-242-76-91.eu-west-1.compute.aws.com', + port: 2626 + }); + }); + + it('should parse URL with scheme, IPv4 address and port', () => { + verifyUrl('bolt://127.0.0.1:9091', { + scheme: 'bolt', + host: '127.0.0.1', + port: 9091 + }); + + verifyUrl('bolt://10.10.192.0:7447', { + scheme: 'bolt', + host: '10.10.192.0', + port: 7447 + }); + + verifyUrl('bolt+routing://172.10.5.1:8888', { + scheme: 'bolt+routing', + host: '172.10.5.1', + port: 8888 + }); + + verifyUrl('https://34.242.76.91:42', { + scheme: 'https', + host: '34.242.76.91', + port: 42 + }); + }); + + it('should parse URL with scheme, IPv6 address and port', () => { + verifyUrl('http://[::1]:9123', { + scheme: 'http', + host: '::1', + port: 9123, + ipv6: true + }); + + verifyUrl('bolt://[ff02::2:ff00:0]:3831', { + scheme: 'bolt', + host: 'ff02::2:ff00:0', + port: 3831, + ipv6: true + }); + + verifyUrl('bolt+routing://[1afc:0:a33:85a3::ff2f]:50505', { + scheme: 'bolt+routing', + host: '1afc:0:a33:85a3::ff2f', + port: 50505, + ipv6: true + }); + + verifyUrl('ftp://[ff0a::101]:4242', { + scheme: 'ftp', + host: 'ff0a::101', + port: 4242, + ipv6: true + }); + + verifyUrl('wss://[2a05:d018:270:f400:6d8c:d425:c5f:97f3]:22', { + scheme: 'wss', + host: '2a05:d018:270:f400:6d8c:d425:c5f:97f3', + port: 22, + ipv6: true + }); + + verifyUrl('wss://[fe80::1%lo0]:22', { + scheme: 'wss', + host: 'fe80::1%lo0', + port: 22, + ipv6: true + }); + }); + + it('should parse URL with scheme, host name, port and query', () => { + verifyUrl('http://localhost:3032/?key1=value1&key2=value2', { + scheme: 'http', + host: 'localhost', + port: 3032, + query: {key1: 'value1', key2: 'value2'} + }); + + verifyUrl('https://neo4j.com:7575?foo=bar&baz=qux', { + scheme: 'https', + host: 'neo4j.com', + port: 7575, + query: {foo: 'bar', baz: 'qux'} + }); + + verifyUrl('bolt+routing://some-neo4j-server.com:14500?key=value', { + scheme: 'bolt+routing', + host: 'some-neo4j-server.com', + port: 14500, + query: {key: 'value'} + }); + + verifyUrl('ws://ec2-34-242-76-91.eu-west-1.compute.aws.com:30270?a=1&b=2&c=3&d=4', { + scheme: 'ws', + host: 'ec2-34-242-76-91.eu-west-1.compute.aws.com', + port: 30270, + query: {a: '1', b: '2', c: '3', d: '4'} + }); + }); + + it('should parse URL with scheme, IPv4 address, port and query', () => { + verifyUrl('bolt://127.0.0.1:30399?key1=value1&key2=value2', { + scheme: 'bolt', + host: '127.0.0.1', + port: 30399, + query: {key1: 'value1', key2: 'value2'} + }); + + verifyUrl('bolt+routing://10.10.192.0:12100/?foo=bar&baz=qux', { + scheme: 'bolt+routing', + host: '10.10.192.0', + port: 12100, + query: {foo: 'bar', baz: 'qux'} + }); + + verifyUrl('bolt://172.10.5.1:22?a=1&b=2&c=3&d=4', { + scheme: 'bolt', + host: '172.10.5.1', + port: 22, + query: {a: '1', b: '2', c: '3', d: '4'} + }); + + verifyUrl('http://34.242.76.91:1829?key=value', { + scheme: 'http', + host: '34.242.76.91', + port: 1829, + query: {key: 'value'} + }); + }); + + it('should parse URL with scheme, IPv6 address, port and query', () => { + verifyUrl('https://[::1]:4217?key=value', { + scheme: 'https', + host: '::1', + port: 4217, + query: {key: 'value'}, + ipv6: true + }); + + verifyUrl('bolt+routing://[ff02::2:ff00:0]:22/?animal1=apa&animal2=dog', { + scheme: 'bolt+routing', + host: 'ff02::2:ff00:0', + port: 22, + query: {animal1: 'apa', animal2: 'dog'}, + ipv6: true + }); + + verifyUrl('bolt://[1afc:0:a33:85a3::ff2f]:4242?a=1&b=2&c=3&d=4', { + scheme: 'bolt', + host: '1afc:0:a33:85a3::ff2f', + port: 4242, + query: {a: '1', b: '2', c: '3', d: '4'}, + ipv6: true + }); + + verifyUrl('wss://[ff0a::101]:24240?foo=bar&baz=qux', { + scheme: 'wss', + host: 'ff0a::101', + port: 24240, + query: {foo: 'bar', baz: 'qux'}, + ipv6: true + }); + + verifyUrl('https://[2a05:d018:270:f400:6d8c:d425:c5f:97f3]:42?key1=value1&key2=value2', { + scheme: 'https', + host: '2a05:d018:270:f400:6d8c:d425:c5f:97f3', + port: 42, + query: {key1: 'value1', key2: 'value2'}, + ipv6: true + }); + + verifyUrl('https://[fe80::1%lo0]:4242?key1=value1', { + scheme: 'https', + host: 'fe80::1%lo0', + port: 4242, + query: {key1: 'value1'}, + ipv6: true + }); + }); + + it('should fail to parse URL without host', () => { + expect(() => parse('http://')).toThrow(); + expect(() => parse('bolt://')).toThrow(); + expect(() => parse('bolt+routing://')).toThrow(); + }); + + it('should fail to parse URL with duplicated query parameters', () => { + expect(() => parse('bolt://localhost/?key=value1&key=value2')).toThrow(); + expect(() => parse('bolt://localhost:8080/?key=value1&key=value2')).toThrow(); + + expect(() => parse('bolt+routing://10.10.127.5?key=value1&key=value2')).toThrow(); + expect(() => parse('bolt+routing://10.10.127.5:8080?key=value1&key=value2')).toThrow(); + + expect(() => parse('https://[ff0a::101]?key=value1&key=value2')).toThrow(); + expect(() => parse('https://[ff0a::101]:8080?key=value1&key=value2')).toThrow(); + }); + + it('should fail to parse URL with empty query key', () => { + expect(() => parse('bolt://localhost?=value')).toThrow(); + expect(() => parse('bolt://localhost:8080?=value')).toThrow(); + + expect(() => parse('bolt+routing://10.10.127.5?=value')).toThrow(); + expect(() => parse('bolt+routing://10.10.127.5:8080?=value')).toThrow(); + + expect(() => parse('https://[ff0a::101]/?value=')).toThrow(); + expect(() => parse('https://[ff0a::101]:8080/?=value')).toThrow(); + }); + + it('should fail to parse URL with empty query value', () => { + expect(() => parse('bolt://localhost?key=')).toThrow(); + expect(() => parse('bolt://localhost:8080?key=')).toThrow(); + + expect(() => parse('bolt+routing://10.10.127.5/?key=')).toThrow(); + expect(() => parse('bolt+routing://10.10.127.5:8080/?key=')).toThrow(); + + expect(() => parse('https://[ff0a::101]?key=')).toThrow(); + expect(() => parse('https://[ff0a::101]:8080?key=')).toThrow(); + }); + + it('should fail to parse URL with no query value', () => { + expect(() => parse('bolt://localhost?key')).toThrow(); + expect(() => parse('bolt://localhost:8080?key')).toThrow(); + + expect(() => parse('bolt+routing://10.10.127.5/?key')).toThrow(); + expect(() => parse('bolt+routing://10.10.127.5:8080/?key')).toThrow(); + + expect(() => parse('https://[ff0a::101]?key')).toThrow(); + expect(() => parse('https://[ff0a::101]:8080?key')).toThrow(); + }); + + it('should fail to parse non-strings', () => { + expect(() => parse({})).toThrowError(TypeError); + expect(() => parse(['bolt://localhost:2020'])).toThrowError(TypeError); + expect(() => parse(() => 'bolt://localhost:8888')).toThrowError(TypeError); + }); + + it('should format IPv4 address', () => { + expect(urlUtil.formatIPv4Address('127.0.0.1', 4242)).toEqual('127.0.0.1:4242'); + expect(urlUtil.formatIPv4Address('192.168.10.10', 8080)).toEqual('192.168.10.10:8080'); + expect(urlUtil.formatIPv4Address('8.8.8.8', 80)).toEqual('8.8.8.8:80'); + }); + + it('should format IPv6 address', () => { + expect(urlUtil.formatIPv6Address('::1', 1200)).toEqual('[::1]:1200'); + expect(urlUtil.formatIPv6Address('ff0a::101', 8080)).toEqual('[ff0a::101]:8080'); + + expect(urlUtil.formatIPv6Address('[::1]', 42)).toEqual('[::1]:42'); + expect(urlUtil.formatIPv6Address('[1afc:0:a33:85a3::ff2f]', 20201)).toEqual('[1afc:0:a33:85a3::ff2f]:20201'); + }); + + it('should fail to format partially escaped IPv6 address', () => { + expect(() => urlUtil.formatIPv6Address('[::1', 1000)).toThrow(); + expect(() => urlUtil.formatIPv6Address('::1]', 2000)).toThrow(); + + expect(() => urlUtil.formatIPv6Address('[1afc:0:a33:85a3::ff2f', 3000)).toThrow(); + expect(() => urlUtil.formatIPv6Address('1afc:0:a33:85a3::ff2f]', 4000)).toThrow(); + }); + + function verifyUrl(urlString, expectedUrl) { + const url = parse(urlString); + + if (expectedUrl.scheme) { + expect(url.scheme).toEqual(expectedUrl.scheme); + } else { + expect(url.scheme).toBeNull(); + } + + expect(url.host).toBeDefined(); + expect(url.host).not.toBeNull(); + expect(url.host).toEqual(expectedUrl.host); + + if (expectedUrl.port) { + expect(url.port).toEqual(expectedUrl.port); + } else { + expect(url.port).toEqual(DEFAULT_PORT); + } + + verifyHostAndPort(url, expectedUrl); + + if (expectedUrl.query) { + expect(url.query).toEqual(expectedUrl.query); + } else { + expect(url.query).toEqual({}); + } + } + + function verifyHostAndPort(url, expectedUrl) { + const port = expectedUrl.port === 0 || expectedUrl.port ? expectedUrl.port : DEFAULT_PORT; + + if (expectedUrl.ipv6) { + expect(url.hostAndPort).toEqual(`[${expectedUrl.host}]:${port}`); + } else { + expect(url.hostAndPort).toEqual(`${expectedUrl.host}:${port}`); + } + } + + function parse(url) { + return urlUtil.parseBoltUrl(url); + } + +}); diff --git a/test/internal/util.test.js b/test/internal/util.test.js index 8a0145b31..5a9d227c4 100644 --- a/test/internal/util.test.js +++ b/test/internal/util.test.js @@ -74,112 +74,6 @@ describe('util', () => { verifyInvalidCypherStatement(console.log); }); - it('should parse scheme', () => { - verifyScheme('bolt://', 'bolt://localhost'); - verifyScheme('bolt://', 'bolt://localhost:7687'); - verifyScheme('bolt://', 'bolt://neo4j.com'); - verifyScheme('bolt://', 'bolt://neo4j.com:80'); - - verifyScheme('bolt+routing://', 'bolt+routing://127.0.0.1'); - verifyScheme('bolt+routing://', 'bolt+routing://127.0.0.1:7687'); - verifyScheme('bolt+routing://', 'bolt+routing://neo4j.com'); - verifyScheme('bolt+routing://', 'bolt+routing://neo4j.com:80'); - - verifyScheme('wss://', 'wss://server.com'); - verifyScheme('wss://', 'wss://server.com:7687'); - verifyScheme('wss://', 'wss://1.1.1.1'); - verifyScheme('wss://', 'wss://8.8.8.8:80'); - - verifyScheme('', 'invalid url'); - verifyScheme('', 'localhost:7676'); - verifyScheme('', '127.0.0.1'); - }); - - it('should fail to parse scheme from non-string argument', () => { - expect(() => util.parseScheme({})).toThrowError(TypeError); - expect(() => util.parseScheme(['bolt://localhost:2020'])).toThrowError(TypeError); - expect(() => util.parseScheme(() => 'bolt://localhost:8888')).toThrowError(TypeError); - }); - - it('should parse url', () => { - verifyUrl('localhost', 'bolt://localhost'); - verifyUrl('localhost:9090', 'bolt://localhost:9090'); - verifyUrl('127.0.0.1', 'bolt://127.0.0.1'); - verifyUrl('127.0.0.1:7687', 'bolt://127.0.0.1:7687'); - verifyUrl('10.198.20.1', 'bolt+routing://10.198.20.1'); - verifyUrl('15.8.8.9:20004', 'wss://15.8.8.9:20004'); - }); - - it('should fail to parse url from non-string argument', () => { - expect(() => util.parseUrl({})).toThrowError(TypeError); - expect(() => util.parseUrl(['bolt://localhost:2020'])).toThrowError(TypeError); - expect(() => util.parseUrl(() => 'bolt://localhost:8888')).toThrowError(TypeError); - }); - - it('should parse host', () => { - verifyHost('localhost', 'bolt://localhost'); - verifyHost('neo4j.com', 'bolt+routing://neo4j.com'); - verifyHost('neo4j.com', 'bolt+routing://neo4j.com:8080'); - verifyHost('127.0.0.1', 'https://127.0.0.1'); - verifyHost('127.0.0.1', 'ws://127.0.0.1:2020'); - }); - - it('should fail to parse host from non-string argument', () => { - expect(() => util.parseHost({})).toThrowError(TypeError); - expect(() => util.parseHost(['bolt://localhost:2020'])).toThrowError(TypeError); - expect(() => util.parseHost(() => 'bolt://localhost:8888')).toThrowError(TypeError); - }); - - it('should parse port', () => { - verifyPort('7474', 'http://localhost:7474'); - verifyPort('8080', 'http://127.0.0.1:8080'); - verifyPort('20005', 'bolt+routing://neo4j.com:20005'); - verifyPort('4242', 'bolt+routing://1.1.1.1:4242'); - verifyPort('42', 'http://10.192.168.5:42'); - - verifyPort(undefined, 'https://localhost'); - verifyPort(undefined, 'ws://8.8.8.8'); - }); - - it('should fail to parse port from non-string argument', () => { - expect(() => util.parsePort({port: 1515})).toThrowError(TypeError); - expect(() => util.parsePort(['bolt://localhost:2020'])).toThrowError(TypeError); - expect(() => util.parsePort(() => 'bolt://localhost:8888')).toThrowError(TypeError); - }); - - it('should parse routing context', () => { - verifyRoutingContext({ - name: 'molly', - age: '1', - color: 'white' - }, 'bolt+routing://localhost:7687/cat?name=molly&age=1&color=white'); - - verifyRoutingContext({ - key1: 'value1', - key2: 'value2' - }, 'bolt+routing://localhost:7687/?key1=value1&key2=value2'); - - verifyRoutingContext({key: 'value'}, 'bolt+routing://10.198.12.2:9999?key=value'); - - verifyRoutingContext({}, 'bolt+routing://localhost:7687?'); - verifyRoutingContext({}, 'bolt+routing://localhost:7687/?'); - verifyRoutingContext({}, 'bolt+routing://localhost:7687/cat?'); - verifyRoutingContext({}, 'bolt+routing://localhost:7687/lala'); - }); - - it('should fail to parse routing context from non-string argument', () => { - expect(() => util.parseRoutingContext({key1: 'value1'})).toThrowError(TypeError); - expect(() => util.parseRoutingContext(['bolt://localhost:2020/?key=value'])).toThrowError(TypeError); - expect(() => util.parseRoutingContext(() => 'bolt://localhost?key1=value&key2=value2')).toThrowError(TypeError); - }); - - it('should fail to parse routing context from illegal parameters', () => { - expect(() => util.parseRoutingContext('bolt+routing://localhost:7687/?justKey')).toThrow(); - expect(() => util.parseRoutingContext('bolt+routing://localhost:7687/?=value1&key2=value2')).toThrow(); - expect(() => util.parseRoutingContext('bolt+routing://localhost:7687/key1?=value1&key2=')).toThrow(); - expect(() => util.parseRoutingContext('bolt+routing://localhost:7687/?key1=value1&key2=value2&key1=value2')).toThrow(); - }); - it('should time out', () => { expect(() => util.promiseOrTimeout(500, new Promise(), null)).toThrow(); }); @@ -253,32 +147,8 @@ describe('util', () => { expect(() => util.assertString(str, 'Test string')).toThrowError(TypeError); } - function verifyValidCypherStatement(str) { - expect(util.assertCypherStatement(str)).toBe(str); - } - function verifyInvalidCypherStatement(str) { expect(() => util.assertCypherStatement(str)).toThrowError(TypeError); } - function verifyScheme(expectedScheme, url) { - expect(util.parseScheme(url)).toEqual(expectedScheme); - } - - function verifyUrl(expectedUrl, url) { - expect(util.parseUrl(url)).toEqual(expectedUrl); - } - - function verifyHost(expectedHost, url) { - expect(util.parseHost(url)).toEqual(expectedHost); - } - - function verifyPort(expectedPort, url) { - expect(util.parsePort(url)).toEqual(expectedPort); - } - - function verifyRoutingContext(expectedRoutingContext, url) { - expect(util.parseRoutingContext(url)).toEqual(expectedRoutingContext); - } - }); diff --git a/test/resources/boltstub/discover_ipv6_servers_and_read.script b/test/resources/boltstub/discover_ipv6_servers_and_read.script new file mode 100644 index 000000000..12728335e --- /dev/null +++ b/test/resources/boltstub/discover_ipv6_servers_and_read.script @@ -0,0 +1,14 @@ +!: AUTO INIT +!: AUTO RESET +!: AUTO PULL_ALL + +C: RUN "CALL dbms.cluster.routing.getServers" {} + PULL_ALL +S: SUCCESS {"fields": ["ttl", "servers"]} + RECORD [9223372036854775807, [{"addresses": ["127.0.0.1:9001", "[::1]:9001"],"role": "READ"}, {"addresses": ["[2001:db8:a0b:12f0::1]:9002","[3731:54:65fe:2::a7]:9003"], "role": "WRITE"},{"addresses": ["[ff02::1]:9001","[684D:1111:222:3333:4444:5555:6:77]:9002","[::1]:9003"], "role": "ROUTE"}]] + SUCCESS {} +C: RUN "MATCH (n) RETURN n.name" {} + PULL_ALL +S: SUCCESS {"fields": ["n.name"]} + SUCCESS {} + diff --git a/test/v1/driver.test.js b/test/v1/driver.test.js index 6297fe3d6..954f53704 100644 --- a/test/v1/driver.test.js +++ b/test/v1/driver.test.js @@ -22,11 +22,22 @@ import sharedNeo4j from '../internal/shared-neo4j'; import FakeConnection from '../internal/fake-connection'; import lolex from 'lolex'; import {DEFAULT_ACQUISITION_TIMEOUT, DEFAULT_MAX_SIZE} from '../../src/v1/internal/pool-config'; +import {ServerVersion, VERSION_3_1_0} from '../../src/v1/internal/server-version'; describe('driver', () => { let clock; let driver; + let serverVersion; + + beforeAll(done => { + const tmpDriver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken); + ServerVersion.fromDriver(tmpDriver).then(version => { + tmpDriver.close(); + serverVersion = version; + done(); + }); + }); afterEach(() => { if (clock) { @@ -69,7 +80,7 @@ describe('driver', () => { it('should handle wrong scheme', () => { expect(() => neo4j.driver("tank://localhost", sharedNeo4j.authToken)) - .toThrow(new Error("Unknown scheme: tank://")); + .toThrow(new Error('Unknown scheme: tank')); }); it('should handle URL parameter string', () => { @@ -158,7 +169,7 @@ describe('driver', () => { // Expect driver.onError = error => { - expect(error.message).toEqual('Server localhost could not perform routing. Make sure you are connecting to a causal cluster'); + expect(error.message).toEqual(`Server at localhost:7687 can't perform routing. Make sure you are connecting to a causal cluster`); expect(error.code).toEqual(neo4j.error.SERVICE_UNAVAILABLE); done(); }; @@ -306,6 +317,30 @@ describe('driver', () => { }); }); + it('should connect to IPv6 address without port', done => { + testIPv6Connection('bolt://[::1]', done); + }); + + it('should connect to IPv6 address with port', done => { + testIPv6Connection('bolt://[::1]:7687', done); + }); + + function testIPv6Connection(url, done) { + if (serverVersion.compareTo(VERSION_3_1_0) < 0) { + // IPv6 listen address only supported starting from neo4j 3.1, so let's ignore the rest + done(); + } + + driver = neo4j.driver(url, sharedNeo4j.authToken); + + const session = driver.session(); + session.run('RETURN 42').then(result => { + expect(result.records[0].get(0).toNumber()).toEqual(42); + session.close(); + done(); + }); + } + /** * Starts new transaction to force new network connection. * @param {Driver} driver - the driver to use. diff --git a/test/v1/routing.driver.boltkit.test.js b/test/v1/routing.driver.boltkit.test.js index 64961197f..92bc736db 100644 --- a/test/v1/routing.driver.boltkit.test.js +++ b/test/v1/routing.driver.boltkit.test.js @@ -38,7 +38,7 @@ describe('routing driver with stub server', () => { jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; }); - it('should discover server', done => { + it('should discover servers', done => { if (!boltStub.supported) { done(); return; @@ -68,6 +68,33 @@ describe('routing driver with stub server', () => { }); }); + it('should discover IPv6 servers', done => { + if (!boltStub.supported) { + done(); + return; + } + + const server = boltStub.start('./test/resources/boltstub/discover_ipv6_servers_and_read.script', 9001); + + boltStub.run(() => { + const driver = boltStub.newDriver('bolt+routing://127.0.0.1:9001'); + const session = driver.session(READ); + session.run('MATCH (n) RETURN n.name').then(() => { + + expect(hasAddressInConnectionPool(driver, '127.0.0.1:9001')).toBeTruthy(); + assertHasReaders(driver, ['127.0.0.1:9001', '[::1]:9001']); + assertHasWriters(driver, ['[2001:db8:a0b:12f0::1]:9002', '[3731:54:65fe:2::a7]:9003']); + assertHasRouters(driver, ['[ff02::1]:9001', '[684D:1111:222:3333:4444:5555:6:77]:9002', '[::1]:9003']); + + driver.close(); + server.exit(code => { + expect(code).toEqual(0); + done(); + }); + }); + }); + }); + it('should purge connections to stale servers after routing table refresh', done => { if (!boltStub.supported) { done(); @@ -564,7 +591,7 @@ describe('routing driver with stub server', () => { const session = driver.session(); session.run("MATCH (n) RETURN n.name").catch(err => { expect(err.code).toEqual(neo4j.error.SERVICE_UNAVAILABLE); - expect(err.message.indexOf('could not perform routing') > 0).toBeTruthy(); + expect(err.message.indexOf('Make sure you are connecting to a causal cluster') > 0).toBeTruthy(); assertHasRouters(driver, ['127.0.0.1:9001']); session.close(); driver.close(); diff --git a/test/v1/session.test.js b/test/v1/session.test.js index 7c9d1b5ca..29eba6890 100644 --- a/test/v1/session.test.js +++ b/test/v1/session.test.js @@ -1118,7 +1118,7 @@ describe('session', () => { function numberOfAcquiredConnectionsFromPool() { const pool = driver._pool; - return pool.activeResourceCount('localhost'); + return pool.activeResourceCount('localhost:7687'); } function testConnectionTimeout(encrypted, done) { diff --git a/test/v1/types.test.js b/test/v1/types.test.js index 05777418e..5bff46101 100644 --- a/test/v1/types.test.js +++ b/test/v1/types.test.js @@ -146,12 +146,10 @@ describe('byte arrays', () => { beforeAll(done => { jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000; - const driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken); - const session = driver.session(); - session.run('RETURN 1').then(result => { - driver.close(); - const serverVersion = ServerVersion.fromString(result.summary.server.version); - serverSupportsByteArrays = serverVersion.compareTo(VERSION_3_2_0) >= 0; + const tmpDriver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken); + ServerVersion.fromDriver(tmpDriver).then(version => { + tmpDriver.close(); + serverSupportsByteArrays = version.compareTo(VERSION_3_2_0) >= 0; done(); }); });