Skip to content

Commit 4f8c75b

Browse files
committed
Improve routing context parsing
Move all URL parsing functions out of `connector` to `util` because they are not really related to network connection and Bolt messages. Prohibit invalid keys and values in routing context. Add tests for URL parsing.
1 parent 46ad5e6 commit 4f8c75b

File tree

6 files changed

+202
-90
lines changed

6 files changed

+202
-90
lines changed

src/v1/index.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@ import Record from './record';
2626
import {Driver, READ, WRITE} from './driver';
2727
import RoutingDriver from './routing-driver';
2828
import VERSION from '../version';
29-
import {parseRoutingContext, parseScheme, parseUrl} from './internal/connector';
30-
import {assertString, isEmptyObjectOrNull} from './internal/util';
29+
import {assertString, isEmptyObjectOrNull, parseRoutingContext, parseScheme, parseUrl} from './internal/util';
3130

3231
const auth = {
3332
basic: (username, password, realm = undefined) => {

src/v1/internal/connector.js

Lines changed: 5 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
* See the License for the specific language governing permissions and
1717
* limitations under the License.
1818
*/
19+
1920
import WebSocketChannel from './ch-websocket';
2021
import NodeChannel from './ch-node';
2122
import {Chunker, Dechunker} from './chunking';
@@ -24,6 +25,7 @@ import {alloc} from './buf';
2425
import {Node, Path, PathSegment, Relationship, UnboundRelationship} from '../graph-types';
2526
import {newError} from './../error';
2627
import ChannelConfig from './ch-config';
28+
import {parseHost, parsePort} from './util';
2729

2830
let Channel;
2931
if( NodeChannel.available ) {
@@ -59,46 +61,6 @@ PATH = 0x50,
5961
MAGIC_PREAMBLE = 0x6060B017,
6062
DEBUG = false;
6163

62-
let URLREGEX = new RegExp([
63-
"([^/]+//)?", // scheme
64-
"(([^:/?#]*)", // hostname
65-
"(?::([0-9]+))?)", // port (optional)
66-
"([^?]*)?", // everything else
67-
"(\\?(.+))?" // query
68-
].join(""));
69-
70-
function parseScheme( url ) {
71-
let scheme = url.match(URLREGEX)[1] || '';
72-
return scheme.toLowerCase();
73-
}
74-
75-
function parseUrl(url) {
76-
return url.match( URLREGEX )[2];
77-
}
78-
79-
function parseHost( url ) {
80-
return url.match( URLREGEX )[3];
81-
}
82-
83-
function parsePort( url ) {
84-
return url.match( URLREGEX )[4];
85-
}
86-
87-
function parseRoutingContext(url) {
88-
const query = url.match(URLREGEX)[7] || '';
89-
const map = {};
90-
if (query.length !== 0) {
91-
query.split("&").forEach(val => {
92-
const keyValue = val.split("=");
93-
if (keyValue.length !== 2) {
94-
throw new Error("Invalid parameters: '" + keyValue + "' in url '" + url + "'.");
95-
}
96-
map[keyValue[0]] = keyValue[1];
97-
});
98-
}
99-
return map;
100-
}
101-
10264
/**
10365
* Very rudimentary log handling, should probably be replaced by something proper at some point.
10466
* @param actor the part that sent the message, 'S' for server and 'C' for client
@@ -507,11 +469,6 @@ function connect(url, config = {}, connectionErrorCode = null) {
507469
}
508470

509471
export {
510-
connect,
511-
parseScheme,
512-
parseUrl,
513-
parseHost,
514-
parsePort,
515-
parseRoutingContext,
516-
Connection
517-
}
472+
connect,
473+
Connection
474+
};

src/v1/internal/host-name-resolvers.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
* limitations under the License.
1818
*/
1919

20-
import {parseHost, parsePort} from './connector';
20+
import {parseHost, parsePort} from './util';
2121

2222
class HostNameResolver {
2323

src/v1/internal/util.js

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,16 @@
2020
const ENCRYPTION_ON = "ENCRYPTION_ON";
2121
const ENCRYPTION_OFF = "ENCRYPTION_OFF";
2222

23+
const URL_REGEX = new RegExp([
24+
'([^/]+//)?', // scheme
25+
'(([^:/?#]*)', // hostname
26+
'(?::([0-9]+))?)', // port (optional)
27+
'([^?]*)?', // everything else
28+
'(\\?(.+))?' // query
29+
].join(''));
30+
2331
function isEmptyObjectOrNull(obj) {
24-
if (isNull(obj)) {
32+
if (obj === null) {
2533
return true;
2634
}
2735

@@ -38,10 +46,6 @@ function isEmptyObjectOrNull(obj) {
3846
return true;
3947
}
4048

41-
function isNull(obj) {
42-
return obj === null;
43-
}
44-
4549
function isObject(obj) {
4650
const type = typeof obj;
4751
return type === 'function' || type === 'object' && Boolean(obj);
@@ -58,9 +62,66 @@ function isString(str) {
5862
return Object.prototype.toString.call(str) === '[object String]';
5963
}
6064

65+
function parseScheme(url) {
66+
assertString(url, 'URL');
67+
const scheme = url.match(URL_REGEX)[1] || '';
68+
return scheme.toLowerCase();
69+
}
70+
71+
function parseUrl(url) {
72+
assertString(url, 'URL');
73+
return url.match(URL_REGEX)[2];
74+
}
75+
76+
function parseHost(url) {
77+
assertString(url, 'URL');
78+
return url.match(URL_REGEX)[3];
79+
}
80+
81+
function parsePort(url) {
82+
assertString(url, 'URL');
83+
return url.match(URL_REGEX)[4];
84+
}
85+
86+
function parseRoutingContext(url) {
87+
const query = url.match(URL_REGEX)[7] || '';
88+
const context = {};
89+
if (query) {
90+
query.split('&').forEach(pair => {
91+
const keyValue = pair.split('=');
92+
if (keyValue.length !== 2) {
93+
throw new Error('Invalid parameters: \'' + keyValue + '\' in URL \'' + url + '\'.');
94+
}
95+
96+
const key = trimAndVerify(keyValue[0], 'key', url);
97+
const value = trimAndVerify(keyValue[1], 'value', url);
98+
99+
if (context[key]) {
100+
throw new Error(`Duplicated query parameters with key '${key}' in URL '${url}'`);
101+
}
102+
103+
context[key] = value;
104+
});
105+
}
106+
return context;
107+
}
108+
109+
function trimAndVerify(string, name, url) {
110+
const result = string.trim();
111+
if (!result) {
112+
throw new Error(`Illegal empty ${name} in URL query '${url}'`);
113+
}
114+
return result;
115+
}
116+
61117
export {
62118
isEmptyObjectOrNull,
63119
assertString,
120+
parseScheme,
121+
parseUrl,
122+
parseHost,
123+
parsePort,
124+
parseRoutingContext,
64125
ENCRYPTION_ON,
65126
ENCRYPTION_OFF
66127
}

test/internal/host-name-resolvers.test.js

Lines changed: 1 addition & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -19,38 +19,7 @@
1919

2020
import {DnsHostNameResolver, DummyHostNameResolver} from '../../src/v1/internal/host-name-resolvers';
2121
import hasFeature from '../../src/v1/internal/features';
22-
import {parseHost, parsePort, parseScheme, parseRoutingContext} from '../../src/v1/internal/connector';
23-
24-
describe('RoutingContextParser', ()=>{
25-
26-
it('should parse routing context', done => {
27-
const url = "bolt://localhost:7687/cat?name=molly&age=1&color=white";
28-
const context = parseRoutingContext(url);
29-
expect(context).toEqual({name:"molly", age:"1", color:"white"});
30-
31-
done();
32-
});
33-
34-
it('should return empty routing context', done =>{
35-
const url1 = "bolt://localhost:7687/cat?";
36-
const context1 = parseRoutingContext(url1);
37-
expect(context1).toEqual({});
38-
39-
const url2 = "bolt://localhost:7687/lalala";
40-
const context2 = parseRoutingContext(url2);
41-
expect(context2).toEqual({});
42-
43-
done();
44-
});
45-
46-
it('should error for unmatched pair', done=>{
47-
const url = "bolt://localhost?cat";
48-
expect(()=>parseRoutingContext(url)).toThrow(
49-
new Error("Invalid parameters: 'cat' in url 'bolt://localhost?cat'."));
50-
51-
done();
52-
});
53-
});
22+
import {parseHost, parsePort, parseScheme} from '../../src/v1/internal/util';
5423

5524
describe('DummyHostNameResolver', () => {
5625

test/internal/util.test.js

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

20-
const util = require('../../lib/v1/internal/util.js');
20+
import * as util from '../../src/v1/internal/util';
2121

22-
describe('util', () => {
22+
fdescribe('util', () => {
2323

2424
it('should check empty objects', () => {
2525
expect(util.isEmptyObjectOrNull(null)).toBeTruthy();
@@ -55,6 +55,112 @@ describe('util', () => {
5555
verifyInvalidString(console.log);
5656
});
5757

58+
it('should parse scheme', () => {
59+
verifyScheme('bolt://', 'bolt://localhost');
60+
verifyScheme('bolt://', 'bolt://localhost:7687');
61+
verifyScheme('bolt://', 'bolt://neo4j.com');
62+
verifyScheme('bolt://', 'bolt://neo4j.com:80');
63+
64+
verifyScheme('bolt+routing://', 'bolt+routing://127.0.0.1');
65+
verifyScheme('bolt+routing://', 'bolt+routing://127.0.0.1:7687');
66+
verifyScheme('bolt+routing://', 'bolt+routing://neo4j.com');
67+
verifyScheme('bolt+routing://', 'bolt+routing://neo4j.com:80');
68+
69+
verifyScheme('wss://', 'wss://server.com');
70+
verifyScheme('wss://', 'wss://server.com:7687');
71+
verifyScheme('wss://', 'wss://1.1.1.1');
72+
verifyScheme('wss://', 'wss://8.8.8.8:80');
73+
74+
verifyScheme('', 'invalid url');
75+
verifyScheme('', 'localhost:7676');
76+
verifyScheme('', '127.0.0.1');
77+
});
78+
79+
it('should fail to parse scheme from non-string argument', () => {
80+
expect(() => util.parseScheme({})).toThrowError(TypeError);
81+
expect(() => util.parseScheme(['bolt://localhost:2020'])).toThrowError(TypeError);
82+
expect(() => util.parseScheme(() => 'bolt://localhost:8888')).toThrowError(TypeError);
83+
});
84+
85+
it('should parse url', () => {
86+
verifyUrl('localhost', 'bolt://localhost');
87+
verifyUrl('localhost:9090', 'bolt://localhost:9090');
88+
verifyUrl('127.0.0.1', 'bolt://127.0.0.1');
89+
verifyUrl('127.0.0.1:7687', 'bolt://127.0.0.1:7687');
90+
verifyUrl('10.198.20.1', 'bolt+routing://10.198.20.1');
91+
verifyUrl('15.8.8.9:20004', 'wss://15.8.8.9:20004');
92+
});
93+
94+
it('should fail to parse url from non-string argument', () => {
95+
expect(() => util.parseUrl({})).toThrowError(TypeError);
96+
expect(() => util.parseUrl(['bolt://localhost:2020'])).toThrowError(TypeError);
97+
expect(() => util.parseUrl(() => 'bolt://localhost:8888')).toThrowError(TypeError);
98+
});
99+
100+
it('should parse host', () => {
101+
verifyHost('localhost', 'bolt://localhost');
102+
verifyHost('neo4j.com', 'bolt+routing://neo4j.com');
103+
verifyHost('neo4j.com', 'bolt+routing://neo4j.com:8080');
104+
verifyHost('127.0.0.1', 'https://127.0.0.1');
105+
verifyHost('127.0.0.1', 'ws://127.0.0.1:2020');
106+
});
107+
108+
it('should fail to parse host from non-string argument', () => {
109+
expect(() => util.parseHost({})).toThrowError(TypeError);
110+
expect(() => util.parseHost(['bolt://localhost:2020'])).toThrowError(TypeError);
111+
expect(() => util.parseHost(() => 'bolt://localhost:8888')).toThrowError(TypeError);
112+
});
113+
114+
it('should parse port', () => {
115+
verifyPort('7474', 'http://localhost:7474');
116+
verifyPort('8080', 'http://127.0.0.1:8080');
117+
verifyPort('20005', 'bolt+routing://neo4j.com:20005');
118+
verifyPort('4242', 'bolt+routing://1.1.1.1:4242');
119+
verifyPort('42', 'http://10.192.168.5:42');
120+
121+
verifyPort(undefined, 'https://localhost');
122+
verifyPort(undefined, 'ws://8.8.8.8');
123+
});
124+
125+
it('should fail to parse port from non-string argument', () => {
126+
expect(() => util.parsePort({port: 1515})).toThrowError(TypeError);
127+
expect(() => util.parsePort(['bolt://localhost:2020'])).toThrowError(TypeError);
128+
expect(() => util.parsePort(() => 'bolt://localhost:8888')).toThrowError(TypeError);
129+
});
130+
131+
it('should parse routing context', () => {
132+
verifyRoutingContext({
133+
name: 'molly',
134+
age: '1',
135+
color: 'white'
136+
}, 'bolt+routing://localhost:7687/cat?name=molly&age=1&color=white');
137+
138+
verifyRoutingContext({
139+
key1: 'value1',
140+
key2: 'value2'
141+
}, 'bolt+routing://localhost:7687/?key1=value1&key2=value2');
142+
143+
verifyRoutingContext({key: 'value'}, 'bolt+routing://10.198.12.2:9999?key=value');
144+
145+
verifyRoutingContext({}, 'bolt+routing://localhost:7687?');
146+
verifyRoutingContext({}, 'bolt+routing://localhost:7687/?');
147+
verifyRoutingContext({}, 'bolt+routing://localhost:7687/cat?');
148+
verifyRoutingContext({}, 'bolt+routing://localhost:7687/lala');
149+
});
150+
151+
it('should fail to parse routing context from non-string argument', () => {
152+
expect(() => util.parseRoutingContext({key1: 'value1'})).toThrowError(TypeError);
153+
expect(() => util.parseRoutingContext(['bolt://localhost:2020/?key=value'])).toThrowError(TypeError);
154+
expect(() => util.parseRoutingContext(() => 'bolt://localhost?key1=value&key2=value2')).toThrowError(TypeError);
155+
});
156+
157+
it('should fail to parse routing context from illegal parameters', () => {
158+
expect(() => util.parseRoutingContext('bolt+routing://localhost:7687/?justKey')).toThrow();
159+
expect(() => util.parseRoutingContext('bolt+routing://localhost:7687/?=value1&key2=value2')).toThrow();
160+
expect(() => util.parseRoutingContext('bolt+routing://localhost:7687/key1?=value1&key2=')).toThrow();
161+
expect(() => util.parseRoutingContext('bolt+routing://localhost:7687/?key1=value1&key2=value2&key1=value2')).toThrow();
162+
});
163+
58164
function verifyValidString(str) {
59165
expect(util.assertString(str, 'Test string')).toBe(str);
60166
}
@@ -63,4 +169,24 @@ describe('util', () => {
63169
expect(() => util.assertString(str, 'Test string')).toThrowError(TypeError);
64170
}
65171

172+
function verifyScheme(expectedScheme, url) {
173+
expect(util.parseScheme(url)).toEqual(expectedScheme);
174+
}
175+
176+
function verifyUrl(expectedUrl, url) {
177+
expect(util.parseUrl(url)).toEqual(expectedUrl);
178+
}
179+
180+
function verifyHost(expectedHost, url) {
181+
expect(util.parseHost(url)).toEqual(expectedHost);
182+
}
183+
184+
function verifyPort(expectedPort, url) {
185+
expect(util.parsePort(url)).toEqual(expectedPort);
186+
}
187+
188+
function verifyRoutingContext(expectedRoutingContext, url) {
189+
expect(util.parseRoutingContext(url)).toEqual(expectedRoutingContext);
190+
}
191+
66192
});

0 commit comments

Comments
 (0)