source: frontend/node_modules/send/index.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 11 days ago

Fix frontend appearance

  • Property mode set to 100644
File size: 22.9 KB
Line 
1/*!
2 * send
3 * Copyright(c) 2012 TJ Holowaychuk
4 * Copyright(c) 2014-2022 Douglas Christopher Wilson
5 * MIT Licensed
6 */
7
8'use strict'
9
10/**
11 * Module dependencies.
12 * @private
13 */
14
15var createError = require('http-errors')
16var debug = require('debug')('send')
17var deprecate = require('depd')('send')
18var destroy = require('destroy')
19var encodeUrl = require('encodeurl')
20var escapeHtml = require('escape-html')
21var etag = require('etag')
22var fresh = require('fresh')
23var fs = require('fs')
24var mime = require('mime')
25var ms = require('ms')
26var onFinished = require('on-finished')
27var parseRange = require('range-parser')
28var path = require('path')
29var statuses = require('statuses')
30var Stream = require('stream')
31var util = require('util')
32
33/**
34 * Path function references.
35 * @private
36 */
37
38var extname = path.extname
39var join = path.join
40var normalize = path.normalize
41var resolve = path.resolve
42var sep = path.sep
43
44/**
45 * Regular expression for identifying a bytes Range header.
46 * @private
47 */
48
49var BYTES_RANGE_REGEXP = /^ *bytes=/
50
51/**
52 * Maximum value allowed for the max age.
53 * @private
54 */
55
56var MAX_MAXAGE = 60 * 60 * 24 * 365 * 1000 // 1 year
57
58/**
59 * Regular expression to match a path with a directory up component.
60 * @private
61 */
62
63var UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/
64
65/**
66 * Module exports.
67 * @public
68 */
69
70module.exports = send
71module.exports.mime = mime
72
73/**
74 * Return a `SendStream` for `req` and `path`.
75 *
76 * @param {object} req
77 * @param {string} path
78 * @param {object} [options]
79 * @return {SendStream}
80 * @public
81 */
82
83function send (req, path, options) {
84 return new SendStream(req, path, options)
85}
86
87/**
88 * Initialize a `SendStream` with the given `path`.
89 *
90 * @param {Request} req
91 * @param {String} path
92 * @param {object} [options]
93 * @private
94 */
95
96function SendStream (req, path, options) {
97 Stream.call(this)
98
99 var opts = options || {}
100
101 this.options = opts
102 this.path = path
103 this.req = req
104
105 this._acceptRanges = opts.acceptRanges !== undefined
106 ? Boolean(opts.acceptRanges)
107 : true
108
109 this._cacheControl = opts.cacheControl !== undefined
110 ? Boolean(opts.cacheControl)
111 : true
112
113 this._etag = opts.etag !== undefined
114 ? Boolean(opts.etag)
115 : true
116
117 this._dotfiles = opts.dotfiles !== undefined
118 ? opts.dotfiles
119 : 'ignore'
120
121 if (this._dotfiles !== 'ignore' && this._dotfiles !== 'allow' && this._dotfiles !== 'deny') {
122 throw new TypeError('dotfiles option must be "allow", "deny", or "ignore"')
123 }
124
125 this._hidden = Boolean(opts.hidden)
126
127 if (opts.hidden !== undefined) {
128 deprecate('hidden: use dotfiles: \'' + (this._hidden ? 'allow' : 'ignore') + '\' instead')
129 }
130
131 // legacy support
132 if (opts.dotfiles === undefined) {
133 this._dotfiles = undefined
134 }
135
136 this._extensions = opts.extensions !== undefined
137 ? normalizeList(opts.extensions, 'extensions option')
138 : []
139
140 this._immutable = opts.immutable !== undefined
141 ? Boolean(opts.immutable)
142 : false
143
144 this._index = opts.index !== undefined
145 ? normalizeList(opts.index, 'index option')
146 : ['index.html']
147
148 this._lastModified = opts.lastModified !== undefined
149 ? Boolean(opts.lastModified)
150 : true
151
152 this._maxage = opts.maxAge || opts.maxage
153 this._maxage = typeof this._maxage === 'string'
154 ? ms(this._maxage)
155 : Number(this._maxage)
156 this._maxage = !isNaN(this._maxage)
157 ? Math.min(Math.max(0, this._maxage), MAX_MAXAGE)
158 : 0
159
160 this._root = opts.root
161 ? resolve(opts.root)
162 : null
163
164 if (!this._root && opts.from) {
165 this.from(opts.from)
166 }
167}
168
169/**
170 * Inherits from `Stream`.
171 */
172
173util.inherits(SendStream, Stream)
174
175/**
176 * Enable or disable etag generation.
177 *
178 * @param {Boolean} val
179 * @return {SendStream}
180 * @api public
181 */
182
183SendStream.prototype.etag = deprecate.function(function etag (val) {
184 this._etag = Boolean(val)
185 debug('etag %s', this._etag)
186 return this
187}, 'send.etag: pass etag as option')
188
189/**
190 * Enable or disable "hidden" (dot) files.
191 *
192 * @param {Boolean} path
193 * @return {SendStream}
194 * @api public
195 */
196
197SendStream.prototype.hidden = deprecate.function(function hidden (val) {
198 this._hidden = Boolean(val)
199 this._dotfiles = undefined
200 debug('hidden %s', this._hidden)
201 return this
202}, 'send.hidden: use dotfiles option')
203
204/**
205 * Set index `paths`, set to a falsy
206 * value to disable index support.
207 *
208 * @param {String|Boolean|Array} paths
209 * @return {SendStream}
210 * @api public
211 */
212
213SendStream.prototype.index = deprecate.function(function index (paths) {
214 var index = !paths ? [] : normalizeList(paths, 'paths argument')
215 debug('index %o', paths)
216 this._index = index
217 return this
218}, 'send.index: pass index as option')
219
220/**
221 * Set root `path`.
222 *
223 * @param {String} path
224 * @return {SendStream}
225 * @api public
226 */
227
228SendStream.prototype.root = function root (path) {
229 this._root = resolve(String(path))
230 debug('root %s', this._root)
231 return this
232}
233
234SendStream.prototype.from = deprecate.function(SendStream.prototype.root,
235 'send.from: pass root as option')
236
237SendStream.prototype.root = deprecate.function(SendStream.prototype.root,
238 'send.root: pass root as option')
239
240/**
241 * Set max-age to `maxAge`.
242 *
243 * @param {Number} maxAge
244 * @return {SendStream}
245 * @api public
246 */
247
248SendStream.prototype.maxage = deprecate.function(function maxage (maxAge) {
249 this._maxage = typeof maxAge === 'string'
250 ? ms(maxAge)
251 : Number(maxAge)
252 this._maxage = !isNaN(this._maxage)
253 ? Math.min(Math.max(0, this._maxage), MAX_MAXAGE)
254 : 0
255 debug('max-age %d', this._maxage)
256 return this
257}, 'send.maxage: pass maxAge as option')
258
259/**
260 * Emit error with `status`.
261 *
262 * @param {number} status
263 * @param {Error} [err]
264 * @private
265 */
266
267SendStream.prototype.error = function error (status, err) {
268 // emit if listeners instead of responding
269 if (hasListeners(this, 'error')) {
270 return this.emit('error', createHttpError(status, err))
271 }
272
273 var res = this.res
274 var msg = statuses.message[status] || String(status)
275 var doc = createHtmlDocument('Error', escapeHtml(msg))
276
277 // clear existing headers
278 clearHeaders(res)
279
280 // add error headers
281 if (err && err.headers) {
282 setHeaders(res, err.headers)
283 }
284
285 // send basic response
286 res.statusCode = status
287 res.setHeader('Content-Type', 'text/html; charset=UTF-8')
288 res.setHeader('Content-Length', Buffer.byteLength(doc))
289 res.setHeader('Content-Security-Policy', "default-src 'none'")
290 res.setHeader('X-Content-Type-Options', 'nosniff')
291 res.end(doc)
292}
293
294/**
295 * Check if the pathname ends with "/".
296 *
297 * @return {boolean}
298 * @private
299 */
300
301SendStream.prototype.hasTrailingSlash = function hasTrailingSlash () {
302 return this.path[this.path.length - 1] === '/'
303}
304
305/**
306 * Check if this is a conditional GET request.
307 *
308 * @return {Boolean}
309 * @api private
310 */
311
312SendStream.prototype.isConditionalGET = function isConditionalGET () {
313 return this.req.headers['if-match'] ||
314 this.req.headers['if-unmodified-since'] ||
315 this.req.headers['if-none-match'] ||
316 this.req.headers['if-modified-since']
317}
318
319/**
320 * Check if the request preconditions failed.
321 *
322 * @return {boolean}
323 * @private
324 */
325
326SendStream.prototype.isPreconditionFailure = function isPreconditionFailure () {
327 var req = this.req
328 var res = this.res
329
330 // if-match
331 var match = req.headers['if-match']
332 if (match) {
333 var etag = res.getHeader('ETag')
334 return !etag || (match !== '*' && parseTokenList(match).every(function (match) {
335 return match !== etag && match !== 'W/' + etag && 'W/' + match !== etag
336 }))
337 }
338
339 // if-unmodified-since
340 var unmodifiedSince = parseHttpDate(req.headers['if-unmodified-since'])
341 if (!isNaN(unmodifiedSince)) {
342 var lastModified = parseHttpDate(res.getHeader('Last-Modified'))
343 return isNaN(lastModified) || lastModified > unmodifiedSince
344 }
345
346 return false
347}
348
349/**
350 * Strip various content header fields for a change in entity.
351 *
352 * @private
353 */
354
355SendStream.prototype.removeContentHeaderFields = function removeContentHeaderFields () {
356 var res = this.res
357
358 res.removeHeader('Content-Encoding')
359 res.removeHeader('Content-Language')
360 res.removeHeader('Content-Length')
361 res.removeHeader('Content-Range')
362 res.removeHeader('Content-Type')
363}
364
365/**
366 * Respond with 304 not modified.
367 *
368 * @api private
369 */
370
371SendStream.prototype.notModified = function notModified () {
372 var res = this.res
373 debug('not modified')
374 this.removeContentHeaderFields()
375 res.statusCode = 304
376 res.end()
377}
378
379/**
380 * Raise error that headers already sent.
381 *
382 * @api private
383 */
384
385SendStream.prototype.headersAlreadySent = function headersAlreadySent () {
386 var err = new Error('Can\'t set headers after they are sent.')
387 debug('headers already sent')
388 this.error(500, err)
389}
390
391/**
392 * Check if the request is cacheable, aka
393 * responded with 2xx or 304 (see RFC 2616 section 14.2{5,6}).
394 *
395 * @return {Boolean}
396 * @api private
397 */
398
399SendStream.prototype.isCachable = function isCachable () {
400 var statusCode = this.res.statusCode
401 return (statusCode >= 200 && statusCode < 300) ||
402 statusCode === 304
403}
404
405/**
406 * Handle stat() error.
407 *
408 * @param {Error} error
409 * @private
410 */
411
412SendStream.prototype.onStatError = function onStatError (error) {
413 switch (error.code) {
414 case 'ENAMETOOLONG':
415 case 'ENOENT':
416 case 'ENOTDIR':
417 this.error(404, error)
418 break
419 default:
420 this.error(500, error)
421 break
422 }
423}
424
425/**
426 * Check if the cache is fresh.
427 *
428 * @return {Boolean}
429 * @api private
430 */
431
432SendStream.prototype.isFresh = function isFresh () {
433 return fresh(this.req.headers, {
434 etag: this.res.getHeader('ETag'),
435 'last-modified': this.res.getHeader('Last-Modified')
436 })
437}
438
439/**
440 * Check if the range is fresh.
441 *
442 * @return {Boolean}
443 * @api private
444 */
445
446SendStream.prototype.isRangeFresh = function isRangeFresh () {
447 var ifRange = this.req.headers['if-range']
448
449 if (!ifRange) {
450 return true
451 }
452
453 // if-range as etag
454 if (ifRange.indexOf('"') !== -1) {
455 var etag = this.res.getHeader('ETag')
456 return Boolean(etag && ifRange.indexOf(etag) !== -1)
457 }
458
459 // if-range as modified date
460 var lastModified = this.res.getHeader('Last-Modified')
461 return parseHttpDate(lastModified) <= parseHttpDate(ifRange)
462}
463
464/**
465 * Redirect to path.
466 *
467 * @param {string} path
468 * @private
469 */
470
471SendStream.prototype.redirect = function redirect (path) {
472 var res = this.res
473
474 if (hasListeners(this, 'directory')) {
475 this.emit('directory', res, path)
476 return
477 }
478
479 if (this.hasTrailingSlash()) {
480 this.error(403)
481 return
482 }
483
484 var loc = encodeUrl(collapseLeadingSlashes(this.path + '/'))
485 var doc = createHtmlDocument('Redirecting', 'Redirecting to ' + escapeHtml(loc))
486
487 // redirect
488 res.statusCode = 301
489 res.setHeader('Content-Type', 'text/html; charset=UTF-8')
490 res.setHeader('Content-Length', Buffer.byteLength(doc))
491 res.setHeader('Content-Security-Policy', "default-src 'none'")
492 res.setHeader('X-Content-Type-Options', 'nosniff')
493 res.setHeader('Location', loc)
494 res.end(doc)
495}
496
497/**
498 * Pipe to `res.
499 *
500 * @param {Stream} res
501 * @return {Stream} res
502 * @api public
503 */
504
505SendStream.prototype.pipe = function pipe (res) {
506 // root path
507 var root = this._root
508
509 // references
510 this.res = res
511
512 // decode the path
513 var path = decode(this.path)
514 if (path === -1) {
515 this.error(400)
516 return res
517 }
518
519 // null byte(s)
520 if (~path.indexOf('\0')) {
521 this.error(400)
522 return res
523 }
524
525 var parts
526 if (root !== null) {
527 // normalize
528 if (path) {
529 path = normalize('.' + sep + path)
530 }
531
532 // malicious path
533 if (UP_PATH_REGEXP.test(path)) {
534 debug('malicious path "%s"', path)
535 this.error(403)
536 return res
537 }
538
539 // explode path parts
540 parts = path.split(sep)
541
542 // join / normalize from optional root dir
543 path = normalize(join(root, path))
544 } else {
545 // ".." is malicious without "root"
546 if (UP_PATH_REGEXP.test(path)) {
547 debug('malicious path "%s"', path)
548 this.error(403)
549 return res
550 }
551
552 // explode path parts
553 parts = normalize(path).split(sep)
554
555 // resolve the path
556 path = resolve(path)
557 }
558
559 // dotfile handling
560 if (containsDotFile(parts)) {
561 var access = this._dotfiles
562
563 // legacy support
564 if (access === undefined) {
565 access = parts[parts.length - 1][0] === '.'
566 ? (this._hidden ? 'allow' : 'ignore')
567 : 'allow'
568 }
569
570 debug('%s dotfile "%s"', access, path)
571 switch (access) {
572 case 'allow':
573 break
574 case 'deny':
575 this.error(403)
576 return res
577 case 'ignore':
578 default:
579 this.error(404)
580 return res
581 }
582 }
583
584 // index file support
585 if (this._index.length && this.hasTrailingSlash()) {
586 this.sendIndex(path)
587 return res
588 }
589
590 this.sendFile(path)
591 return res
592}
593
594/**
595 * Transfer `path`.
596 *
597 * @param {String} path
598 * @api public
599 */
600
601SendStream.prototype.send = function send (path, stat) {
602 var len = stat.size
603 var options = this.options
604 var opts = {}
605 var res = this.res
606 var req = this.req
607 var ranges = req.headers.range
608 var offset = options.start || 0
609
610 if (headersSent(res)) {
611 // impossible to send now
612 this.headersAlreadySent()
613 return
614 }
615
616 debug('pipe "%s"', path)
617
618 // set header fields
619 this.setHeader(path, stat)
620
621 // set content-type
622 this.type(path)
623
624 // conditional GET support
625 if (this.isConditionalGET()) {
626 if (this.isPreconditionFailure()) {
627 this.error(412)
628 return
629 }
630
631 if (this.isCachable() && this.isFresh()) {
632 this.notModified()
633 return
634 }
635 }
636
637 // adjust len to start/end options
638 len = Math.max(0, len - offset)
639 if (options.end !== undefined) {
640 var bytes = options.end - offset + 1
641 if (len > bytes) len = bytes
642 }
643
644 // Range support
645 if (this._acceptRanges && BYTES_RANGE_REGEXP.test(ranges)) {
646 // parse
647 ranges = parseRange(len, ranges, {
648 combine: true
649 })
650
651 // If-Range support
652 if (!this.isRangeFresh()) {
653 debug('range stale')
654 ranges = -2
655 }
656
657 // unsatisfiable
658 if (ranges === -1) {
659 debug('range unsatisfiable')
660
661 // Content-Range
662 res.setHeader('Content-Range', contentRange('bytes', len))
663
664 // 416 Requested Range Not Satisfiable
665 return this.error(416, {
666 headers: { 'Content-Range': res.getHeader('Content-Range') }
667 })
668 }
669
670 // valid (syntactically invalid/multiple ranges are treated as a regular response)
671 if (ranges !== -2 && ranges.length === 1) {
672 debug('range %j', ranges)
673
674 // Content-Range
675 res.statusCode = 206
676 res.setHeader('Content-Range', contentRange('bytes', len, ranges[0]))
677
678 // adjust for requested range
679 offset += ranges[0].start
680 len = ranges[0].end - ranges[0].start + 1
681 }
682 }
683
684 // clone options
685 for (var prop in options) {
686 opts[prop] = options[prop]
687 }
688
689 // set read options
690 opts.start = offset
691 opts.end = Math.max(offset, offset + len - 1)
692
693 // content-length
694 res.setHeader('Content-Length', len)
695
696 // HEAD support
697 if (req.method === 'HEAD') {
698 res.end()
699 return
700 }
701
702 this.stream(path, opts)
703}
704
705/**
706 * Transfer file for `path`.
707 *
708 * @param {String} path
709 * @api private
710 */
711SendStream.prototype.sendFile = function sendFile (path) {
712 var i = 0
713 var self = this
714
715 debug('stat "%s"', path)
716 fs.stat(path, function onstat (err, stat) {
717 if (err && err.code === 'ENOENT' && !extname(path) && path[path.length - 1] !== sep) {
718 // not found, check extensions
719 return next(err)
720 }
721 if (err) return self.onStatError(err)
722 if (stat.isDirectory()) return self.redirect(path)
723 self.emit('file', path, stat)
724 self.send(path, stat)
725 })
726
727 function next (err) {
728 if (self._extensions.length <= i) {
729 return err
730 ? self.onStatError(err)
731 : self.error(404)
732 }
733
734 var p = path + '.' + self._extensions[i++]
735
736 debug('stat "%s"', p)
737 fs.stat(p, function (err, stat) {
738 if (err) return next(err)
739 if (stat.isDirectory()) return next()
740 self.emit('file', p, stat)
741 self.send(p, stat)
742 })
743 }
744}
745
746/**
747 * Transfer index for `path`.
748 *
749 * @param {String} path
750 * @api private
751 */
752SendStream.prototype.sendIndex = function sendIndex (path) {
753 var i = -1
754 var self = this
755
756 function next (err) {
757 if (++i >= self._index.length) {
758 if (err) return self.onStatError(err)
759 return self.error(404)
760 }
761
762 var p = join(path, self._index[i])
763
764 debug('stat "%s"', p)
765 fs.stat(p, function (err, stat) {
766 if (err) return next(err)
767 if (stat.isDirectory()) return next()
768 self.emit('file', p, stat)
769 self.send(p, stat)
770 })
771 }
772
773 next()
774}
775
776/**
777 * Stream `path` to the response.
778 *
779 * @param {String} path
780 * @param {Object} options
781 * @api private
782 */
783
784SendStream.prototype.stream = function stream (path, options) {
785 var self = this
786 var res = this.res
787
788 // pipe
789 var stream = fs.createReadStream(path, options)
790 this.emit('stream', stream)
791 stream.pipe(res)
792
793 // cleanup
794 function cleanup () {
795 destroy(stream, true)
796 }
797
798 // response finished, cleanup
799 onFinished(res, cleanup)
800
801 // error handling
802 stream.on('error', function onerror (err) {
803 // clean up stream early
804 cleanup()
805
806 // error
807 self.onStatError(err)
808 })
809
810 // end
811 stream.on('end', function onend () {
812 self.emit('end')
813 })
814}
815
816/**
817 * Set content-type based on `path`
818 * if it hasn't been explicitly set.
819 *
820 * @param {String} path
821 * @api private
822 */
823
824SendStream.prototype.type = function type (path) {
825 var res = this.res
826
827 if (res.getHeader('Content-Type')) return
828
829 var type = mime.lookup(path)
830
831 if (!type) {
832 debug('no content-type')
833 return
834 }
835
836 var charset = mime.charsets.lookup(type)
837
838 debug('content-type %s', type)
839 res.setHeader('Content-Type', type + (charset ? '; charset=' + charset : ''))
840}
841
842/**
843 * Set response header fields, most
844 * fields may be pre-defined.
845 *
846 * @param {String} path
847 * @param {Object} stat
848 * @api private
849 */
850
851SendStream.prototype.setHeader = function setHeader (path, stat) {
852 var res = this.res
853
854 this.emit('headers', res, path, stat)
855
856 if (this._acceptRanges && !res.getHeader('Accept-Ranges')) {
857 debug('accept ranges')
858 res.setHeader('Accept-Ranges', 'bytes')
859 }
860
861 if (this._cacheControl && !res.getHeader('Cache-Control')) {
862 var cacheControl = 'public, max-age=' + Math.floor(this._maxage / 1000)
863
864 if (this._immutable) {
865 cacheControl += ', immutable'
866 }
867
868 debug('cache-control %s', cacheControl)
869 res.setHeader('Cache-Control', cacheControl)
870 }
871
872 if (this._lastModified && !res.getHeader('Last-Modified')) {
873 var modified = stat.mtime.toUTCString()
874 debug('modified %s', modified)
875 res.setHeader('Last-Modified', modified)
876 }
877
878 if (this._etag && !res.getHeader('ETag')) {
879 var val = etag(stat)
880 debug('etag %s', val)
881 res.setHeader('ETag', val)
882 }
883}
884
885/**
886 * Clear all headers from a response.
887 *
888 * @param {object} res
889 * @private
890 */
891
892function clearHeaders (res) {
893 var headers = getHeaderNames(res)
894
895 for (var i = 0; i < headers.length; i++) {
896 res.removeHeader(headers[i])
897 }
898}
899
900/**
901 * Collapse all leading slashes into a single slash
902 *
903 * @param {string} str
904 * @private
905 */
906function collapseLeadingSlashes (str) {
907 for (var i = 0; i < str.length; i++) {
908 if (str[i] !== '/') {
909 break
910 }
911 }
912
913 return i > 1
914 ? '/' + str.substr(i)
915 : str
916}
917
918/**
919 * Determine if path parts contain a dotfile.
920 *
921 * @api private
922 */
923
924function containsDotFile (parts) {
925 for (var i = 0; i < parts.length; i++) {
926 var part = parts[i]
927 if (part.length > 1 && part[0] === '.') {
928 return true
929 }
930 }
931
932 return false
933}
934
935/**
936 * Create a Content-Range header.
937 *
938 * @param {string} type
939 * @param {number} size
940 * @param {array} [range]
941 */
942
943function contentRange (type, size, range) {
944 return type + ' ' + (range ? range.start + '-' + range.end : '*') + '/' + size
945}
946
947/**
948 * Create a minimal HTML document.
949 *
950 * @param {string} title
951 * @param {string} body
952 * @private
953 */
954
955function createHtmlDocument (title, body) {
956 return '<!DOCTYPE html>\n' +
957 '<html lang="en">\n' +
958 '<head>\n' +
959 '<meta charset="utf-8">\n' +
960 '<title>' + title + '</title>\n' +
961 '</head>\n' +
962 '<body>\n' +
963 '<pre>' + body + '</pre>\n' +
964 '</body>\n' +
965 '</html>\n'
966}
967
968/**
969 * Create a HttpError object from simple arguments.
970 *
971 * @param {number} status
972 * @param {Error|object} err
973 * @private
974 */
975
976function createHttpError (status, err) {
977 if (!err) {
978 return createError(status)
979 }
980
981 return err instanceof Error
982 ? createError(status, err, { expose: false })
983 : createError(status, err)
984}
985
986/**
987 * decodeURIComponent.
988 *
989 * Allows V8 to only deoptimize this fn instead of all
990 * of send().
991 *
992 * @param {String} path
993 * @api private
994 */
995
996function decode (path) {
997 try {
998 return decodeURIComponent(path)
999 } catch (err) {
1000 return -1
1001 }
1002}
1003
1004/**
1005 * Get the header names on a respnse.
1006 *
1007 * @param {object} res
1008 * @returns {array[string]}
1009 * @private
1010 */
1011
1012function getHeaderNames (res) {
1013 return typeof res.getHeaderNames !== 'function'
1014 ? Object.keys(res._headers || {})
1015 : res.getHeaderNames()
1016}
1017
1018/**
1019 * Determine if emitter has listeners of a given type.
1020 *
1021 * The way to do this check is done three different ways in Node.js >= 0.8
1022 * so this consolidates them into a minimal set using instance methods.
1023 *
1024 * @param {EventEmitter} emitter
1025 * @param {string} type
1026 * @returns {boolean}
1027 * @private
1028 */
1029
1030function hasListeners (emitter, type) {
1031 var count = typeof emitter.listenerCount !== 'function'
1032 ? emitter.listeners(type).length
1033 : emitter.listenerCount(type)
1034
1035 return count > 0
1036}
1037
1038/**
1039 * Determine if the response headers have been sent.
1040 *
1041 * @param {object} res
1042 * @returns {boolean}
1043 * @private
1044 */
1045
1046function headersSent (res) {
1047 return typeof res.headersSent !== 'boolean'
1048 ? Boolean(res._header)
1049 : res.headersSent
1050}
1051
1052/**
1053 * Normalize the index option into an array.
1054 *
1055 * @param {boolean|string|array} val
1056 * @param {string} name
1057 * @private
1058 */
1059
1060function normalizeList (val, name) {
1061 var list = [].concat(val || [])
1062
1063 for (var i = 0; i < list.length; i++) {
1064 if (typeof list[i] !== 'string') {
1065 throw new TypeError(name + ' must be array of strings or false')
1066 }
1067 }
1068
1069 return list
1070}
1071
1072/**
1073 * Parse an HTTP Date into a number.
1074 *
1075 * @param {string} date
1076 * @private
1077 */
1078
1079function parseHttpDate (date) {
1080 var timestamp = date && Date.parse(date)
1081
1082 return typeof timestamp === 'number'
1083 ? timestamp
1084 : NaN
1085}
1086
1087/**
1088 * Parse a HTTP token list.
1089 *
1090 * @param {string} str
1091 * @private
1092 */
1093
1094function parseTokenList (str) {
1095 var end = 0
1096 var list = []
1097 var start = 0
1098
1099 // gather tokens
1100 for (var i = 0, len = str.length; i < len; i++) {
1101 switch (str.charCodeAt(i)) {
1102 case 0x20: /* */
1103 if (start === end) {
1104 start = end = i + 1
1105 }
1106 break
1107 case 0x2c: /* , */
1108 if (start !== end) {
1109 list.push(str.substring(start, end))
1110 }
1111 start = end = i + 1
1112 break
1113 default:
1114 end = i + 1
1115 break
1116 }
1117 }
1118
1119 // final token
1120 if (start !== end) {
1121 list.push(str.substring(start, end))
1122 }
1123
1124 return list
1125}
1126
1127/**
1128 * Set an object of headers on a response.
1129 *
1130 * @param {object} res
1131 * @param {object} headers
1132 * @private
1133 */
1134
1135function setHeaders (res, headers) {
1136 var keys = Object.keys(headers)
1137
1138 for (var i = 0; i < keys.length; i++) {
1139 var key = keys[i]
1140 res.setHeader(key, headers[key])
1141 }
1142}
Note: See TracBrowser for help on using the repository browser.