request.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. 'use strict';
  2. const querystring = require('querystring');
  3. const accepts = require('accepts');
  4. const _querycache = Symbol('_querycache');
  5. const _queriesCache = Symbol('_queriesCache');
  6. const PROTOCOL = Symbol('PROTOCOL');
  7. const HOST = Symbol('HOST');
  8. const ACCEPTS = Symbol('ACCEPTS');
  9. const IPS = Symbol('IPS');
  10. const RE_ARRAY_KEY = /[^\[\]]+\[\]$/;
  11. module.exports = {
  12. /**
  13. * Parse the "Host" header field host
  14. * and support X-Forwarded-Host when a
  15. * proxy is enabled.
  16. * @member {String} Request#host
  17. * @example
  18. * ip + port
  19. * ```js
  20. * this.request.host
  21. * => '127.0.0.1:7001'
  22. * ```
  23. * or domain
  24. * ```js
  25. * this.request.host
  26. * => 'demo.eggjs.org'
  27. * ```
  28. */
  29. get host() {
  30. if (this[HOST]) return this[HOST];
  31. let host;
  32. if (this.app.config.proxy) {
  33. host = getFromHeaders(this, this.app.config.hostHeaders);
  34. }
  35. host = host || this.get('host') || '';
  36. this[HOST] = host.split(/\s*,\s*/)[0];
  37. return this[HOST];
  38. },
  39. /**
  40. * @member {String} Request#protocol
  41. * @example
  42. * ```js
  43. * this.request.protocol
  44. * => 'https'
  45. * ```
  46. */
  47. get protocol() {
  48. if (this[PROTOCOL]) return this[PROTOCOL];
  49. // detect encrypted socket
  50. if (this.socket && this.socket.encrypted) {
  51. this[PROTOCOL] = 'https';
  52. return this[PROTOCOL];
  53. }
  54. // get from headers specified in `app.config.protocolHeaders`
  55. if (this.app.config.proxy) {
  56. const proto = getFromHeaders(this, this.app.config.protocolHeaders);
  57. if (proto) {
  58. this[PROTOCOL] = proto.split(/\s*,\s*/)[0];
  59. return this[PROTOCOL];
  60. }
  61. }
  62. // use protocol specified in `app.conig.protocol`
  63. this[PROTOCOL] = this.app.config.protocol || 'http';
  64. return this[PROTOCOL];
  65. },
  66. /**
  67. * Get all pass through ip addresses from the request.
  68. * Enable only on `app.config.proxy = true`
  69. *
  70. * @member {Array} Request#ips
  71. * @example
  72. * ```js
  73. * this.request.ips
  74. * => ['100.23.1.2', '201.10.10.2']
  75. * ```
  76. */
  77. get ips() {
  78. if (this[IPS]) return this[IPS];
  79. // return empty array when proxy=false
  80. if (!this.app.config.proxy) {
  81. this[IPS] = [];
  82. return this[IPS];
  83. }
  84. const val = getFromHeaders(this, this.app.config.ipHeaders) || '';
  85. this[IPS] = val ? val.split(/\s*,\s*/) : [];
  86. let maxIpsCount = this.app.config.maxIpsCount;
  87. // Compatible with maxProxyCount logic (previous logic is wrong, only for compatibility with legacy logic)
  88. if (!maxIpsCount && this.app.config.maxProxyCount) maxIpsCount = this.app.config.maxProxyCount + 1;
  89. if (maxIpsCount > 0) {
  90. // if maxIpsCount present, only keep `maxIpsCount` ips
  91. // [ illegalIp, clientRealIp, proxyIp1, proxyIp2 ...]
  92. this[IPS] = this[IPS].slice(-maxIpsCount);
  93. }
  94. return this[IPS];
  95. },
  96. /**
  97. * Get the request remote IPv4 address
  98. * @member {String} Request#ip
  99. * @return {String} IPv4 address
  100. * @example
  101. * ```js
  102. * this.request.ip
  103. * => '127.0.0.1'
  104. * => '111.10.2.1'
  105. * ```
  106. */
  107. get ip() {
  108. if (this._ip) {
  109. return this._ip;
  110. }
  111. const ip = this.ips[0] || this.socket.remoteAddress;
  112. // will be '::ffff:x.x.x.x', should convert to standard IPv4 format
  113. // https://zh.wikipedia.org/wiki/IPv6
  114. this._ip = ip && ip.indexOf('::ffff:') > -1 ? ip.substring(7) : ip;
  115. return this._ip;
  116. },
  117. /**
  118. * Set the request remote IPv4 address
  119. * @member {String} Request#ip
  120. * @param {String} ip - IPv4 address
  121. * @example
  122. * ```js
  123. * this.request.ip
  124. * => '127.0.0.1'
  125. * => '111.10.2.1'
  126. * ```
  127. */
  128. set ip(ip) {
  129. this._ip = ip;
  130. },
  131. /**
  132. * detect if response should be json
  133. * 1. url path ends with `.json`
  134. * 2. response type is set to json
  135. * 3. detect by request accept header
  136. *
  137. * @member {Boolean} Request#acceptJSON
  138. * @since 1.0.0
  139. */
  140. get acceptJSON() {
  141. if (this.path.endsWith('.json')) return true;
  142. if (this.response.type && this.response.type.indexOf('json') >= 0) return true;
  143. if (this.accepts('html', 'text', 'json') === 'json') return true;
  144. return false;
  145. },
  146. // How to read query safely
  147. // https://github.com/koajs/qs/issues/5
  148. _customQuery(cacheName, filter) {
  149. const str = this.querystring || '';
  150. let c = this[cacheName];
  151. if (!c) {
  152. c = this[cacheName] = {};
  153. }
  154. let cacheQuery = c[str];
  155. if (!cacheQuery) {
  156. cacheQuery = c[str] = {};
  157. const isQueries = cacheName === _queriesCache;
  158. // `querystring.parse` CANNOT parse something like `a[foo]=1&a[bar]=2`
  159. const query = str ? querystring.parse(str) : {};
  160. for (const key in query) {
  161. if (!key) {
  162. // key is '', like `a=b&`
  163. continue;
  164. }
  165. const value = filter(query[key]);
  166. cacheQuery[key] = value;
  167. if (isQueries && RE_ARRAY_KEY.test(key)) {
  168. // `this.queries['key'] => this.queries['key[]']` is compatibly supported
  169. const subkey = key.substring(0, key.length - 2);
  170. if (!cacheQuery[subkey]) {
  171. cacheQuery[subkey] = value;
  172. }
  173. }
  174. }
  175. }
  176. return cacheQuery;
  177. },
  178. /**
  179. * get params pass by querystring, all values are of string type.
  180. * @member {Object} Request#query
  181. * @example
  182. * ```js
  183. * GET http://127.0.0.1:7001?name=Foo&age=20&age=21
  184. * this.query
  185. * => { 'name': 'Foo', 'age': '20' }
  186. *
  187. * GET http://127.0.0.1:7001?a=b&a=c&o[foo]=bar&b[]=1&b[]=2&e=val
  188. * this.query
  189. * =>
  190. * {
  191. * "a": "b",
  192. * "o[foo]": "bar",
  193. * "b[]": "1",
  194. * "e": "val"
  195. * }
  196. * ```
  197. */
  198. get query() {
  199. return this._customQuery(_querycache, firstValue);
  200. },
  201. /**
  202. * get params pass by querystring, all value are Array type. {@link Request#query}
  203. * @member {Array} Request#queries
  204. * @example
  205. * ```js
  206. * GET http://127.0.0.1:7001?a=b&a=c&o[foo]=bar&b[]=1&b[]=2&e=val
  207. * this.queries
  208. * =>
  209. * {
  210. * "a": ["b", "c"],
  211. * "o[foo]": ["bar"],
  212. * "b[]": ["1", "2"],
  213. * "e": ["val"]
  214. * }
  215. * ```
  216. */
  217. get queries() {
  218. return this._customQuery(_queriesCache, arrayValue);
  219. },
  220. get accept() {
  221. let accept = this[ACCEPTS];
  222. if (accept) {
  223. return accept;
  224. }
  225. accept = this[ACCEPTS] = accepts(this.req);
  226. return accept;
  227. },
  228. /**
  229. * Set query-string as an object.
  230. *
  231. * @function Request#query
  232. * @param {Object} obj set querystring and query object for request.
  233. * @return {void}
  234. */
  235. set query(obj) {
  236. this.querystring = querystring.stringify(obj);
  237. },
  238. };
  239. function firstValue(value) {
  240. if (Array.isArray(value)) {
  241. value = value[0];
  242. }
  243. return value;
  244. }
  245. function arrayValue(value) {
  246. if (!Array.isArray(value)) {
  247. value = [ value ];
  248. }
  249. return value;
  250. }
  251. function getFromHeaders(ctx, names) {
  252. if (!names) return '';
  253. names = names.split(/\s*,\s*/);
  254. for (const name of names) {
  255. const value = ctx.get(name);
  256. if (value) return value;
  257. }
  258. return '';
  259. }