'use strict';
const querystring = require('querystring');
const accepts = require('accepts');
const _querycache = Symbol('_querycache');
const _queriesCache = Symbol('_queriesCache');
const PROTOCOL = Symbol('PROTOCOL');
const HOST = Symbol('HOST');
const ACCEPTS = Symbol('ACCEPTS');
const IPS = Symbol('IPS');
const RE_ARRAY_KEY = /[^\[\]]+\[\]$/;
module.exports = {
/**
* Parse the "Host" header field host
* and support X-Forwarded-Host when a
* proxy is enabled.
* @member {String} Request#host
* @example
* ip + port
* ```js
* this.request.host
* => '127.0.0.1:7001'
* ```
* or domain
* ```js
* this.request.host
* => 'demo.eggjs.org'
* ```
*/
get host() {
if (this[HOST]) return this[HOST];
let host;
if (this.app.config.proxy) {
host = getFromHeaders(this, this.app.config.hostHeaders);
}
host = host || this.get('host') || '';
this[HOST] = host.split(/\s*,\s*/)[0];
return this[HOST];
},
/**
* @member {String} Request#protocol
* @example
* ```js
* this.request.protocol
* => 'https'
* ```
*/
get protocol() {
if (this[PROTOCOL]) return this[PROTOCOL];
// detect encrypted socket
if (this.socket && this.socket.encrypted) {
this[PROTOCOL] = 'https';
return this[PROTOCOL];
}
// get from headers specified in `app.config.protocolHeaders`
if (this.app.config.proxy) {
const proto = getFromHeaders(this, this.app.config.protocolHeaders);
if (proto) {
this[PROTOCOL] = proto.split(/\s*,\s*/)[0];
return this[PROTOCOL];
}
}
// use protocol specified in `app.conig.protocol`
this[PROTOCOL] = this.app.config.protocol || 'http';
return this[PROTOCOL];
},
/**
* Get all pass through ip addresses from the request.
* Enable only on `app.config.proxy = true`
*
* @member {Array} Request#ips
* @example
* ```js
* this.request.ips
* => ['100.23.1.2', '201.10.10.2']
* ```
*/
get ips() {
if (this[IPS]) return this[IPS];
// return empty array when proxy=false
if (!this.app.config.proxy) {
this[IPS] = [];
return this[IPS];
}
const val = getFromHeaders(this, this.app.config.ipHeaders) || '';
this[IPS] = val ? val.split(/\s*,\s*/) : [];
let maxIpsCount = this.app.config.maxIpsCount;
// Compatible with maxProxyCount logic (previous logic is wrong, only for compatibility with legacy logic)
if (!maxIpsCount && this.app.config.maxProxyCount) maxIpsCount = this.app.config.maxProxyCount + 1;
if (maxIpsCount > 0) {
// if maxIpsCount present, only keep `maxIpsCount` ips
// [ illegalIp, clientRealIp, proxyIp1, proxyIp2 ...]
this[IPS] = this[IPS].slice(-maxIpsCount);
}
return this[IPS];
},
/**
* Get the request remote IPv4 address
* @member {String} Request#ip
* @return {String} IPv4 address
* @example
* ```js
* this.request.ip
* => '127.0.0.1'
* => '111.10.2.1'
* ```
*/
get ip() {
if (this._ip) {
return this._ip;
}
const ip = this.ips[0] || this.socket.remoteAddress;
// will be '::ffff:x.x.x.x', should conver to standard IPv4 format
// https://zh.wikipedia.org/wiki/IPv6
this._ip = ip && ip.indexOf('::ffff:') > -1 ? ip.substring(7) : ip;
return this._ip;
},
/**
* Set the request remote IPv4 address
* @member {String} Request#ip
* @param {String} ip - IPv4 address
* @example
* ```js
* this.request.ip
* => '127.0.0.1'
* => '111.10.2.1'
* ```
*/
set ip(ip) {
this._ip = ip;
},
/**
* detect if response should be json
* 1. url path ends with `.json`
* 2. response type is set to json
* 3. detect by request accept header
*
* @member {Boolean} Request#acceptJSON
* @since 1.0.0
*/
get acceptJSON() {
if (this.path.endsWith('.json')) return true;
if (this.response.type && this.response.type.indexOf('json') >= 0) return true;
if (this.accepts('html', 'text', 'json') === 'json') return true;
return false;
},
// How to read query safely
// https://github.com/koajs/qs/issues/5
_customQuery(cacheName, filter) {
const str = this.querystring || '';
let c = this[cacheName];
if (!c) {
c = this[cacheName] = {};
}
let cacheQuery = c[str];
if (!cacheQuery) {
cacheQuery = c[str] = {};
const isQueries = cacheName === _queriesCache;
// `querystring.parse` CANNOT parse something like `a[foo]=1&a[bar]=2`
const query = str ? querystring.parse(str) : {};
for (const key in query) {
if (!key) {
// key is '', like `a=b&`
continue;
}
const value = filter(query[key]);
cacheQuery[key] = value;
if (isQueries && RE_ARRAY_KEY.test(key)) {
// `this.queries['key'] => this.queries['key[]']` is compatibly supported
const subkey = key.substring(0, key.length - 2);
if (!cacheQuery[subkey]) {
cacheQuery[subkey] = value;
}
}
}
}
return cacheQuery;
},
/**
* get params pass by querystring, all values are of string type.
* @member {Object} Request#query
* @example
* ```js
* GET http://127.0.0.1:7001?name=Foo&age=20&age=21
* this.query
* => { 'name': 'Foo', 'age': '20' }
*
* GET http://127.0.0.1:7001?a=b&a=c&o[foo]=bar&b[]=1&b[]=2&e=val
* this.query
* =>
* {
* "a": "b",
* "o[foo]": "bar",
* "b[]": "1",
* "e": "val"
* }
* ```
*/
get query() {
return this._customQuery(_querycache, firstValue);
},
/**
* get params pass by querystring, all value are Array type. {@link Request#query}
* @member {Array} Request#queries
* @example
* ```js
* GET http://127.0.0.1:7001?a=b&a=c&o[foo]=bar&b[]=1&b[]=2&e=val
* this.queries
* =>
* {
* "a": ["b", "c"],
* "o[foo]": ["bar"],
* "b[]": ["1", "2"],
* "e": ["val"]
* }
* ```
*/
get queries() {
return this._customQuery(_queriesCache, arrayValue);
},
get accept() {
let accept = this[ACCEPTS];
if (accept) {
return accept;
}
accept = this[ACCEPTS] = accepts(this.req);
return accept;
},
/**
* Set query-string as an object.
*
* @function Request#query
* @param {Object} obj set querystring and query object for request.
* @return {void}
*/
set query(obj) {
this.querystring = querystring.stringify(obj);
},
};
function firstValue(value) {
if (Array.isArray(value)) {
value = value[0];
}
return value;
}
function arrayValue(value) {
if (!Array.isArray(value)) {
value = [ value ];
}
return value;
}
function getFromHeaders(ctx, names) {
if (!names) return '';
names = names.split(/\s*,\s*/);
for (const name of names) {
const value = ctx.get(name);
if (value) return value;
}
return '';
}