Name: js-handler/node_modules/restify/lib/router.js 
1:
// Copyright 2012 Mark Cavage, Inc.  All rights reserved.
2:
 
3:
var EventEmitter = require('events').EventEmitter;
4:
var url = require('url');
5:
var util = require('util');
6:
 
7:
var assert = require('assert-plus');
8:
var deepEqual = require('deep-equal');
9:
var LRU = require('lru-cache');
10:
var Negotiator = require('negotiator');
11:
var semver = require('semver');
12:
 
13:
var cors = require('./plugins/cors');
14:
var errors = require('./errors');
15:
var utils = require('./utils');
16:
 
17:
 
18:
 
19:
///--- Globals
20:
 
21:
var DEF_CT = 'application/octet-stream';
22:
 
23:
var maxSatisfying = semver.maxSatisfying;
24:
 
25:
var BadRequestError = errors.BadRequestError;
26:
var InternalError = errors.InternalError;
27:
var InvalidArgumentError = errors.InvalidArgumentError;
28:
var InvalidVersionError = errors.InvalidVersionError;
29:
var MethodNotAllowedError = errors.MethodNotAllowedError;
30:
var ResourceNotFoundError = errors.ResourceNotFoundError;
31:
var UnsupportedMediaTypeError = errors.UnsupportedMediaTypeError;
32:
 
33:
var shallowCopy = utils.shallowCopy;
34:
 
35:
 
36:
 
37:
///--- Helpers
38:
 
39:
function createCachedRoute(o, path, version, route) {
40:
        if (!o.hasOwnProperty(path))
41:
                o[path] = {};
42:
 
43:
        if (!o[path].hasOwnProperty(version))
44:
                o[path][version] = route;
45:
}
46:
 
47:
 
48:
function matchURL(re, req) {
49:
        var i = 0;
50:
        var result = re.exec(req.path());
51:
        var params = {};
52:
 
53:
        if (!result)
54:
                return (false);
55:
 
56:
        // This means the user original specified a regexp match, not a url
57:
        // string like /:foo/:bar
58:
        if (!re.restifyParams) {
59:
                for (i = 1; i < result.length; i++)
60:
                        params[(i - 1)] = result[i];
61:
 
62:
                return (params);
63:
        }
64:
 
65:
        // This was a static string, like /foo
66:
        if (re.restifyParams.length === 0)
67:
                return (params);
68:
 
69:
        // This was the "normal" case, of /foo/:id
70:
        re.restifyParams.forEach(function (p) {
71:
                if (++i < result.length)
72:
                        params[p] = decodeURIComponent(result[i]);
73:
        });
74:
 
75:
        return (params);
76:
}
77:
 
78:
 
79:
function compileURL(options) {
80:
        if (options.url instanceof RegExp)
81:
                return (options.url);
82:
        assert.string(options.url, 'url');
83:
 
84:
        var params = [];
85:
        var pattern = '^';
86:
        var re;
87:
        var _url = url.parse(options.url).pathname;
88:
        _url.split('/').forEach(function (frag) {
89:
                if (frag.length <= 0)
90:
                        return (false);
91:
 
92:
                pattern += '\\/+';
93:
                if (frag.charAt(0) === ':') {
94:
                        if (options.urlParamPattern) {
95:
                                pattern += '(' + options.urlParamPattern + ')';
96:
                        } else {
97:
                                // Strictly adhere to RFC3986
98:
                                pattern += '([a-zA-Z0-9-_~\\.%@]+)';
99:
                        }
100:
                        params.push(frag.slice(1));
101:
                } else {
102:
                        pattern += frag;
103:
                }
104:
 
105:
                return (true);
106:
        });
107:
 
108:
        if (pattern === '^')
109:
                pattern += '\\/';
110:
        pattern += '$';
111:
 
112:
        re = new RegExp(pattern, options.flags);
113:
        re.restifyParams = params;
114:
 
115:
        return (re);
116:
}
117:
 
118:
 
119:
 
120:
///--- API
121:
 
122:
function Router(options) {
123:
        assert.object(options, 'options');
124:
        assert.object(options.log, 'options.log');
125:
 
126:
        EventEmitter.call(this);
127:
 
128:
        this.cache = LRU({max: 100});
129:
        this.contentType = options.contentType || [];
130:
        if (!Array.isArray(this.contentType))
131:
                this.contentType = [this.contentType];
132:
        assert.arrayOfString(this.contentType, 'options.contentType');
133:
 
134:
        this.log = options.log;
135:
        this.mounts = {};
136:
        this.name = 'RestifyRouter';
137:
 
138:
        // A list of methods to routes
139:
        this.routes = {
140:
                DELETE: [],
141:
                GET: [],
142:
                HEAD: [],
143:
                OPTIONS: [],
144:
                PATCH: [],
145:
                POST: [],
146:
                PUT: []
147:
        };
148:
 
149:
        // So we can retrun 405 vs 404, we maintain a reverse mapping of URLs
150:
        // to method
151:
        this.reverse = {};
152:
 
153:
        this.versions = options.versions || options.version || [];
154:
        if (!Array.isArray(this.versions))
155:
                this.versions = [this.versions];
156:
        assert.arrayOfString(this.versions, 'options.versions');
157:
 
158:
        this.versions.forEach(function (v) {
159:
                if (semver.valid(v))
160:
                        return (true);
161:
 
162:
                throw new InvalidArgumentError('%s is not a valid semver', v);
163:
        });
164:
        this.versions.sort();
165:
 
166:
}
167:
util.inherits(Router, EventEmitter);
168:
module.exports = Router;
169:
 
170:
 
171:
Router.prototype.mount = function mount(options) {
172:
        assert.object(options, 'options');
173:
        assert.string(options.method, 'options.method');
174:
        assert.string(options.name, 'options.name');
175:
 
176:
        var exists;
177:
        var name = options.name;
178:
        var route;
179:
        var routes = this.routes[options.method];
180:
        var self = this;
181:
        var type = options.contentType || self.contentType;
182:
        var versions = options.versions || options.version || self.versions;
183:
 
184:
        if (type) {
185:
                if (!Array.isArray(type))
186:
                        type = [type];
187:
                type.filter(function (t) {
188:
                        return (t);
189:
                }).sort().join();
190:
        }
191:
 
192:
        if (versions) {
193:
                if (!Array.isArray(versions))
194:
                        versions = [versions];
195:
                versions.sort();
196:
        }
197:
 
198:
        exists = routes.some(function (r) {
199:
                return (r.name === name);
200:
        });
201:
        if (exists)
202:
                return (false);
203:
 
204:
        route = {
205:
                name: name,
206:
                method: options.method,
207:
                path: compileURL({
208:
                        url: options.path || options.url,
209:
                        flags: options.flags,
210:
                        urlParamPattern: options.urlParamPattern
211:
                }),
212:
                spec: options,
213:
                types: type,
214:
                versions: versions
215:
        };
216:
        routes.push(route);
217:
 
218:
        if (!this.reverse[route.path.source])
219:
                this.reverse[route.path.source] = [];
220:
 
221:
        if (this.reverse[route.path.source].indexOf(route.method) === -1)
222:
                this.reverse[route.path.source].push(route.method);
223:
 
224:
        this.mounts[route.name] = route;
225:
 
226:
        this.emit('mount',
227:
                  route.method,
228:
                  route.path,
229:
                  route.types,
230:
                  route.versions);
231:
 
232:
        return (route.name);
233:
};
234:
 
235:
 
236:
Router.prototype.unmount = function unmount(name) {
237:
        var route = this.mounts[name];
238:
        if (!route) {
239:
                this.log.warn('router.unmount(%s): route does not exist', name);
240:
                return (false);
241:
        }
242:
 
243:
        var reverse = this.reverse[route.path.source];
244:
        var routes = this.routes[route.method];
245:
        this.routes[route.method] = routes.filter(function (r) {
246:
                return (r.name !== route.name);
247:
        });
248:
 
249:
        this.reverse[route.path.source] = reverse.filter(function (r) {
250:
                return (r !== route.method);
251:
        });
252:
 
253:
        if (this.reverse[route.path.source].length === 0)
254:
                delete this.reverse[route.path.source];
255:
 
256:
        delete this.mounts[name];
257:
 
258:
        return (name);
259:
};
260:
 
261:
 
262:
Router.prototype.get = function get(name, req, cb) {
263:
        var params;
264:
        var route = false;
265:
        var routes = this.routes[req.method] || [];
266:
 
267:
        for (var i = 0; i < routes.length; i++) {
268:
                if (routes[i].name === name) {
269:
                        route = routes[i];
270:
                        try {
271:
                                params = matchURL(route.path, req);
272:
                        } catch (e) {}
273:
                        break;
274:
                }
275:
        }
276:
 
277:
        if (route) {
278:
                cb(null, route, params || {});
279:
        } else {
280:
                cb(new InternalError());
281:
        }
282:
};
283:
 
284:
 
285:
Router.prototype.find = function find(req, res, callback) {
286:
        var candidates = [];
287:
        var ct = req.headers['content-type'] || DEF_CT;
288:
        var cacheKey = req.method + req.url + req.version() + ct;
289:
        var cacheVal;
290:
        var neg;
291:
        var params;
292:
        var r;
293:
        var reverse;
294:
        var routes = this.routes[req.method] || [];
295:
        var typed;
296:
        var versioned;
297:
 
298:
        if ((cacheVal = this.cache.get(cacheKey))) {
299:
                res.methods = cacheVal.methods.slice();
300:
                callback(null, cacheVal, shallowCopy(cacheVal.params));
301:
                return;
302:
        }
303:
 
304:
        for (var i = 0; i < routes.length; i++) {
305:
                try {
306:
                        params = matchURL(routes[i].path, req);
307:
                } catch (e) {
308:
                        this.log.trace({err: e}, 'error parsing URL');
309:
                        callback(new BadRequestError(e.message));
310:
                        return;
311:
                }
312:
 
313:
                if (params === false)
314:
                        continue;
315:
 
316:
                reverse = this.reverse[routes[i].path.source];
317:
 
318:
                if (routes[i].types.length && req.isUpload()) {
319:
                        candidates.push({
320:
                                p: params,
321:
                                r: routes[i]
322:
                        });
323:
                        typed = true;
324:
                        continue;
325:
                }
326:
 
327:
                // GH-283: we want to find the latest version for a given route,
328:
                // not the first one.  However, if neither the client nor
329:
                // server specified any version, we're done, because neither
330:
                // cared
331:
                if (routes[i].versions.length === 0 && req.version() === '*') {
332:
                        r = routes[i];
333:
                        break;
334:
                }
335:
 
336:
                if (routes[i].versions.length > 0) {
337:
                        candidates.push({
338:
                                p: params,
339:
                                r: routes[i]
340:
                        });
341:
                        versioned = true;
342:
                }
343:
        }
344:
 
345:
        if (!r) {
346:
                // If upload and typed
347:
                if (typed) {
348:
                        /* JSSTYLED */
349:
                        var _t = ct.split(/\s*,\s*/);
350:
                        candidates = candidates.filter(function (c) {
351:
                                neg = new Negotiator({
352:
                                        headers: {
353:
                                                accept: c.r.types.join(', ')
354:
                                        }
355:
                                });
356:
                                var tmp = neg.preferredMediaType(_t);
357:
                                return (tmp && tmp.length);
358:
                        });
359:
 
360:
                        // Pick the first one in case not versioned
361:
                        if (candidates.length) {
362:
                                r = candidates[0].r;
363:
                                params = candidates[0].p;
364:
                        }
365:
                }
366:
 
367:
                if (versioned) {
368:
                        candidates.forEach(function (c) {
369:
                                var k = c.r.versions;
370:
                                var v = semver.maxSatisfying(k, req.version());
371:
                                if (v && (!r || semver.gt(v, r.versions))) {
372:
                                        r = c.r;
373:
                                        params = c.p;
374:
                                }
375:
                        });
376:
                }
377:
        }
378:
 
379:
        // In order, we check if the route exists, in which case, we're good.
380:
        // Otherwise we look to see if ver was set to false; that would tell us
381:
        // we indeed did find a matching route (method+url), but the version
382:
        // field didn't line up, so we return bad version.  If no route and no
383:
        // version, we now need to go walk the reverse map and look at whether
384:
        // we should return 405 or 404.  If it was an OPTIONS request, we need
385:
        // to handle this having been a preflight request.
386:
        if (params && r) {
387:
                cacheVal = {
388:
                        methods: reverse,
389:
                        name: r.name,
390:
                        params: params,
391:
                        spec: r.spec
392:
                };
393:
                this.cache.set(cacheKey, cacheVal);
394:
                res.methods = reverse.slice();
395:
                callback(null, cacheVal, shallowCopy(params));
396:
                return;
397:
        }
398:
 
399:
        if (typed) {
400:
                callback(new UnsupportedMediaTypeError(ct));
401:
                return;
402:
        }
403:
        if (versioned) {
404:
                callback(new InvalidVersionError('%s is not supported by %s %s',
405:
                                                 req.version() || '?',
406:
                                                 req.method,
407:
                                                 req.path()));
408:
                return;
409:
        }
410:
 
411:
        // This is a very generic preflight handler - it does
412:
        // not handle requiring authentication, nor does it do
413:
        // any special checking for extra user headers. The
414:
        // user will need to defined their own .opts handler to
415:
        // do that
416:
        function preflight(methods) {
417:
                var headers = req.headers['access-control-request-headers'];
418:
                var method = req.headers['access-control-request-method'];
419:
                var origin = req.headers['origin'];
420:
 
421:
                if (req.method !== 'OPTIONS' ||
422:
                    !origin ||
423:
                    !method ||
424:
                    methods.indexOf(method) === -1) {
425:
                        return (false);
426:
                }
427:
                // Last, check request-headers
428:
                var ok = true;
429:
                /* JSSTYLED */
430:
                (headers || '').split(/\s*,\s*/).forEach(function (h) {
431:
                        if (!h)
432:
                                return;
433:
 
434:
                        h = h.toLowerCase();
435:
                        ok = cors.ALLOW_HEADERS.indexOf(h) !== -1 && ok;
436:
                });
437:
                if (!ok)
438:
                        return (false);
439:
 
440:
                res.setHeader('Access-Control-Allow-Origin', '*');
441:
                res.setHeader('Access-Control-Allow-Methods',
442:
                              methods.join(', '));
443:
                res.setHeader('Access-Control-Allow-Headers',
444:
                              cors.ALLOW_HEADERS.join(', '));
445:
                res.setHeader('Access-Control-Max-Age', 3600);
446:
 
447:
                return (true);
448:
        }
449:
 
450:
        // Check for 405 instead of 404
451:
        var urls = Object.keys(this.reverse);
452:
        for (i = 0; i < urls.length; i++) {
453:
                if (matchURL(new RegExp(urls[i]), req)) {
454:
                        res.methods = this.reverse[urls[i]].slice();
455:
                        res.setHeader('Allow', res.methods.join(', '));
456:
                        if (preflight(res.methods)) {
457:
                                callback(null, { name: 'preflight' });
458:
                                return;
459:
                        }
460:
                        var err = new MethodNotAllowedError('%s is not allowed',
461:
                                                            req.method);
462:
                        callback(err);
463:
                        return;
464:
                }
465:
        }
466:
 
467:
        callback(new ResourceNotFoundError('%s does not exist', req.url));
468:
};
469:
 
470:
 
471:
Router.prototype.toString = function toString() {
472:
        var self = this;
473:
        var str = this.name + ':\n';
474:
 
475:
        Object.keys(this.routes).forEach(function (k) {
476:
                var routes = self.routes[k].map(function (r) {
477:
                        return (r.name);
478:
                });
479:
 
480:
                str += '\t\t' + k + ': [' + routes.join(', ') + ']\n';
481:
        });
482:
 
483:
        return (str);
484:
};