Open RubyLouvre opened 9 years ago
// server.js
/**
* Module dependencies.
*/
var contentType = require('content-type');
var stringify = require('url').format;
var parse = require('parseurl');
var qs = require('querystring');
var typeis = require('type-is');
var fresh = require('fresh');
/**
* Prototype.
*/
module.exports = {
/**
* 直接取得req.headers
*/
get header() {
return this.req.headers;
},
/**
* 直接取得req.headers
*/
get headers() {
return this.req.headers;
},
/**
* 直接取得req.url
*/
get url() {
return this.req.url;
},
/**
* 对req.url进行简单包装
*/
set url(val) {
this.req.url = val;
},
/**
* Get full request URL.
*
* @return {String}
* @api public
*/
get href() {
// support: `GET http://example.com/foo`
if (/^https?:\/\//i.test(this.originalUrl)) {
return this.originalUrl;
}
return this.protocol + '://' + this.host + this.originalUrl;
},
/**
* Get request method.
*
* @return {String}
* @api public
*/
get method() {
return this.req.method;
},
/**
* Set request method.
*
* @param {String} val
* @api public
*/
set method(val) {
this.req.method = val;
},
/**
* 使用更高效的parseurl模块取pathname
*/
get path() {
return parse(this.req).pathname;
},
/**
* Set pathname, retaining the query-string when present.
*
* @param {String} path
* @api public
*/
set path(path) {
//此parse 非原生模块,性能更高 https://github.com/pillarjs/parseurl/blob/master/index.js
var url = parse(this.req);
url.pathname = path;
url.path = null;
this.url = stringify(url);
},
/**
* Get parsed query-string.
*
* @return {Object}
* @api public
*/
get query() {
var str = this.querystring;
if (!str) return {};
var c = this._querycache = this._querycache || {};
return c[str] || (c[str] = qs.parse(str));
},
/**
* Set query-string as an object.
*
* @param {Object} obj
* @api public
*/
set query(obj) {
this.querystring = qs.stringify(obj);
},
/**
* Get query string.
*
* @return {String}
* @api public
*/
get querystring() {
return parse(this.req).query || '';
},
/**
* querystring是没有?
* query是有?
*/
set querystring(str) {
var url = parse(this.req);
url.search = str;
url.path = null;
this.url = stringify(url);
},
get search() {
if (!this.querystring) return '';
return '?' + this.querystring;
},
set search(str) {
this.querystring = str;
},
/**
* Parse the "Host" header field host
* and support X-Forwarded-Host when a
* proxy is enabled.
*z
* @return {String} hostname:port
* @api public
*/
get host() {
var proxy = this.app.proxy;
var host = proxy && this.get('X-Forwarded-Host');
host = host || this.get('Host');
if (!host) return;
return host.split(/\s*,\s*/)[0];
},
/**
* Parse the "Host" header field hostname
* and support X-Forwarded-Host when a
* proxy is enabled.
*
* @return {String} hostname
* @api public
*/
get hostname() {
var host = this.host;
if (!host) return;
return host.split(':')[0];
},
/**
* Check if the request is fresh, aka
* Last-Modified and/or the ETag
* still match.
*
* @return {Boolean}
* @api public
*/
get fresh() {
var method = this.method;
var s = this.ctx.status;
// GET or HEAD for weak freshness validation only
if ('GET' != method && 'HEAD' != method) return false;
// 2xx or 304 as per rfc2616 14.26
if ((s >= 200 && s < 300) || 304 == s) {
return fresh(this.header, this.ctx.response.header);
}
return false;
},
/**
* Check if the request is stale, aka
* "Last-Modified" and / or the "ETag" for the
* resource has changed.
*
* @return {Boolean}
* @api public
*/
get stale() {
return !this.fresh;
},
/**
* Check if the request is idempotent.
*
* @return {Boolean}
* @api public
*/
get idempotent() {
var methods = ['GET', 'HEAD', 'PUT', 'DELETE', 'OPTIONS', 'TRACE'];
return !!~methods.indexOf(this.method);
},
/**
* Return the request socket.
*
* @return {Connection}
* @api public
*/
get socket() {
// TODO: TLS
return this.req.socket;
},
/**
* Get the charset when present or undefined.
*
* @return {String}
* @api public
*/
get charset() {
var type = this.get('Content-Type');
if (!type) return;
return contentType.parse(type).parameters.charset;
},
/**
* Return parsed Content-Length when present.
*
* @return {Number}
* @api public
*/
get length() {
var len = this.get('Content-Length');
if (null == len) return;
return ~~len;
},
/**
* Return the protocol string "http" or "https"
* when requested with TLS. When the proxy setting
* is enabled the "X-Forwarded-Proto" header
* field will be trusted. If you're running behind
* a reverse proxy that supplies https for you this
* may be enabled.
*
* @return {String}
* @api public
*/
get protocol() {
var proxy = this.app.proxy;
if (this.socket.encrypted) return 'https';
if (!proxy) return 'http';
var proto = this.get('X-Forwarded-Proto') || 'http';
return proto.split(/\s*,\s*/)[0];
},
/**
* Short-hand for:
*
* this.protocol == 'https'
*
* @return {Boolean}
* @api public
*/
get secure() {
return 'https' == this.protocol;
},
/**
* Return the remote address, or when
* `app.proxy` is `true` return
* the upstream addr.
*
* @return {String}
* @api public
*/
get ip() {
return this.ips[0] || this.socket.remoteAddress;
},
/**
* When `app.proxy` is `true`, parse
* the "X-Forwarded-For" ip address list.
*
* For example if the value were "client, proxy1, proxy2"
* you would receive the array `["client", "proxy1", "proxy2"]`
* where "proxy2" is the furthest down-stream.
*
* @return {Array}
* @api public
*/
get ips() {
var proxy = this.app.proxy;
var val = this.get('X-Forwarded-For');
return proxy && val
? val.split(/\s*,\s*/)
: [];
},
/**
* Return subdomains as an array.
*
* Subdomains are the dot-separated parts of the host before the main domain of
* the app. By default, the domain of the app is assumed to be the last two
* parts of the host. This can be changed by setting `app.subdomainOffset`.
*
* For example, if the domain is "tobi.ferrets.example.com":
* If `app.subdomainOffset` is not set, this.subdomains is `["ferrets", "tobi"]`.
* If `app.subdomainOffset` is 3, this.subdomains is `["tobi"]`.
*
* @return {Array}
* @api public
*/
get subdomains() {
var offset = this.app.subdomainOffset;
return (this.host || '')
.split('.')
.reverse()
.slice(offset);
},
/**
* Check if the given `type(s)` is acceptable, returning
* the best match when true, otherwise `undefined`, in which
* case you should respond with 406 "Not Acceptable".
*
* The `type` value may be a single mime type string
* such as "application/json", the extension name
* such as "json" or an array `["json", "html", "text/plain"]`. When a list
* or array is given the _best_ match, if any is returned.
*
* Examples:
*
* // Accept: text/html
* this.accepts('html');
* // => "html"
*
* // Accept: text/*, application/json
* this.accepts('html');
* // => "html"
* this.accepts('text/html');
* // => "text/html"
* this.accepts('json', 'text');
* // => "json"
* this.accepts('application/json');
* // => "application/json"
*
* // Accept: text/*, application/json
* this.accepts('image/png');
* this.accepts('png');
* // => undefined
*
* // Accept: text/*;q=.5, application/json
* this.accepts(['html', 'json']);
* this.accepts('html', 'json');
* // => "json"
*
* @param {String|Array} type(s)...
* @return {String|Array|Boolean}
* @api public
*/
accepts: function(){
return this.accept.types.apply(this.accept, arguments);
},
/**
* Return accepted encodings or best fit based on `encodings`.
*
* Given `Accept-Encoding: gzip, deflate`
* an array sorted by quality is returned:
*
* ['gzip', 'deflate']
*
* @param {String|Array} encoding(s)...
* @return {String|Array}
* @api public
*/
acceptsEncodings: function(){
return this.accept.encodings.apply(this.accept, arguments);
},
/**
* Return accepted charsets or best fit based on `charsets`.
*
* Given `Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5`
* an array sorted by quality is returned:
*
* ['utf-8', 'utf-7', 'iso-8859-1']
*
* @param {String|Array} charset(s)...
* @return {String|Array}
* @api public
*/
acceptsCharsets: function(){
return this.accept.charsets.apply(this.accept, arguments);
},
/**
* Return accepted languages or best fit based on `langs`.
*
* Given `Accept-Language: en;q=0.8, es, pt`
* an array sorted by quality is returned:
*
* ['es', 'pt', 'en']
*
* @param {String|Array} lang(s)...
* @return {Array|String}
* @api public
*/
acceptsLanguages: function(){
return this.accept.languages.apply(this.accept, arguments);
},
/**
* Check if the incoming request contains the "Content-Type"
* header field, and it contains any of the give mime `type`s.
* If there is no request body, `null` is returned.
* If there is no content type, `false` is returned.
* Otherwise, it returns the first `type` that matches.
*
* Examples:
*
* // With Content-Type: text/html; charset=utf-8
* this.is('html'); // => 'html'
* this.is('text/html'); // => 'text/html'
* this.is('text/*', 'application/json'); // => 'text/html'
*
* // When Content-Type is application/json
* this.is('json', 'urlencoded'); // => 'json'
* this.is('application/json'); // => 'application/json'
* this.is('html', 'application/*'); // => 'application/json'
*
* this.is('html'); // => false
*
* @param {String|Array} types...
* @return {String|false|null}
* @api public
*/
is: function(types){
if (!types) return typeis(this.req);
if (!Array.isArray(types)) types = [].slice.call(arguments);
return typeis(this.req, types);
},
/**
* Return the request mime type void of
* parameters such as "charset".
*
* @return {String}
* @api public
*/
get type() {
var type = this.get('Content-Type');
if (!type) return;
return type.split(';')[0];
},
/**
* Return request header.
*
* The `Referrer` header field is special-cased,
* both `Referrer` and `Referer` are interchangeable.
*
* Examples:
*
* this.get('Content-Type');
* // => "text/plain"
*
* this.get('content-type');
* // => "text/plain"
*
* this.get('Something');
* // => undefined
*
* @param {String} field
* @return {String}
* @api public
*/
get: function(field){
var req = this.req;
switch (field = field.toLowerCase()) {
case 'referer':
case 'referrer':
return req.headers.referrer || req.headers.referer;
default:
return req.headers[field];
}
},
/**
* Inspect implementation.
*
* @return {Object}
* @api public
*/
inspect: function(){
if (!this.req) return;
return this.toJSON();
},
/**
* Return JSON representation.
*
* @return {Object}
* @api public
*/
toJSON: function(){
return {
method: this.method,
url: this.url,
header: this.header
}
}
};
//response
/**
* Module dependencies.
*/
//https://github.com/jshttp/content-disposition/blob/master/index.js
var contentDisposition = require('content-disposition');
var ensureErrorHandler = require('error-inject');
var getType = require('mime-types').contentType;
//Execute a callback when a request closes, finishes, or errors.
//https://github.com/jshttp/on-finished/blob/master/index.js
var onFinish = require('on-finished');
var isJSON = require('koa-is-json');
var escape = require('escape-html');
var typeis = require('type-is').is;
var statuses = require('statuses');
var destroy = require('destroy');
var assert = require('assert');
var path = require('path');
var vary = require('vary');
var extname = path.extname;
/**
* Prototype.
*/
module.exports = {
/**
* Return the request socket.
*
* @return {Connection}
* @api public
*/
get socket() {
// TODO: TLS
return this.ctx.req.socket;
},
/**
* Return response header.
*
* @return {Object}
* @api public
*/
get header() {
// TODO: wtf
return this.res._headers || {};
},
/**
* Get response status code.
*
* @return {Number}
* @api public
*/
get status() {
return this.res.statusCode;
},
/**
* Set response status code.
*
* @param {Number} code
* @api public
*/
set status(code) {
assert('number' == typeof code, 'status code must be a number');
assert(statuses[code], 'invalid status code: ' + code);
this._explicitStatus = true;
this.res.statusCode = code;
this.res.statusMessage = statuses[code];
if (this.body && statuses.empty[code]) this.body = null;
},
/**
* Get response status message
*
* @return {String}
* @api public
*/
get message() {
return this.res.statusMessage || statuses[this.status];
},
/**
* Set response status message
*
* @param {String} msg
* @api public
*/
set message(msg) {
this.res.statusMessage = msg;
},
/**
* Get response body.
*
* @return {Mixed}
* @api public
*/
get body() {
return this._body;
},
/**
* Set response body.
*
* @param {String|Buffer|Object|Stream} val
* @api public
*/
set body(val) {
var original = this._body;
this._body = val;
// no content
if (null == val) {
if (!statuses.empty[this.status]) this.status = 204;
this.res.removeHeader('Content-Type');
this.res.removeHeader('Content-Length');
this.res.removeHeader('Transfer-Encoding');
return;
}
// set the status
if (!this._explicitStatus) this.status = 200;
// set the content-type only if not yet set
var setType = !this.header['content-type'];
// string
if ('string' == typeof val) {
if (setType) this.type = /^\s*</.test(val) ? 'html' : 'text';
this.length = Buffer.byteLength(val);
return;
}
// buffer
if (Buffer.isBuffer(val)) {
if (setType) this.type = 'bin';
this.length = val.length;
return;
}
// stream
if ('function' == typeof val.pipe) {
onFinish(this.res, destroy.bind(null, val));
//ensureErrorHandler
//https://github.com/stream-utils/error-inject/blob/master/index.js
/*
var Stream = require('stream');
module.exports = function (stream, error) {
if (stream instanceof Stream
&& !~stream.listeners('error').indexOf(error)) {
stream.on('error', error);
}
return stream;
};
*/
ensureErrorHandler(val, this.ctx.onerror);
// overwriting
if (null != original && original != val) this.remove('Content-Length');
if (setType) this.type = 'bin';
return;
}
// json
this.remove('Content-Length');
this.type = 'json';
},
/**
* Set Content-Length field to `n`.
*
* @param {Number} n
* @api public
*/
set length(n) {
this.set('Content-Length', n);
},
/**
* Return parsed response Content-Length when present.
*
* @return {Number}
* @api public
*/
get length() {
var len = this.header['content-length'];
var body = this.body;
if (null == len) {
if (!body) return;
if ('string' == typeof body) return Buffer.byteLength(body);
if (Buffer.isBuffer(body)) return body.length;
if (isJSON(body)) return Buffer.byteLength(JSON.stringify(body));
return;
}
return ~~len;
},
/**
* Check if a header has been written to the socket.
*
* @return {Boolean}
* @api public
*/
get headerSent() {
return this.res.headersSent;
},
/**
https://www.imququ.com/post/vary-header-in-http.html
*/
vary: function(field){
vary(this.res, field);
},
/**
* Perform a 302 redirect to `url`.
*
* The string "back" is special-cased
* to provide Referrer support, when Referrer
* is not present `alt` or "/" is used.
*
* Examples:
*
* this.redirect('back');
* this.redirect('back', '/index.html');
* this.redirect('/login');
* this.redirect('http://google.com');
*
* @param {String} url
* @param {String} alt
* @api public
*/
redirect: function(url, alt){
// location
if ('back' == url) url = this.ctx.get('Referrer') || alt || '/';
this.set('Location', url);
// status
if (!statuses.redirect[this.status]) this.status = 302;
// html
if (this.ctx.accepts('html')) {
url = escape(url);
this.type = 'text/html; charset=utf-8';
this.body = 'Redirecting to <a href="' + url + '">' + url + '</a>.';
return;
}
// text
this.type = 'text/plain; charset=utf-8';
this.body = 'Redirecting to ' + url + '.';
},
/**
* Set Content-Disposition header to "attachment" with optional `filename`.
*
* @param {String} filename
* @api public
*/
attachment: function(filename){
if (filename) this.type = extname(filename);
this.set('Content-Disposition', contentDisposition(filename));
},
/**
* Set Content-Type response header with `type` through `mime.lookup()`
* when it does not contain a charset.
*
* Examples:
*
* this.type = '.html';
* this.type = 'html';
* this.type = 'json';
* this.type = 'application/json';
* this.type = 'png';
*
* @param {String} type
* @api public
*/
set type(type) {
this.set('Content-Type', getType(type) || 'application/octet-stream');
},
/**
* Set the Last-Modified date using a string or a Date.
*
* this.response.lastModified = new Date();
* this.response.lastModified = '2013-09-13';
*
* @param {String|Date} type
* @api public
*/
set lastModified(val) {
if ('string' == typeof val) val = new Date(val);
this.set('Last-Modified', val.toUTCString());
},
/**
* Get the Last-Modified date in Date form, if it exists.
*
* @return {Date}
* @api public
*/
get lastModified() {
var date = this.get('last-modified');
if (date) return new Date(date);
},
/**
* Set the ETag of a response.
* This will normalize the quotes if necessary.
*
* this.response.etag = 'md5hashsum';
* this.response.etag = '"md5hashsum"';
* this.response.etag = 'W/"123456789"';
*
* @param {String} etag
* @api public
*/
set etag(val) {
if (!/^(W\/)?"/.test(val)) val = '"' + val + '"';
this.set('ETag', val);
},
/**
* Get the ETag of a response.
*
* @return {String}
* @api public
*/
get etag() {
return this.get('ETag');
},
/**
* Return the request mime type void of
* parameters such as "charset".
*
* @return {String}
* @api public
*/
get type() {
var type = this.get('Content-Type');
if (!type) return;
return type.split(';')[0];
},
/**
* Check whether the response is one of the listed types.
* Pretty much the same as `this.request.is()`.
*
* @param {String|Array} types...
* @return {String|false}
* @api public
*/
is: function(types){
var type = this.type;
if (!types) return type || false;
if (!Array.isArray(types)) types = [].slice.call(arguments);
return typeis(type, types);
},
/**
* Return response header.
*
* Examples:
*
* this.get('Content-Type');
* // => "text/plain"
*
* this.get('content-type');
* // => "text/plain"
*
* @param {String} field
* @return {String}
* @api public
*/
get: function(field){
return this.header[field.toLowerCase()];
},
/**
* Set header `field` to `val`, or pass
* an object of header fields.
*
* Examples:
*
* this.set('Foo', ['bar', 'baz']);
* this.set('Accept', 'application/json');
* this.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' });
*
* @param {String|Object|Array} field
* @param {String} val
* @api public
*/
set: function(field, val){
if (2 == arguments.length) {
if (Array.isArray(val)) val = val.map(String);
else val = String(val);
this.res.setHeader(field, val);
} else {
for (var key in field) {
this.set(key, field[key]);
}
}
},
/**
* Append additional header `field` with value `val`.
*
* Examples:
*
* this.append('Link', ['<http://localhost/>', '<http://localhost:3000/>']);
* this.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly');
* this.append('Warning', '199 Miscellaneous warning');
*
* @param {String} field
* @param {String|Array} val
* @api public
*/
append: function(field, val){
var prev = this.get(field);
if (prev) {
val = Array.isArray(prev)
? prev.concat(val)
: [prev].concat(val);
}
return this.set(field, val);
},
/**
* Remove header `field`.
*
* @param {String} name
* @api public
*/
remove: function(field){
this.res.removeHeader(field);
},
/**
* Checks if the request is writable.
* Tests for the existence of the socket
* as node sometimes does not set it.
*
* @return {Boolean}
* @api private
*/
get writable() {
var socket = this.res.socket;
if (!socket) return false;
return socket.writable;
},
/**
* Inspect implementation.
*
* @return {Object}
* @api public
*/
inspect: function(){
if (!this.res) return;
var o = this.toJSON();
o.body = this.body;
return o;
},
/**
* Return JSON representation.
*
* @return {Object}
* @api public
*/
toJSON: function(){
return {
status: this.status,
message: this.message,
header: this.header
}
}
};
/**
* Module dependencies.
*/
//https://github.com/jshttp/http-errors/blob/master/index.js
var createError = require('http-errors');
var httpAssert = require('http-assert');
var delegate = require('delegates');
var statuses = require('statuses');
var assert = require('assert');
/**
* Context prototype.
*/
var proto = module.exports = {
/**
* util.inspect() implementation, which
* just returns the JSON output.
*
* @return {Object}
* @api public
*/
inspect: function(){
return this.toJSON();
},
/**
* Return JSON representation.
*
* Here we explicitly invoke .toJSON() on each
* object, as iteration will otherwise fail due
* to the getters and cause utilities such as
* clone() to fail.
*
* @return {Object}
* @api public
*/
toJSON: function(){
return {
request: this.request.toJSON(),
response: this.response.toJSON(),
app: this.app.toJSON(),
originalUrl: this.originalUrl,
req: '<original node req>',
res: '<original node res>',
socket: '<original node socket>',
}
},
/**
* Similar to .throw(), adds assertion.
*
* this.assert(this.user, 401, 'Please login!');
*
* See: https://github.com/jshttp/http-assert
*
* @param {Mixed} test
* @param {Number} status
* @param {String} message
* @api public
*/
assert: httpAssert,
/**
* Throw an error with `msg` and optional `status`
* defaulting to 500. Note that these are user-level
* errors, and the message may be exposed to the client.
*
* this.throw(403)
* this.throw('name required', 400)
* this.throw(400, 'name required')
* this.throw('something exploded')
* this.throw(new Error('invalid'), 400);
* this.throw(400, new Error('invalid'));
*
* See: https://github.com/jshttp/http-errors
*
* @param {String|Number|Error} err, msg or status
* @param {String|Number|Error} [err, msg or status]
* @param {Object} [props]
* @api public
*/
throw: function(){
throw createError.apply(null, arguments);
},
/**
* Default error handling.
*
* @param {Error} err
* @api private
*/
onerror: function(err){
// don't do anything if there is no error.
// this allows you to pass `this.onerror`
// to node-style callbacks.
if (null == err) return;
assert(err instanceof Error, 'non-error thrown: ' + err);
// delegate
this.app.emit('error', err, this);
// nothing we can do here other
// than delegate to the app-level
// handler and log.
if (this.headerSent || !this.writable) {
err.headerSent = true;
return;
}
// unset all headers
this.res._headers = {};
// force text/plain
this.type = 'text';
// ENOENT support
if ('ENOENT' == err.code) err.status = 404;
// default to 500
if ('number' != typeof err.status || !statuses[err.status]) err.status = 500;
// respond
var code = statuses[err.status];
var msg = err.expose ? err.message : code;
this.status = err.status;
this.length = Buffer.byteLength(msg);
this.res.end(msg);
}
};
/**
* Response delegation.
*/
delegate(proto, 'response')
.method('attachment')
.method('redirect')
.method('remove')
.method('vary')
.method('set')
.method('append')
.access('status')
.access('message')
.access('body')
.access('length')
.access('type')
.access('lastModified')
.access('etag')
.getter('headerSent')
.getter('writable');
/**
* Request delegation.
*/
delegate(proto, 'request')
.method('acceptsLanguages')
.method('acceptsEncodings')
.method('acceptsCharsets')
.method('accepts')
.method('get')
.method('is')
.access('querystring')
.access('idempotent')
.access('socket')
.access('search')
.access('method')
.access('query')
.access('path')
.access('url')
.getter('href')
.getter('subdomains')
.getter('protocol')
.getter('host')
.getter('hostname')
.getter('header')
.getter('headers')
.getter('secure')
.getter('stale')
.getter('fresh')
.getter('ips')
.getter('ip');
//最核心的部分
app.callback = function(){
var mw = [respond].concat(this.middleware);
var fn = co.wrap(compose(mw));
var self = this;
if (!this.listeners('error').length) this.on('error', this.onerror);
return function(req, res){
res.statusCode = 404;
var ctx = self.createContext(req, res);
onFinished(res, ctx.onerror);
fn.call(ctx).catch(ctx.onerror);
}
};
respect