|
18 | 18 | */ |
19 | 19 |
|
20 | 20 | import ParsedUrl from 'url-parse'; |
| 21 | +import {assertString} from './util'; |
| 22 | +import {DEFAULT_PORT} from './ch-config'; |
21 | 23 |
|
22 | | -class Url { |
| 24 | +export class Url { |
23 | 25 |
|
24 | | - constructor(scheme, host, port, query) { |
| 26 | + constructor(scheme, host, port, hostAndPort, query) { |
25 | 27 | /** |
26 | 28 | * Nullable scheme (protocol) of the URL. |
| 29 | + * Example: 'bolt', 'bolt+routing', 'http', 'https', etc. |
27 | 30 | * @type {string} |
28 | 31 | */ |
29 | 32 | this.scheme = scheme; |
30 | 33 |
|
31 | 34 | /** |
32 | | - * Nonnull host name or IP address. IPv6 always wrapped in square brackets. |
| 35 | + * Nonnull host name or IP address. IPv6 not wrapped in square brackets. |
| 36 | + * Example: 'neo4j.com', 'localhost', '127.0.0.1', '192.168.10.15', '::1', '2001:4860:4860::8844', etc. |
33 | 37 | * @type {string} |
34 | 38 | */ |
35 | 39 | this.host = host; |
36 | 40 |
|
37 | 41 | /** |
38 | | - * Nullable number representing port. |
| 42 | + * Nonnull number representing port. Default port {@link DEFAULT_PORT} value is used if given URL string |
| 43 | + * does not contain port. Example: 7687, 12000, etc. |
39 | 44 | * @type {number} |
40 | 45 | */ |
41 | 46 | this.port = port; |
42 | 47 |
|
43 | 48 | /** |
44 | | - * Nonnull host name or IP address plus port, separated by ':'. |
| 49 | + * Nonnull host name or IP address plus port, separated by ':'. IPv6 wrapped in square brackets. |
| 50 | + * Example: 'neo4j.com', 'neo4j.com:7687', '127.0.0.1', '127.0.0.1:8080', '[2001:4860:4860::8844]', |
| 51 | + * '[2001:4860:4860::8844]:9090', etc. |
45 | 52 | * @type {string} |
46 | 53 | */ |
47 | | - this.hostAndPort = port ? `${host}:${port}` : host; |
| 54 | + this.hostAndPort = hostAndPort; |
48 | 55 |
|
49 | 56 | /** |
50 | 57 | * Nonnull object representing parsed query string key-value pairs. Duplicated keys not supported. |
| 58 | + * Example: '{}', '{'key1': 'value1', 'key2': 'value2'}', etc. |
51 | 59 | * @type {object} |
52 | 60 | */ |
53 | 61 | this.query = query; |
54 | 62 | } |
55 | 63 | } |
56 | 64 |
|
57 | | -function parse(url) { |
| 65 | +function parseBoltUrl(url) { |
| 66 | + assertString(url, 'URL'); |
| 67 | + |
58 | 68 | const sanitized = sanitizeUrl(url); |
59 | 69 | const parsedUrl = new ParsedUrl(sanitized.url, {}, query => extractQuery(query, url)); |
60 | 70 |
|
61 | 71 | const scheme = sanitized.schemeMissing ? null : extractScheme(parsedUrl.protocol); |
62 | | - const host = extractHost(parsedUrl.hostname); |
| 72 | + const rawHost = extractHost(parsedUrl.hostname); // has square brackets for IPv6 |
| 73 | + const host = unescapeIPv6Address(rawHost); // no square brackets for IPv6 |
63 | 74 | const port = extractPort(parsedUrl.port); |
| 75 | + const hostAndPort = port ? `${rawHost}:${port}` : rawHost; |
64 | 76 | const query = parsedUrl.query; |
65 | 77 |
|
66 | | - return new Url(scheme, host, port, query); |
| 78 | + return new Url(scheme, host, port, hostAndPort, query); |
67 | 79 | } |
68 | 80 |
|
69 | 81 | function sanitizeUrl(url) { |
@@ -97,7 +109,7 @@ function extractHost(host, url) { |
97 | 109 |
|
98 | 110 | function extractPort(portString) { |
99 | 111 | const port = parseInt(portString, 10); |
100 | | - return port ? port : null; |
| 112 | + return (port === 0 || port) ? port : DEFAULT_PORT; |
101 | 113 | } |
102 | 114 |
|
103 | 115 | function extractQuery(queryString, url) { |
@@ -141,6 +153,43 @@ function trimAndVerifyQueryElement(element, name, url) { |
141 | 153 | return element; |
142 | 154 | } |
143 | 155 |
|
| 156 | +function escapeIPv6Address(address) { |
| 157 | + const startsWithSquareBracket = address.charAt(0) === '['; |
| 158 | + const endsWithSquareBracket = address.charAt(address.length - 1) === ']'; |
| 159 | + |
| 160 | + if (!startsWithSquareBracket && !endsWithSquareBracket) { |
| 161 | + return `[${address}]`; |
| 162 | + } else if (startsWithSquareBracket && endsWithSquareBracket) { |
| 163 | + return address; |
| 164 | + } else { |
| 165 | + throw new Error(`Illegal IPv6 address ${address}`); |
| 166 | + } |
| 167 | +} |
| 168 | + |
| 169 | +function unescapeIPv6Address(address) { |
| 170 | + const startsWithSquareBracket = address.charAt(0) === '['; |
| 171 | + const endsWithSquareBracket = address.charAt(address.length - 1) === ']'; |
| 172 | + |
| 173 | + if (!startsWithSquareBracket && !endsWithSquareBracket) { |
| 174 | + return address; |
| 175 | + } else if (startsWithSquareBracket && endsWithSquareBracket) { |
| 176 | + return address.substring(1, address.length - 1); |
| 177 | + } else { |
| 178 | + throw new Error(`Illegal IPv6 address ${address}`); |
| 179 | + } |
| 180 | +} |
| 181 | + |
| 182 | +function formatIPv4Address(address, port) { |
| 183 | + return `${address}:${port}`; |
| 184 | +} |
| 185 | + |
| 186 | +function formatIPv6Address(address, port) { |
| 187 | + const escapedAddress = escapeIPv6Address(address); |
| 188 | + return `${escapedAddress}:${port}`; |
| 189 | +} |
| 190 | + |
144 | 191 | export default { |
145 | | - parse: parse |
| 192 | + parseBoltUrl: parseBoltUrl, |
| 193 | + formatIPv4Address: formatIPv4Address, |
| 194 | + formatIPv6Address: formatIPv6Address |
146 | 195 | }; |
0 commit comments