UNPKG

10.8 kBJavaScriptView Raw
1'use strict';
2
3// Load modules
4
5const Hoek = require('hoek');
6const Boom = require('boom');
7
8const Regex = require('./regex');
9const Segment = require('./segment');
10
11
12// Declare internals
13
14const internals = {
15 pathRegex: Regex.generate(),
16 defaults: {
17 isCaseSensitive: true
18 }
19};
20
21
22exports.Router = internals.Router = function (options) {
23
24 this.settings = Hoek.applyToDefaults(internals.defaults, options || {});
25
26 this.routes = {}; // Key: HTTP method or * for catch-all, value: sorted array of routes
27 this.ids = {}; // Key: route id, value: record
28 this.vhosts = null; // {} where Key: hostname, value: see this.routes
29
30 this.specials = {
31 badRequest: null,
32 notFound: null,
33 options: null
34 };
35};
36
37
38internals.Router.prototype.add = function (config, route) {
39
40 const method = config.method.toLowerCase();
41
42 const vhost = config.vhost || '*';
43 if (vhost !== '*') {
44 this.vhosts = this.vhosts || {};
45 this.vhosts[vhost] = this.vhosts[vhost] || {};
46 }
47
48 const table = (vhost === '*' ? this.routes : this.vhosts[vhost]);
49 table[method] = table[method] || { routes: [], router: new Segment() };
50
51 const analysis = config.analysis || this.analyze(config.path);
52 const record = {
53 path: config.path,
54 route: route || config.path,
55 segments: analysis.segments,
56 params: analysis.params,
57 fingerprint: analysis.fingerprint,
58 settings: this.settings
59 };
60
61 // Add route
62
63 table[method].router.add(analysis.segments, record);
64 table[method].routes.push(record);
65 table[method].routes.sort(internals.sort);
66
67 const last = record.segments[record.segments.length - 1];
68 if (last.empty) {
69 table[method].router.add(analysis.segments.slice(0, -1), record);
70 }
71
72 if (config.id) {
73 Hoek.assert(!this.ids[config.id], 'Route id', config.id, 'for path', config.path, 'conflicts with existing path', this.ids[config.id] && this.ids[config.id].path);
74 this.ids[config.id] = record;
75 }
76
77 return record;
78};
79
80
81internals.Router.prototype.special = function (type, route) {
82
83 Hoek.assert(Object.keys(this.specials).indexOf(type) !== -1, 'Unknown special route type:', type);
84
85 this.specials[type] = { route };
86};
87
88
89internals.Router.prototype.route = function (method, path, hostname) {
90
91 const segments = path.split('/').slice(1);
92
93 const vhost = (this.vhosts && hostname && this.vhosts[hostname]);
94 const route = (vhost && this._lookup(path, segments, vhost, method)) ||
95 this._lookup(path, segments, this.routes, method) ||
96 (method === 'head' && vhost && this._lookup(path, segments, vhost, 'get')) ||
97 (method === 'head' && this._lookup(path, segments, this.routes, 'get')) ||
98 (method === 'options' && this.specials.options) ||
99 (vhost && this._lookup(path, segments, vhost, '*')) ||
100 this._lookup(path, segments, this.routes, '*') ||
101 this.specials.notFound || Boom.notFound();
102
103 return route;
104};
105
106
107internals.Router.prototype._lookup = function (path, segments, table, method) {
108
109 const set = table[method];
110 if (!set) {
111 return null;
112 }
113
114 const match = set.router.lookup(path, segments, this.settings);
115 if (!match) {
116 return null;
117 }
118
119 const assignments = {};
120 const array = [];
121 for (let i = 0; i < match.array.length; ++i) {
122 const name = match.record.params[i];
123 const value = internals.decode(match.array[i]);
124 if (value.isBoom) {
125 return this.specials.badRequest || value;
126 }
127
128 if (assignments[name] !== undefined) {
129 assignments[name] = assignments[name] + '/' + value;
130 }
131 else {
132 assignments[name] = value;
133 }
134
135 if (i + 1 === match.array.length || // Only include the last segment of a multi-segment param
136 name !== match.record.params[i + 1]) {
137
138 array.push(assignments[name]);
139 }
140 }
141
142 return { params: assignments, paramsArray: array, route: match.record.route };
143};
144
145
146internals.decode = function (value) {
147
148 try {
149 return decodeURIComponent(value);
150 }
151 catch (err) {
152 return Boom.badRequest('Invalid request path');
153 }
154};
155
156
157internals.Router.prototype.normalize = function (path) {
158
159 if (path &&
160 path.indexOf('%') !== -1) {
161
162 // Uppercase %encoded values
163
164 const uppercase = path.replace(/%[0-9a-fA-F][0-9a-fA-F]/g, (encoded) => encoded.toUpperCase());
165
166 // Decode non-reserved path characters: a-z A-Z 0-9 _!$&'()*+,;=:@-.~
167 // ! (%21) $ (%24) & (%26) ' (%27) ( (%28) ) (%29) * (%2A) + (%2B) , (%2C) - (%2D) . (%2E)
168 // 0-9 (%30-39) : (%3A) ; (%3B) = (%3D)
169 // @ (%40) A-Z (%41-5A) _ (%5F) a-z (%61-7A) ~ (%7E)
170
171 const decoded = uppercase.replace(/%(?:2[146-9A-E]|3[\dABD]|4[\dA-F]|5[\dAF]|6[1-9A-F]|7[\dAE])/g, (encoded) => String.fromCharCode(parseInt(encoded.substring(1), 16)));
172
173 path = decoded;
174 }
175
176 // Normalize path segments
177
178 if (path &&
179 (path.indexOf('/.') !== -1 || path[0] === '.')) {
180
181 const hasLeadingDash = path[0] === '/';
182 const segments = path.split('/');
183 const normalized = [];
184 let segment;
185
186 for (let i = 0; i < segments.length; ++i) {
187 segment = segments[i];
188 if (segment === '..') {
189 normalized.pop();
190 }
191 else if (segment !== '.') {
192 normalized.push(segment);
193 }
194 }
195
196 if (segment === '.' ||
197 segment === '..') { // Add trailing slash when needed
198
199 normalized.push('');
200 }
201
202 path = normalized.join('/');
203
204 if (path[0] !== '/' &&
205 hasLeadingDash) {
206
207 path = '/' + path;
208 }
209 }
210
211 return path;
212};
213
214
215internals.Router.prototype.analyze = function (path) {
216
217 Hoek.assert(internals.pathRegex.validatePath.test(path), 'Invalid path:', path);
218 Hoek.assert(!internals.pathRegex.validatePathEncoded.test(path), 'Path cannot contain encoded non-reserved path characters:', path);
219
220 const pathParts = path.split('/');
221 const segments = [];
222 const params = [];
223 const fingers = [];
224
225 for (let i = 1; i < pathParts.length; ++i) { // Skip first empty segment
226 let segment = pathParts[i];
227
228 // Literal
229
230 if (segment.indexOf('{') === -1) {
231 segment = this.settings.isCaseSensitive ? segment : segment.toLowerCase();
232 fingers.push(segment);
233 segments.push({ literal: segment });
234 continue;
235 }
236
237 // Parameter
238
239 const parts = internals.parseParams(segment);
240 if (parts.length === 1) {
241
242 // Simple parameter
243
244 const item = parts[0];
245 Hoek.assert(params.indexOf(item.name) === -1, 'Cannot repeat the same parameter name:', item.name, 'in:', path);
246 params.push(item.name);
247
248 if (item.wilcard) {
249 if (item.count) {
250 for (let j = 0; j < item.count; ++j) {
251 fingers.push('?');
252 segments.push({});
253 if (j) {
254 params.push(item.name);
255 }
256 }
257 }
258 else {
259 fingers.push('#');
260 segments.push({ wildcard: true });
261 }
262 }
263 else {
264 fingers.push('?');
265 segments.push({ empty: item.empty });
266 }
267 }
268 else {
269
270 // Mixed parameter
271
272 const seg = {
273 length: parts.length,
274 first: typeof parts[0] !== 'string',
275 segments: []
276 };
277
278 let finger = '';
279 let regex = '^';
280 for (let j = 0; j < parts.length; ++j) {
281 const part = parts[j];
282 if (typeof part === 'string') {
283 finger = finger + part;
284 regex = regex + Hoek.escapeRegex(part);
285 seg.segments.push(part);
286 }
287 else {
288 Hoek.assert(params.indexOf(part.name) === -1, 'Cannot repeat the same parameter name:', part.name, 'in:', path);
289 params.push(part.name);
290
291 finger = finger + '?';
292 regex = regex + '(.' + (part.empty ? '*' : '+') + ')';
293 }
294 }
295
296 seg.mixed = new RegExp(regex + '$', (!this.settings.isCaseSensitive ? 'i' : ''));
297 fingers.push(finger);
298 segments.push(seg);
299 }
300 }
301
302 return {
303 segments,
304 fingerprint: '/' + fingers.join('/'),
305 params
306 };
307};
308
309
310internals.parseParams = function (segment) {
311
312 const parts = [];
313 segment.replace(internals.pathRegex.parseParam, (match, literal, name, wilcard, count, empty) => {
314
315 if (literal) {
316 parts.push(literal);
317 }
318 else {
319 parts.push({
320 name,
321 wilcard: !!wilcard,
322 count: count && parseInt(count, 10),
323 empty: !!empty
324 });
325 }
326
327 return '';
328 });
329
330 return parts;
331};
332
333
334internals.Router.prototype.table = function (host) {
335
336 const result = [];
337 const collect = (table) => {
338
339 if (!table) {
340 return;
341 }
342
343 Object.keys(table).forEach((method) => {
344
345 table[method].routes.forEach((record) => {
346
347 result.push(record.route);
348 });
349 });
350 };
351
352 if (this.vhosts) {
353 const vhosts = host ? [].concat(host) : Object.keys(this.vhosts);
354 for (let i = 0; i < vhosts.length; ++i) {
355 collect(this.vhosts[vhosts[i]]);
356 }
357 }
358
359 collect(this.routes);
360
361 return result;
362};
363
364
365internals.sort = function (a, b) {
366
367 const aFirst = -1;
368 const bFirst = 1;
369
370 const as = a.segments;
371 const bs = b.segments;
372
373 if (as.length !== bs.length) {
374 return (as.length > bs.length ? bFirst : aFirst);
375 }
376
377 for (let i = 0; ; ++i) {
378 if (as[i].literal) {
379 if (bs[i].literal) {
380 if (as[i].literal === bs[i].literal) {
381 continue;
382 }
383
384 return (as[i].literal > bs[i].literal ? bFirst : aFirst);
385 }
386
387 return aFirst;
388 }
389
390 if (bs[i].literal) {
391 return bFirst;
392 }
393
394 return (as[i].wildcard ? bFirst : aFirst);
395 }
396};