UNPKG

hapi

Version:

HTTP Server framework

400 lines (298 loc) 13.8 kB
// Load modules var Boom = require('boom'); var Catbox = require('catbox'); var Files = require('./files'); var Proxy = require('./proxy'); var Schema = require('./schema'); var Utils = require('./utils'); var Views = require('./views'); var Request = require('./request'); // Declare internals var internals = {}; exports = module.exports = internals.Route = function (options, server, env) { var self = this; Utils.assert(this.constructor === internals.Route, 'Route must be instantiated using new'); // Setup and validate route configuration Utils.assert(!!options.handler ^ !!(options.config && options.config.handler), 'Handler must appear once and only once'); // XOR this.settings = Utils.clone(options.config) || {}; this.settings.handler = this.settings.handler || options.handler; Utils.assert((typeof this.settings.handler === 'function') ^ !!this.settings.handler.proxy ^ !!this.settings.handler.file ^ !!this.settings.handler.directory ^ !!this.settings.handler.view ^ (this.settings.handler === 'notFound'), 'Handler must be a function or equal notFound or be an object with a proxy, file, directory, or view'); var schemaError = Schema.routeOptions(options); Utils.assert(!schemaError, 'Invalid route options for', options.path, ':', schemaError); schemaError = Schema.routeConfig(this.settings); Utils.assert(!schemaError, 'Invalid route config for', options.path, ':', schemaError); this.server = server; this.env = env || {}; // Plugin-specific environment this.method = options.method.toLowerCase(); Utils.assert(this.method !== 'head', 'Cannot add HEAD route'); this.path = options.path; this.settings.method = this.method; // Expose method in settings Utils.assert(this.path.match(internals.Route.validatePathRegex), 'Invalid path:', this.path); Utils.assert(this.path.match(internals.Route.validatePathEncodedRegex) === null, 'Path cannot contain encoded non-reserved path characters'); this.settings.plugins = this.settings.plugins || {}; // Route-specific plugins settings, namespaced using plugin name this.settings.app = this.settings.app || {}; // Route-specific application settings // Payload configuration ('stream', 'raw', 'parse') // Default is 'parse' for POST and PUT otherwise 'stream' this.settings.validate = this.settings.validate || {}; Utils.assert(!this.settings.validate.payload || !this.settings.payload || this.settings.payload === 'parse', 'Route payload must be set to \'parse\' when payload validation enabled'); Utils.assert(!this.settings.jsonp || typeof this.settings.jsonp === 'string', 'Bad route JSONP parameter name'); // Authentication configuration this.settings.auth = this.server._auth.setupRoute(this.settings.auth); // Parse path this._generateRegex(); // Sets this.regexp, this.params, this.fingerprint // Cache if (this.settings.cache) { Utils.assert(this.method === 'get', 'Only GET routes can use a cache'); this.settings.cache.mode = (this.settings.cache.mode || 'client').split('+'); var modes = {}; this.settings.cache.mode.forEach(function (mode) { Utils.assert(mode === 'client' || mode === 'server', 'Unknown cache mode:', mode); modes[mode] = true; }); this.settings.cache.mode = modes; Utils.assert(!this.settings.cache.segment || this.settings.cache.mode.server, 'Cannot set cache segment without server-side caching'); Utils.assert(!this.settings.cache.privacy || this.settings.cache.mode.client, 'Cannot set cache privacy setting without client-side caching'); this.settings.cache.privacy = this.settings.cache.privacy || 'default'; this.cache = (this.settings.cache.mode.server ? this.server.pack._provisionCache(this.settings.cache, 'route', this.fingerprint, this.settings.cache.segment) : new Catbox.Policy(this.settings.cache)); } else { this.settings.cache = { mode: {} }; this.cache = new Catbox.Policy(); } // Prerequisites /* [ function (request, next) {}, { method: function (request, next) {} assign: key1 }, { method: function (request, next) {}, assign: key2, mode: parallel }, 'user(params.id)' ] */ this.prerequisites = { parallel: [], serial: [] }; (this.settings.pre || []).forEach(function (pre) { if (typeof pre !== 'object') { pre = { method: pre }; } Utils.assert(pre.method, 'Prerequisite config missing method'); Utils.assert(typeof pre.method === 'function' || typeof pre.method === 'string', 'Prerequisite method must be a function or helper name'); pre.mode = pre.mode || 'serial'; Utils.assert(pre.mode === 'serial' || pre.mode === 'parallel', 'Unknown prerequisite mode:', pre.mode); if (typeof pre.method === 'string') { var preMethodParts = pre.method.match(/^(\w+)(?:\s*)\((\s*\w+(?:\.\w+)*\s*(?:\,\s*\w+(?:\.\w+)*\s*)*)?\)$/); Utils.assert(preMethodParts, 'Invalid prerequisite string method syntax'); var helper = preMethodParts[1]; Utils.assert(preMethodParts && self.server.helpers[helper], 'Unknown server helper method in prerequisite string'); pre.assign = pre.assign || helper; var helperArgs = preMethodParts[2].split(/\s*\,\s*/); pre.method = function (helper, helperArgs, request, next) { var args = []; helperArgs.forEach(function (arg) { args.push(Utils.reach(request, arg)); }); args.push(next); request.server.helpers[helper].apply(null, args); }.bind(null, helper, helperArgs); } self.prerequisites[pre.mode].push(Request.bindPre(pre)); }); // Object handler if (typeof this.settings.handler === 'object') { Utils.assert(!!this.settings.handler.proxy ^ !!this.settings.handler.file ^ !!this.settings.handler.directory ^ !!this.settings.handler.view, 'Object handler must include one and only one of proxy, file, directory or view'); if (this.settings.handler.proxy) { this.proxy = new Proxy(this.settings.handler.proxy, this); this.settings.handler = this.proxy.handler(); } else if (this.settings.handler.file) { this.settings.handler = Files.fileHandler(this, this.settings.handler.file); } else if (this.settings.handler.directory) { this.settings.handler = Files.directoryHandler(this, this.settings.handler.directory); } else if (this.settings.handler.view) { this.settings.handler = Views.handler(this, this.settings.handler.view); } } else if (this.settings.handler === 'notFound') { this.settings.handler = internals.notFound(); } return this; }; /* /path/{param}/path/{param?} /path/{param*2}/path /path/{param*2} /{param*} */ // |--/-| |------------------------------------------/segment/segment/.../{param?}----------------------------------------------| // . . . |-------------------------------/segments-----------------------------------||-------/optional-param--------------| . // . . . . |---------------------------segment-content----------------------------| .. |--------{param*|?}-------------| . . // . . . . .|----------------path-characters--------------| . .. |------decorators-----| . . . // . . . . ..|-------legal-characters------| |--%encode-| . |-------{param}------|. .. |-----*n------| |?-| . . . internals.Route.validatePathRegex = /(^\/$)|(^(\/(([\w\!\$&'\(\)\*\+\,;\=\:@\-\.~]|%[A-F0-9]{2})+|(\{\w+(\*[1-9]\d*)?\})))*(\/(\{\w+((\*([1-9]\d*)?)|(\?))?\})?)?$)/; // a a b c de f f e g h h gdc i j kl m m l n nk j i b internals.Route.validatePathEncodedRegex = /%(?:2[146-9A-E]|3[\dABD]|4[\dA-F]|5[\dAF]|6[1-9A-F]|7[\dAE])/g; internals.Route.prototype._generateRegex = function () { // Split on / var segments = this.path.split('/'); var params = {}; var pathRX = ''; var fingers = []; var paramRegex = /^\{(\w+)(?:(\*)(\d+)?)?(\?)?\}$/; // $1: name, $2: *, $3: segments, $4: optional for (var i = 1, il = segments.length; i < il; ++i) { // Skip first empty segment var segment = segments[i]; var param = segment.match(paramRegex); if (param) { // Parameter var name = param[1]; var isMulti = !!param[2]; var multiCount = param[3] && parseInt(param[3], 10); var isOptional = !!param[4]; Utils.assert(!params[name], 'Cannot repeat the same parameter name'); params[name] = true; var segmentRX = '\\/'; if (isMulti) { if (multiCount) { segmentRX += '((?:[^\\/]+)(?:\\/(?:[^\\/]+)){' + (multiCount - 1) + '})'; } else { segmentRX += '(.*)'; } } else { segmentRX += '([^\\/]+)'; } if (isOptional || (isMulti && !multiCount)) { pathRX += '(?:(?:\\/)|(?:' + segmentRX + '))'; } else { pathRX += segmentRX; } if (isMulti) { if (multiCount) { for (var m = 0; m < multiCount; ++m) { fingers.push('/?'); } } else { fingers.push('/*'); } } else { fingers.push('/?'); } } else { // Literal if (segment) { pathRX += '\\/' + Utils.escapeRegex(segment); if (this.server.settings.router.isCaseSensitive) { fingers.push('/' + segment); } else { fingers.push('/' + segment.toLowerCase()); } } else { pathRX += '\\/'; fingers.push('/'); } } } if (this.server.settings.router.isCaseSensitive) { this.regexp = new RegExp('^' + pathRX + '$'); } else { this.regexp = new RegExp('^' + pathRX + '$', 'i'); } this.fingerprint = fingers.join(''); this._fingerprintParts = fingers; this.params = Object.keys(params); }; internals.Route.prototype.match = function (request) { var match = this.regexp.exec(request.path); if (!match) { return false; } request.params = {}; request._paramsArray = []; if (this.params.length > 0) { for (var i = 1, il = match.length; i < il; ++i) { var key = this.params[i - 1]; if (key) { try { request.params[key] = (typeof match[i] === 'string' ? decodeURIComponent(match[i]) : match[i]); request._paramsArray.push(request.params[key]); } catch (err) { // decodeURIComponent can throw return false; } } } } return true; }; internals.Route.prototype.test = function (path) { var match = this.regexp.exec(path); return !!match; }; exports.sort = function (a, b) { // Biased for less and shorter segments which are faster to compare var aFirst = -1; var bFirst = 1; // Prepare fingerprints var aFingers = a._fingerprintParts; var bFingers = b._fingerprintParts; var al = aFingers.length; var bl = bFingers.length; // Comare fingerprints if ((aFingers[al - 1] === '/*') ^ (bFingers[bl - 1] === '/*')) { return (aFingers[al - 1] === '/*' ? bFirst : aFirst); } var size = Math.min(al, bl); for (var i = 0; i < size; ++i) { var aSegment = aFingers[i]; var bSegment = bFingers[i]; if (aSegment === bSegment) { continue; } if (aSegment === '/*' || bSegment === '/*') { return (aSegment === '/*' ? bFirst : aFirst); } if (aSegment === '/?' || bSegment === '/?') { if (aSegment === '/?') { return (al >= bl ? bFirst : aFirst); } else { return (bl < al ? bFirst : aFirst); } } if (al === bl) { if (aSegment.length === bSegment.length) { return (aSegment > bSegment ? bFirst : aFirst); } return (aSegment.length > bSegment.length ? bFirst : aFirst); } return (al > bl ? bFirst : aFirst); } return (al > bl ? bFirst : aFirst); }; internals.notFound = function () { return function (request) { return request.reply(Boom.notFound()); }; };