1 | 'use strict';
|
2 |
|
3 |
|
4 |
|
5 | const Hoek = require('hoek');
|
6 | const Boom = require('boom');
|
7 |
|
8 | const Regex = require('./regex');
|
9 | const Segment = require('./segment');
|
10 |
|
11 |
|
12 |
|
13 |
|
14 | const internals = {
|
15 | pathRegex: Regex.generate(),
|
16 | defaults: {
|
17 | isCaseSensitive: true
|
18 | }
|
19 | };
|
20 |
|
21 |
|
22 | exports.Router = internals.Router = function (options) {
|
23 |
|
24 | this.settings = Hoek.applyToDefaults(internals.defaults, options || {});
|
25 |
|
26 | this.routes = {};
|
27 | this.ids = {};
|
28 | this.vhosts = null;
|
29 |
|
30 | this.specials = {
|
31 | badRequest: null,
|
32 | notFound: null,
|
33 | options: null
|
34 | };
|
35 | };
|
36 |
|
37 |
|
38 | internals.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 |
|
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 |
|
81 | internals.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 |
|
89 | internals.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 |
|
107 | internals.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 ||
|
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 |
|
146 | internals.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 |
|
157 | internals.Router.prototype.normalize = function (path) {
|
158 |
|
159 | if (path &&
|
160 | path.indexOf('%') !== -1) {
|
161 |
|
162 |
|
163 |
|
164 | const uppercase = path.replace(/%[0-9a-fA-F][0-9a-fA-F]/g, (encoded) => encoded.toUpperCase());
|
165 |
|
166 |
|
167 |
|
168 |
|
169 |
|
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 |
|
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 === '..') {
|
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 |
|
215 | internals.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) {
|
226 | let segment = pathParts[i];
|
227 |
|
228 |
|
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 |
|
238 |
|
239 | const parts = internals.parseParams(segment);
|
240 | if (parts.length === 1) {
|
241 |
|
242 |
|
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 |
|
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 |
|
310 | internals.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 |
|
334 | internals.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 |
|
365 | internals.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 | };
|