1 |
|
2 | ;
|
3 |
|
4 | /**
|
5 | * Module dependencies.
|
6 | */
|
7 |
|
8 | const URL = require('url').URL;
|
9 | const net = require('net');
|
10 | const accepts = require('accepts');
|
11 | const contentType = require('content-type');
|
12 | const stringify = require('url').format;
|
13 | const parse = require('parseurl');
|
14 | const qs = require('querystring');
|
15 | const typeis = require('type-is');
|
16 | const fresh = require('fresh');
|
17 | const only = require('only');
|
18 | const util = require('util');
|
19 |
|
20 | const IP = Symbol('context#ip');
|
21 |
|
22 | /**
|
23 | * Prototype.
|
24 | */
|
25 |
|
26 | module.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 */
|
722 | if (util.inspect.custom) {
|
723 | module.exports[util.inspect.custom] = module.exports.inspect;
|
724 | }
|