Skip to content

Commit 8a5a2a3

Browse files
committed
Fix handling of bolt URL with port 80
URLs are currently parsed in two places in the driver. First is before driver creation to determine if it should be a direct, routing or http driver based on the URL scheme. Then scheme is stripped off and rest of the code uses host and port string. Second time is before network connection is established. This time code parses host and port string, which is either the one used to create the driver or arrived in a routing procedure response. Scheme is missing in this case and dummy "http://" was attached just to make parser correctly handle the given string. Turned out "url-parse" library used for parsing removes port from the parsed URL if it is the default one for the scheme. So port 80 is removed when URL scheme is "http". This made driver always treat "localhost:80" as "localhost:7687" where 7687 is the default bolt port during the second parsing step. Default port 80 was removed and driver treated absence of the port as a sign to use the default bolt port. Same logic handles URLs like "bolt://localhost". Removal of the default port is problematic because driver can't know if no port was specified or default port was specified. Experimental HTTP and HTTPS support also suffers from the same problem. Default port for HTTP should be 7474 and not 80. This commit fixes the problem by using a different parsing library "uri-js" that always preserves the port. Dummy "http://" prefix is also changed to "none://" just in case.
1 parent 9532461 commit 8a5a2a3

File tree

5 files changed

+80
-35
lines changed

5 files changed

+80
-35
lines changed

package-lock.json

Lines changed: 17 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,6 @@
7373
},
7474
"dependencies": {
7575
"babel-runtime": "^6.18.0",
76-
"url-parse": "^1.2.0"
76+
"uri-js": "^4.2.1"
7777
}
7878
}

src/v1/internal/url-util.js

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

20-
import ParsedUrl from 'url-parse';
20+
import {parse as uriJsParse} from 'uri-js';
2121
import {assertString} from './util';
2222

2323
const DEFAULT_BOLT_PORT = 7687;
@@ -69,14 +69,14 @@ function parseDatabaseUrl(url) {
6969
assertString(url, 'URL');
7070

7171
const sanitized = sanitizeUrl(url);
72-
const parsedUrl = new ParsedUrl(sanitized.url, {}, query => extractQuery(query, url));
72+
const parsedUrl = uriJsParse(sanitized.url);
7373

74-
const scheme = sanitized.schemeMissing ? null : extractScheme(parsedUrl.protocol);
75-
const rawHost = extractHost(parsedUrl.hostname); // has square brackets for IPv6
76-
const host = unescapeIPv6Address(rawHost); // no square brackets for IPv6
74+
const scheme = sanitized.schemeMissing ? null : extractScheme(parsedUrl.scheme);
75+
const host = extractHost(parsedUrl.host); // no square brackets for IPv6
76+
const formattedHost = formatHost(host); // has square brackets for IPv6
7777
const port = extractPort(parsedUrl.port, scheme);
78-
const hostAndPort = `${rawHost}:${port}`;
79-
const query = parsedUrl.query;
78+
const hostAndPort = `${formattedHost}:${port}`;
79+
const query = extractQuery(parsedUrl.query, url);
8080

8181
return new Url(scheme, host, port, hostAndPort, query);
8282
}
@@ -85,8 +85,8 @@ function sanitizeUrl(url) {
8585
url = url.trim();
8686

8787
if (url.indexOf('://') === -1) {
88-
// url does not contain scheme, add dummy 'http://' to make parser work correctly
89-
return {schemeMissing: true, url: `http://${url}`};
88+
// url does not contain scheme, add dummy 'none://' to make parser work correctly
89+
return {schemeMissing: true, url: `none://${url}`};
9090
}
9191

9292
return {schemeMissing: false, url: url};
@@ -169,17 +169,12 @@ function escapeIPv6Address(address) {
169169
}
170170
}
171171

172-
function unescapeIPv6Address(address) {
173-
const startsWithSquareBracket = address.charAt(0) === '[';
174-
const endsWithSquareBracket = address.charAt(address.length - 1) === ']';
175-
176-
if (!startsWithSquareBracket && !endsWithSquareBracket) {
177-
return address;
178-
} else if (startsWithSquareBracket && endsWithSquareBracket) {
179-
return address.substring(1, address.length - 1);
180-
} else {
181-
throw new Error(`Illegal IPv6 address ${address}`);
172+
function formatHost(host) {
173+
if (!host) {
174+
throw new Error(`Illegal host ${host}`);
182175
}
176+
const isIPv6Address = host.indexOf(':') >= 0;
177+
return isIPv6Address ? escapeIPv6Address(host) : host;
183178
}
184179

185180
function formatIPv4Address(address, port) {

test/internal/url-util.test.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,31 @@ describe('url-util', () => {
723723
expect(parse('https://localhost').port).toEqual(urlUtil.defaultPortForScheme('https'));
724724
});
725725

726+
it('should parse URLs with port 80', () => {
727+
['http', 'https', 'ws', 'wss', 'bolt', 'bolt+routing'].forEach(scheme => {
728+
verifyUrl(`${scheme}://localhost:80`, {
729+
scheme: scheme,
730+
host: 'localhost',
731+
port: 80
732+
});
733+
});
734+
735+
['localhost', '127.0.0.1', '192.168.10.29'].forEach(host => {
736+
verifyUrl(`${host}:80`, {
737+
host: host,
738+
port: 80
739+
});
740+
});
741+
742+
['::1', '1afc:0:a33:85a3::ff2f'].forEach(host => {
743+
verifyUrl(`[${host}]:80`, {
744+
host: host,
745+
port: 80,
746+
ipv6: true
747+
});
748+
});
749+
});
750+
726751
function verifyUrl(urlString, expectedUrl) {
727752
const url = parse(urlString);
728753

test/v1/driver.test.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import FakeConnection from '../internal/fake-connection';
2323
import lolex from 'lolex';
2424
import {DEFAULT_ACQUISITION_TIMEOUT, DEFAULT_MAX_SIZE} from '../../src/v1/internal/pool-config';
2525
import {ServerVersion, VERSION_3_1_0} from '../../src/v1/internal/server-version';
26+
import testUtils from '../internal/test-utils';
2627

2728
describe('driver', () => {
2829

@@ -78,6 +79,28 @@ describe('driver', () => {
7879
startNewTransaction(driver);
7980
}, 10000);
8081

82+
it('should fail with correct error message when connecting to port 80', done => {
83+
if (testUtils.isClient()) {
84+
// good error message is not available in browser
85+
done();
86+
return;
87+
}
88+
89+
driver = neo4j.driver('bolt://localhost:80', sharedNeo4j.authToken);
90+
91+
driver.session().run('RETURN 1').then(result => {
92+
done.fail('Should not be able to connect. Result: ' + JSON.stringify(result));
93+
}).catch(error => {
94+
const doesNotContainAddress = error.message.indexOf(':80') < 0;
95+
if (doesNotContainAddress) {
96+
done.fail(`Expected to contain ':80' but was: ${error.message}`);
97+
} else {
98+
expect(error.code).toEqual(neo4j.error.SERVICE_UNAVAILABLE);
99+
done();
100+
}
101+
});
102+
});
103+
81104
it('should handle wrong scheme', () => {
82105
expect(() => neo4j.driver("tank://localhost", sharedNeo4j.authToken))
83106
.toThrow(new Error('Unknown scheme: tank'));

0 commit comments

Comments
 (0)