request.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726
  1. 'use strict';
  2. /**
  3. * Module dependencies.
  4. */
  5. const URL = require('url').URL;
  6. const net = require('net');
  7. const accepts = require('accepts');
  8. const contentType = require('content-type');
  9. const stringify = require('url').format;
  10. const parse = require('parseurl');
  11. const qs = require('querystring');
  12. const typeis = require('type-is');
  13. const fresh = require('fresh');
  14. const only = require('only');
  15. const util = require('util');
  16. const IP = Symbol('context#ip');
  17. /**
  18. * Prototype.
  19. */
  20. module.exports = {
  21. /**
  22. * Return request header.
  23. *
  24. * @return {Object}
  25. * @api public
  26. */
  27. get header() {
  28. return this.req.headers;
  29. },
  30. /**
  31. * Set request header.
  32. *
  33. * @api public
  34. */
  35. set header(val) {
  36. this.req.headers = val;
  37. },
  38. /**
  39. * Return request header, alias as request.header
  40. *
  41. * @return {Object}
  42. * @api public
  43. */
  44. get headers() {
  45. return this.req.headers;
  46. },
  47. /**
  48. * Set request header, alias as request.header
  49. *
  50. * @api public
  51. */
  52. set headers(val) {
  53. this.req.headers = val;
  54. },
  55. /**
  56. * Get request URL.
  57. *
  58. * @return {String}
  59. * @api public
  60. */
  61. get url() {
  62. return this.req.url;
  63. },
  64. /**
  65. * Set request URL.
  66. *
  67. * @api public
  68. */
  69. set url(val) {
  70. this.req.url = val;
  71. },
  72. /**
  73. * Get origin of URL.
  74. *
  75. * @return {String}
  76. * @api public
  77. */
  78. get origin() {
  79. return `${this.protocol}://${this.host}`;
  80. },
  81. /**
  82. * Get full request URL.
  83. *
  84. * @return {String}
  85. * @api public
  86. */
  87. get href() {
  88. // support: `GET http://example.com/foo`
  89. if (/^https?:\/\//i.test(this.originalUrl)) return this.originalUrl;
  90. return this.origin + this.originalUrl;
  91. },
  92. /**
  93. * Get request method.
  94. *
  95. * @return {String}
  96. * @api public
  97. */
  98. get method() {
  99. return this.req.method;
  100. },
  101. /**
  102. * Set request method.
  103. *
  104. * @param {String} val
  105. * @api public
  106. */
  107. set method(val) {
  108. this.req.method = val;
  109. },
  110. /**
  111. * Get request pathname.
  112. *
  113. * @return {String}
  114. * @api public
  115. */
  116. get path() {
  117. return parse(this.req).pathname;
  118. },
  119. /**
  120. * Set pathname, retaining the query string when present.
  121. *
  122. * @param {String} path
  123. * @api public
  124. */
  125. set path(path) {
  126. const url = parse(this.req);
  127. if (url.pathname === path) return;
  128. url.pathname = path;
  129. url.path = null;
  130. this.url = stringify(url);
  131. },
  132. /**
  133. * Get parsed query string.
  134. *
  135. * @return {Object}
  136. * @api public
  137. */
  138. get query() {
  139. const str = this.querystring;
  140. const c = this._querycache = this._querycache || {};
  141. return c[str] || (c[str] = qs.parse(str));
  142. },
  143. /**
  144. * Set query string as an object.
  145. *
  146. * @param {Object} obj
  147. * @api public
  148. */
  149. set query(obj) {
  150. this.querystring = qs.stringify(obj);
  151. },
  152. /**
  153. * Get query string.
  154. *
  155. * @return {String}
  156. * @api public
  157. */
  158. get querystring() {
  159. if (!this.req) return '';
  160. return parse(this.req).query || '';
  161. },
  162. /**
  163. * Set query string.
  164. *
  165. * @param {String} str
  166. * @api public
  167. */
  168. set querystring(str) {
  169. const url = parse(this.req);
  170. if (url.search === `?${str}`) return;
  171. url.search = str;
  172. url.path = null;
  173. this.url = stringify(url);
  174. },
  175. /**
  176. * Get the search string. Same as the query string
  177. * except it includes the leading ?.
  178. *
  179. * @return {String}
  180. * @api public
  181. */
  182. get search() {
  183. if (!this.querystring) return '';
  184. return `?${this.querystring}`;
  185. },
  186. /**
  187. * Set the search string. Same as
  188. * request.querystring= but included for ubiquity.
  189. *
  190. * @param {String} str
  191. * @api public
  192. */
  193. set search(str) {
  194. this.querystring = str;
  195. },
  196. /**
  197. * Parse the "Host" header field host
  198. * and support X-Forwarded-Host when a
  199. * proxy is enabled.
  200. *
  201. * @return {String} hostname:port
  202. * @api public
  203. */
  204. get host() {
  205. const proxy = this.app.proxy;
  206. let host = proxy && this.get('X-Forwarded-Host');
  207. if (!host) {
  208. if (this.req.httpVersionMajor >= 2) host = this.get(':authority');
  209. if (!host) host = this.get('Host');
  210. }
  211. if (!host) return '';
  212. return host.split(/\s*,\s*/, 1)[0];
  213. },
  214. /**
  215. * Parse the "Host" header field hostname
  216. * and support X-Forwarded-Host when a
  217. * proxy is enabled.
  218. *
  219. * @return {String} hostname
  220. * @api public
  221. */
  222. get hostname() {
  223. const host = this.host;
  224. if (!host) return '';
  225. if ('[' === host[0]) return this.URL.hostname || ''; // IPv6
  226. return host.split(':', 1)[0];
  227. },
  228. /**
  229. * Get WHATWG parsed URL.
  230. * Lazily memoized.
  231. *
  232. * @return {URL|Object}
  233. * @api public
  234. */
  235. get URL() {
  236. /* istanbul ignore else */
  237. if (!this.memoizedURL) {
  238. const originalUrl = this.originalUrl || ''; // avoid undefined in template string
  239. try {
  240. this.memoizedURL = new URL(`${this.origin}${originalUrl}`);
  241. } catch (err) {
  242. this.memoizedURL = Object.create(null);
  243. }
  244. }
  245. return this.memoizedURL;
  246. },
  247. /**
  248. * Check if the request is fresh, aka
  249. * Last-Modified and/or the ETag
  250. * still match.
  251. *
  252. * @return {Boolean}
  253. * @api public
  254. */
  255. get fresh() {
  256. const method = this.method;
  257. const s = this.ctx.status;
  258. // GET or HEAD for weak freshness validation only
  259. if ('GET' !== method && 'HEAD' !== method) return false;
  260. // 2xx or 304 as per rfc2616 14.26
  261. if ((s >= 200 && s < 300) || 304 === s) {
  262. return fresh(this.header, this.response.header);
  263. }
  264. return false;
  265. },
  266. /**
  267. * Check if the request is stale, aka
  268. * "Last-Modified" and / or the "ETag" for the
  269. * resource has changed.
  270. *
  271. * @return {Boolean}
  272. * @api public
  273. */
  274. get stale() {
  275. return !this.fresh;
  276. },
  277. /**
  278. * Check if the request is idempotent.
  279. *
  280. * @return {Boolean}
  281. * @api public
  282. */
  283. get idempotent() {
  284. const methods = ['GET', 'HEAD', 'PUT', 'DELETE', 'OPTIONS', 'TRACE'];
  285. return !!~methods.indexOf(this.method);
  286. },
  287. /**
  288. * Return the request socket.
  289. *
  290. * @return {Connection}
  291. * @api public
  292. */
  293. get socket() {
  294. return this.req.socket;
  295. },
  296. /**
  297. * Get the charset when present or undefined.
  298. *
  299. * @return {String}
  300. * @api public
  301. */
  302. get charset() {
  303. try {
  304. const { parameters } = contentType.parse(this.req);
  305. return parameters.charset || '';
  306. } catch (e) {
  307. return '';
  308. }
  309. },
  310. /**
  311. * Return parsed Content-Length when present.
  312. *
  313. * @return {Number}
  314. * @api public
  315. */
  316. get length() {
  317. const len = this.get('Content-Length');
  318. if (len === '') return;
  319. return ~~len;
  320. },
  321. /**
  322. * Return the protocol string "http" or "https"
  323. * when requested with TLS. When the proxy setting
  324. * is enabled the "X-Forwarded-Proto" header
  325. * field will be trusted. If you're running behind
  326. * a reverse proxy that supplies https for you this
  327. * may be enabled.
  328. *
  329. * @return {String}
  330. * @api public
  331. */
  332. get protocol() {
  333. if (this.socket.encrypted) return 'https';
  334. if (!this.app.proxy) return 'http';
  335. const proto = this.get('X-Forwarded-Proto');
  336. return proto ? proto.split(/\s*,\s*/, 1)[0] : 'http';
  337. },
  338. /**
  339. * Shorthand for:
  340. *
  341. * this.protocol == 'https'
  342. *
  343. * @return {Boolean}
  344. * @api public
  345. */
  346. get secure() {
  347. return 'https' === this.protocol;
  348. },
  349. /**
  350. * When `app.proxy` is `true`, parse
  351. * the "X-Forwarded-For" ip address list.
  352. *
  353. * For example if the value was "client, proxy1, proxy2"
  354. * you would receive the array `["client", "proxy1", "proxy2"]`
  355. * where "proxy2" is the furthest down-stream.
  356. *
  357. * @return {Array}
  358. * @api public
  359. */
  360. get ips() {
  361. const proxy = this.app.proxy;
  362. const val = this.get(this.app.proxyIpHeader);
  363. let ips = proxy && val
  364. ? val.split(/\s*,\s*/)
  365. : [];
  366. if (this.app.maxIpsCount > 0) {
  367. ips = ips.slice(-this.app.maxIpsCount);
  368. }
  369. return ips;
  370. },
  371. /**
  372. * Return request's remote address
  373. * When `app.proxy` is `true`, parse
  374. * the "X-Forwarded-For" ip address list and return the first one
  375. *
  376. * @return {String}
  377. * @api public
  378. */
  379. get ip() {
  380. if (!this[IP]) {
  381. this[IP] = this.ips[0] || this.socket.remoteAddress || '';
  382. }
  383. return this[IP];
  384. },
  385. set ip(_ip) {
  386. this[IP] = _ip;
  387. },
  388. /**
  389. * Return subdomains as an array.
  390. *
  391. * Subdomains are the dot-separated parts of the host before the main domain
  392. * of the app. By default, the domain of the app is assumed to be the last two
  393. * parts of the host. This can be changed by setting `app.subdomainOffset`.
  394. *
  395. * For example, if the domain is "tobi.ferrets.example.com":
  396. * If `app.subdomainOffset` is not set, this.subdomains is
  397. * `["ferrets", "tobi"]`.
  398. * If `app.subdomainOffset` is 3, this.subdomains is `["tobi"]`.
  399. *
  400. * @return {Array}
  401. * @api public
  402. */
  403. get subdomains() {
  404. const offset = this.app.subdomainOffset;
  405. const hostname = this.hostname;
  406. if (net.isIP(hostname)) return [];
  407. return hostname
  408. .split('.')
  409. .reverse()
  410. .slice(offset);
  411. },
  412. /**
  413. * Get accept object.
  414. * Lazily memoized.
  415. *
  416. * @return {Object}
  417. * @api private
  418. */
  419. get accept() {
  420. return this._accept || (this._accept = accepts(this.req));
  421. },
  422. /**
  423. * Set accept object.
  424. *
  425. * @param {Object}
  426. * @api private
  427. */
  428. set accept(obj) {
  429. this._accept = obj;
  430. },
  431. /**
  432. * Check if the given `type(s)` is acceptable, returning
  433. * the best match when true, otherwise `false`, in which
  434. * case you should respond with 406 "Not Acceptable".
  435. *
  436. * The `type` value may be a single mime type string
  437. * such as "application/json", the extension name
  438. * such as "json" or an array `["json", "html", "text/plain"]`. When a list
  439. * or array is given the _best_ match, if any is returned.
  440. *
  441. * Examples:
  442. *
  443. * // Accept: text/html
  444. * this.accepts('html');
  445. * // => "html"
  446. *
  447. * // Accept: text/*, application/json
  448. * this.accepts('html');
  449. * // => "html"
  450. * this.accepts('text/html');
  451. * // => "text/html"
  452. * this.accepts('json', 'text');
  453. * // => "json"
  454. * this.accepts('application/json');
  455. * // => "application/json"
  456. *
  457. * // Accept: text/*, application/json
  458. * this.accepts('image/png');
  459. * this.accepts('png');
  460. * // => false
  461. *
  462. * // Accept: text/*;q=.5, application/json
  463. * this.accepts(['html', 'json']);
  464. * this.accepts('html', 'json');
  465. * // => "json"
  466. *
  467. * @param {String|Array} type(s)...
  468. * @return {String|Array|false}
  469. * @api public
  470. */
  471. accepts(...args) {
  472. return this.accept.types(...args);
  473. },
  474. /**
  475. * Return accepted encodings or best fit based on `encodings`.
  476. *
  477. * Given `Accept-Encoding: gzip, deflate`
  478. * an array sorted by quality is returned:
  479. *
  480. * ['gzip', 'deflate']
  481. *
  482. * @param {String|Array} encoding(s)...
  483. * @return {String|Array}
  484. * @api public
  485. */
  486. acceptsEncodings(...args) {
  487. return this.accept.encodings(...args);
  488. },
  489. /**
  490. * Return accepted charsets or best fit based on `charsets`.
  491. *
  492. * Given `Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5`
  493. * an array sorted by quality is returned:
  494. *
  495. * ['utf-8', 'utf-7', 'iso-8859-1']
  496. *
  497. * @param {String|Array} charset(s)...
  498. * @return {String|Array}
  499. * @api public
  500. */
  501. acceptsCharsets(...args) {
  502. return this.accept.charsets(...args);
  503. },
  504. /**
  505. * Return accepted languages or best fit based on `langs`.
  506. *
  507. * Given `Accept-Language: en;q=0.8, es, pt`
  508. * an array sorted by quality is returned:
  509. *
  510. * ['es', 'pt', 'en']
  511. *
  512. * @param {String|Array} lang(s)...
  513. * @return {Array|String}
  514. * @api public
  515. */
  516. acceptsLanguages(...args) {
  517. return this.accept.languages(...args);
  518. },
  519. /**
  520. * Check if the incoming request contains the "Content-Type"
  521. * header field and if it contains any of the given mime `type`s.
  522. * If there is no request body, `null` is returned.
  523. * If there is no content type, `false` is returned.
  524. * Otherwise, it returns the first `type` that matches.
  525. *
  526. * Examples:
  527. *
  528. * // With Content-Type: text/html; charset=utf-8
  529. * this.is('html'); // => 'html'
  530. * this.is('text/html'); // => 'text/html'
  531. * this.is('text/*', 'application/json'); // => 'text/html'
  532. *
  533. * // When Content-Type is application/json
  534. * this.is('json', 'urlencoded'); // => 'json'
  535. * this.is('application/json'); // => 'application/json'
  536. * this.is('html', 'application/*'); // => 'application/json'
  537. *
  538. * this.is('html'); // => false
  539. *
  540. * @param {String|String[]} [type]
  541. * @param {String[]} [types]
  542. * @return {String|false|null}
  543. * @api public
  544. */
  545. is(type, ...types) {
  546. return typeis(this.req, type, ...types);
  547. },
  548. /**
  549. * Return the request mime type void of
  550. * parameters such as "charset".
  551. *
  552. * @return {String}
  553. * @api public
  554. */
  555. get type() {
  556. const type = this.get('Content-Type');
  557. if (!type) return '';
  558. return type.split(';')[0];
  559. },
  560. /**
  561. * Return request header.
  562. *
  563. * The `Referrer` header field is special-cased,
  564. * both `Referrer` and `Referer` are interchangeable.
  565. *
  566. * Examples:
  567. *
  568. * this.get('Content-Type');
  569. * // => "text/plain"
  570. *
  571. * this.get('content-type');
  572. * // => "text/plain"
  573. *
  574. * this.get('Something');
  575. * // => ''
  576. *
  577. * @param {String} field
  578. * @return {String}
  579. * @api public
  580. */
  581. get(field) {
  582. const req = this.req;
  583. switch (field = field.toLowerCase()) {
  584. case 'referer':
  585. case 'referrer':
  586. return req.headers.referrer || req.headers.referer || '';
  587. default:
  588. return req.headers[field] || '';
  589. }
  590. },
  591. /**
  592. * Inspect implementation.
  593. *
  594. * @return {Object}
  595. * @api public
  596. */
  597. inspect() {
  598. if (!this.req) return;
  599. return this.toJSON();
  600. },
  601. /**
  602. * Return JSON representation.
  603. *
  604. * @return {Object}
  605. * @api public
  606. */
  607. toJSON() {
  608. return only(this, [
  609. 'method',
  610. 'url',
  611. 'header'
  612. ]);
  613. }
  614. };
  615. /**
  616. * Custom inspection implementation for newer Node.js versions.
  617. *
  618. * @return {Object}
  619. * @api public
  620. */
  621. /* istanbul ignore else */
  622. if (util.inspect.custom) {
  623. module.exports[util.inspect.custom] = module.exports.inspect;
  624. }