Projekt

Obecné

Profil

Stáhnout (22.7 KB) Statistiky
| Větev: | Revize:
1 3a515b92 cagy
/*!
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 '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
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 '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
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
  } 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
606
SendStream.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
 */
716
SendStream.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
 */
757
SendStream.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
789
SendStream.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
833
SendStream.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
860
SendStream.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
901
function 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
 */
915
function 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
933
function 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
952
function 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
964
function 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
987
function 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
1003
function 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
1021
function 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
1037
function 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
1051
function 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
1070
function 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
1085
function 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
1122
function 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
}