UNPKG

23.3 kBJavaScriptView Raw
1/*!
2 * send
3 * Copyright(c) 2012 TJ Holowaychuk
4 * Copyright(c) 2014-2016 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', createError(status, err, {
271 expose: false
272 }))
273 }
274
275 var res = this.res
276 var msg = statuses[status] || String(status)
277 var doc = createHtmlDocument('Error', escapeHtml(msg))
278
279 // clear existing headers
280 clearHeaders(res)
281
282 // add error headers
283 if (err && err.headers) {
284 setHeaders(res, err.headers)
285 }
286
287 // send basic response
288 res.statusCode = status
289 res.setHeader('Content-Type', 'text/html; charset=UTF-8')
290 res.setHeader('Content-Length', Buffer.byteLength(doc))
291 res.setHeader('Content-Security-Policy', "default-src 'none'")
292 res.setHeader('X-Content-Type-Options', 'nosniff')
293 res.end(doc)
294}
295
296/**
297 * Check if the pathname ends with "/".
298 *
299 * @return {boolean}
300 * @private
301 */
302
303SendStream.prototype.hasTrailingSlash = function hasTrailingSlash () {
304 return this.path[this.path.length - 1] === '/'
305}
306
307/**
308 * Check if this is a conditional GET request.
309 *
310 * @return {Boolean}
311 * @api private
312 */
313
314SendStream.prototype.isConditionalGET = function isConditionalGET () {
315 return this.req.headers['if-match'] ||
316 this.req.headers['if-unmodified-since'] ||
317 this.req.headers['if-none-match'] ||
318 this.req.headers['if-modified-since']
319}
320
321/**
322 * Check if the request preconditions failed.
323 *
324 * @return {boolean}
325 * @private
326 */
327
328SendStream.prototype.isPreconditionFailure = function isPreconditionFailure () {
329 var req = this.req
330 var res = this.res
331
332 // if-match
333 var match = req.headers['if-match']
334 if (match) {
335 var etag = res.getHeader('ETag')
336 return !etag || (match !== '*' && parseTokenList(match).every(function (match) {
337 return match !== etag && match !== 'W/' + etag && 'W/' + match !== etag
338 }))
339 }
340
341 // if-unmodified-since
342 var unmodifiedSince = parseHttpDate(req.headers['if-unmodified-since'])
343 if (!isNaN(unmodifiedSince)) {
344 var lastModified = parseHttpDate(res.getHeader('Last-Modified'))
345 return isNaN(lastModified) || lastModified > unmodifiedSince
346 }
347
348 return false
349}
350
351/**
352 * Strip content-* header fields.
353 *
354 * @private
355 */
356
357SendStream.prototype.removeContentHeaderFields = function removeContentHeaderFields () {
358 var res = this.res
359 var headers = getHeaderNames(res)
360
361 for (var i = 0; i < headers.length; i++) {
362 var header = headers[i]
363 if (header.substr(0, 8) === 'content-' && header !== 'content-location') {
364 res.removeHeader(header)
365 }
366 }
367}
368
369/**
370 * Respond with 304 not modified.
371 *
372 * @api private
373 */
374
375SendStream.prototype.notModified = function notModified () {
376 var res = this.res
377 debug('not modified')
378 this.removeContentHeaderFields()
379 res.statusCode = 304
380 res.end()
381}
382
383/**
384 * Raise error that headers already sent.
385 *
386 * @api private
387 */
388
389SendStream.prototype.headersAlreadySent = function headersAlreadySent () {
390 var err = new Error('Can\'t set headers after they are sent.')
391 debug('headers already sent')
392 this.error(500, err)
393}
394
395/**
396 * Check if the request is cacheable, aka
397 * responded with 2xx or 304 (see RFC 2616 section 14.2{5,6}).
398 *
399 * @return {Boolean}
400 * @api private
401 */
402
403SendStream.prototype.isCachable = function isCachable () {
404 var statusCode = this.res.statusCode
405 return (statusCode >= 200 && statusCode < 300) ||
406 statusCode === 304
407}
408
409/**
410 * Handle stat() error.
411 *
412 * @param {Error} error
413 * @private
414 */
415
416SendStream.prototype.onStatError = function onStatError (error) {
417 switch (error.code) {
418 case 'ENAMETOOLONG':
419 case 'ENOENT':
420 case 'ENOTDIR':
421 this.error(404, error)
422 break
423 default:
424 this.error(500, error)
425 break
426 }
427}
428
429/**
430 * Check if the cache is fresh.
431 *
432 * @return {Boolean}
433 * @api private
434 */
435
436SendStream.prototype.isFresh = function isFresh () {
437 return fresh(this.req.headers, {
438 'etag': this.res.getHeader('ETag'),
439 'last-modified': this.res.getHeader('Last-Modified')
440 })
441}
442
443/**
444 * Check if the range is fresh.
445 *
446 * @return {Boolean}
447 * @api private
448 */
449
450SendStream.prototype.isRangeFresh = function isRangeFresh () {
451 var ifRange = this.req.headers['if-range']
452
453 if (!ifRange) {
454 return true
455 }
456
457 // if-range as etag
458 if (ifRange.indexOf('"') !== -1) {
459 var etag = this.res.getHeader('ETag')
460 return Boolean(etag && ifRange.indexOf(etag) !== -1)
461 }
462
463 // if-range as modified date
464 var lastModified = this.res.getHeader('Last-Modified')
465 return parseHttpDate(lastModified) <= parseHttpDate(ifRange)
466}
467
468/**
469 * Redirect to path.
470 *
471 * @param {string} path
472 * @private
473 */
474
475SendStream.prototype.redirect = function redirect (path) {
476 var res = this.res
477
478 if (hasListeners(this, 'directory')) {
479 this.emit('directory', res, path)
480 return
481 }
482
483 if (this.hasTrailingSlash()) {
484 this.error(403)
485 return
486 }
487
488 var loc = encodeUrl(collapseLeadingSlashes(this.path + '/'))
489 var doc = createHtmlDocument('Redirecting', 'Redirecting to <a href="' + escapeHtml(loc) + '">' +
490 escapeHtml(loc) + '</a>')
491
492 // redirect
493 res.statusCode = 301
494 res.setHeader('Content-Type', 'text/html; charset=UTF-8')
495 res.setHeader('Content-Length', Buffer.byteLength(doc))
496 res.setHeader('Content-Security-Policy', "default-src 'none'")
497 res.setHeader('X-Content-Type-Options', 'nosniff')
498 res.setHeader('Location', loc)
499 res.end(doc)
500}
501
502/**
503 * Pipe to `res.
504 *
505 * @param {Stream} res
506 * @return {Stream} res
507 * @api public
508 */
509
510SendStream.prototype.pipe = function pipe (res) {
511 // root path
512 var root = this._root
513
514 // references
515 this.res = res
516
517 // decode the path
518 var path = decode(this.path)
519 if (path === -1) {
520 this.error(400)
521 return res
522 }
523
524 // null byte(s)
525 if (~path.indexOf('\0')) {
526 this.error(400)
527 return res
528 }
529
530 var parts
531 if (root !== null) {
532 // normalize
533 if (path) {
534 path = normalize('.' + sep + path)
535 }
536
537 // malicious path
538 if (UP_PATH_REGEXP.test(path)) {
539 debug('malicious path "%s"', path)
540 this.error(403)
541 return res
542 }
543
544 // explode path parts
545 parts = path.split(sep)
546
547 // join / normalize from optional root dir
548 path = normalize(join(root, path))
549 } else {
550 // ".." is malicious without "root"
551 if (UP_PATH_REGEXP.test(path)) {
552 debug('malicious path "%s"', path)
553 this.error(403)
554 return res
555 }
556
557 // explode path parts
558 parts = normalize(path).split(sep)
559
560 // resolve the path
561 path = resolve(path)
562 }
563
564 // dotfile handling
565 if (containsDotFile(parts)) {
566 var access = this._dotfiles
567
568 // legacy support
569 if (access === undefined) {
570 access = parts[parts.length - 1][0] === '.'
571 ? (this._hidden ? 'allow' : 'ignore')
572 : 'allow'
573 }
574
575 debug('%s dotfile "%s"', access, path)
576 switch (access) {
577 case 'allow':
578 break
579 case 'deny':
580 this.error(403)
581 return res
582 case 'ignore':
583 default:
584 this.error(404)
585 return res
586 }
587 }
588
589 // index file support
590 if (this._index.length && this.hasTrailingSlash()) {
591 this.sendIndex(path)
592 return res
593 }
594
595 this.sendFile(path)
596 return res
597}
598
599/**
600 * Transfer `path`.
601 *
602 * @param {String} path
603 * @api public
604 */
605
606SendStream.prototype.send = function send (path, stat) {
607 var len = stat.size
608 var options = this.options
609 var opts = {}
610 var res = this.res
611 var req = this.req
612 var ranges = req.headers.range
613 var offset = options.start || 0
614
615 if (headersSent(res)) {
616 // impossible to send now
617 this.headersAlreadySent()
618 return
619 }
620
621 debug('pipe "%s"', path)
622
623 // set header fields
624 this.setHeader(path, stat)
625
626 // set content-type
627 this.type(path)
628
629 // conditional GET support
630 if (this.isConditionalGET()) {
631 if (this.isPreconditionFailure()) {
632 this.error(412)
633 return
634 }
635
636 if (this.isCachable() && this.isFresh()) {
637 this.notModified()
638 return
639 }
640 }
641
642 // adjust len to start/end options
643 len = Math.max(0, len - offset)
644 if (options.end !== undefined) {
645 var bytes = options.end - offset + 1
646 if (len > bytes) len = bytes
647 }
648
649 // Range support
650 if (this._acceptRanges && BYTES_RANGE_REGEXP.test(ranges)) {
651 // parse
652 ranges = parseRange(len, ranges, {
653 combine: true
654 })
655
656 // If-Range support
657 if (!this.isRangeFresh()) {
658 debug('range stale')
659 ranges = -2
660 }
661
662 // unsatisfiable
663 if (ranges === -1) {
664 debug('range unsatisfiable')
665
666 // Content-Range
667 res.setHeader('Content-Range', contentRange('bytes', len))
668
669 // 416 Requested Range Not Satisfiable
670 return this.error(416, {
671 headers: { 'Content-Range': res.getHeader('Content-Range') }
672 })
673 }
674
675 // valid (syntactically invalid/multiple ranges are treated as a regular response)
676 if (ranges !== -2 && ranges.length === 1) {
677 debug('range %j', ranges)
678
679 // Content-Range
680 res.statusCode = 206
681 res.setHeader('Content-Range', contentRange('bytes', len, ranges[0]))
682
683 // adjust for requested range
684 offset += ranges[0].start
685 len = ranges[0].end - ranges[0].start + 1
686 }
687 }
688
689 // clone options
690 for (var prop in options) {
691 opts[prop] = options[prop]
692 }
693
694 // set read options
695 opts.start = offset
696 opts.end = Math.max(offset, offset + len - 1)
697
698 // content-length
699 res.setHeader('Content-Length', len)
700
701 // HEAD support
702 if (req.method === 'HEAD') {
703 res.end()
704 return
705 }
706
707 this.stream(path, opts)
708}
709
710/**
711 * Transfer file for `path`.
712 *
713 * @param {String} path
714 * @api private
715 */
716SendStream.prototype.sendFile = function sendFile (path) {
717 var i = 0
718 var self = this
719
720 debug('stat "%s"', path)
721 fs.stat(path, function onstat (err, stat) {
722 if (err && err.code === 'ENOENT' && !extname(path) && path[path.length - 1] !== sep) {
723 // not found, check extensions
724 return next(err)
725 }
726 if (err) return self.onStatError(err)
727 if (stat.isDirectory()) return self.redirect(path)
728 self.emit('file', path, stat)
729 self.send(path, stat)
730 })
731
732 function next (err) {
733 if (self._extensions.length <= i) {
734 return err
735 ? self.onStatError(err)
736 : self.error(404)
737 }
738
739 var p = path + '.' + self._extensions[i++]
740
741 debug('stat "%s"', p)
742 fs.stat(p, function (err, stat) {
743 if (err) return next(err)
744 if (stat.isDirectory()) return next()
745 self.emit('file', p, stat)
746 self.send(p, stat)
747 })
748 }
749}
750
751/**
752 * Transfer index for `path`.
753 *
754 * @param {String} path
755 * @api private
756 */
757SendStream.prototype.sendIndex = function sendIndex (path) {
758 var i = -1
759 var self = this
760
761 function next (err) {
762 if (++i >= self._index.length) {
763 if (err) return self.onStatError(err)
764 return self.error(404)
765 }
766
767 var p = join(path, self._index[i])
768
769 debug('stat "%s"', p)
770 fs.stat(p, function (err, stat) {
771 if (err) return next(err)
772 if (stat.isDirectory()) return next()
773 self.emit('file', p, stat)
774 self.send(p, stat)
775 })
776 }
777
778 next()
779}
780
781/**
782 * Stream `path` to the response.
783 *
784 * @param {String} path
785 * @param {Object} options
786 * @api private
787 */
788
789SendStream.prototype.stream = function stream (path, options) {
790 // TODO: this is all lame, refactor meeee
791 var finished = false
792 var self = this
793 var res = this.res
794
795 // pipe
796 var stream = fs.createReadStream(path, options)
797 this.emit('stream', stream)
798 stream.pipe(res)
799
800 // response finished, done with the fd
801 onFinished(res, function onfinished () {
802 finished = true
803 destroy(stream)
804 })
805
806 // error handling code-smell
807 stream.on('error', function onerror (err) {
808 // request already finished
809 if (finished) return
810
811 // clean up stream
812 finished = true
813 destroy(stream)
814
815 // error
816 self.onStatError(err)
817 })
818
819 // end
820 stream.on('end', function onend () {
821 self.emit('end')
822 })
823}
824
825/**
826 * Set content-type based on `path`
827 * if it hasn't been explicitly set.
828 *
829 * @param {String} path
830 * @api private
831 */
832
833SendStream.prototype.type = function type (path) {
834 var res = this.res
835
836 if (res.getHeader('Content-Type')) return
837
838 var type = mime.lookup(path)
839
840 if (!type) {
841 debug('no content-type')
842 return
843 }
844
845 var charset = mime.charsets.lookup(type)
846
847 debug('content-type %s', type)
848 res.setHeader('Content-Type', type + (charset ? '; charset=' + charset : ''))
849}
850
851/**
852 * Set response header fields, most
853 * fields may be pre-defined.
854 *
855 * @param {String} path
856 * @param {Object} stat
857 * @api private
858 */
859
860SendStream.prototype.setHeader = function setHeader (path, stat) {
861 var res = this.res
862
863 this.emit('headers', res, path, stat)
864
865 if (this._acceptRanges && !res.getHeader('Accept-Ranges')) {
866 debug('accept ranges')
867 res.setHeader('Accept-Ranges', 'bytes')
868 }
869
870 if (this._cacheControl && !res.getHeader('Cache-Control')) {
871 var cacheControl = 'public, max-age=' + Math.floor(this._maxage / 1000)
872
873 if (this._immutable) {
874 cacheControl += ', immutable'
875 }
876
877 debug('cache-control %s', cacheControl)
878 res.setHeader('Cache-Control', cacheControl)
879 }
880
881 if (this._lastModified && !res.getHeader('Last-Modified')) {
882 var modified = stat.mtime.toUTCString()
883 debug('modified %s', modified)
884 res.setHeader('Last-Modified', modified)
885 }
886
887 if (this._etag && !res.getHeader('ETag')) {
888 var val = etag(stat)
889 debug('etag %s', val)
890 res.setHeader('ETag', val)
891 }
892}
893
894/**
895 * Clear all headers from a response.
896 *
897 * @param {object} res
898 * @private
899 */
900
901function clearHeaders (res) {
902 var headers = getHeaderNames(res)
903
904 for (var i = 0; i < headers.length; i++) {
905 res.removeHeader(headers[i])
906 }
907}
908
909/**
910 * Collapse all leading slashes into a single slash
911 *
912 * @param {string} str
913 * @private
914 */
915function collapseLeadingSlashes (str) {
916 for (var i = 0; i < str.length; i++) {
917 if (str[i] !== '/') {
918 break
919 }
920 }
921
922 return i > 1
923 ? '/' + str.substr(i)
924 : str
925}
926
927/**
928 * Determine if path parts contain a dotfile.
929 *
930 * @api private
931 */
932
933function containsDotFile (parts) {
934 for (var i = 0; i < parts.length; i++) {
935 var part = parts[i]
936 if (part.length > 1 && part[0] === '.') {
937 return true
938 }
939 }
940
941 return false
942}
943
944/**
945 * Create a Content-Range header.
946 *
947 * @param {string} type
948 * @param {number} size
949 * @param {array} [range]
950 */
951
952function contentRange (type, size, range) {
953 return type + ' ' + (range ? range.start + '-' + range.end : '*') + '/' + size
954}
955
956/**
957 * Create a minimal HTML document.
958 *
959 * @param {string} title
960 * @param {string} body
961 * @private
962 */
963
964function createHtmlDocument (title, body) {
965 return '<!DOCTYPE html>\n' +
966 '<html lang="en">\n' +
967 '<head>\n' +
968 '<meta charset="utf-8">\n' +
969 '<title>' + title + '</title>\n' +
970 '</head>\n' +
971 '<body>\n' +
972 '<pre>' + body + '</pre>\n' +
973 '</body>\n' +
974 '</html>\n'
975}
976
977/**
978 * decodeURIComponent.
979 *
980 * Allows V8 to only deoptimize this fn instead of all
981 * of send().
982 *
983 * @param {String} path
984 * @api private
985 */
986
987function decode (path) {
988 try {
989 return decodeURIComponent(path)
990 } catch (err) {
991 return -1
992 }
993}
994
995/**
996 * Get the header names on a respnse.
997 *
998 * @param {object} res
999 * @returns {array[string]}
1000 * @private
1001 */
1002
1003function getHeaderNames (res) {
1004 return typeof res.getHeaderNames !== 'function'
1005 ? Object.keys(res._headers || {})
1006 : res.getHeaderNames()
1007}
1008
1009/**
1010 * Determine if emitter has listeners of a given type.
1011 *
1012 * The way to do this check is done three different ways in Node.js >= 0.8
1013 * so this consolidates them into a minimal set using instance methods.
1014 *
1015 * @param {EventEmitter} emitter
1016 * @param {string} type
1017 * @returns {boolean}
1018 * @private
1019 */
1020
1021function hasListeners (emitter, type) {
1022 var count = typeof emitter.listenerCount !== 'function'
1023 ? emitter.listeners(type).length
1024 : emitter.listenerCount(type)
1025
1026 return count > 0
1027}
1028
1029/**
1030 * Determine if the response headers have been sent.
1031 *
1032 * @param {object} res
1033 * @returns {boolean}
1034 * @private
1035 */
1036
1037function headersSent (res) {
1038 return typeof res.headersSent !== 'boolean'
1039 ? Boolean(res._header)
1040 : res.headersSent
1041}
1042
1043/**
1044 * Normalize the index option into an array.
1045 *
1046 * @param {boolean|string|array} val
1047 * @param {string} name
1048 * @private
1049 */
1050
1051function normalizeList (val, name) {
1052 var list = [].concat(val || [])
1053
1054 for (var i = 0; i < list.length; i++) {
1055 if (typeof list[i] !== 'string') {
1056 throw new TypeError(name + ' must be array of strings or false')
1057 }
1058 }
1059
1060 return list
1061}
1062
1063/**
1064 * Parse an HTTP Date into a number.
1065 *
1066 * @param {string} date
1067 * @private
1068 */
1069
1070function parseHttpDate (date) {
1071 var timestamp = date && Date.parse(date)
1072
1073 return typeof timestamp === 'number'
1074 ? timestamp
1075 : NaN
1076}
1077
1078/**
1079 * Parse a HTTP token list.
1080 *
1081 * @param {string} str
1082 * @private
1083 */
1084
1085function parseTokenList (str) {
1086 var end = 0
1087 var list = []
1088 var start = 0
1089
1090 // gather tokens
1091 for (var i = 0, len = str.length; i < len; i++) {
1092 switch (str.charCodeAt(i)) {
1093 case 0x20: /* */
1094 if (start === end) {
1095 start = end = i + 1
1096 }
1097 break
1098 case 0x2c: /* , */
1099 list.push(str.substring(start, end))
1100 start = end = i + 1
1101 break
1102 default:
1103 end = i + 1
1104 break
1105 }
1106 }
1107
1108 // final token
1109 list.push(str.substring(start, end))
1110
1111 return list
1112}
1113
1114/**
1115 * Set an object of headers on a response.
1116 *
1117 * @param {object} res
1118 * @param {object} headers
1119 * @private
1120 */
1121
1122function setHeaders (res, headers) {
1123 var keys = Object.keys(headers)
1124
1125 for (var i = 0; i < keys.length; i++) {
1126 var key = keys[i]
1127 res.setHeader(key, headers[key])
1128 }
1129}