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.
283 lines
6.0 KiB
283 lines
6.0 KiB
/*! |
|
* connect |
|
* Copyright(c) 2010 Sencha Inc. |
|
* Copyright(c) 2011 TJ Holowaychuk |
|
* Copyright(c) 2015 Douglas Christopher Wilson |
|
* MIT Licensed |
|
*/ |
|
|
|
'use strict'; |
|
|
|
/** |
|
* Module dependencies. |
|
* @private |
|
*/ |
|
|
|
var debug = require('debug')('connect:dispatcher'); |
|
var EventEmitter = require('events').EventEmitter; |
|
var finalhandler = require('finalhandler'); |
|
var http = require('http'); |
|
var merge = require('utils-merge'); |
|
var parseUrl = require('parseurl'); |
|
|
|
/** |
|
* Module exports. |
|
* @public |
|
*/ |
|
|
|
module.exports = createServer; |
|
|
|
/** |
|
* Module variables. |
|
* @private |
|
*/ |
|
|
|
var env = process.env.NODE_ENV || 'development'; |
|
var proto = {}; |
|
|
|
/* istanbul ignore next */ |
|
var defer = typeof setImmediate === 'function' |
|
? setImmediate |
|
: function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) } |
|
|
|
/** |
|
* Create a new connect server. |
|
* |
|
* @return {function} |
|
* @public |
|
*/ |
|
|
|
function createServer() { |
|
function app(req, res, next){ app.handle(req, res, next); } |
|
merge(app, proto); |
|
merge(app, EventEmitter.prototype); |
|
app.route = '/'; |
|
app.stack = []; |
|
return app; |
|
} |
|
|
|
/** |
|
* Utilize the given middleware `handle` to the given `route`, |
|
* defaulting to _/_. This "route" is the mount-point for the |
|
* middleware, when given a value other than _/_ the middleware |
|
* is only effective when that segment is present in the request's |
|
* pathname. |
|
* |
|
* For example if we were to mount a function at _/admin_, it would |
|
* be invoked on _/admin_, and _/admin/settings_, however it would |
|
* not be invoked for _/_, or _/posts_. |
|
* |
|
* @param {String|Function|Server} route, callback or server |
|
* @param {Function|Server} callback or server |
|
* @return {Server} for chaining |
|
* @public |
|
*/ |
|
|
|
proto.use = function use(route, fn) { |
|
var handle = fn; |
|
var path = route; |
|
|
|
// default route to '/' |
|
if (typeof route !== 'string') { |
|
handle = route; |
|
path = '/'; |
|
} |
|
|
|
// wrap sub-apps |
|
if (typeof handle.handle === 'function') { |
|
var server = handle; |
|
server.route = path; |
|
handle = function (req, res, next) { |
|
server.handle(req, res, next); |
|
}; |
|
} |
|
|
|
// wrap vanilla http.Servers |
|
if (handle instanceof http.Server) { |
|
handle = handle.listeners('request')[0]; |
|
} |
|
|
|
// strip trailing slash |
|
if (path[path.length - 1] === '/') { |
|
path = path.slice(0, -1); |
|
} |
|
|
|
// add the middleware |
|
debug('use %s %s', path || '/', handle.name || 'anonymous'); |
|
this.stack.push({ route: path, handle: handle }); |
|
|
|
return this; |
|
}; |
|
|
|
/** |
|
* Handle server requests, punting them down |
|
* the middleware stack. |
|
* |
|
* @private |
|
*/ |
|
|
|
proto.handle = function handle(req, res, out) { |
|
var index = 0; |
|
var protohost = getProtohost(req.url) || ''; |
|
var removed = ''; |
|
var slashAdded = false; |
|
var stack = this.stack; |
|
|
|
// final function handler |
|
var done = out || finalhandler(req, res, { |
|
env: env, |
|
onerror: logerror |
|
}); |
|
|
|
// store the original URL |
|
req.originalUrl = req.originalUrl || req.url; |
|
|
|
function next(err) { |
|
if (slashAdded) { |
|
req.url = req.url.substr(1); |
|
slashAdded = false; |
|
} |
|
|
|
if (removed.length !== 0) { |
|
req.url = protohost + removed + req.url.substr(protohost.length); |
|
removed = ''; |
|
} |
|
|
|
// next callback |
|
var layer = stack[index++]; |
|
|
|
// all done |
|
if (!layer) { |
|
defer(done, err); |
|
return; |
|
} |
|
|
|
// route data |
|
var path = parseUrl(req).pathname || '/'; |
|
var route = layer.route; |
|
|
|
// skip this layer if the route doesn't match |
|
if (path.toLowerCase().substr(0, route.length) !== route.toLowerCase()) { |
|
return next(err); |
|
} |
|
|
|
// skip if route match does not border "/", ".", or end |
|
var c = path.length > route.length && path[route.length]; |
|
if (c && c !== '/' && c !== '.') { |
|
return next(err); |
|
} |
|
|
|
// trim off the part of the url that matches the route |
|
if (route.length !== 0 && route !== '/') { |
|
removed = route; |
|
req.url = protohost + req.url.substr(protohost.length + removed.length); |
|
|
|
// ensure leading slash |
|
if (!protohost && req.url[0] !== '/') { |
|
req.url = '/' + req.url; |
|
slashAdded = true; |
|
} |
|
} |
|
|
|
// call the layer handle |
|
call(layer.handle, route, err, req, res, next); |
|
} |
|
|
|
next(); |
|
}; |
|
|
|
/** |
|
* Listen for connections. |
|
* |
|
* This method takes the same arguments |
|
* as node's `http.Server#listen()`. |
|
* |
|
* HTTP and HTTPS: |
|
* |
|
* If you run your application both as HTTP |
|
* and HTTPS you may wrap them individually, |
|
* since your Connect "server" is really just |
|
* a JavaScript `Function`. |
|
* |
|
* var connect = require('connect') |
|
* , http = require('http') |
|
* , https = require('https'); |
|
* |
|
* var app = connect(); |
|
* |
|
* http.createServer(app).listen(80); |
|
* https.createServer(options, app).listen(443); |
|
* |
|
* @return {http.Server} |
|
* @api public |
|
*/ |
|
|
|
proto.listen = function listen() { |
|
var server = http.createServer(this); |
|
return server.listen.apply(server, arguments); |
|
}; |
|
|
|
/** |
|
* Invoke a route handle. |
|
* @private |
|
*/ |
|
|
|
function call(handle, route, err, req, res, next) { |
|
var arity = handle.length; |
|
var error = err; |
|
var hasError = Boolean(err); |
|
|
|
debug('%s %s : %s', handle.name || '<anonymous>', route, req.originalUrl); |
|
|
|
try { |
|
if (hasError && arity === 4) { |
|
// error-handling middleware |
|
handle(err, req, res, next); |
|
return; |
|
} else if (!hasError && arity < 4) { |
|
// request-handling middleware |
|
handle(req, res, next); |
|
return; |
|
} |
|
} catch (e) { |
|
// replace the error |
|
error = e; |
|
} |
|
|
|
// continue |
|
next(error); |
|
} |
|
|
|
/** |
|
* Log error using console.error. |
|
* |
|
* @param {Error} err |
|
* @private |
|
*/ |
|
|
|
function logerror(err) { |
|
if (env !== 'test') console.error(err.stack || err.toString()); |
|
} |
|
|
|
/** |
|
* Get get protocol + host for a URL. |
|
* |
|
* @param {string} url |
|
* @private |
|
*/ |
|
|
|
function getProtohost(url) { |
|
if (url.length === 0 || url[0] === '/') { |
|
return undefined; |
|
} |
|
|
|
var searchIndex = url.indexOf('?'); |
|
var pathLength = searchIndex !== -1 |
|
? searchIndex |
|
: url.length; |
|
var fqdnIndex = url.substr(0, pathLength).indexOf('://'); |
|
|
|
return fqdnIndex !== -1 |
|
? url.substr(0, url.indexOf('/', 3 + fqdnIndex)) |
|
: undefined; |
|
}
|
|
|