From 3b8b35cd21c65677be8ec62cd805782d9e5cab26 Mon Sep 17 00:00:00 2001 From: lutovich Date: Wed, 27 Dec 2017 23:34:26 +0100 Subject: [PATCH 01/14] Added URL parser It supports host names, IPv4 and IPv6 addresses. Existing regex-based parsing does not allow IPv6. --- src/v1/internal/url.js | 256 ++++++++++++++++ test/internal/url.test.js | 625 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 881 insertions(+) create mode 100644 src/v1/internal/url.js create mode 100644 test/internal/url.test.js diff --git a/src/v1/internal/url.js b/src/v1/internal/url.js new file mode 100644 index 000000000..55c2d0094 --- /dev/null +++ b/src/v1/internal/url.js @@ -0,0 +1,256 @@ +/** + * Copyright (c) 2002-2017 "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. + */ + +class Url { + + constructor(scheme, host, port, query) { + /** + * Nullable scheme (protocol) of the URL. + * @type {string} + */ + this.scheme = scheme; + + /** + * Nonnull host name or IP address. IPv6 always wrapped in square brackets. + * @type {string} + */ + this.host = host; + + /** + * Nullable number representing port. + * @type {number} + */ + this.port = port; + + /** + * Nonnull object representing parsed query string key-value pairs. Duplicated keys not supported. + * @type {object} + */ + this.query = query; + } +} + +class UrlParser { + + parse(url) { + throw new Error('Abstract function'); + } +} + +class NodeUrlParser extends UrlParser { + + constructor() { + super(); + this._url = require('url'); + } + + static isAvailable() { + try { + const parseFunction = require('url').parse; + if (parseFunction && typeof parseFunction === 'function') { + return true; + } + } catch (e) { + } + return false; + } + + parse(url) { + url = url.trim(); + + let schemeMissing = false; + if (url.indexOf('://') === -1) { + // url does not contain scheme, add dummy 'http://' to make parser work correctly + schemeMissing = true; + url = `http://${url}`; + } + + const parsed = this._url.parse(url); + + const scheme = schemeMissing ? null : NodeUrlParser.extractScheme(parsed); + const host = NodeUrlParser.extractHost(url, parsed); + const port = extractPort(parsed.port); + const query = extractQuery(parsed.search, url); + + return new Url(scheme, host, port, query); + } + + static extractScheme(parsedUrl) { + try { + const protocol = parsedUrl.protocol; // results in scheme with ':', like 'bolt:', 'http:'... + return protocol.substring(0, protocol.length - 1); // remove the trailing ':' + } catch (e) { + return null; + } + } + + static extractHost(originalUrl, parsedUrl) { + const hostname = parsedUrl.hostname; // results in host name or IP address, square brackets removed for IPv6 + const host = parsedUrl.host || ''; // results in hostname + port, like: 'localhost:7687', '[::1]:7687',...; includes square brackets for IPv6 + + if (!hostname) { + throw new Error(`Unable to parse host name in ${originalUrl}`); + } + + if (!startsWith(hostname, '[') && startsWith(host, '[')) { + // looks like an IPv6 address, add square brackets to the host name + return `[${hostname}]`; + } + return hostname; + } +} + +class BrowserUrlParser extends UrlParser { + + constructor() { + super(); + } + + static isAvailable() { + return document && typeof document === 'object'; + } + + + parse(url) { + const urlAndScheme = BrowserUrlParser.sanitizeUrlAndExtractScheme(url); + + url = urlAndScheme.url; + + const parsed = document.createElement('a'); + parsed.href = url; + + const scheme = urlAndScheme.scheme; + const host = BrowserUrlParser.extractHost(url, parsed); + const port = extractPort(parsed.port); + const query = extractQuery(parsed.search, url); + + return new Url(scheme, host, port, query); + } + + static sanitizeUrlAndExtractScheme(url) { + url = url.trim(); + + let schemeMissing = false; + if (url.indexOf('://') === -1) { + // url does not contain scheme, add dummy 'http://' to make parser work correctly + schemeMissing = true; + url = `http://${url}`; + } + + const schemeAndRestSplit = url.split('://'); + if (schemeAndRestSplit.length !== 2) { + throw new Error(`Unable to extract scheme from ${url}`); + } + + const splitScheme = schemeAndRestSplit[0]; + const splitRest = schemeAndRestSplit[1]; + + if (!splitScheme) { + // url probably looks like '://localhost:7687', add dummy 'http://' to make parser work correctly + schemeMissing = true; + url = `http://${url}`; + } else if (splitScheme !== 'http') { + // parser does not seem to work with schemes other than 'http' and 'https', add dummy 'http' + url = `http://${splitRest}`; + } + + const scheme = schemeMissing ? null : splitScheme; + return {scheme: scheme, url: url}; + } + + static extractHost(originalUrl, parsedUrl) { + const hostname = parsedUrl.hostname; // results in host name or IP address, IPv6 address always in square brackets + if (!hostname) { + throw new Error(`Unable to parse host name in ${originalUrl}`); + } + return hostname; + } +} + +function extractPort(portString) { + try { + const port = parseInt(portString, 10); + if (port) { + return port; + } + } catch (e) { + } + return null; +} + +function extractQuery(queryString, url) { + const query = trimAndSanitizeQueryString(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 trimAndSanitizeQueryString(queryString) { + if (queryString) { + queryString = queryString.trim(); + if (startsWith(queryString, '?')) { + queryString = queryString.substring(1, queryString.length); + } + } + return queryString; +} + +function trimAndVerifyQueryElement(string, name, url) { + const result = string.trim(); + if (!result) { + throw new Error(`Illegal empty ${name} in URL query '${url}'`); + } + return result; +} + +function createParser() { + if (NodeUrlParser.isAvailable()) { + return new NodeUrlParser(); + } else if (BrowserUrlParser.isAvailable()) { + return new BrowserUrlParser(); + } else { + throw new Error('Unable to create a URL parser, neither NodeJS nor Browser version is available'); + } +} + +function startsWith(string, prefix) { + return string.lastIndexOf(prefix, 0) === 0; +} + +const parser = createParser(); + +export default parser; diff --git a/test/internal/url.test.js b/test/internal/url.test.js new file mode 100644 index 000000000..d1798375f --- /dev/null +++ b/test/internal/url.test.js @@ -0,0 +1,625 @@ +/** + * Copyright (c) 2002-2017 "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 urlParser from '../../src/v1/internal/url'; + +fdescribe('url', () => { + + 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]' + }); + + verifyUrl('[ff02::2:ff00:0]', { + host: '[ff02::2:ff00:0]' + }); + + verifyUrl('[1afc:0:a33:85a3::ff2f]', { + host: '[1afc:0:a33:85a3::ff2f]' + }); + + verifyUrl('[ff0a::101]', { + host: '[ff0a::101]' + }); + + verifyUrl('[2a05:d018:270:f400:6d8c:d425:c5f:97f3]', { + host: '[2a05:d018:270:f400:6d8c:d425:c5f:97f3]' + }); + }); + + 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'} + }); + + verifyUrl('[ff02::2:ff00:0]?key1=1&key2=2', { + host: '[ff02::2:ff00:0]', + query: {key1: '1', key2: '2'} + }); + + 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'} + }); + + verifyUrl('[ff0a::101]/?foo=1&bar=2&baz=3&qux=4', { + host: '[ff0a::101]', + query: {foo: '1', bar: '2', baz: '3', qux: '4'} + }); + + verifyUrl('[2a05:d018:270:f400:6d8c:d425:c5f:97f3]?animal=apa', { + host: '[2a05:d018:270:f400:6d8c:d425:c5f:97f3]', + query: {animal: 'apa'} + }); + }); + + 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'} + }); + + verifyUrl('http://[ff02::2:ff00:0]?key1=1&key2=2', { + scheme: 'http', + host: '[ff02::2:ff00:0]', + query: {key1: '1', key2: '2'} + }); + + 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'} + }); + + 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'} + }); + + 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'} + }); + }); + + 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:80', { + host: 'some-neo4j-server.com', + port: 80 + }); + + 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:80', { + host: '172.10.5.1', + port: 80 + }); + + 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 + }); + + verifyUrl('[ff02::2:ff00:0]:8080', { + host: '[ff02::2:ff00:0]', + port: 8080 + }); + + verifyUrl('[1afc:0:a33:85a3::ff2f]:7474', { + host: '[1afc:0:a33:85a3::ff2f]', + port: 7474 + }); + + verifyUrl('[ff0a::101]:1000', { + host: '[ff0a::101]', + port: 1000 + }); + + verifyUrl('[2a05:d018:270:f400:6d8c:d425:c5f:97f3]:7475', { + host: '[2a05:d018:270:f400:6d8c:d425:c5f:97f3]', + port: 7475 + }); + }); + + 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]' + }); + + verifyUrl('http://[ff02::2:ff00:0]', { + scheme: 'http', + host: '[ff02::2:ff00:0]' + }); + + verifyUrl('bolt+routing://[1afc:0:a33:85a3::ff2f]', { + scheme: 'bolt+routing', + host: '[1afc:0:a33:85a3::ff2f]' + }); + + verifyUrl('bolt://[ff0a::101]', { + scheme: 'bolt', + host: '[ff0a::101]' + }); + + verifyUrl('bolt+routing://[2a05:d018:270:f400:6d8c:d425:c5f:97f3]', { + scheme: 'bolt+routing', + host: '[2a05:d018:270:f400:6d8c:d425:c5f:97f3]' + }); + }); + + it('should parse URL with scheme, host name and port', () => { + verifyUrl('http://localhost:8080', { + scheme: 'http', + host: 'localhost', + port: 8080 + }); + + verifyUrl('bolt://neo4j.com:80', { + scheme: 'bolt', + host: 'neo4j.com', + port: 80 + }); + + 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 + }); + + verifyUrl('bolt://[ff02::2:ff00:0]:3831', { + scheme: 'bolt', + host: '[ff02::2:ff00:0]', + port: 3831 + }); + + verifyUrl('bolt+routing://[1afc:0:a33:85a3::ff2f]:50505', { + scheme: 'bolt+routing', + host: '[1afc:0:a33:85a3::ff2f]', + port: 50505 + }); + + verifyUrl('ftp://[ff0a::101]:4242', { + scheme: 'ftp', + host: '[ff0a::101]', + port: 4242 + }); + + verifyUrl('wss://[2a05:d018:270:f400:6d8c:d425:c5f:97f3]:22', { + scheme: 'wss', + host: '[2a05:d018:270:f400:6d8c:d425:c5f:97f3]', + port: 22 + }); + }); + + 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'} + }); + + 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'} + }); + + 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'} + }); + + verifyUrl('wss://[ff0a::101]:24240?foo=bar&baz=qux', { + scheme: 'wss', + host: '[ff0a::101]', + port: 24240, + query: {foo: 'bar', baz: 'qux'} + }); + + verifyUrl('https://[2a05:d018:270:f400:6d8c:d425:c5f:97f3]:80?key1=value1&key2=value2', { + scheme: 'https', + host: '[2a05:d018:270:f400:6d8c:d425:c5f:97f3]', + port: 80, + query: {key1: 'value1', key2: 'value2'} + }); + }); + + it('should fail to parse URL without host', () => { + expect(() => urlParser.parse('http://')).toThrow(); + expect(() => urlParser.parse('bolt://')).toThrow(); + expect(() => urlParser.parse('bolt+routing://')).toThrow(); + }); + + it('should fail to parse URL with duplicated query parameters', () => { + expect(() => urlParser.parse('bolt://localhost/?key=value1&key=value2')).toThrow(); + expect(() => urlParser.parse('bolt://localhost:8080/?key=value1&key=value2')).toThrow(); + + expect(() => urlParser.parse('bolt+routing://10.10.127.5?key=value1&key=value2')).toThrow(); + expect(() => urlParser.parse('bolt+routing://10.10.127.5:8080?key=value1&key=value2')).toThrow(); + + expect(() => urlParser.parse('https://[ff0a::101]?key=value1&key=value2')).toThrow(); + expect(() => urlParser.parse('https://[ff0a::101]:8080?key=value1&key=value2')).toThrow(); + }); + + it('should fail to parse URL with empty query key', () => { + expect(() => urlParser.parse('bolt://localhost?=value')).toThrow(); + expect(() => urlParser.parse('bolt://localhost:8080?=value')).toThrow(); + + expect(() => urlParser.parse('bolt+routing://10.10.127.5?=value')).toThrow(); + expect(() => urlParser.parse('bolt+routing://10.10.127.5:8080?=value')).toThrow(); + + expect(() => urlParser.parse('https://[ff0a::101]/?value=')).toThrow(); + expect(() => urlParser.parse('https://[ff0a::101]:8080/?=value')).toThrow(); + }); + + it('should fail to parse URL with empty query value', () => { + expect(() => urlParser.parse('bolt://localhost?key=')).toThrow(); + expect(() => urlParser.parse('bolt://localhost:8080?key=')).toThrow(); + + expect(() => urlParser.parse('bolt+routing://10.10.127.5/?key=')).toThrow(); + expect(() => urlParser.parse('bolt+routing://10.10.127.5:8080/?key=')).toThrow(); + + expect(() => urlParser.parse('https://[ff0a::101]?key=')).toThrow(); + expect(() => urlParser.parse('https://[ff0a::101]:8080?key=')).toThrow(); + }); + + function verifyUrl(urlString, expectedUrl) { + const url = urlParser.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).toBeNull(); + } + + if (expectedUrl.query) { + expect(url.query).toEqual(expectedUrl.query); + } else { + expect(url.query).toEqual({}); + } + } + +}); From eae1633771453d39c521dab7cab287713466e1bf Mon Sep 17 00:00:00 2001 From: lutovich Date: Thu, 28 Dec 2017 11:22:44 +0100 Subject: [PATCH 02/14] Use url-parse instead of handwritten parser This commit adds a new runtime dependency on url-parse library which replaces previously written parsing code. Library is most likely less fragile and requires less maintenance. All unit tests remain and pass. --- package-lock.json | 999 +++++++++++++++++++++++++++++++++++++- package.json | 3 +- src/v1/internal/url.js | 204 ++------ test/internal/url.test.js | 27 +- 4 files changed, 1060 insertions(+), 173 deletions(-) diff --git a/package-lock.json b/package-lock.json index e84aa7a0d..1c2223925 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4695,6 +4695,986 @@ } } }, + "first-chunk-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/first-chunk-stream/-/first-chunk-stream-1.0.0.tgz", + "integrity": "sha1-Wb+1DNkF9g18OUzT2ayqtOatk04=", + "dev": true + }, + "flagged-respawn": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-0.3.2.tgz", + "integrity": "sha1-/xke3c1wiKZ1smEP/8l2vpuAdLU=", + "dev": true + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "dev": true, + "requires": { + "for-in": "1.0.2" + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz", + "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=", + "dev": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.5", + "mime-types": "2.1.17" + } + }, + "fs-access": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fs-access/-/fs-access-1.0.1.tgz", + "integrity": "sha1-1qh/JiJxzv6+wwxVNAf7mV2od3o=", + "dev": true, + "requires": { + "null-check": "1.0.0" + } + }, + "fs-exists-sync": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz", + "integrity": "sha1-mC1ok6+RjnLQjeyehnP/K1qNat0=", + "dev": true + }, + "fs-extra": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-1.0.0.tgz", + "integrity": "sha1-zTzl9+fLYUWIP8rjGR6Yd/hYeVA=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "jsonfile": "2.4.0", + "klaw": "1.3.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.3.tgz", + "integrity": "sha512-WIr7iDkdmdbxu/Gh6eKEZJL6KPE74/5MEsf2whTOFNxbIoIixogroLdKYqB6FDav4Wavh/lZdzzd3b2KxIXC5Q==", + "dev": true, + "optional": true, + "requires": { + "nan": "2.8.0", + "node-pre-gyp": "0.6.39" + }, + "dependencies": { + "abbrev": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "ajv": { + "version": "4.11.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "co": "4.6.0", + "json-stable-stringify": "1.0.1" + } + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "aproba": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.2.9" + } + }, + "asn1": { + "version": "0.2.3", + "bundled": true, + "dev": true, + "optional": true + }, + "assert-plus": { + "version": "0.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "asynckit": { + "version": "0.4.0", + "bundled": true, + "dev": true, + "optional": true + }, + "aws-sign2": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "aws4": { + "version": "1.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "balanced-match": { + "version": "0.4.2", + "bundled": true, + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "block-stream": { + "version": "0.0.9", + "bundled": true, + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, + "boom": { + "version": "2.10.1", + "bundled": true, + "dev": true, + "requires": { + "hoek": "2.16.3" + } + }, + "brace-expansion": { + "version": "1.1.7", + "bundled": true, + "dev": true, + "requires": { + "balanced-match": "0.4.2", + "concat-map": "0.0.1" + } + }, + "buffer-shims": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "caseless": { + "version": "0.12.0", + "bundled": true, + "dev": true, + "optional": true + }, + "co": { + "version": "4.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "combined-stream": { + "version": "1.0.5", + "bundled": true, + "dev": true, + "requires": { + "delayed-stream": "1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "cryptiles": { + "version": "2.0.5", + "bundled": true, + "dev": true, + "requires": { + "boom": "2.10.1" + } + }, + "dashdash": { + "version": "1.14.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "debug": { + "version": "2.6.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.4.2", + "bundled": true, + "dev": true, + "optional": true + }, + "delayed-stream": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "ecc-jsbn": { + "version": "0.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "extend": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "extsprintf": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "bundled": true, + "dev": true, + "optional": true + }, + "form-data": { + "version": "2.1.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.5", + "mime-types": "2.1.15" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "fstream": { + "version": "1.0.11", + "bundled": true, + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "inherits": "2.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.6.1" + } + }, + "fstream-ignore": { + "version": "1.0.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "fstream": "1.0.11", + "inherits": "2.0.3", + "minimatch": "3.0.4" + } + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "1.1.1", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.2" + } + }, + "getpass": { + "version": "0.1.7", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "glob": { + "version": "7.1.2", + "bundled": true, + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "graceful-fs": { + "version": "4.1.11", + "bundled": true, + "dev": true + }, + "har-schema": { + "version": "1.0.5", + "bundled": true, + "dev": true, + "optional": true + }, + "har-validator": { + "version": "4.2.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ajv": "4.11.8", + "har-schema": "1.0.5" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "hawk": { + "version": "3.1.3", + "bundled": true, + "dev": true, + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } + }, + "hoek": { + "version": "2.16.3", + "bundled": true, + "dev": true + }, + "http-signature": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "assert-plus": "0.2.0", + "jsprim": "1.4.0", + "sshpk": "1.13.0" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true + }, + "ini": { + "version": "1.3.4", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-typedarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "isstream": { + "version": "0.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "jodid25519": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "jsbn": { + "version": "0.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "bundled": true, + "dev": true, + "optional": true + }, + "json-stable-stringify": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "jsonify": "0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "jsonify": { + "version": "0.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "jsprim": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.0.2", + "json-schema": "0.2.3", + "verror": "1.3.6" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "mime-db": { + "version": "1.27.0", + "bundled": true, + "dev": true + }, + "mime-types": { + "version": "2.1.15", + "bundled": true, + "dev": true, + "requires": { + "mime-db": "1.27.0" + } + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "requires": { + "brace-expansion": "1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "node-pre-gyp": { + "version": "0.6.39", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "detect-libc": "1.0.2", + "hawk": "3.1.3", + "mkdirp": "0.5.1", + "nopt": "4.0.1", + "npmlog": "4.1.0", + "rc": "1.2.1", + "request": "2.81.0", + "rimraf": "2.6.1", + "semver": "5.3.0", + "tar": "2.2.1", + "tar-pack": "3.4.0" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1.1.0", + "osenv": "0.1.4" + } + }, + "npmlog": { + "version": "4.1.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "1.1.4", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "oauth-sign": { + "version": "0.8.2", + "bundled": true, + "dev": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "performance-now": { + "version": "0.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "1.0.7", + "bundled": true, + "dev": true + }, + "punycode": { + "version": "1.4.1", + "bundled": true, + "dev": true, + "optional": true + }, + "qs": { + "version": "6.4.0", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "0.4.2", + "ini": "1.3.4", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.2.9", + "bundled": true, + "dev": true, + "requires": { + "buffer-shims": "1.0.0", + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "string_decoder": "1.0.1", + "util-deprecate": "1.0.2" + } + }, + "request": { + "version": "2.81.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aws-sign2": "0.6.0", + "aws4": "1.6.0", + "caseless": "0.12.0", + "combined-stream": "1.0.5", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.1.4", + "har-validator": "4.2.1", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.15", + "oauth-sign": "0.8.2", + "performance-now": "0.2.0", + "qs": "6.4.0", + "safe-buffer": "5.0.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.2", + "tunnel-agent": "0.6.0", + "uuid": "3.0.1" + } + }, + "rimraf": { + "version": "2.6.1", + "bundled": true, + "dev": true, + "requires": { + "glob": "7.1.2" + } + }, + "safe-buffer": { + "version": "5.0.1", + "bundled": true, + "dev": true + }, + "semver": { + "version": "5.3.0", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sntp": { + "version": "1.0.9", + "bundled": true, + "dev": true, + "requires": { + "hoek": "2.16.3" + } + }, + "sshpk": { + "version": "1.13.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jodid25519": "1.0.2", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string_decoder": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "requires": { + "safe-buffer": "5.0.1" + } + }, + "stringstream": { + "version": "0.0.5", + "bundled": true, + "dev": true, + "optional": true + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "2.2.1", + "bundled": true, + "dev": true, + "requires": { + "block-stream": "0.0.9", + "fstream": "1.0.11", + "inherits": "2.0.3" + } + }, + "tar-pack": { + "version": "3.4.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "2.6.8", + "fstream": "1.0.11", + "fstream-ignore": "1.0.5", + "once": "1.4.0", + "readable-stream": "2.2.9", + "rimraf": "2.6.1", + "tar": "2.2.1", + "uid-number": "0.0.6" + } + }, + "tough-cookie": { + "version": "2.3.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "punycode": "1.4.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "bundled": true, + "dev": true, + "optional": true + }, + "uid-number": { + "version": "0.0.6", + "bundled": true, + "dev": true, + "optional": true + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "uuid": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "verror": { + "version": "1.3.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "extsprintf": "1.0.2" + } + }, + "wide-align": { + "version": "1.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true + } + } + }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -8233,6 +9213,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", @@ -8549,8 +9534,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", @@ -10028,7 +11012,14 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", "dev": true - } + }, + "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" } }, "user-home": { @@ -10458,4 +11449,4 @@ "dev": true } } -} +}} 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/internal/url.js b/src/v1/internal/url.js index 55c2d0094..bb90efedb 100644 --- a/src/v1/internal/url.js +++ b/src/v1/internal/url.js @@ -17,6 +17,8 @@ * limitations under the License. */ +import ParsedUrl from 'url-parse'; + class Url { constructor(scheme, host, port, query) { @@ -46,156 +48,54 @@ class Url { } } -class UrlParser { - - parse(url) { - throw new Error('Abstract function'); - } -} - -class NodeUrlParser extends UrlParser { - - constructor() { - super(); - this._url = require('url'); - } - - static isAvailable() { - try { - const parseFunction = require('url').parse; - if (parseFunction && typeof parseFunction === 'function') { - return true; - } - } catch (e) { - } - return false; - } - - parse(url) { - url = url.trim(); - - let schemeMissing = false; - if (url.indexOf('://') === -1) { - // url does not contain scheme, add dummy 'http://' to make parser work correctly - schemeMissing = true; - url = `http://${url}`; - } +function parse(url) { + const sanitized = sanitizeUrl(url); + const parsedUrl = new ParsedUrl(sanitized.url, {}, query => extractQuery(query, url)); - const parsed = this._url.parse(url); + const scheme = sanitized.schemeMissing ? null : extractScheme(parsedUrl.protocol); + const host = extractHost(parsedUrl.hostname); + const port = extractPort(parsedUrl.port); + const query = parsedUrl.query; - const scheme = schemeMissing ? null : NodeUrlParser.extractScheme(parsed); - const host = NodeUrlParser.extractHost(url, parsed); - const port = extractPort(parsed.port); - const query = extractQuery(parsed.search, url); - - return new Url(scheme, host, port, query); - } - - static extractScheme(parsedUrl) { - try { - const protocol = parsedUrl.protocol; // results in scheme with ':', like 'bolt:', 'http:'... - return protocol.substring(0, protocol.length - 1); // remove the trailing ':' - } catch (e) { - return null; - } - } - - static extractHost(originalUrl, parsedUrl) { - const hostname = parsedUrl.hostname; // results in host name or IP address, square brackets removed for IPv6 - const host = parsedUrl.host || ''; // results in hostname + port, like: 'localhost:7687', '[::1]:7687',...; includes square brackets for IPv6 - - if (!hostname) { - throw new Error(`Unable to parse host name in ${originalUrl}`); - } - - if (!startsWith(hostname, '[') && startsWith(host, '[')) { - // looks like an IPv6 address, add square brackets to the host name - return `[${hostname}]`; - } - return hostname; - } + return new Url(scheme, host, port, query); } -class BrowserUrlParser extends UrlParser { - - constructor() { - super(); - } - - static isAvailable() { - return document && typeof document === 'object'; - } - - - parse(url) { - const urlAndScheme = BrowserUrlParser.sanitizeUrlAndExtractScheme(url); - - url = urlAndScheme.url; - - const parsed = document.createElement('a'); - parsed.href = url; - - const scheme = urlAndScheme.scheme; - const host = BrowserUrlParser.extractHost(url, parsed); - const port = extractPort(parsed.port); - const query = extractQuery(parsed.search, url); +function sanitizeUrl(url) { + url = url.trim(); - return new Url(scheme, host, port, query); + if (url.indexOf('://') === -1) { + // url does not contain scheme, add dummy 'http://' to make parser work correctly + return {schemeMissing: true, url: `http://${url}`}; } - static sanitizeUrlAndExtractScheme(url) { - url = url.trim(); - - let schemeMissing = false; - if (url.indexOf('://') === -1) { - // url does not contain scheme, add dummy 'http://' to make parser work correctly - schemeMissing = true; - url = `http://${url}`; - } - - const schemeAndRestSplit = url.split('://'); - if (schemeAndRestSplit.length !== 2) { - throw new Error(`Unable to extract scheme from ${url}`); - } - - const splitScheme = schemeAndRestSplit[0]; - const splitRest = schemeAndRestSplit[1]; + return {schemeMissing: false, url: url}; +} - if (!splitScheme) { - // url probably looks like '://localhost:7687', add dummy 'http://' to make parser work correctly - schemeMissing = true; - url = `http://${url}`; - } else if (splitScheme !== 'http') { - // parser does not seem to work with schemes other than 'http' and 'https', add dummy 'http' - url = `http://${splitRest}`; +function extractScheme(scheme) { + if (scheme) { + scheme = scheme.trim(); + if (scheme.charAt(scheme.length - 1) === ':') { + scheme = scheme.substring(0, scheme.length - 1); } - - const scheme = schemeMissing ? null : splitScheme; - return {scheme: scheme, url: url}; + return scheme; } + return null; +} - static extractHost(originalUrl, parsedUrl) { - const hostname = parsedUrl.hostname; // results in host name or IP address, IPv6 address always in square brackets - if (!hostname) { - throw new Error(`Unable to parse host name in ${originalUrl}`); - } - return hostname; +function extractHost(host, url) { + if (!host) { + throw new Error(`Unable to extract host from ${url}`); } + return host.trim(); } function extractPort(portString) { - try { - const port = parseInt(portString, 10); - if (port) { - return port; - } - } catch (e) { - } - return null; + const port = parseInt(portString, 10); + return port ? port : null; } function extractQuery(queryString, url) { - const query = trimAndSanitizeQueryString(queryString); + const query = trimAndSanitizeQuery(queryString); const context = {}; if (query) { @@ -219,38 +119,22 @@ function extractQuery(queryString, url) { return context; } -function trimAndSanitizeQueryString(queryString) { - if (queryString) { - queryString = queryString.trim(); - if (startsWith(queryString, '?')) { - queryString = queryString.substring(1, queryString.length); - } +function trimAndSanitizeQuery(query) { + query = (query || '').trim(); + if (query && query.charAt(0) === '?') { + query = query.substring(1, query.length); } - return queryString; + return query; } -function trimAndVerifyQueryElement(string, name, url) { - const result = string.trim(); - if (!result) { +function trimAndVerifyQueryElement(element, name, url) { + element = (element || '').trim(); + if (!element) { throw new Error(`Illegal empty ${name} in URL query '${url}'`); } - return result; + return element; } -function createParser() { - if (NodeUrlParser.isAvailable()) { - return new NodeUrlParser(); - } else if (BrowserUrlParser.isAvailable()) { - return new BrowserUrlParser(); - } else { - throw new Error('Unable to create a URL parser, neither NodeJS nor Browser version is available'); - } -} - -function startsWith(string, prefix) { - return string.lastIndexOf(prefix, 0) === 0; -} - -const parser = createParser(); - -export default parser; +export default { + parse: parse +}; diff --git a/test/internal/url.test.js b/test/internal/url.test.js index d1798375f..8b76902f3 100644 --- a/test/internal/url.test.js +++ b/test/internal/url.test.js @@ -245,9 +245,9 @@ fdescribe('url', () => { port: 8888 }); - verifyUrl('some-neo4j-server.com:80', { + verifyUrl('some-neo4j-server.com:42', { host: 'some-neo4j-server.com', - port: 80 + port: 42 }); verifyUrl('ec2-34-242-76-91.eu-west-1.compute.aws.com:62220', { @@ -267,9 +267,9 @@ fdescribe('url', () => { port: 22000 }); - verifyUrl('172.10.5.1:80', { + verifyUrl('172.10.5.1:42', { host: '172.10.5.1', - port: 80 + port: 42 }); verifyUrl('34.242.76.91:7687', { @@ -383,10 +383,10 @@ fdescribe('url', () => { port: 8080 }); - verifyUrl('bolt://neo4j.com:80', { + verifyUrl('bolt://neo4j.com:42', { scheme: 'bolt', host: 'neo4j.com', - port: 80 + port: 42 }); verifyUrl('bolt+routing://some-neo4j-server.com:12000', { @@ -549,10 +549,10 @@ fdescribe('url', () => { query: {foo: 'bar', baz: 'qux'} }); - verifyUrl('https://[2a05:d018:270:f400:6d8c:d425:c5f:97f3]:80?key1=value1&key2=value2', { + 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: 80, + port: 42, query: {key1: 'value1', key2: 'value2'} }); }); @@ -596,6 +596,17 @@ fdescribe('url', () => { expect(() => urlParser.parse('https://[ff0a::101]:8080?key=')).toThrow(); }); + it('should fail to parse URL with no query value', () => { + expect(() => urlParser.parse('bolt://localhost?key')).toThrow(); + expect(() => urlParser.parse('bolt://localhost:8080?key')).toThrow(); + + expect(() => urlParser.parse('bolt+routing://10.10.127.5/?key')).toThrow(); + expect(() => urlParser.parse('bolt+routing://10.10.127.5:8080/?key')).toThrow(); + + expect(() => urlParser.parse('https://[ff0a::101]?key')).toThrow(); + expect(() => urlParser.parse('https://[ff0a::101]:8080?key')).toThrow(); + }); + function verifyUrl(urlString, expectedUrl) { const url = urlParser.parse(urlString); From 3d0059e7f17431c7661442bb4368a518b035015f Mon Sep 17 00:00:00 2001 From: lutovich Date: Thu, 28 Dec 2017 11:24:13 +0100 Subject: [PATCH 03/14] Added IE karma config To be able to run tests in IE. It's currently not part of the main build but useful for testing things locally. --- gulpfile.babel.js | 6 ++++++ test/browser/karma-ie.conf.js | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 test/browser/karma-ie.conf.js 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/test/browser/karma-ie.conf.js b/test/browser/karma-ie.conf.js new file mode 100644 index 000000000..9369af402 --- /dev/null +++ b/test/browser/karma-ie.conf.js @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2002-2017 "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, + }); +}; From 297396bf19541282c9735a0d2f83f6a581b50e8c Mon Sep 17 00:00:00 2001 From: lutovich Date: Thu, 28 Dec 2017 11:33:47 +0100 Subject: [PATCH 04/14] Expose host + port in the parsed URL It is used as connection identifier i routing table and connection pool. --- src/v1/internal/url.js | 6 ++++++ test/internal/url.test.js | 2 ++ 2 files changed, 8 insertions(+) diff --git a/src/v1/internal/url.js b/src/v1/internal/url.js index bb90efedb..beb95491f 100644 --- a/src/v1/internal/url.js +++ b/src/v1/internal/url.js @@ -40,6 +40,12 @@ class Url { */ this.port = port; + /** + * Nonnull host name or IP address plus port, separated by ':'. + * @type {string} + */ + this.hostAndPort = port ? `${host}:${port}` : host; + /** * Nonnull object representing parsed query string key-value pairs. Duplicated keys not supported. * @type {object} diff --git a/test/internal/url.test.js b/test/internal/url.test.js index 8b76902f3..fda63ce9a 100644 --- a/test/internal/url.test.js +++ b/test/internal/url.test.js @@ -622,8 +622,10 @@ fdescribe('url', () => { if (expectedUrl.port) { expect(url.port).toEqual(expectedUrl.port); + expect(url.hostAndPort).toEqual(`${expectedUrl.host}:${expectedUrl.port}`); } else { expect(url.port).toBeNull(); + expect(url.hostAndPort).toEqual(expectedUrl.host); } if (expectedUrl.query) { From 54622dad95980ed447cca36b7bbd391ab1dc7036 Mon Sep 17 00:00:00 2001 From: lutovich Date: Thu, 28 Dec 2017 19:14:03 +0100 Subject: [PATCH 05/14] 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. --- src/v1/index.js | 18 +- src/v1/internal/ch-config.js | 13 +- src/v1/internal/ch-node.js | 12 +- src/v1/internal/ch-websocket.js | 2 +- src/v1/internal/connector.js | 11 +- src/v1/internal/host-name-resolvers.js | 18 +- src/v1/internal/routing-util.js | 5 +- src/v1/internal/{url.js => url-util.js} | 71 ++++- src/v1/internal/util.js | 65 ----- test/internal/ch-config.test.js | 43 ++- test/internal/host-name-resolvers.test.js | 52 ++-- test/internal/routing-util.test.js | 6 +- test/internal/tls.test.js | 15 +- .../{url.test.js => url-util.test.js} | 264 ++++++++++++------ test/internal/util.test.js | 130 --------- test/v1/driver.test.js | 11 +- test/v1/routing.driver.boltkit.test.js | 2 +- test/v1/session.test.js | 2 +- 18 files changed, 347 insertions(+), 393 deletions(-) rename src/v1/internal/{url.js => url-util.js} (57%) rename test/internal/{url.test.js => url-util.test.js} (67%) diff --git a/src/v1/index.js b/src/v1/index.js index e86c6c20e..95378487f 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 @@ -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..7ede1f48c 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); 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..076041949 100644 --- a/src/v1/internal/ch-websocket.js +++ b/src/v1/internal/ch-websocket.js @@ -52,7 +52,7 @@ class WebSocketChannel { return; } } - this._url = scheme + '://' + config.host + ':' + config.port; + this._url = scheme + '://' + config.url.hostAndPort; this._ws = new WebSocket(this._url); this._ws.binaryType = "arraybuffer"; 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/url.js b/src/v1/internal/url-util.js similarity index 57% rename from src/v1/internal/url.js rename to src/v1/internal/url-util.js index beb95491f..c33dd4c86 100644 --- a/src/v1/internal/url.js +++ b/src/v1/internal/url-util.js @@ -18,52 +18,64 @@ */ import ParsedUrl from 'url-parse'; +import {assertString} from './util'; +import {DEFAULT_PORT} from './ch-config'; -class Url { +export class Url { - constructor(scheme, host, port, query) { + 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 always wrapped in square brackets. + * 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; /** - * Nullable number representing port. + * 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 ':'. + * 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 = port ? `${host}:${port}` : host; + 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 parse(url) { +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 host = extractHost(parsedUrl.hostname); + 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 = port ? `${rawHost}:${port}` : rawHost; const query = parsedUrl.query; - return new Url(scheme, host, port, query); + return new Url(scheme, host, port, hostAndPort, query); } function sanitizeUrl(url) { @@ -97,7 +109,7 @@ function extractHost(host, url) { function extractPort(portString) { const port = parseInt(portString, 10); - return port ? port : null; + return (port === 0 || port) ? port : DEFAULT_PORT; } function extractQuery(queryString, url) { @@ -141,6 +153,43 @@ function trimAndVerifyQueryElement(element, name, 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 { - parse: parse + 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/internal/ch-config.test.js b/test/internal/ch-config.test.js index 30cf53d13..367d558e6 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,50 +69,50 @@ 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); }); 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/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.test.js b/test/internal/url-util.test.js similarity index 67% rename from test/internal/url.test.js rename to test/internal/url-util.test.js index fda63ce9a..0a25244b7 100644 --- a/test/internal/url.test.js +++ b/test/internal/url-util.test.js @@ -17,9 +17,10 @@ * limitations under the License. */ -import urlParser from '../../src/v1/internal/url'; +import urlUtil from '../../src/v1/internal/url-util'; +import {DEFAULT_PORT} from '../../src/v1/internal/ch-config'; -fdescribe('url', () => { +describe('url', () => { it('should parse URL with just host name', () => { verifyUrl('localhost', { @@ -59,23 +60,28 @@ fdescribe('url', () => { it('should parse URL with just IPv6 address', () => { verifyUrl('[::1]', { - host: '[::1]' + host: '::1', + ipv6: true }); verifyUrl('[ff02::2:ff00:0]', { - host: '[ff02::2:ff00:0]' + host: 'ff02::2:ff00:0', + ipv6: true }); verifyUrl('[1afc:0:a33:85a3::ff2f]', { - host: '[1afc:0:a33:85a3::ff2f]' + host: '1afc:0:a33:85a3::ff2f', + ipv6: true }); verifyUrl('[ff0a::101]', { - host: '[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]' + host: '2a05:d018:270:f400:6d8c:d425:c5f:97f3', + ipv6: true }); }); @@ -125,28 +131,33 @@ fdescribe('url', () => { it('should parse URL with IPv6 address and query', () => { verifyUrl('[::1]?key1=value1&key2=value2', { - host: '[::1]', - query: {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'} + 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'} + 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'} + 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'} + host: '2a05:d018:270:f400:6d8c:d425:c5f:97f3', + query: {animal: 'apa'}, + ipv6: true }); }); @@ -205,32 +216,37 @@ fdescribe('url', () => { 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'} + 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'} + 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'} + 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'} + 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'} + host: '2a05:d018:270:f400:6d8c:d425:c5f:97f3', + query: {animal: 'apa'}, + ipv6: true }); }); @@ -280,28 +296,33 @@ fdescribe('url', () => { it('should parse URL with IPv6 address and port', () => { verifyUrl('[::1]:36000', { - host: '[::1]', - port: 36000 + host: '::1', + port: 36000, + ipv6: true }); verifyUrl('[ff02::2:ff00:0]:8080', { - host: '[ff02::2:ff00:0]', - port: 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 + host: '1afc:0:a33:85a3::ff2f', + port: 7474, + ipv6: true }); verifyUrl('[ff0a::101]:1000', { - host: '[ff0a::101]', - port: 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 + host: '2a05:d018:270:f400:6d8c:d425:c5f:97f3', + port: 7475, + ipv6: true }); }); @@ -352,27 +373,32 @@ fdescribe('url', () => { it('should parse URL with scheme and IPv6 address', () => { verifyUrl('https://[::1]', { scheme: 'https', - host: '[::1]' + host: '::1', + ipv6: true }); verifyUrl('http://[ff02::2:ff00:0]', { scheme: 'http', - host: '[ff02::2:ff00:0]' + host: 'ff02::2:ff00:0', + ipv6: true }); verifyUrl('bolt+routing://[1afc:0:a33:85a3::ff2f]', { scheme: 'bolt+routing', - host: '[1afc:0:a33:85a3::ff2f]' + host: '1afc:0:a33:85a3::ff2f', + ipv6: true }); verifyUrl('bolt://[ff0a::101]', { scheme: 'bolt', - host: '[ff0a::101]' + 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]' + host: '2a05:d018:270:f400:6d8c:d425:c5f:97f3', + ipv6: true }); }); @@ -431,32 +457,37 @@ fdescribe('url', () => { it('should parse URL with scheme, IPv6 address and port', () => { verifyUrl('http://[::1]:9123', { scheme: 'http', - host: '[::1]', - port: 9123 + host: '::1', + port: 9123, + ipv6: true }); verifyUrl('bolt://[ff02::2:ff00:0]:3831', { scheme: 'bolt', - host: '[ff02::2:ff00:0]', - port: 3831 + 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 + host: '1afc:0:a33:85a3::ff2f', + port: 50505, + ipv6: true }); verifyUrl('ftp://[ff0a::101]:4242', { scheme: 'ftp', - host: '[ff0a::101]', - port: 4242 + 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 + host: '2a05:d018:270:f400:6d8c:d425:c5f:97f3', + port: 22, + ipv6: true }); }); @@ -523,92 +554,125 @@ fdescribe('url', () => { it('should parse URL with scheme, IPv6 address, port and query', () => { verifyUrl('https://[::1]:4217?key=value', { scheme: 'https', - host: '[::1]', + host: '::1', port: 4217, - query: {key: 'value'} + 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]', + host: 'ff02::2:ff00:0', port: 22, - query: {animal1: 'apa', animal2: 'dog'} + 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]', + host: '1afc:0:a33:85a3::ff2f', port: 4242, - query: {a: '1', b: '2', c: '3', d: '4'} + 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]', + host: 'ff0a::101', port: 24240, - query: {foo: 'bar', baz: 'qux'} + 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]', + host: '2a05:d018:270:f400:6d8c:d425:c5f:97f3', port: 42, - query: {key1: 'value1', key2: 'value2'} + query: {key1: 'value1', key2: 'value2'}, + ipv6: true }); }); it('should fail to parse URL without host', () => { - expect(() => urlParser.parse('http://')).toThrow(); - expect(() => urlParser.parse('bolt://')).toThrow(); - expect(() => urlParser.parse('bolt+routing://')).toThrow(); + expect(() => parse('http://')).toThrow(); + expect(() => parse('bolt://')).toThrow(); + expect(() => parse('bolt+routing://')).toThrow(); }); it('should fail to parse URL with duplicated query parameters', () => { - expect(() => urlParser.parse('bolt://localhost/?key=value1&key=value2')).toThrow(); - expect(() => urlParser.parse('bolt://localhost:8080/?key=value1&key=value2')).toThrow(); + expect(() => parse('bolt://localhost/?key=value1&key=value2')).toThrow(); + expect(() => parse('bolt://localhost:8080/?key=value1&key=value2')).toThrow(); - expect(() => urlParser.parse('bolt+routing://10.10.127.5?key=value1&key=value2')).toThrow(); - expect(() => urlParser.parse('bolt+routing://10.10.127.5: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(() => urlParser.parse('https://[ff0a::101]?key=value1&key=value2')).toThrow(); - expect(() => urlParser.parse('https://[ff0a::101]: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(() => urlParser.parse('bolt://localhost?=value')).toThrow(); - expect(() => urlParser.parse('bolt://localhost:8080?=value')).toThrow(); + expect(() => parse('bolt://localhost?=value')).toThrow(); + expect(() => parse('bolt://localhost:8080?=value')).toThrow(); - expect(() => urlParser.parse('bolt+routing://10.10.127.5?=value')).toThrow(); - expect(() => urlParser.parse('bolt+routing://10.10.127.5: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(() => urlParser.parse('https://[ff0a::101]/?value=')).toThrow(); - expect(() => urlParser.parse('https://[ff0a::101]: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(() => urlParser.parse('bolt://localhost?key=')).toThrow(); - expect(() => urlParser.parse('bolt://localhost:8080?key=')).toThrow(); + expect(() => parse('bolt://localhost?key=')).toThrow(); + expect(() => parse('bolt://localhost:8080?key=')).toThrow(); - expect(() => urlParser.parse('bolt+routing://10.10.127.5/?key=')).toThrow(); - expect(() => urlParser.parse('bolt+routing://10.10.127.5: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(() => urlParser.parse('https://[ff0a::101]?key=')).toThrow(); - expect(() => urlParser.parse('https://[ff0a::101]: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(() => urlParser.parse('bolt://localhost?key')).toThrow(); - expect(() => urlParser.parse('bolt://localhost:8080?key')).toThrow(); + expect(() => parse('bolt://localhost?key')).toThrow(); + expect(() => parse('bolt://localhost:8080?key')).toThrow(); - expect(() => urlParser.parse('bolt+routing://10.10.127.5/?key')).toThrow(); - expect(() => urlParser.parse('bolt+routing://10.10.127.5: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(); + }); - expect(() => urlParser.parse('https://[ff0a::101]?key')).toThrow(); - expect(() => urlParser.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 = urlParser.parse(urlString); + const url = parse(urlString); if (expectedUrl.scheme) { expect(url.scheme).toEqual(expectedUrl.scheme); @@ -622,12 +686,12 @@ fdescribe('url', () => { if (expectedUrl.port) { expect(url.port).toEqual(expectedUrl.port); - expect(url.hostAndPort).toEqual(`${expectedUrl.host}:${expectedUrl.port}`); } else { - expect(url.port).toBeNull(); - expect(url.hostAndPort).toEqual(expectedUrl.host); + expect(url.port).toEqual(DEFAULT_PORT); } + verifyHostAndPort(url, expectedUrl); + if (expectedUrl.query) { expect(url.query).toEqual(expectedUrl.query); } else { @@ -635,4 +699,18 @@ fdescribe('url', () => { } } + 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/v1/driver.test.js b/test/v1/driver.test.js index 6297fe3d6..5aa1db58d 100644 --- a/test/v1/driver.test.js +++ b/test/v1/driver.test.js @@ -39,6 +39,13 @@ describe('driver', () => { } }); + fit('apa', done => { + driver = neo4j.driver('bolt://[::1]', sharedNeo4j.authToken); + driver.session().run('return 1').then(result => console.log(result.records)) + .catch(error => console.log(error)) + .then(() => done()); + }); + it('should expose sessions', () => { // Given driver = neo4j.driver("bolt://localhost", sharedNeo4j.authToken); @@ -69,7 +76,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 +165,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(); }; diff --git a/test/v1/routing.driver.boltkit.test.js b/test/v1/routing.driver.boltkit.test.js index 64961197f..af092d2c3 100644 --- a/test/v1/routing.driver.boltkit.test.js +++ b/test/v1/routing.driver.boltkit.test.js @@ -564,7 +564,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) { From b857656189aee979a3331c5b52509c2f82e760bf Mon Sep 17 00:00:00 2001 From: lutovich Date: Fri, 29 Dec 2017 11:21:37 +0100 Subject: [PATCH 06/14] Couple integration tests for IPv6 support Added tests to verify that driver can connect to real single-instance database using localhost IPv6 address. Added test to check that driver can receive IPv6 addresses in routing procedure response (using stub server). Introduced convenience method to read server version using given driver instance. --- src/v1/internal/server-version.js | 15 ++++++- test/internal/server-version.test.js | 29 +++++++++++++ test/internal/shared-neo4j.js | 26 +++++++++++- .../discover_ipv6_servers_and_read.script | 14 +++++++ test/v1/driver.test.js | 42 +++++++++++++++---- test/v1/routing.driver.boltkit.test.js | 29 ++++++++++++- test/v1/types.test.js | 10 ++--- 7 files changed, 149 insertions(+), 16 deletions(-) create mode 100644 test/resources/boltstub/discover_ipv6_servers_and_read.script 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/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/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 5aa1db58d..95c196006 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) { @@ -39,13 +50,6 @@ describe('driver', () => { } }); - fit('apa', done => { - driver = neo4j.driver('bolt://[::1]', sharedNeo4j.authToken); - driver.session().run('return 1').then(result => console.log(result.records)) - .catch(error => console.log(error)) - .then(() => done()); - }); - it('should expose sessions', () => { // Given driver = neo4j.driver("bolt://localhost", sharedNeo4j.authToken); @@ -313,6 +317,30 @@ describe('driver', () => { }); }); + it('should connect to IPv6 address without port', done => { + testIPv6Connection('bolt://[::1]', done); + }); + + it('should connect to IPv6 address wit 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('bolt://[::1]', 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 af092d2c3..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(); 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(); }); }); From de26265d9478d8c33e0b3bd7a6446e1dc1fb3d89 Mon Sep 17 00:00:00 2001 From: lutovich Date: Fri, 29 Dec 2017 12:19:27 +0100 Subject: [PATCH 07/14] Small cleanup around url parsing --- src/v1/internal/url-util.js | 4 ++-- test/v1/driver.test.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/v1/internal/url-util.js b/src/v1/internal/url-util.js index c33dd4c86..9e54e758b 100644 --- a/src/v1/internal/url-util.js +++ b/src/v1/internal/url-util.js @@ -21,7 +21,7 @@ import ParsedUrl from 'url-parse'; import {assertString} from './util'; import {DEFAULT_PORT} from './ch-config'; -export class Url { +class Url { constructor(scheme, host, port, hostAndPort, query) { /** @@ -72,7 +72,7 @@ function parseBoltUrl(url) { 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 = port ? `${rawHost}:${port}` : rawHost; + const hostAndPort = `${rawHost}:${port}`; const query = parsedUrl.query; return new Url(scheme, host, port, hostAndPort, query); diff --git a/test/v1/driver.test.js b/test/v1/driver.test.js index 95c196006..954f53704 100644 --- a/test/v1/driver.test.js +++ b/test/v1/driver.test.js @@ -321,7 +321,7 @@ describe('driver', () => { testIPv6Connection('bolt://[::1]', done); }); - it('should connect to IPv6 address wit port', done => { + it('should connect to IPv6 address with port', done => { testIPv6Connection('bolt://[::1]:7687', done); }); @@ -331,7 +331,7 @@ describe('driver', () => { done(); } - driver = neo4j.driver('bolt://[::1]', sharedNeo4j.authToken); + driver = neo4j.driver(url, sharedNeo4j.authToken); const session = driver.session(); session.run('RETURN 42').then(result => { From e4abfc28f4f825dff93515762df5c407caffc58b Mon Sep 17 00:00:00 2001 From: lutovich Date: Fri, 29 Dec 2017 18:39:38 +0100 Subject: [PATCH 08/14] Make IPv6 WebSocket work in Edge WebSocket in IE and Edge does not understand IPv6 addresses because they contain ':'. Which is an invalid symbol for UNC https://en.wikipedia.org/wiki/IPv6_address#Literal_IPv6_addresses_in_UNC_path_names. This caused `new WebSocket()` call to throw `SyntaxError` for all given IPv6 addresses. Following URL rewriting is needed to make WebSocket work with IPv6: 1) replace all ':' with '-' 2) replace '%' with 's' for link-local addresses 3) append magic '.ipv6-literal.net' suffix After such changes WebSocket in IE and Edge can connect to an IPv6 address. This commit makes `WebSocketChannel` catch `SyntaxError` and rewrite IPv6 address. It chooses not to detect browser/OS and things like this upfront because it seems hard. User-agent string changes often and differs for different versions of IE. See following links for more details: https://social.msdn.microsoft.com/Forums/ie/en-US/06cca73b-63c2-4bf9-899b-b229c50449ff/whether-ie10-websocket-support-ipv6?forum=iewebdevelopment https://www.itdojo.com/ipv6-addresses-and-unc-path-names-overcoming-illegal/ https://en.wikipedia.org/wiki/IPv6_address#Literal_IPv6_addresses_in_UNC_path_names --- src/v1/internal/ch-websocket.js | 55 +++++++++++++++++++++- test/internal/ch-websocket.test.js | 74 ++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 test/internal/ch-websocket.test.js diff --git a/src/v1/internal/ch-websocket.js b/src/v1/internal/ch-websocket.js index 076041949..4df60e80a 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.url.hostAndPort; - this._ws = new WebSocket(this._url); + + this._ws = createWebSocket(scheme, config.url); this._ws.binaryType = "arraybuffer"; let self = this; @@ -169,4 +169,55 @@ class WebSocketChannel { 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/test/internal/ch-websocket.test.js b/test/internal/ch-websocket.test.js new file mode 100644 index 000000000..765626965 --- /dev/null +++ b/test/internal/ch-websocket.test.js @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2002-2017 "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'; + +describe('WebSocketChannel', () => { + + const WebSocketChannel = wsChannel.channel; + const webSocketChannelAvailable = wsChannel.available; + + let OriginalWebSocket; + + beforeEach(() => { + if (webSocketChannelAvailable) { + OriginalWebSocket = WebSocket; + } + }); + + afterEach(() => { + if (webSocketChannelAvailable) { + WebSocket = OriginalWebSocket; + } + }); + + 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'); + }); + + 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}; + }; + + 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); + const webSocketChannel = new WebSocketChannel(channelConfig); + + expect(webSocketChannel._ws.url).toEqual(expectedWsAddress); + } + +}); From 14fbc44d97dcf36fad26a9c50cea200f7049c79f Mon Sep 17 00:00:00 2001 From: lutovich Date: Fri, 29 Dec 2017 18:43:12 +0100 Subject: [PATCH 09/14] Added tests for parsing of link-local IPv6 addresses --- test/internal/url-util.test.js | 45 ++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/test/internal/url-util.test.js b/test/internal/url-util.test.js index 0a25244b7..1a2f496aa 100644 --- a/test/internal/url-util.test.js +++ b/test/internal/url-util.test.js @@ -83,6 +83,11 @@ describe('url', () => { 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', () => { @@ -159,6 +164,12 @@ describe('url', () => { 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', () => { @@ -248,6 +259,13 @@ describe('url', () => { 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', () => { @@ -324,6 +342,12 @@ describe('url', () => { 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', () => { @@ -400,6 +424,12 @@ describe('url', () => { 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', () => { @@ -489,6 +519,13 @@ describe('url', () => { 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', () => { @@ -591,6 +628,14 @@ describe('url', () => { 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', () => { From 86d625c4e32db4e12aba4f769ccc9d4203998ed3 Mon Sep 17 00:00:00 2001 From: lutovich Date: Fri, 29 Dec 2017 18:46:46 +0100 Subject: [PATCH 10/14] Better test suite name --- test/internal/url-util.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/internal/url-util.test.js b/test/internal/url-util.test.js index 1a2f496aa..d813eb078 100644 --- a/test/internal/url-util.test.js +++ b/test/internal/url-util.test.js @@ -20,7 +20,7 @@ import urlUtil from '../../src/v1/internal/url-util'; import {DEFAULT_PORT} from '../../src/v1/internal/ch-config'; -describe('url', () => { +describe('url-util', () => { it('should parse URL with just host name', () => { verifyUrl('localhost', { From 3867f7a0d7d97a8f7dec4432254d01da718a7255 Mon Sep 17 00:00:00 2001 From: lutovich Date: Sun, 31 Dec 2017 19:40:40 +0100 Subject: [PATCH 11/14] Fix parsing of connectionTimeout setting Connection timeout limits amount of time connection spends waiting for the remote side to respond. It makes connect not hang forever if remote side does not respond. Configured positive values define the actual timeout. Zero and negative values mean infinite timeout. Previously configured value was not correctly interpreted and thus it was not possible to disable the timeout. Zero and negative values resulted in a default value of 5 seconds to be used. This commit fixes the problem so that zero and negative values mean no timeout. --- src/v1/index.js | 6 +++--- src/v1/internal/ch-config.js | 13 +++++++++++-- test/internal/ch-config.test.js | 12 ++++++++++++ 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/v1/index.js b/src/v1/index.js index 95378487f..b42314d44 100644 --- a/src/v1/index.js +++ b/src/v1/index.js @@ -153,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 * } * diff --git a/src/v1/internal/ch-config.js b/src/v1/internal/ch-config.js index 7ede1f48c..318fc073f 100644 --- a/src/v1/internal/ch-config.js +++ b/src/v1/internal/ch-config.js @@ -68,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/test/internal/ch-config.test.js b/test/internal/ch-config.test.js index 367d558e6..deabf6d21 100644 --- a/test/internal/ch-config.test.js +++ b/test/internal/ch-config.test.js @@ -117,4 +117,16 @@ describe('ChannelConfig', () => { 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(); + }); + }); From 76b889ef40ab8a715f1d7ad442126a4c4fba6d67 Mon Sep 17 00:00:00 2001 From: lutovich Date: Tue, 2 Jan 2018 10:18:06 +0100 Subject: [PATCH 12/14] Clear WebSocket connection timeout when closing channel `WebSocketChannel` is built on top of a `WebSocket` and contains property that references it. `WebSocket` connection timeout is enforced by a separate setTimeout timer that closes the socket after configured amount of time. Previously this timeout has only been cleared when connection is established. It has not been cleared when channel is closed, resulting in potential existence of stray timers. This commit makes `WebSocketChannel` clear the connection timeout timer when it's closed. So connect timeout timer is removed even if channel is closed before connection is established. --- src/v1/internal/ch-websocket.js | 20 +++++++++-- test/internal/ch-websocket.test.js | 55 ++++++++++++++++++++++++++++-- 2 files changed, 70 insertions(+), 5 deletions(-) diff --git a/src/v1/internal/ch-websocket.js b/src/v1/internal/ch-websocket.js index 4df60e80a..3b185befd 100644 --- a/src/v1/internal/ch-websocket.js +++ b/src/v1/internal/ch-websocket.js @@ -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,6 +165,19 @@ 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'; diff --git a/test/internal/ch-websocket.test.js b/test/internal/ch-websocket.test.js index 765626965..3969a160b 100644 --- a/test/internal/ch-websocket.test.js +++ b/test/internal/ch-websocket.test.js @@ -21,6 +21,7 @@ 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', () => { @@ -28,6 +29,7 @@ describe('WebSocketChannel', () => { const webSocketChannelAvailable = wsChannel.available; let OriginalWebSocket; + let webSocketChannel; beforeEach(() => { if (webSocketChannelAvailable) { @@ -39,6 +41,9 @@ describe('WebSocketChannel', () => { if (webSocketChannelAvailable) { WebSocket = OriginalWebSocket; } + if (webSocketChannel) { + webSocketChannel.close(); + } }); it('should fallback to literal IPv6 when SyntaxError is thrown', () => { @@ -49,6 +54,47 @@ describe('WebSocketChannel', () => { 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; @@ -59,14 +105,19 @@ describe('WebSocketChannel', () => { if (url.indexOf('[') !== -1) { throw new SyntaxError(); } - return {url: url}; + 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); - const webSocketChannel = new WebSocketChannel(channelConfig); + + webSocketChannel = new WebSocketChannel(channelConfig); expect(webSocketChannel._ws.url).toEqual(expectedWsAddress); } From 8f8517c4ce16d5739672f8d68c26b548ca4a248c Mon Sep 17 00:00:00 2001 From: lutovich Date: Tue, 2 Jan 2018 11:27:38 +0100 Subject: [PATCH 13/14] Updated licence headers to 2018 --- src/v1/internal/url-util.js | 2 +- test/browser/karma-ie.conf.js | 2 +- test/internal/ch-websocket.test.js | 2 +- test/internal/url-util.test.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/v1/internal/url-util.js b/src/v1/internal/url-util.js index 9e54e758b..9a414e71c 100644 --- a/src/v1/internal/url-util.js +++ b/src/v1/internal/url-util.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2002-2017 "Neo Technology,"," + * Copyright (c) 2002-2018 "Neo Technology," * Network Engine for Objects in Lund AB [http://neotechnology.com] * * This file is part of Neo4j. diff --git a/test/browser/karma-ie.conf.js b/test/browser/karma-ie.conf.js index 9369af402..d620df25a 100644 --- a/test/browser/karma-ie.conf.js +++ b/test/browser/karma-ie.conf.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2002-2017 "Neo Technology,"," + * Copyright (c) 2002-2018 "Neo Technology," * Network Engine for Objects in Lund AB [http://neotechnology.com] * * This file is part of Neo4j. diff --git a/test/internal/ch-websocket.test.js b/test/internal/ch-websocket.test.js index 3969a160b..204db69a4 100644 --- a/test/internal/ch-websocket.test.js +++ b/test/internal/ch-websocket.test.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2002-2017 "Neo Technology,"," + * Copyright (c) 2002-2018 "Neo Technology," * Network Engine for Objects in Lund AB [http://neotechnology.com] * * This file is part of Neo4j. diff --git a/test/internal/url-util.test.js b/test/internal/url-util.test.js index d813eb078..a54b2f15b 100644 --- a/test/internal/url-util.test.js +++ b/test/internal/url-util.test.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2002-2017 "Neo Technology,"," + * Copyright (c) 2002-2018 "Neo Technology," * Network Engine for Objects in Lund AB [http://neotechnology.com] * * This file is part of Neo4j. From 28b61b8d2c1bdecd7c29d1713dbbe36e4a95e05e Mon Sep 17 00:00:00 2001 From: lutovich Date: Mon, 8 Jan 2018 10:23:45 +0100 Subject: [PATCH 14/14] Update package-lock.json after rebase --- package-lock.json | 1020 +-------------------------------------------- 1 file changed, 21 insertions(+), 999 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1c2223925..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" } }, @@ -4695,986 +4695,6 @@ } } }, - "first-chunk-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/first-chunk-stream/-/first-chunk-stream-1.0.0.tgz", - "integrity": "sha1-Wb+1DNkF9g18OUzT2ayqtOatk04=", - "dev": true - }, - "flagged-respawn": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-0.3.2.tgz", - "integrity": "sha1-/xke3c1wiKZ1smEP/8l2vpuAdLU=", - "dev": true - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", - "dev": true - }, - "for-own": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", - "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", - "dev": true, - "requires": { - "for-in": "1.0.2" - } - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true - }, - "form-data": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz", - "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=", - "dev": true, - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.5", - "mime-types": "2.1.17" - } - }, - "fs-access": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fs-access/-/fs-access-1.0.1.tgz", - "integrity": "sha1-1qh/JiJxzv6+wwxVNAf7mV2od3o=", - "dev": true, - "requires": { - "null-check": "1.0.0" - } - }, - "fs-exists-sync": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz", - "integrity": "sha1-mC1ok6+RjnLQjeyehnP/K1qNat0=", - "dev": true - }, - "fs-extra": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-1.0.0.tgz", - "integrity": "sha1-zTzl9+fLYUWIP8rjGR6Yd/hYeVA=", - "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "jsonfile": "2.4.0", - "klaw": "1.3.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "fsevents": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.3.tgz", - "integrity": "sha512-WIr7iDkdmdbxu/Gh6eKEZJL6KPE74/5MEsf2whTOFNxbIoIixogroLdKYqB6FDav4Wavh/lZdzzd3b2KxIXC5Q==", - "dev": true, - "optional": true, - "requires": { - "nan": "2.8.0", - "node-pre-gyp": "0.6.39" - }, - "dependencies": { - "abbrev": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "ajv": { - "version": "4.11.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "co": "4.6.0", - "json-stable-stringify": "1.0.1" - } - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "aproba": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "1.0.0", - "readable-stream": "2.2.9" - } - }, - "asn1": { - "version": "0.2.3", - "bundled": true, - "dev": true, - "optional": true - }, - "assert-plus": { - "version": "0.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "asynckit": { - "version": "0.4.0", - "bundled": true, - "dev": true, - "optional": true - }, - "aws-sign2": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "aws4": { - "version": "1.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "balanced-match": { - "version": "0.4.2", - "bundled": true, - "dev": true - }, - "bcrypt-pbkdf": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "tweetnacl": "0.14.5" - } - }, - "block-stream": { - "version": "0.0.9", - "bundled": true, - "dev": true, - "requires": { - "inherits": "2.0.3" - } - }, - "boom": { - "version": "2.10.1", - "bundled": true, - "dev": true, - "requires": { - "hoek": "2.16.3" - } - }, - "brace-expansion": { - "version": "1.1.7", - "bundled": true, - "dev": true, - "requires": { - "balanced-match": "0.4.2", - "concat-map": "0.0.1" - } - }, - "buffer-shims": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "caseless": { - "version": "0.12.0", - "bundled": true, - "dev": true, - "optional": true - }, - "co": { - "version": "4.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "combined-stream": { - "version": "1.0.5", - "bundled": true, - "dev": true, - "requires": { - "delayed-stream": "1.0.0" - } - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "cryptiles": { - "version": "2.0.5", - "bundled": true, - "dev": true, - "requires": { - "boom": "2.10.1" - } - }, - "dashdash": { - "version": "1.14.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "debug": { - "version": "2.6.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.4.2", - "bundled": true, - "dev": true, - "optional": true - }, - "delayed-stream": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "ecc-jsbn": { - "version": "0.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "jsbn": "0.1.1" - } - }, - "extend": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "extsprintf": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "forever-agent": { - "version": "0.6.1", - "bundled": true, - "dev": true, - "optional": true - }, - "form-data": { - "version": "2.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.5", - "mime-types": "2.1.15" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "fstream": { - "version": "1.0.11", - "bundled": true, - "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "inherits": "2.0.3", - "mkdirp": "0.5.1", - "rimraf": "2.6.1" - } - }, - "fstream-ignore": { - "version": "1.0.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fstream": "1.0.11", - "inherits": "2.0.3", - "minimatch": "3.0.4" - } - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "1.1.1", - "console-control-strings": "1.1.0", - "has-unicode": "2.0.1", - "object-assign": "4.1.1", - "signal-exit": "3.0.2", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wide-align": "1.1.2" - } - }, - "getpass": { - "version": "0.1.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "glob": { - "version": "7.1.2", - "bundled": true, - "dev": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "graceful-fs": { - "version": "4.1.11", - "bundled": true, - "dev": true - }, - "har-schema": { - "version": "1.0.5", - "bundled": true, - "dev": true, - "optional": true - }, - "har-validator": { - "version": "4.2.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ajv": "4.11.8", - "har-schema": "1.0.5" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "hawk": { - "version": "3.1.3", - "bundled": true, - "dev": true, - "requires": { - "boom": "2.10.1", - "cryptiles": "2.0.5", - "hoek": "2.16.3", - "sntp": "1.0.9" - } - }, - "hoek": { - "version": "2.16.3", - "bundled": true, - "dev": true - }, - "http-signature": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "assert-plus": "0.2.0", - "jsprim": "1.4.0", - "sshpk": "1.13.0" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true - }, - "ini": { - "version": "1.3.4", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "is-typedarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "isstream": { - "version": "0.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "jodid25519": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "jsbn": "0.1.1" - } - }, - "jsbn": { - "version": "0.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "json-schema": { - "version": "0.2.3", - "bundled": true, - "dev": true, - "optional": true - }, - "json-stable-stringify": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "jsonify": "0.0.0" - } - }, - "json-stringify-safe": { - "version": "5.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "jsonify": { - "version": "0.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "jsprim": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.0.2", - "json-schema": "0.2.3", - "verror": "1.3.6" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "mime-db": { - "version": "1.27.0", - "bundled": true, - "dev": true - }, - "mime-types": { - "version": "2.1.15", - "bundled": true, - "dev": true, - "requires": { - "mime-db": "1.27.0" - } - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "requires": { - "brace-expansion": "1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "node-pre-gyp": { - "version": "0.6.39", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "1.0.2", - "hawk": "3.1.3", - "mkdirp": "0.5.1", - "nopt": "4.0.1", - "npmlog": "4.1.0", - "rc": "1.2.1", - "request": "2.81.0", - "rimraf": "2.6.1", - "semver": "5.3.0", - "tar": "2.2.1", - "tar-pack": "3.4.0" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1.1.0", - "osenv": "0.1.4" - } - }, - "npmlog": { - "version": "4.1.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "1.1.4", - "console-control-strings": "1.1.0", - "gauge": "2.7.4", - "set-blocking": "2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "oauth-sign": { - "version": "0.8.2", - "bundled": true, - "dev": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "performance-now": { - "version": "0.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "1.0.7", - "bundled": true, - "dev": true - }, - "punycode": { - "version": "1.4.1", - "bundled": true, - "dev": true, - "optional": true - }, - "qs": { - "version": "6.4.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "0.4.2", - "ini": "1.3.4", - "minimist": "1.2.0", - "strip-json-comments": "2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.2.9", - "bundled": true, - "dev": true, - "requires": { - "buffer-shims": "1.0.0", - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "1.0.7", - "string_decoder": "1.0.1", - "util-deprecate": "1.0.2" - } - }, - "request": { - "version": "2.81.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aws-sign2": "0.6.0", - "aws4": "1.6.0", - "caseless": "0.12.0", - "combined-stream": "1.0.5", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.1.4", - "har-validator": "4.2.1", - "hawk": "3.1.3", - "http-signature": "1.1.1", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.15", - "oauth-sign": "0.8.2", - "performance-now": "0.2.0", - "qs": "6.4.0", - "safe-buffer": "5.0.1", - "stringstream": "0.0.5", - "tough-cookie": "2.3.2", - "tunnel-agent": "0.6.0", - "uuid": "3.0.1" - } - }, - "rimraf": { - "version": "2.6.1", - "bundled": true, - "dev": true, - "requires": { - "glob": "7.1.2" - } - }, - "safe-buffer": { - "version": "5.0.1", - "bundled": true, - "dev": true - }, - "semver": { - "version": "5.3.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sntp": { - "version": "1.0.9", - "bundled": true, - "dev": true, - "requires": { - "hoek": "2.16.3" - } - }, - "sshpk": { - "version": "1.13.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "asn1": "0.2.3", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.1", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.1", - "getpass": "0.1.7", - "jodid25519": "1.0.2", - "jsbn": "0.1.1", - "tweetnacl": "0.14.5" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - }, - "string_decoder": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "5.0.1" - } - }, - "stringstream": { - "version": "0.0.5", - "bundled": true, - "dev": true, - "optional": true - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "2.2.1", - "bundled": true, - "dev": true, - "requires": { - "block-stream": "0.0.9", - "fstream": "1.0.11", - "inherits": "2.0.3" - } - }, - "tar-pack": { - "version": "3.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "2.6.8", - "fstream": "1.0.11", - "fstream-ignore": "1.0.5", - "once": "1.4.0", - "readable-stream": "2.2.9", - "rimraf": "2.6.1", - "tar": "2.2.1", - "uid-number": "0.0.6" - } - }, - "tough-cookie": { - "version": "2.3.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "punycode": "1.4.1" - } - }, - "tunnel-agent": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "bundled": true, - "dev": true, - "optional": true - }, - "uid-number": { - "version": "0.0.6", - "bundled": true, - "dev": true, - "optional": true - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "uuid": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "verror": { - "version": "1.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "extsprintf": "1.0.2" - } - }, - "wide-align": { - "version": "1.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true - } - } - }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -6008,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": { @@ -8338,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", @@ -8351,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", @@ -8535,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", @@ -9278,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": { @@ -10936,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", @@ -11012,14 +10041,7 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", "dev": true - }, - "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" + } } }, "user-home": { @@ -11449,4 +10471,4 @@ "dev": true } } -}} +}