'use strict'; var EventEmitter = require('events').EventEmitter; var util = require('util'); var http = require('http'); var _ = require('lodash'); var assert = require('assert-plus'); var errors = require('restify-errors'); var uuid = require('uuid'); var Chain = require('./chain'); var RouterRegistryRadix = require('./routerRegistryRadix'); ///--- Globals var MethodNotAllowedError = errors.MethodNotAllowedError; var ResourceNotFoundError = errors.ResourceNotFoundError; ///--- API /** * Router class handles mapping of http verbs and a regexp path, * to an array of handler functions. * * @class * @public * @param {Object} options - an options object * @param {Bunyan} options.log - Bunyan logger instance * @param {Boolean} [options.onceNext=false] - Prevents calling next multiple * times * @param {Boolean} [options.strictNext=false] - Throws error when next() is * called more than once, enabled onceNext option * @param {Object} [options.registry] - route registry * @param {Boolean} [options.ignoreTrailingSlash=false] - ignore trailing slash * on paths */ function Router(options) { assert.object(options, 'options'); assert.object(options.log, 'options.log'); assert.optionalBool(options.onceNext, 'options.onceNext'); assert.optionalBool(options.strictNext, 'options.strictNext'); assert.optionalBool( options.ignoreTrailingSlash, 'options.ignoreTrailingSlash' ); EventEmitter.call(this); this.log = options.log; this.onceNext = !!options.onceNext; this.strictNext = !!options.strictNext; this.name = 'RestifyRouter'; // Internals this._anonymousHandlerCounter = 0; this._registry = options.registry || new RouterRegistryRadix(options); } util.inherits(Router, EventEmitter); /** * Lookup for route * * @public * @memberof Router * @instance * @function lookup * @param {Request} req - request * @param {Response} res - response * @returns {Chain|undefined} handler or undefined */ Router.prototype.lookup = function lookup(req, res) { var pathname = req.getUrl().pathname; // Find route var registryRoute = this._registry.lookup(req.method, pathname); // Not found if (!registryRoute) { return undefined; } // Decorate req req.params = Object.assign(req.params, registryRoute.params); req.route = registryRoute.route; // Call handler chain return registryRoute.handler; }; /** * Lookup by name * * @public * @memberof Router * @instance * @function lookupByName * @param {String} name - route name * @param {Request} req - request * @param {Response} res - response * @returns {Chain|undefined} handler or undefined */ Router.prototype.lookupByName = function lookupByName(name, req, res) { var self = this; var route = self._registry.get()[name]; if (!route) { return undefined; } // Decorate req req.route = route; return route.chain.run.bind(route.chain); }; /** * Takes an object of route params and query params, and 'renders' a URL. * * @public * @function render * @param {String} routeName - the route name * @param {Object} params - an object of route params * @param {Object} query - an object of query params * @returns {String} URL * @example * server.get({ * name: 'cities', * path: '/countries/:name/states/:state/cities' * }, (req, res, next) => ...)); * let cities = server.router.render('cities', { * name: 'Australia', * state: 'New South Wales' * }); * // cities: '/countries/Australia/states/New%20South%20Wales/cities' */ Router.prototype.render = function render(routeName, params, query) { var self = this; function pathItem(match, key) { if (params.hasOwnProperty(key) === false) { throw new Error( 'Route <' + routeName + '> is missing parameter <' + key + '>' ); } return '/' + encodeURIComponent(params[key]); } function queryItem(key) { return encodeURIComponent(key) + '=' + encodeURIComponent(query[key]); } var route = self._registry.get()[routeName]; if (!route) { return null; } var _path = route.spec.path; var _url = _path.replace(/\/:([A-Za-z0-9_]+)(\([^\\]+?\))?/g, pathItem); var items = Object.keys(query || {}).map(queryItem); var queryString = items.length > 0 ? '?' + items.join('&') : ''; return _url + queryString; }; /** * Adds a route. * * @public * @memberof Router * @instance * @function mount * @param {Object} opts - an options object * @param {String} opts.name - name * @param {String} opts.method - method * @param {String} opts.path - path can be any String * @param {Function[]} handlers - handlers * @returns {String} returns the route name if creation is successful. * @fires ...String#mount */ Router.prototype.mount = function mount(opts, handlers) { var self = this; assert.object(opts, 'opts'); assert.string(opts.method, 'opts.method'); assert.arrayOfFunc(handlers, 'handlers'); assert.optionalString(opts.name, 'opts.name'); var chain = new Chain({ onceNext: self.onceNext, strictNext: self.strictNext }); // Route var route = { name: self._getRouteName(opts.name, opts.method, opts.path), method: opts.method, path: opts.path, spec: opts, chain: chain }; handlers.forEach(function forEach(handler) { // Assign name to anonymous functions handler._name = handler.name || 'handler-' + self._anonymousHandlerCounter++; // Attach to middleware chain chain.add(handler); }); self._registry.add(route); self.emit('mount', route.method, route.path); return route; }; /** * Unmounts a route. * * @public * @memberof Router * @instance * @function unmount * @param {String} name - the route name * @returns {Object|undefined} removed route if found */ Router.prototype.unmount = function unmount(name) { assert.string(name, 'name'); var route = this._registry.remove(name); return route; }; /** * toString() serialization. * * @public * @memberof Router * @instance * @function toString * @returns {String} stringified router */ Router.prototype.toString = function toString() { return this._registry.toString(); }; /** * Return information about the routes registered in the router. * * @public * @memberof Router * @instance * @returns {object} The routes in the router. */ Router.prototype.getDebugInfo = function getDebugInfo() { var routes = this._registry.get(); return _.mapValues(routes, function mapValues(route, routeName) { return { name: route.name, method: route.method.toLowerCase(), path: route.path, handlers: route.chain.getHandlers() }; }); }; /** * Return mounted routes * * @public * @memberof Router * @instance * @returns {object} The routes in the router. */ Router.prototype.getRoutes = function getRoutes() { return this._registry.get(); }; /** * Returns true if the router generated a 404 for an options request. * * TODO: this is relevant for CORS only. Should move this out eventually to a * userland middleware? This also seems a little like overreach, as there is no * option to opt out of this behavior today. * * @private * @static * @function _optionsError * @param {Object} req - the request object * @param {Object} res - the response object * @returns {Boolean} is options error */ Router._optionsError = function _optionsError(req, res) { var pathname = req.getUrl().pathname; return req.method === 'OPTIONS' && pathname === '*'; }; /** * Default route, when no route found * Responds with a ResourceNotFoundError error. * * @private * @memberof Router * @instance * @function defaultRoute * @param {Request} req - request * @param {Response} res - response * @param {Function} next - next * @returns {undefined} no return value */ Router.prototype.defaultRoute = function defaultRoute(req, res, next) { var self = this; var pathname = req.getUrl().pathname; // Allow CORS if (Router._optionsError(req, res, pathname)) { res.send(200); next(null, req, res); return; } // Check for 405 instead of 404 var allowedMethods = http.METHODS.filter(function some(method) { return method !== req.method && self._registry.lookup(method, pathname); }); if (allowedMethods.length) { res.methods = allowedMethods; res.setHeader('Allow', allowedMethods.join(', ')); var methodErr = new MethodNotAllowedError( '%s is not allowed', req.method ); next(methodErr, req, res); return; } // clean up the url in case of potential xss // https://github.com/restify/node-restify/issues/1018 var err = new ResourceNotFoundError('%s does not exist', pathname); next(err, req, res); }; /** * Generate route name * * @private * @memberof Router * @instance * @function _getRouteName * @param {String|undefined} name - Name of the route * @param {String} method - HTTP method * @param {String} path - path * @returns {String} name of the route */ Router.prototype._getRouteName = function _getRouteName(name, method, path) { // Generate name if (!name) { name = method + '-' + path; name = name.replace(/\W/g, '').toLowerCase(); } // Avoid name conflict: GH-401 if (this._registry.get()[name]) { name += uuid.v4().substr(0, 7); } return name; }; module.exports = Router;