UNPKG

24.1 kBJavaScriptView Raw
1// Copyright 2012 Mark Cavage, Inc. All rights reserved.
2
3'use strict';
4
5const { emitWarning } = require('node:process');
6var url = require('url');
7var sprintf = require('util').format;
8
9var assert = require('assert-plus');
10var mime = require('mime');
11var Negotiator = require('negotiator');
12var uuid = require('uuid');
13
14var dtrace = require('./dtrace');
15
16///-- Helpers
17/**
18 * Creates and sets negotiator on request if one doesn't already exist,
19 * then returns it.
20 *
21 * @private
22 * @function negotiator
23 * @param {Object} req - the request object
24 * @returns {Object} a negotiator
25 */
26function negotiator(req) {
27 var h = req.headers;
28
29 if (!req._negotiator) {
30 req._negotiator = new Negotiator({
31 headers: {
32 accept: h.accept || '*/*',
33 'accept-encoding': h['accept-encoding'] || 'identity'
34 }
35 });
36 }
37
38 return req._negotiator;
39}
40
41///--- API
42
43/**
44 * Patch Request object and extends with extra functionalities
45 *
46 * @private
47 * @function patch
48 * @param {http.IncomingMessage|http2.Http2ServerRequest} Request -
49 * Server Request
50 * @returns {undefined} No return value
51 */
52function patch(Request) {
53 /**
54 * Wraps all of the node
55 * [http.IncomingMessage](https://nodejs.org/api/http.html)
56 * APIs, events and properties, plus the following.
57 * @class Request
58 * @extends http.IncomingMessage
59 */
60
61 ///--- Patches
62
63 /**
64 * Builds an absolute URI for the request.
65 *
66 * @private
67 * @memberof Request
68 * @instance
69 * @function absoluteUri
70 * @param {String} path - a url path
71 * @returns {String} uri
72 */
73 Request.prototype.absoluteUri = function absoluteUri(path) {
74 assert.string(path, 'path');
75
76 var protocol = this.isSecure() ? 'https://' : 'http://';
77 var hostname = this.headers.host;
78 return url.resolve(protocol + hostname + this.path() + '/', path);
79 };
80
81 /**
82 * Check if the Accept header is present, and includes the given type.
83 * When the Accept header is not present true is returned.
84 * Otherwise the given type is matched by an exact match, and then subtypes.
85 *
86 * @public
87 * @memberof Request
88 * @instance
89 * @function accepts
90 * @param {String | String[]} types - an array of accept type headers
91 * @returns {Boolean} is accepteed
92 * @example
93 * <caption>
94 * You may pass the subtype such as html which is then converted internally
95 * to text/html using the mime lookup table:
96 * </caption>
97 * // Accept: text/html
98 * req.accepts('html');
99 * // => true
100 *
101 * // Accept: text/*; application/json
102 * req.accepts('html');
103 * req.accepts('text/html');
104 * req.accepts('text/plain');
105 * req.accepts('application/json');
106 * // => true
107 *
108 * req.accepts('image/png');
109 * req.accepts('png');
110 * // => false
111 */
112 Request.prototype.accepts = function accepts(types) {
113 if (typeof types === 'string') {
114 types = [types];
115 }
116
117 types = types.map(function map(t) {
118 assert.string(t, 'type');
119
120 if (t.indexOf('/') === -1) {
121 t = mime.getType(t);
122 }
123 return t;
124 });
125
126 negotiator(this);
127
128 return this._negotiator.preferredMediaType(types);
129 };
130
131 /**
132 * Checks if the request accepts the encoding type(s) specified.
133 *
134 * @public
135 * @memberof Request
136 * @instance
137 * @function acceptsEncoding
138 * @param {String | String[]} types - an array of accept type headers
139 * @returns {Boolean} is accepted encoding
140 */
141 Request.prototype.acceptsEncoding = function acceptsEncoding(types) {
142 if (typeof types === 'string') {
143 types = [types];
144 }
145
146 assert.arrayOfString(types, 'types');
147
148 negotiator(this);
149
150 return this._negotiator.preferredEncoding(types);
151 };
152
153 /**
154 * Returns the value of the content-length header.
155 *
156 * @private
157 * @memberof Request
158 * @instance
159 * @function getContentLength
160 * @returns {Number} content length
161 */
162 Request.prototype.getContentLength = function getContentLength() {
163 if (this._clen !== undefined) {
164 return this._clen === false ? undefined : this._clen;
165 }
166
167 // We should not attempt to read and parse the body of an
168 // Upgrade request, so force Content-Length to zero:
169 if (this.isUpgradeRequest()) {
170 return 0;
171 }
172
173 var len = this.header('content-length');
174
175 if (!len) {
176 this._clen = false;
177 } else {
178 this._clen = parseInt(len, 10);
179 }
180
181 return this._clen === false ? undefined : this._clen;
182 };
183 /**
184 * Returns the value of the content-length header.
185 * @public
186 * @memberof Request
187 * @instance
188 * @function contentLength
189 * @returns {Number}
190 */
191 Request.prototype.contentLength = Request.prototype.getContentLength;
192
193 /**
194 * Returns the value of the content-type header. If a content-type is not
195 * set, this will return a default value of `application/octet-stream`.
196 *
197 * @private
198 * @memberof Request
199 * @instance
200 * @function getContentType
201 * @returns {String} content type
202 */
203 Request.prototype.getContentType = function getContentType() {
204 if (this._contentType !== undefined) {
205 return this._contentType;
206 }
207
208 var index;
209 var type = this.headers['content-type'];
210
211 if (!type) {
212 // RFC2616 section 7.2.1
213 this._contentType = 'application/octet-stream';
214 } else if ((index = type.indexOf(';')) === -1) {
215 this._contentType = type;
216 } else {
217 this._contentType = type.substring(0, index);
218 }
219
220 // #877 content-types need to be case insensitive.
221 this._contentType = this._contentType.toLowerCase();
222
223 return this._contentType;
224 };
225
226 /**
227 * Returns the value of the content-type header. If a content-type is not
228 * set, this will return a default value of `application/octet-stream`
229 * @public
230 * @memberof Request
231 * @instance
232 * @function getContentType
233 * @returns {String} content type
234 */
235 Request.prototype.contentType = Request.prototype.getContentType;
236
237 /**
238 * Returns a Date object representing when the request was setup.
239 * Like `time()`, but returns a Date object.
240 *
241 * @public
242 * @memberof Request
243 * @instance
244 * @function date
245 * @returns {Date} date when request began being processed
246 */
247 Request.prototype.date = function date() {
248 return this._date;
249 };
250
251 /**
252 * Retrieves the complete URI requested by the client.
253 *
254 * @private
255 * @memberof Request
256 * @instance
257 * @function getHref
258 * @returns {String} URI
259 */
260 Request.prototype.getHref = function getHref() {
261 return this.getUrl().href;
262 };
263
264 /**
265 * Returns the full requested URL.
266 * @public
267 * @memberof Request
268 * @instance
269 * @function href
270 * @returns {String}
271 * @example
272 * // incoming request is http://localhost:3000/foo/bar?a=1
273 * server.get('/:x/bar', function(req, res, next) {
274 * console.warn(req.href());
275 * // => /foo/bar/?a=1
276 * });
277 */
278 Request.prototype.href = Request.prototype.getHref;
279
280 /**
281 * Retrieves the request uuid. was created when the request was setup.
282 *
283 * @private
284 * @memberof Request
285 * @instance
286 * @function getId
287 * @returns {String} id
288 */
289 Request.prototype.getId = function getId() {
290 if (this._id !== undefined) {
291 return this._id;
292 }
293
294 this._id = uuid.v4();
295
296 return this._id;
297 };
298
299 /**
300 * Returns the request id. If a `reqId` value is passed in,
301 * this will become the request’s new id. The request id is immutable,
302 * and can only be set once. Attempting to set the request id more than
303 * once will cause restify to throw.
304 *
305 * @public
306 * @memberof Request
307 * @instance
308 * @function id
309 * @param {String} reqId - request id
310 * @returns {String} id
311 */
312 Request.prototype.id = function id(reqId) {
313 var self = this;
314
315 if (reqId) {
316 if (self._id) {
317 throw new Error(
318 'request id is immutable, cannot be set again!'
319 );
320 } else {
321 assert.string(reqId, 'reqId');
322 self._id = reqId;
323 return self._id;
324 }
325 }
326
327 return self.getId();
328 };
329
330 /**
331 * Retrieves the cleaned up url path.
332 * e.g., /foo?a=1 => /foo
333 *
334 * @private
335 * @memberof Request
336 * @instance
337 * @function getPath
338 * @returns {String} path
339 */
340 Request.prototype.getPath = function getPath() {
341 return this.getUrl().pathname;
342 };
343
344 /**
345 * Returns the cleaned up requested URL.
346 * @public
347 * @memberof Request
348 * @instance
349 * @function getPath
350 * @returns {String}
351 * @example
352 * // incoming request is http://localhost:3000/foo/bar?a=1
353 * server.get('/:x/bar', function(req, res, next) {
354 * console.warn(req.path());
355 * // => /foo/bar
356 * });
357 */
358 Request.prototype.path = Request.prototype.getPath;
359
360 /**
361 * Returns the raw query string. Returns empty string
362 * if no query string is found.
363 *
364 * @public
365 * @memberof Request
366 * @instance
367 * @function getQuery
368 * @returns {String} query
369 * @example
370 * // incoming request is /foo?a=1
371 * req.getQuery();
372 * // => 'a=1'
373 * @example
374 * <caption>
375 * If the queryParser plugin is used, the parsed query string is
376 * available under the req.query:
377 * </caption>
378 * // incoming request is /foo?a=1
379 * server.use(restify.plugins.queryParser());
380 * req.query;
381 * // => { a: 1 }
382 */
383 Request.prototype.getQuery = function getQuery() {
384 // always return a string, because this is the raw query string.
385 // if the queryParser plugin is used, req.query will provide an empty
386 // object fallback.
387 return this.getUrl().query || '';
388 };
389
390 /**
391 * Returns the raw query string. Returns empty string
392 * if no query string is found
393 * @private
394 * @memberof Request
395 * @instance
396 * @function query
397 * @returns {String}
398 */
399 Request.prototype.query = Request.prototype.getQuery;
400
401 /**
402 * The number of ms since epoch of when this request began being processed.
403 * Like date(), but returns a number.
404 *
405 * @public
406 * @memberof Request
407 * @instance
408 * @function time
409 * @returns {Number} time when request began being processed in epoch:
410 * ellapsed milliseconds since
411 * January 1, 1970, 00:00:00 UTC
412 */
413 Request.prototype.time = function time() {
414 return this._date.getTime();
415 };
416
417 /**
418 * returns a parsed URL object.
419 *
420 * @private
421 * @memberof Request
422 * @instance
423 * @function getUrl
424 * @returns {Object} url
425 */
426 Request.prototype.getUrl = function getUrl() {
427 if (this._cacheURL !== this.url) {
428 this._url = url.parse(this.url);
429 this._cacheURL = this.url;
430 }
431 return this._url;
432 };
433
434 /**
435 * Returns the accept-version header.
436 *
437 * @private
438 * @memberof Request
439 * @instance
440 * @function getVersion
441 * @returns {String} version
442 */
443 Request.prototype.getVersion = function getVersion() {
444 if (this._version !== undefined) {
445 return this._version;
446 }
447
448 this._version =
449 this.headers['accept-version'] ||
450 this.headers['x-api-version'] ||
451 '*';
452
453 return this._version;
454 };
455
456 /**
457 * Returns the accept-version header.
458 * @public
459 * @memberof Request
460 * @instance
461 * @function version
462 * @returns {String}
463 */
464 Request.prototype.version = Request.prototype.getVersion;
465
466 /**
467 * Returns the version of the route that matched.
468 *
469 * @private
470 * @memberof Request
471 * @instance
472 * @function matchedVersion
473 * @returns {String} version
474 */
475 Request.prototype.matchedVersion = function matchedVersion() {
476 if (this._matchedVersion !== undefined) {
477 return this._matchedVersion;
478 } else {
479 return this.version();
480 }
481 };
482
483 /**
484 * Get the case-insensitive request header key,
485 * and optionally provide a default value (express-compliant).
486 * Returns any header off the request. also, 'correct' any
487 * correctly spelled 'referrer' header to the actual spelling used.
488 *
489 * @public
490 * @memberof Request
491 * @instance
492 * @function header
493 * @param {String} key - the key of the header
494 * @param {String} [defaultValue] - default value if header isn't
495 * found on the req
496 * @returns {String} header value
497 * @example
498 * req.header('Host');
499 * req.header('HOST');
500 * req.header('Accept', '*\/*');
501 */
502 Request.prototype.header = function header(key, defaultValue) {
503 assert.string(key, 'key');
504
505 key = key.toLowerCase();
506
507 if (key === 'referer' || key === 'referrer') {
508 key = 'referer';
509 }
510
511 return this.headers[key] || defaultValue;
512 };
513
514 /**
515 * Returns any trailer header off the request. Also, 'correct' any
516 * correctly spelled 'referrer' header to the actual spelling used.
517 *
518 * @public
519 * @memberof Request
520 * @instance
521 * @function trailer
522 * @param {String} name - the name of the header
523 * @param {String} value - default value if header isn't found on the req
524 * @returns {String} trailer value
525 */
526 Request.prototype.trailer = function trailer(name, value) {
527 assert.string(name, 'name');
528 name = name.toLowerCase();
529
530 if (name === 'referer' || name === 'referrer') {
531 name = 'referer';
532 }
533
534 return (this.trailers || {})[name] || value;
535 };
536
537 /**
538 * Check if the incoming request contains the `Content-Type` header field,
539 * and if it contains the given mime type.
540 *
541 * @public
542 * @memberof Request
543 * @instance
544 * @function is
545 * @param {String} type - a content-type header value
546 * @returns {Boolean} is content-type header
547 * @example
548 * // With Content-Type: text/html; charset=utf-8
549 * req.is('html');
550 * req.is('text/html');
551 * // => true
552 *
553 * // When Content-Type is application/json
554 * req.is('json');
555 * req.is('application/json');
556 * // => true
557 *
558 * req.is('html');
559 * // => false
560 */
561 Request.prototype.is = function is(type) {
562 assert.string(type, 'type');
563
564 var contentType = this.getContentType();
565 var matches = true;
566
567 if (!contentType) {
568 return false;
569 }
570
571 if (type.indexOf('/') === -1) {
572 type = mime.getType(type);
573 }
574
575 if (type.indexOf('*') !== -1) {
576 type = type.split('/');
577 contentType = contentType.split('/');
578 matches &= type[0] === '*' || type[0] === contentType[0];
579 matches &= type[1] === '*' || type[1] === contentType[1];
580 } else {
581 matches = contentType === type;
582 }
583
584 return matches;
585 };
586
587 /**
588 * Check if the incoming request is chunked.
589 *
590 * @public
591 * @memberof Request
592 * @instance
593 * @function isChunked
594 * @returns {Boolean} is chunked
595 */
596 Request.prototype.isChunked = function isChunked() {
597 return this.headers['transfer-encoding'] === 'chunked';
598 };
599
600 /**
601 * Check if the incoming request is kept alive.
602 *
603 * @public
604 * @memberof Request
605 * @instance
606 * @function isKeepAlive
607 * @returns {Boolean} is keep alive
608 */
609 Request.prototype.isKeepAlive = function isKeepAlive() {
610 if (this._keepAlive !== undefined) {
611 return this._keepAlive;
612 }
613
614 if (this.headers.connection) {
615 this._keepAlive = /keep-alive/i.test(this.headers.connection);
616 } else {
617 this._keepAlive = this.httpVersion === '1.0' ? false : true;
618 }
619
620 return this._keepAlive;
621 };
622
623 /**
624 * Check if the incoming request is encrypted.
625 *
626 * @public
627 * @memberof Request
628 * @instance
629 * @function isSecure
630 * @returns {Boolean} is secure
631 */
632 Request.prototype.isSecure = function isSecure() {
633 if (this._secure !== undefined) {
634 return this._secure;
635 }
636
637 this._secure = this.connection.encrypted ? true : false;
638 return this._secure;
639 };
640
641 /**
642 * Check if the incoming request has been upgraded.
643 *
644 * @public
645 * @memberof Request
646 * @instance
647 * @function isUpgradeRequest
648 * @returns {Boolean} is upgraded
649 */
650 Request.prototype.isUpgradeRequest = function isUpgradeRequest() {
651 if (this._upgradeRequest !== undefined) {
652 return this._upgradeRequest;
653 } else {
654 return false;
655 }
656 };
657
658 /**
659 * Check if the incoming request is an upload verb.
660 *
661 * @public
662 * @memberof Request
663 * @instance
664 * @function isUpload
665 * @returns {Boolean} is upload
666 */
667 Request.prototype.isUpload = function isUpload() {
668 var m = this.method;
669 return m === 'PATCH' || m === 'POST' || m === 'PUT';
670 };
671
672 /**
673 * toString serialization
674 *
675 * @public
676 * @memberof Request
677 * @instance
678 * @function toString
679 * @returns {String} serialized request
680 */
681 Request.prototype.toString = function toString() {
682 var headers = '';
683 var self = this;
684 var str;
685
686 Object.keys(this.headers).forEach(function forEach(k) {
687 headers += sprintf('%s: %s\n', k, self.headers[k]);
688 });
689
690 str = sprintf(
691 '%s %s HTTP/%s\n%s',
692 this.method,
693 this.url,
694 this.httpVersion,
695 headers
696 );
697
698 return str;
699 };
700
701 /**
702 * Returns the user-agent header.
703 *
704 * @public
705 * @memberof Request
706 * @instance
707 * @function userAgent
708 * @returns {String} user agent
709 */
710 Request.prototype.userAgent = function userAgent() {
711 return this.headers['user-agent'];
712 };
713
714 /**
715 * Start the timer for a request handler.
716 * By default, restify uses calls this automatically for all handlers
717 * registered in your handler chain.
718 * However, this can be called manually for nested functions inside the
719 * handler chain to record timing information.
720 *
721 * @public
722 * @memberof Request
723 * @instance
724 * @function startHandlerTimer
725 * @param {String} handlerName - The name of the handler.
726 * @returns {undefined} no return value
727 * @example
728 * <caption>
729 * You must explicitly invoke
730 * endHandlerTimer() after invoking this function. Otherwise timing
731 * information will be inaccurate.
732 * </caption>
733 * server.get('/', function fooHandler(req, res, next) {
734 * vasync.pipeline({
735 * funcs: [
736 * function nestedHandler1(req, res, next) {
737 * req.startHandlerTimer('nestedHandler1');
738 * // do something
739 * req.endHandlerTimer('nestedHandler1');
740 * return next();
741 * },
742 * function nestedHandler1(req, res, next) {
743 * req.startHandlerTimer('nestedHandler2');
744 * // do something
745 * req.endHandlerTimer('nestedHandler2');
746 * return next();
747 *
748 * }...
749 * ]...
750 * }, next);
751 * });
752 */
753 Request.prototype.startHandlerTimer = function startHandlerTimer(
754 handlerName
755 ) {
756 var self = this;
757
758 // For nested handlers, we prepend the top level handler func name
759 var name =
760 self._currentHandler === handlerName
761 ? handlerName
762 : self._currentHandler + '-' + handlerName;
763
764 if (!self._timerMap) {
765 self._timerMap = {};
766 }
767
768 self._timerMap[name] = process.hrtime();
769
770 if (self.dtrace) {
771 dtrace._rstfy_probes['handler-start'].fire(function fire() {
772 return [
773 self.serverName,
774 self._currentRoute, // set in server._run
775 name,
776 self._dtraceId
777 ];
778 });
779 }
780 };
781
782 /**
783 * End the timer for a request handler.
784 * You must invoke this function if you called `startRequestHandler` on a
785 * handler. Otherwise the time recorded will be incorrect.
786 *
787 * @public
788 * @memberof Request
789 * @instance
790 * @function endHandlerTimer
791 * @param {String} handlerName - The name of the handler.
792 * @returns {undefined} no return value
793 */
794 Request.prototype.endHandlerTimer = function endHandlerTimer(handlerName) {
795 var self = this;
796
797 // For nested handlers, we prepend the top level handler func name
798 var name =
799 self._currentHandler === handlerName
800 ? handlerName
801 : self._currentHandler + '-' + handlerName;
802
803 if (!self.timers) {
804 self.timers = [];
805 }
806
807 self._timerMap[name] = process.hrtime(self._timerMap[name]);
808 self.timers.push({
809 name: name,
810 time: self._timerMap[name]
811 });
812
813 if (self.dtrace) {
814 dtrace._rstfy_probes['handler-done'].fire(function fire() {
815 return [
816 self.serverName,
817 self._currentRoute, // set in server._run
818 name,
819 self._dtraceId
820 ];
821 });
822 }
823 };
824
825 /**
826 * Returns the connection state of the request. Current possible values are:
827 * - `close` - when the request has been closed by the clien
828 *
829 * @public
830 * @memberof Request
831 * @instance
832 * @function connectionState
833 * @returns {String} connection state (`"close"`)
834 */
835 Request.prototype.connectionState = function connectionState() {
836 var self = this;
837 return self._connectionState;
838 };
839
840 /**
841 * Returns true when connection state is "close"
842 *
843 * @private
844 * @memberof Request
845 * @instance
846 * @function closed
847 * @returns {Boolean} is closed
848 */
849 Request.prototype.closed = function closed() {
850 emitWarning(
851 'restify req.closed is deprecated, will be removed on Restify 10',
852 'RestifyDeprecationWarning',
853 'RestifyDEPReqClosed'
854 );
855 var self = this;
856 return self.connectionState() === 'close';
857 };
858
859 /**
860 * Returns the route object to which the current request was matched to.
861 *
862 * @public
863 * @memberof Request
864 * @instance
865 * @function getRoute
866 * @returns {Object} route
867 * @example
868 * <caption>Route info object structure:</caption>
869 * {
870 * path: '/ping/:name',
871 * method: 'GET',
872 * versions: [],
873 * name: 'getpingname'
874 * }
875 */
876 Request.prototype.getRoute = function getRoute() {
877 var self = this;
878 return self.route;
879 };
880}
881
882module.exports = patch;