Skip to content

Commit 3b8b35c

Browse files
committed
Added URL parser
It supports host names, IPv4 and IPv6 addresses. Existing regex-based parsing does not allow IPv6.
1 parent 8769596 commit 3b8b35c

File tree

2 files changed

+881
-0
lines changed

2 files changed

+881
-0
lines changed

src/v1/internal/url.js

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
/**
2+
* Copyright (c) 2002-2017 "Neo Technology,","
3+
* Network Engine for Objects in Lund AB [http://neotechnology.com]
4+
*
5+
* This file is part of Neo4j.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
20+
class Url {
21+
22+
constructor(scheme, host, port, query) {
23+
/**
24+
* Nullable scheme (protocol) of the URL.
25+
* @type {string}
26+
*/
27+
this.scheme = scheme;
28+
29+
/**
30+
* Nonnull host name or IP address. IPv6 always wrapped in square brackets.
31+
* @type {string}
32+
*/
33+
this.host = host;
34+
35+
/**
36+
* Nullable number representing port.
37+
* @type {number}
38+
*/
39+
this.port = port;
40+
41+
/**
42+
* Nonnull object representing parsed query string key-value pairs. Duplicated keys not supported.
43+
* @type {object}
44+
*/
45+
this.query = query;
46+
}
47+
}
48+
49+
class UrlParser {
50+
51+
parse(url) {
52+
throw new Error('Abstract function');
53+
}
54+
}
55+
56+
class NodeUrlParser extends UrlParser {
57+
58+
constructor() {
59+
super();
60+
this._url = require('url');
61+
}
62+
63+
static isAvailable() {
64+
try {
65+
const parseFunction = require('url').parse;
66+
if (parseFunction && typeof parseFunction === 'function') {
67+
return true;
68+
}
69+
} catch (e) {
70+
}
71+
return false;
72+
}
73+
74+
parse(url) {
75+
url = url.trim();
76+
77+
let schemeMissing = false;
78+
if (url.indexOf('://') === -1) {
79+
// url does not contain scheme, add dummy 'http://' to make parser work correctly
80+
schemeMissing = true;
81+
url = `http://${url}`;
82+
}
83+
84+
const parsed = this._url.parse(url);
85+
86+
const scheme = schemeMissing ? null : NodeUrlParser.extractScheme(parsed);
87+
const host = NodeUrlParser.extractHost(url, parsed);
88+
const port = extractPort(parsed.port);
89+
const query = extractQuery(parsed.search, url);
90+
91+
return new Url(scheme, host, port, query);
92+
}
93+
94+
static extractScheme(parsedUrl) {
95+
try {
96+
const protocol = parsedUrl.protocol; // results in scheme with ':', like 'bolt:', 'http:'...
97+
return protocol.substring(0, protocol.length - 1); // remove the trailing ':'
98+
} catch (e) {
99+
return null;
100+
}
101+
}
102+
103+
static extractHost(originalUrl, parsedUrl) {
104+
const hostname = parsedUrl.hostname; // results in host name or IP address, square brackets removed for IPv6
105+
const host = parsedUrl.host || ''; // results in hostname + port, like: 'localhost:7687', '[::1]:7687',...; includes square brackets for IPv6
106+
107+
if (!hostname) {
108+
throw new Error(`Unable to parse host name in ${originalUrl}`);
109+
}
110+
111+
if (!startsWith(hostname, '[') && startsWith(host, '[')) {
112+
// looks like an IPv6 address, add square brackets to the host name
113+
return `[${hostname}]`;
114+
}
115+
return hostname;
116+
}
117+
}
118+
119+
class BrowserUrlParser extends UrlParser {
120+
121+
constructor() {
122+
super();
123+
}
124+
125+
static isAvailable() {
126+
return document && typeof document === 'object';
127+
}
128+
129+
130+
parse(url) {
131+
const urlAndScheme = BrowserUrlParser.sanitizeUrlAndExtractScheme(url);
132+
133+
url = urlAndScheme.url;
134+
135+
const parsed = document.createElement('a');
136+
parsed.href = url;
137+
138+
const scheme = urlAndScheme.scheme;
139+
const host = BrowserUrlParser.extractHost(url, parsed);
140+
const port = extractPort(parsed.port);
141+
const query = extractQuery(parsed.search, url);
142+
143+
return new Url(scheme, host, port, query);
144+
}
145+
146+
static sanitizeUrlAndExtractScheme(url) {
147+
url = url.trim();
148+
149+
let schemeMissing = false;
150+
if (url.indexOf('://') === -1) {
151+
// url does not contain scheme, add dummy 'http://' to make parser work correctly
152+
schemeMissing = true;
153+
url = `http://${url}`;
154+
}
155+
156+
const schemeAndRestSplit = url.split('://');
157+
if (schemeAndRestSplit.length !== 2) {
158+
throw new Error(`Unable to extract scheme from ${url}`);
159+
}
160+
161+
const splitScheme = schemeAndRestSplit[0];
162+
const splitRest = schemeAndRestSplit[1];
163+
164+
if (!splitScheme) {
165+
// url probably looks like '://localhost:7687', add dummy 'http://' to make parser work correctly
166+
schemeMissing = true;
167+
url = `http://${url}`;
168+
} else if (splitScheme !== 'http') {
169+
// parser does not seem to work with schemes other than 'http' and 'https', add dummy 'http'
170+
url = `http://${splitRest}`;
171+
}
172+
173+
const scheme = schemeMissing ? null : splitScheme;
174+
return {scheme: scheme, url: url};
175+
}
176+
177+
static extractHost(originalUrl, parsedUrl) {
178+
const hostname = parsedUrl.hostname; // results in host name or IP address, IPv6 address always in square brackets
179+
if (!hostname) {
180+
throw new Error(`Unable to parse host name in ${originalUrl}`);
181+
}
182+
return hostname;
183+
}
184+
}
185+
186+
function extractPort(portString) {
187+
try {
188+
const port = parseInt(portString, 10);
189+
if (port) {
190+
return port;
191+
}
192+
} catch (e) {
193+
}
194+
return null;
195+
}
196+
197+
function extractQuery(queryString, url) {
198+
const query = trimAndSanitizeQueryString(queryString);
199+
const context = {};
200+
201+
if (query) {
202+
query.split('&').forEach(pair => {
203+
const keyValue = pair.split('=');
204+
if (keyValue.length !== 2) {
205+
throw new Error(`Invalid parameters: '${keyValue}' in URL '${url}'.`);
206+
}
207+
208+
const key = trimAndVerifyQueryElement(keyValue[0], 'key', url);
209+
const value = trimAndVerifyQueryElement(keyValue[1], 'value', url);
210+
211+
if (context[key]) {
212+
throw new Error(`Duplicated query parameters with key '${key}' in URL '${url}'`);
213+
}
214+
215+
context[key] = value;
216+
});
217+
}
218+
219+
return context;
220+
}
221+
222+
function trimAndSanitizeQueryString(queryString) {
223+
if (queryString) {
224+
queryString = queryString.trim();
225+
if (startsWith(queryString, '?')) {
226+
queryString = queryString.substring(1, queryString.length);
227+
}
228+
}
229+
return queryString;
230+
}
231+
232+
function trimAndVerifyQueryElement(string, name, url) {
233+
const result = string.trim();
234+
if (!result) {
235+
throw new Error(`Illegal empty ${name} in URL query '${url}'`);
236+
}
237+
return result;
238+
}
239+
240+
function createParser() {
241+
if (NodeUrlParser.isAvailable()) {
242+
return new NodeUrlParser();
243+
} else if (BrowserUrlParser.isAvailable()) {
244+
return new BrowserUrlParser();
245+
} else {
246+
throw new Error('Unable to create a URL parser, neither NodeJS nor Browser version is available');
247+
}
248+
}
249+
250+
function startsWith(string, prefix) {
251+
return string.lastIndexOf(prefix, 0) === 0;
252+
}
253+
254+
const parser = createParser();
255+
256+
export default parser;

0 commit comments

Comments
 (0)