6392ee0407
Before this change, calling request.query when there was no querystring resulted in a new object created and returned each time. If the resulting object was ever changed, accessing request.query would not reflect it and cause weird bugs.
610 lines
12 KiB
JavaScript
610 lines
12 KiB
JavaScript
|
|
/**
|
|
* 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 = {
|
|
|
|
/**
|
|
* Return request header.
|
|
*
|
|
* @return {Object}
|
|
* @api public
|
|
*/
|
|
|
|
get header() {
|
|
return this.req.headers;
|
|
},
|
|
|
|
/**
|
|
* Return request header, alias as request.header
|
|
*
|
|
* @return {Object}
|
|
* @api public
|
|
*/
|
|
|
|
get headers() {
|
|
return this.req.headers;
|
|
},
|
|
|
|
/**
|
|
* Get request URL.
|
|
*
|
|
* @return {String}
|
|
* @api public
|
|
*/
|
|
|
|
get url() {
|
|
return this.req.url;
|
|
},
|
|
|
|
/**
|
|
* Set request URL.
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
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;
|
|
},
|
|
|
|
/**
|
|
* Get request pathname.
|
|
*
|
|
* @return {String}
|
|
* @api public
|
|
*/
|
|
|
|
get path() {
|
|
return parse(this.req).pathname;
|
|
},
|
|
|
|
/**
|
|
* Set pathname, retaining the query-string when present.
|
|
*
|
|
* @param {String} path
|
|
* @api public
|
|
*/
|
|
|
|
set path(path) {
|
|
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;
|
|
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 || '';
|
|
},
|
|
|
|
/**
|
|
* Set querystring.
|
|
*
|
|
* @param {String} str
|
|
* @api public
|
|
*/
|
|
|
|
set querystring(str) {
|
|
var url = parse(this.req);
|
|
url.search = str;
|
|
url.path = null;
|
|
|
|
this.url = stringify(url);
|
|
},
|
|
|
|
/**
|
|
* Get the search string. Same as the querystring
|
|
* except it includes the leading ?.
|
|
*
|
|
* @return {String}
|
|
* @api public
|
|
*/
|
|
|
|
get search() {
|
|
if (!this.querystring) return '';
|
|
return '?' + this.querystring;
|
|
},
|
|
|
|
/**
|
|
* Set the search string. Same as
|
|
* response.querystring= but included for ubiquity.
|
|
*
|
|
* @param {String} str
|
|
* @api public
|
|
*/
|
|
|
|
set search(str) {
|
|
this.querystring = str;
|
|
},
|
|
|
|
/**
|
|
* Parse the "Host" header field host
|
|
* and support X-Forwarded-Host when a
|
|
* proxy is enabled.
|
|
*
|
|
* @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 (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
|
|
};
|
|
}
|
|
};
|