UNPKG

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