You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
582 lines
12 KiB
582 lines
12 KiB
/*! |
|
* Express - HTTPServer |
|
* Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca> |
|
* MIT Licensed |
|
*/ |
|
|
|
/** |
|
* Module dependencies. |
|
*/ |
|
|
|
var qs = require('qs') |
|
, connect = require('connect') |
|
, router = require('./router') |
|
, Router = require('./router') |
|
, view = require('./view') |
|
, toArray = require('./utils').toArray |
|
, methods = router.methods.concat('del', 'all') |
|
, url = require('url') |
|
, utils = connect.utils; |
|
|
|
/** |
|
* Expose `HTTPServer`. |
|
*/ |
|
|
|
exports = module.exports = HTTPServer; |
|
|
|
/** |
|
* Server proto. |
|
*/ |
|
|
|
var app = HTTPServer.prototype; |
|
|
|
/** |
|
* Initialize a new `HTTPServer` with optional `middleware`. |
|
* |
|
* @param {Array} middleware |
|
* @api public |
|
*/ |
|
|
|
function HTTPServer(middleware){ |
|
connect.HTTPServer.call(this, []); |
|
this.init(middleware); |
|
}; |
|
|
|
/** |
|
* Inherit from `connect.HTTPServer`. |
|
*/ |
|
|
|
app.__proto__ = connect.HTTPServer.prototype; |
|
|
|
/** |
|
* Initialize the server. |
|
* |
|
* @param {Array} middleware |
|
* @api private |
|
*/ |
|
|
|
app.init = function(middleware){ |
|
var self = this; |
|
this.cache = {}; |
|
this.settings = {}; |
|
this.redirects = {}; |
|
this.isCallbacks = {}; |
|
this._locals = {}; |
|
this.dynamicViewHelpers = {}; |
|
this.errorHandlers = []; |
|
|
|
this.set('env', process.env.NODE_ENV || 'development'); |
|
|
|
// expose objects to each other |
|
this.use(function(req, res, next){ |
|
req.query = req.query || {}; |
|
res.setHeader('X-Powered-By', 'Express'); |
|
req.app = res.app = self; |
|
req.res = res; |
|
res.req = req; |
|
req.next = next; |
|
// assign req.query |
|
if (req.url.indexOf('?') > 0) { |
|
var query = url.parse(req.url).query; |
|
req.query = qs.parse(query); |
|
} |
|
next(); |
|
}); |
|
|
|
// apply middleware |
|
if (middleware) middleware.forEach(self.use.bind(self)); |
|
|
|
// router |
|
this.routes = new Router(this); |
|
this.__defineGetter__('router', function(){ |
|
this.__usedRouter = true; |
|
return self.routes.middleware; |
|
}); |
|
|
|
// default locals |
|
this.locals({ |
|
settings: this.settings |
|
, app: this |
|
}); |
|
|
|
// default development configuration |
|
this.configure('development', function(){ |
|
this.enable('hints'); |
|
}); |
|
|
|
// default production configuration |
|
this.configure('production', function(){ |
|
this.enable('view cache'); |
|
}); |
|
|
|
// register error handlers on "listening" |
|
// so that they disregard definition position. |
|
this.on('listening', this.registerErrorHandlers.bind(this)); |
|
|
|
// route manipulation methods |
|
methods.forEach(function(method){ |
|
self.lookup[method] = function(path){ |
|
return self.routes.lookup(method, path); |
|
}; |
|
|
|
self.match[method] = function(path){ |
|
return self.routes.match(method, path); |
|
}; |
|
|
|
self.remove[method] = function(path){ |
|
return self.routes.lookup(method, path).remove(); |
|
}; |
|
}); |
|
|
|
// del -> delete |
|
self.lookup.del = self.lookup.delete; |
|
self.match.del = self.match.delete; |
|
self.remove.del = self.remove.delete; |
|
}; |
|
|
|
/** |
|
* Remove routes matching the given `path`. |
|
* |
|
* @param {Route} path |
|
* @return {Boolean} |
|
* @api public |
|
*/ |
|
|
|
app.remove = function(path){ |
|
return this.routes.lookup('all', path).remove(); |
|
}; |
|
|
|
/** |
|
* Lookup routes defined with a path |
|
* equivalent to `path`. |
|
* |
|
* @param {Stirng} path |
|
* @return {Array} |
|
* @api public |
|
*/ |
|
|
|
app.lookup = function(path){ |
|
return this.routes.lookup('all', path); |
|
}; |
|
|
|
/** |
|
* Lookup routes matching the given `url`. |
|
* |
|
* @param {Stirng} url |
|
* @return {Array} |
|
* @api public |
|
*/ |
|
|
|
app.match = function(url){ |
|
return this.routes.match('all', url); |
|
}; |
|
|
|
/** |
|
* When using the vhost() middleware register error handlers. |
|
*/ |
|
|
|
app.onvhost = function(){ |
|
this.registerErrorHandlers(); |
|
}; |
|
|
|
/** |
|
* Register error handlers. |
|
* |
|
* @return {Server} for chaining |
|
* @api public |
|
*/ |
|
|
|
app.registerErrorHandlers = function(){ |
|
this.errorHandlers.forEach(function(fn){ |
|
this.use(function(err, req, res, next){ |
|
fn.apply(this, arguments); |
|
}); |
|
}, this); |
|
return this; |
|
}; |
|
|
|
/** |
|
* Proxy `connect.HTTPServer#use()` to apply settings to |
|
* mounted applications. |
|
* |
|
* @param {String|Function|Server} route |
|
* @param {Function|Server} middleware |
|
* @return {Server} for chaining |
|
* @api public |
|
*/ |
|
|
|
app.use = function(route, middleware){ |
|
var app, base, handle; |
|
|
|
if ('string' != typeof route) { |
|
middleware = route, route = '/'; |
|
} |
|
|
|
// express app |
|
if (middleware.handle && middleware.set) app = middleware; |
|
|
|
// restore .app property on req and res |
|
if (app) { |
|
app.route = route; |
|
middleware = function(req, res, next) { |
|
var orig = req.app; |
|
app.handle(req, res, function(err){ |
|
req.app = res.app = orig; |
|
next(err); |
|
}); |
|
}; |
|
} |
|
|
|
connect.HTTPServer.prototype.use.call(this, route, middleware); |
|
|
|
// mounted an app, invoke the hook |
|
// and adjust some settings |
|
if (app) { |
|
base = this.set('basepath') || this.route; |
|
if ('/' == base) base = ''; |
|
base = base + (app.set('basepath') || app.route); |
|
app.set('basepath', base); |
|
app.parent = this; |
|
if (app.__mounted) app.__mounted.call(app, this); |
|
} |
|
|
|
return this; |
|
}; |
|
|
|
/** |
|
* Assign a callback `fn` which is called |
|
* when this `Server` is passed to `Server#use()`. |
|
* |
|
* Examples: |
|
* |
|
* var app = express.createServer() |
|
* , blog = express.createServer(); |
|
* |
|
* blog.mounted(function(parent){ |
|
* // parent is app |
|
* // "this" is blog |
|
* }); |
|
* |
|
* app.use(blog); |
|
* |
|
* @param {Function} fn |
|
* @return {Server} for chaining |
|
* @api public |
|
*/ |
|
|
|
app.mounted = function(fn){ |
|
this.__mounted = fn; |
|
return this; |
|
}; |
|
|
|
/** |
|
* See: view.register. |
|
* |
|
* @return {Server} for chaining |
|
* @api public |
|
*/ |
|
|
|
app.register = function(){ |
|
view.register.apply(this, arguments); |
|
return this; |
|
}; |
|
|
|
/** |
|
* Register the given view helpers `obj`. This method |
|
* can be called several times to apply additional helpers. |
|
* |
|
* @param {Object} obj |
|
* @return {Server} for chaining |
|
* @api public |
|
*/ |
|
|
|
app.helpers = |
|
app.locals = function(obj){ |
|
utils.merge(this._locals, obj); |
|
return this; |
|
}; |
|
|
|
/** |
|
* Register the given dynamic view helpers `obj`. This method |
|
* can be called several times to apply additional helpers. |
|
* |
|
* @param {Object} obj |
|
* @return {Server} for chaining |
|
* @api public |
|
*/ |
|
|
|
app.dynamicHelpers = function(obj){ |
|
utils.merge(this.dynamicViewHelpers, obj); |
|
return this; |
|
}; |
|
|
|
/** |
|
* Map the given param placeholder `name`(s) to the given callback(s). |
|
* |
|
* Param mapping is used to provide pre-conditions to routes |
|
* which us normalized placeholders. This callback has the same |
|
* signature as regular middleware, for example below when ":userId" |
|
* is used this function will be invoked in an attempt to load the user. |
|
* |
|
* app.param('userId', function(req, res, next, id){ |
|
* User.find(id, function(err, user){ |
|
* if (err) { |
|
* next(err); |
|
* } else if (user) { |
|
* req.user = user; |
|
* next(); |
|
* } else { |
|
* next(new Error('failed to load user')); |
|
* } |
|
* }); |
|
* }); |
|
* |
|
* Passing a single function allows you to map logic |
|
* to the values passed to `app.param()`, for example |
|
* this is useful to provide coercion support in a concise manner. |
|
* |
|
* The following example maps regular expressions to param values |
|
* ensuring that they match, otherwise passing control to the next |
|
* route: |
|
* |
|
* app.param(function(name, regexp){ |
|
* if (regexp instanceof RegExp) { |
|
* return function(req, res, next, val){ |
|
* var captures; |
|
* if (captures = regexp.exec(String(val))) { |
|
* req.params[name] = captures; |
|
* next(); |
|
* } else { |
|
* next('route'); |
|
* } |
|
* } |
|
* } |
|
* }); |
|
* |
|
* We can now use it as shown below, where "/commit/:commit" expects |
|
* that the value for ":commit" is at 5 or more digits. The capture |
|
* groups are then available as `req.params.commit` as we defined |
|
* in the function above. |
|
* |
|
* app.param('commit', /^\d{5,}$/); |
|
* |
|
* For more of this useful functionality take a look |
|
* at [express-params](http://github.com/visionmedia/express-params). |
|
* |
|
* @param {String|Array|Function} name |
|
* @param {Function} fn |
|
* @return {Server} for chaining |
|
* @api public |
|
*/ |
|
|
|
app.param = function(name, fn){ |
|
var self = this |
|
, fns = [].slice.call(arguments, 1); |
|
|
|
// array |
|
if (Array.isArray(name)) { |
|
name.forEach(function(name){ |
|
fns.forEach(function(fn){ |
|
self.param(name, fn); |
|
}); |
|
}); |
|
// param logic |
|
} else if ('function' == typeof name) { |
|
this.routes.param(name); |
|
// single |
|
} else { |
|
if (':' == name[0]) name = name.substr(1); |
|
fns.forEach(function(fn){ |
|
self.routes.param(name, fn); |
|
}); |
|
} |
|
|
|
return this; |
|
}; |
|
|
|
/** |
|
* Assign a custom exception handler callback `fn`. |
|
* These handlers are always _last_ in the middleware stack. |
|
* |
|
* @param {Function} fn |
|
* @return {Server} for chaining |
|
* @api public |
|
*/ |
|
|
|
app.error = function(fn){ |
|
this.errorHandlers.push(fn); |
|
return this; |
|
}; |
|
|
|
/** |
|
* Register the given callback `fn` for the given `type`. |
|
* |
|
* @param {String} type |
|
* @param {Function} fn |
|
* @return {Server} for chaining |
|
* @api public |
|
*/ |
|
|
|
app.is = function(type, fn){ |
|
if (!fn) return this.isCallbacks[type]; |
|
this.isCallbacks[type] = fn; |
|
return this; |
|
}; |
|
|
|
/** |
|
* Assign `setting` to `val`, or return `setting`'s value. |
|
* Mounted servers inherit their parent server's settings. |
|
* |
|
* @param {String} setting |
|
* @param {String} val |
|
* @return {Server|Mixed} for chaining, or the setting value |
|
* @api public |
|
*/ |
|
|
|
app.set = function(setting, val){ |
|
if (val === undefined) { |
|
if (this.settings.hasOwnProperty(setting)) { |
|
return this.settings[setting]; |
|
} else if (this.parent) { |
|
return this.parent.set(setting); |
|
} |
|
} else { |
|
this.settings[setting] = val; |
|
return this; |
|
} |
|
}; |
|
|
|
/** |
|
* Check if `setting` is enabled. |
|
* |
|
* @param {String} setting |
|
* @return {Boolean} |
|
* @api public |
|
*/ |
|
|
|
app.enabled = function(setting){ |
|
return !!this.set(setting); |
|
}; |
|
|
|
/** |
|
* Check if `setting` is disabled. |
|
* |
|
* @param {String} setting |
|
* @return {Boolean} |
|
* @api public |
|
*/ |
|
|
|
app.disabled = function(setting){ |
|
return !this.set(setting); |
|
}; |
|
|
|
/** |
|
* Enable `setting`. |
|
* |
|
* @param {String} setting |
|
* @return {Server} for chaining |
|
* @api public |
|
*/ |
|
|
|
app.enable = function(setting){ |
|
return this.set(setting, true); |
|
}; |
|
|
|
/** |
|
* Disable `setting`. |
|
* |
|
* @param {String} setting |
|
* @return {Server} for chaining |
|
* @api public |
|
*/ |
|
|
|
app.disable = function(setting){ |
|
return this.set(setting, false); |
|
}; |
|
|
|
/** |
|
* Redirect `key` to `url`. |
|
* |
|
* @param {String} key |
|
* @param {String} url |
|
* @return {Server} for chaining |
|
* @api public |
|
*/ |
|
|
|
app.redirect = function(key, url){ |
|
this.redirects[key] = url; |
|
return this; |
|
}; |
|
|
|
/** |
|
* Configure callback for zero or more envs, |
|
* when no env is specified that callback will |
|
* be invoked for all environments. Any combination |
|
* can be used multiple times, in any order desired. |
|
* |
|
* Examples: |
|
* |
|
* app.configure(function(){ |
|
* // executed for all envs |
|
* }); |
|
* |
|
* app.configure('stage', function(){ |
|
* // executed staging env |
|
* }); |
|
* |
|
* app.configure('stage', 'production', function(){ |
|
* // executed for stage and production |
|
* }); |
|
* |
|
* @param {String} env... |
|
* @param {Function} fn |
|
* @return {Server} for chaining |
|
* @api public |
|
*/ |
|
|
|
app.configure = function(env, fn){ |
|
var envs = 'all' |
|
, args = toArray(arguments); |
|
fn = args.pop(); |
|
if (args.length) envs = args; |
|
if ('all' == envs || ~envs.indexOf(this.settings.env)) fn.call(this); |
|
return this; |
|
}; |
|
|
|
/** |
|
* Delegate `.VERB(...)` calls to `.route(VERB, ...)`. |
|
*/ |
|
|
|
methods.forEach(function(method){ |
|
app[method] = function(path){ |
|
if (1 == arguments.length) return this.routes.lookup(method, path); |
|
var args = [method].concat(toArray(arguments)); |
|
if (!this.__usedRouter) this.use(this.router); |
|
return this.routes._route.apply(this.routes, args); |
|
} |
|
}); |
|
|
|
/** |
|
* Special-cased "all" method, applying the given route `path`, |
|
* middleware, and callback to _every_ HTTP method. |
|
* |
|
* @param {String} path |
|
* @param {Function} ... |
|
* @return {Server} for chaining |
|
* @api public |
|
*/ |
|
|
|
app.all = function(path){ |
|
var args = arguments; |
|
if (1 == args.length) return this.routes.lookup('all', path); |
|
methods.forEach(function(method){ |
|
if ('all' == method || 'del' == method) return; |
|
app[method].apply(this, args); |
|
}, this); |
|
return this; |
|
}; |
|
|
|
// del -> delete alias |
|
|
|
app.del = app.delete; |
|
|
|
|