second
[josuexyz/.git] / node_modules / send / index.js
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
15 var createError = require('http-errors')
16 var debug = require('debug')('send')
17 var deprecate = require('depd')('send')
18 var destroy = require('destroy')
19 var encodeUrl = require('encodeurl')
20 var escapeHtml = require('escape-html')
21 var etag = require('etag')
22 var fresh = require('fresh')
23 var fs = require('fs')
24 var mime = require('mime')
25 var ms = require('ms')
26 var onFinished = require('on-finished')
27 var parseRange = require('range-parser')
28 var path = require('path')
29 var statuses = require('statuses')
30 var Stream = require('stream')
31 var util = require('util')
32
33 /**
34  * Path function references.
35  * @private
36  */
37
38 var extname = path.extname
39 var join = path.join
40 var normalize = path.normalize
41 var resolve = path.resolve
42 var sep = path.sep
43
44 /**
45  * Regular expression for identifying a bytes Range header.
46  * @private
47  */
48
49 var BYTES_RANGE_REGEXP = /^ *bytes=/
50
51 /**
52  * Maximum value allowed for the max age.
53  * @private
54  */
55
56 var 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
63 var UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/
64
65 /**
66  * Module exports.
67  * @public
68  */
69
70 module.exports = send
71 module.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
83 function 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
96 function 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
173 util.inherits(SendStream, Stream)
174
175 /**
176  * Enable or disable etag generation.
177  *
178  * @param {Boolean} val
179  * @return {SendStream}
180  * @api public
181  */
182
183 SendStream.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
197 SendStream.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
213 SendStream.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
228 SendStream.prototype.root = function root (path) {
229   this._root = resolve(String(path))
230   debug('root %s', this._root)
231   return this
232 }
233
234 SendStream.prototype.from = deprecate.function(SendStream.prototype.root,
235   'send.from: pass root as option')
236
237 SendStream.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
248 SendStream.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
267 SendStream.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 'self'")
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
303 SendStream.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
314 SendStream.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
328 SendStream.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
357 SendStream.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
375 SendStream.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
389 SendStream.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
403 SendStream.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
416 SendStream.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
436 SendStream.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
450 SendStream.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
475 SendStream.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 'self'")
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
510 SendStream.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     root = normalize(root + sep)
550   } else {
551     // ".." is malicious without "root"
552     if (UP_PATH_REGEXP.test(path)) {
553       debug('malicious path "%s"', path)
554       this.error(403)
555       return res
556     }
557
558     // explode path parts
559     parts = normalize(path).split(sep)
560
561     // resolve the path
562     path = resolve(path)
563   }
564
565   // dotfile handling
566   if (containsDotFile(parts)) {
567     var access = this._dotfiles
568
569     // legacy support
570     if (access === undefined) {
571       access = parts[parts.length - 1][0] === '.'
572         ? (this._hidden ? 'allow' : 'ignore')
573         : 'allow'
574     }
575
576     debug('%s dotfile "%s"', access, path)
577     switch (access) {
578       case 'allow':
579         break
580       case 'deny':
581         this.error(403)
582         return res
583       case 'ignore':
584       default:
585         this.error(404)
586         return res
587     }
588   }
589
590   // index file support
591   if (this._index.length && this.hasTrailingSlash()) {
592     this.sendIndex(path)
593     return res
594   }
595
596   this.sendFile(path)
597   return res
598 }
599
600 /**
601  * Transfer `path`.
602  *
603  * @param {String} path
604  * @api public
605  */
606
607 SendStream.prototype.send = function send (path, stat) {
608   var len = stat.size
609   var options = this.options
610   var opts = {}
611   var res = this.res
612   var req = this.req
613   var ranges = req.headers.range
614   var offset = options.start || 0
615
616   if (headersSent(res)) {
617     // impossible to send now
618     this.headersAlreadySent()
619     return
620   }
621
622   debug('pipe "%s"', path)
623
624   // set header fields
625   this.setHeader(path, stat)
626
627   // set content-type
628   this.type(path)
629
630   // conditional GET support
631   if (this.isConditionalGET()) {
632     if (this.isPreconditionFailure()) {
633       this.error(412)
634       return
635     }
636
637     if (this.isCachable() && this.isFresh()) {
638       this.notModified()
639       return
640     }
641   }
642
643   // adjust len to start/end options
644   len = Math.max(0, len - offset)
645   if (options.end !== undefined) {
646     var bytes = options.end - offset + 1
647     if (len > bytes) len = bytes
648   }
649
650   // Range support
651   if (this._acceptRanges && BYTES_RANGE_REGEXP.test(ranges)) {
652     // parse
653     ranges = parseRange(len, ranges, {
654       combine: true
655     })
656
657     // If-Range support
658     if (!this.isRangeFresh()) {
659       debug('range stale')
660       ranges = -2
661     }
662
663     // unsatisfiable
664     if (ranges === -1) {
665       debug('range unsatisfiable')
666
667       // Content-Range
668       res.setHeader('Content-Range', contentRange('bytes', len))
669
670       // 416 Requested Range Not Satisfiable
671       return this.error(416, {
672         headers: {'Content-Range': res.getHeader('Content-Range')}
673       })
674     }
675
676     // valid (syntactically invalid/multiple ranges are treated as a regular response)
677     if (ranges !== -2 && ranges.length === 1) {
678       debug('range %j', ranges)
679
680       // Content-Range
681       res.statusCode = 206
682       res.setHeader('Content-Range', contentRange('bytes', len, ranges[0]))
683
684       // adjust for requested range
685       offset += ranges[0].start
686       len = ranges[0].end - ranges[0].start + 1
687     }
688   }
689
690   // clone options
691   for (var prop in options) {
692     opts[prop] = options[prop]
693   }
694
695   // set read options
696   opts.start = offset
697   opts.end = Math.max(offset, offset + len - 1)
698
699   // content-length
700   res.setHeader('Content-Length', len)
701
702   // HEAD support
703   if (req.method === 'HEAD') {
704     res.end()
705     return
706   }
707
708   this.stream(path, opts)
709 }
710
711 /**
712  * Transfer file for `path`.
713  *
714  * @param {String} path
715  * @api private
716  */
717 SendStream.prototype.sendFile = function sendFile (path) {
718   var i = 0
719   var self = this
720
721   debug('stat "%s"', path)
722   fs.stat(path, function onstat (err, stat) {
723     if (err && err.code === 'ENOENT' && !extname(path) && path[path.length - 1] !== sep) {
724       // not found, check extensions
725       return next(err)
726     }
727     if (err) return self.onStatError(err)
728     if (stat.isDirectory()) return self.redirect(path)
729     self.emit('file', path, stat)
730     self.send(path, stat)
731   })
732
733   function next (err) {
734     if (self._extensions.length <= i) {
735       return err
736         ? self.onStatError(err)
737         : self.error(404)
738     }
739
740     var p = path + '.' + self._extensions[i++]
741
742     debug('stat "%s"', p)
743     fs.stat(p, function (err, stat) {
744       if (err) return next(err)
745       if (stat.isDirectory()) return next()
746       self.emit('file', p, stat)
747       self.send(p, stat)
748     })
749   }
750 }
751
752 /**
753  * Transfer index for `path`.
754  *
755  * @param {String} path
756  * @api private
757  */
758 SendStream.prototype.sendIndex = function sendIndex (path) {
759   var i = -1
760   var self = this
761
762   function next (err) {
763     if (++i >= self._index.length) {
764       if (err) return self.onStatError(err)
765       return self.error(404)
766     }
767
768     var p = join(path, self._index[i])
769
770     debug('stat "%s"', p)
771     fs.stat(p, function (err, stat) {
772       if (err) return next(err)
773       if (stat.isDirectory()) return next()
774       self.emit('file', p, stat)
775       self.send(p, stat)
776     })
777   }
778
779   next()
780 }
781
782 /**
783  * Stream `path` to the response.
784  *
785  * @param {String} path
786  * @param {Object} options
787  * @api private
788  */
789
790 SendStream.prototype.stream = function stream (path, options) {
791   // TODO: this is all lame, refactor meeee
792   var finished = false
793   var self = this
794   var res = this.res
795
796   // pipe
797   var stream = fs.createReadStream(path, options)
798   this.emit('stream', stream)
799   stream.pipe(res)
800
801   // response finished, done with the fd
802   onFinished(res, function onfinished () {
803     finished = true
804     destroy(stream)
805   })
806
807   // error handling code-smell
808   stream.on('error', function onerror (err) {
809     // request already finished
810     if (finished) return
811
812     // clean up stream
813     finished = true
814     destroy(stream)
815
816     // error
817     self.onStatError(err)
818   })
819
820   // end
821   stream.on('end', function onend () {
822     self.emit('end')
823   })
824 }
825
826 /**
827  * Set content-type based on `path`
828  * if it hasn't been explicitly set.
829  *
830  * @param {String} path
831  * @api private
832  */
833
834 SendStream.prototype.type = function type (path) {
835   var res = this.res
836
837   if (res.getHeader('Content-Type')) return
838
839   var type = mime.lookup(path)
840
841   if (!type) {
842     debug('no content-type')
843     return
844   }
845
846   var charset = mime.charsets.lookup(type)
847
848   debug('content-type %s', type)
849   res.setHeader('Content-Type', type + (charset ? '; charset=' + charset : ''))
850 }
851
852 /**
853  * Set response header fields, most
854  * fields may be pre-defined.
855  *
856  * @param {String} path
857  * @param {Object} stat
858  * @api private
859  */
860
861 SendStream.prototype.setHeader = function setHeader (path, stat) {
862   var res = this.res
863
864   this.emit('headers', res, path, stat)
865
866   if (this._acceptRanges && !res.getHeader('Accept-Ranges')) {
867     debug('accept ranges')
868     res.setHeader('Accept-Ranges', 'bytes')
869   }
870
871   if (this._cacheControl && !res.getHeader('Cache-Control')) {
872     var cacheControl = 'public, max-age=' + Math.floor(this._maxage / 1000)
873
874     if (this._immutable) {
875       cacheControl += ', immutable'
876     }
877
878     debug('cache-control %s', cacheControl)
879     res.setHeader('Cache-Control', cacheControl)
880   }
881
882   if (this._lastModified && !res.getHeader('Last-Modified')) {
883     var modified = stat.mtime.toUTCString()
884     debug('modified %s', modified)
885     res.setHeader('Last-Modified', modified)
886   }
887
888   if (this._etag && !res.getHeader('ETag')) {
889     var val = etag(stat)
890     debug('etag %s', val)
891     res.setHeader('ETag', val)
892   }
893 }
894
895 /**
896  * Clear all headers from a response.
897  *
898  * @param {object} res
899  * @private
900  */
901
902 function clearHeaders (res) {
903   var headers = getHeaderNames(res)
904
905   for (var i = 0; i < headers.length; i++) {
906     res.removeHeader(headers[i])
907   }
908 }
909
910 /**
911  * Collapse all leading slashes into a single slash
912  *
913  * @param {string} str
914  * @private
915  */
916 function collapseLeadingSlashes (str) {
917   for (var i = 0; i < str.length; i++) {
918     if (str[i] !== '/') {
919       break
920     }
921   }
922
923   return i > 1
924     ? '/' + str.substr(i)
925     : str
926 }
927
928 /**
929  * Determine if path parts contain a dotfile.
930  *
931  * @api private
932  */
933
934 function containsDotFile (parts) {
935   for (var i = 0; i < parts.length; i++) {
936     var part = parts[i]
937     if (part.length > 1 && part[0] === '.') {
938       return true
939     }
940   }
941
942   return false
943 }
944
945 /**
946  * Create a Content-Range header.
947  *
948  * @param {string} type
949  * @param {number} size
950  * @param {array} [range]
951  */
952
953 function contentRange (type, size, range) {
954   return type + ' ' + (range ? range.start + '-' + range.end : '*') + '/' + size
955 }
956
957 /**
958  * Create a minimal HTML document.
959  *
960  * @param {string} title
961  * @param {string} body
962  * @private
963  */
964
965 function createHtmlDocument (title, body) {
966   return '<!DOCTYPE html>\n' +
967     '<html lang="en">\n' +
968     '<head>\n' +
969     '<meta charset="utf-8">\n' +
970     '<title>' + title + '</title>\n' +
971     '</head>\n' +
972     '<body>\n' +
973     '<pre>' + body + '</pre>\n' +
974     '</body>\n' +
975     '</html>\n'
976 }
977
978 /**
979  * decodeURIComponent.
980  *
981  * Allows V8 to only deoptimize this fn instead of all
982  * of send().
983  *
984  * @param {String} path
985  * @api private
986  */
987
988 function decode (path) {
989   try {
990     return decodeURIComponent(path)
991   } catch (err) {
992     return -1
993   }
994 }
995
996 /**
997  * Get the header names on a respnse.
998  *
999  * @param {object} res
1000  * @returns {array[string]}
1001  * @private
1002  */
1003
1004 function getHeaderNames (res) {
1005   return typeof res.getHeaderNames !== 'function'
1006     ? Object.keys(res._headers || {})
1007     : res.getHeaderNames()
1008 }
1009
1010 /**
1011  * Determine if emitter has listeners of a given type.
1012  *
1013  * The way to do this check is done three different ways in Node.js >= 0.8
1014  * so this consolidates them into a minimal set using instance methods.
1015  *
1016  * @param {EventEmitter} emitter
1017  * @param {string} type
1018  * @returns {boolean}
1019  * @private
1020  */
1021
1022 function hasListeners (emitter, type) {
1023   var count = typeof emitter.listenerCount !== 'function'
1024     ? emitter.listeners(type).length
1025     : emitter.listenerCount(type)
1026
1027   return count > 0
1028 }
1029
1030 /**
1031  * Determine if the response headers have been sent.
1032  *
1033  * @param {object} res
1034  * @returns {boolean}
1035  * @private
1036  */
1037
1038 function headersSent (res) {
1039   return typeof res.headersSent !== 'boolean'
1040     ? Boolean(res._header)
1041     : res.headersSent
1042 }
1043
1044 /**
1045  * Normalize the index option into an array.
1046  *
1047  * @param {boolean|string|array} val
1048  * @param {string} name
1049  * @private
1050  */
1051
1052 function normalizeList (val, name) {
1053   var list = [].concat(val || [])
1054
1055   for (var i = 0; i < list.length; i++) {
1056     if (typeof list[i] !== 'string') {
1057       throw new TypeError(name + ' must be array of strings or false')
1058     }
1059   }
1060
1061   return list
1062 }
1063
1064 /**
1065  * Parse an HTTP Date into a number.
1066  *
1067  * @param {string} date
1068  * @private
1069  */
1070
1071 function parseHttpDate (date) {
1072   var timestamp = date && Date.parse(date)
1073
1074   return typeof timestamp === 'number'
1075     ? timestamp
1076     : NaN
1077 }
1078
1079 /**
1080  * Parse a HTTP token list.
1081  *
1082  * @param {string} str
1083  * @private
1084  */
1085
1086 function parseTokenList (str) {
1087   var end = 0
1088   var list = []
1089   var start = 0
1090
1091   // gather tokens
1092   for (var i = 0, len = str.length; i < len; i++) {
1093     switch (str.charCodeAt(i)) {
1094       case 0x20: /*   */
1095         if (start === end) {
1096           start = end = i + 1
1097         }
1098         break
1099       case 0x2c: /* , */
1100         list.push(str.substring(start, end))
1101         start = end = i + 1
1102         break
1103       default:
1104         end = i + 1
1105         break
1106     }
1107   }
1108
1109   // final token
1110   list.push(str.substring(start, end))
1111
1112   return list
1113 }
1114
1115 /**
1116  * Set an object of headers on a response.
1117  *
1118  * @param {object} res
1119  * @param {object} headers
1120  * @private
1121  */
1122
1123 function setHeaders (res, headers) {
1124   var keys = Object.keys(headers)
1125
1126   for (var i = 0; i < keys.length; i++) {
1127     var key = keys[i]
1128     res.setHeader(key, headers[key])
1129   }
1130 }