프로젝트

일반

사용자정보

통계
| 개정판:

root / HServer / 00.Server / 00.Program / node_modules / send / index.js

이력 | 보기 | 이력해설 | 다운로드 (22.8 KB)

1 39 HKM
/*!
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
}