UNPKG

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