Projekt

Obecné

Profil

Stáhnout (22.7 KB) Statistiky
| Větev: | Revize:
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 '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
}
(4-4/5)