arnaucube
arnaucube/socketioMEANseedPublic Self-hosted repository
socketioMEANseed/server/node_modules/send/index.js
server/node_modules/send/index.jsRaw
  1
  2/**
  3 * Module dependencies.
  4 */
  5
  6var debug = require('debug')('send')
  7var deprecate = require('depd')('send')
  8var destroy = require('destroy')
  9var escapeHtml = require('escape-html')
 10  , parseRange = require('range-parser')
 11  , Stream = require('stream')
 12  , mime = require('mime')
 13  , fresh = require('fresh')
 14  , path = require('path')
 15  , http = require('http')
 16  , fs = require('fs')
 17  , normalize = path.normalize
 18  , join = path.join
 19var etag = require('etag')
 20var EventEmitter = require('events').EventEmitter;
 21var ms = require('ms');
 22var onFinished = require('on-finished')
 23
 24/**
 25 * Variables.
 26 */
 27var extname = path.extname
 28var maxMaxAge = 60 * 60 * 24 * 365 * 1000; // 1 year
 29var resolve = path.resolve
 30var sep = path.sep
 31var toString = Object.prototype.toString
 32var upPathRegexp = /(?:^|[\\\/])\.\.(?:[\\\/]|$)/
 33
 34/**
 35 * Expose `send`.
 36 */
 37
 38exports = module.exports = send;
 39
 40/**
 41 * Expose mime module.
 42 */
 43
 44exports.mime = mime;
 45
 46/**
 47 * Shim EventEmitter.listenerCount for node.js < 0.10
 48 */
 49
 50/* istanbul ignore next */
 51var listenerCount = EventEmitter.listenerCount
 52  || function(emitter, type){ return emitter.listeners(type).length; };
 53
 54/**
 55 * Return a `SendStream` for `req` and `path`.
 56 *
 57 * @param {Request} req
 58 * @param {String} path
 59 * @param {Object} options
 60 * @return {SendStream}
 61 * @api public
 62 */
 63
 64function send(req, path, options) {
 65  return new SendStream(req, path, options);
 66}
 67
 68/**
 69 * Initialize a `SendStream` with the given `path`.
 70 *
 71 * @param {Request} req
 72 * @param {String} path
 73 * @param {Object} options
 74 * @api private
 75 */
 76
 77function SendStream(req, path, options) {
 78  var self = this;
 79  options = options || {};
 80  this.req = req;
 81  this.path = path;
 82  this.options = options;
 83
 84  this._etag = options.etag !== undefined
 85    ? Boolean(options.etag)
 86    : true
 87
 88  this._dotfiles = options.dotfiles !== undefined
 89    ? options.dotfiles
 90    : 'ignore'
 91
 92  if (['allow', 'deny', 'ignore'].indexOf(this._dotfiles) === -1) {
 93    throw new TypeError('dotfiles option must be "allow", "deny", or "ignore"')
 94  }
 95
 96  this._hidden = Boolean(options.hidden)
 97
 98  if ('hidden' in options) {
 99    deprecate('hidden: use dotfiles: \'' + (this._hidden ? 'allow' : 'ignore') + '\' instead')
100  }
101
102  // legacy support
103  if (!('dotfiles' in options)) {
104    this._dotfiles = undefined
105  }
106
107  this._extensions = options.extensions !== undefined
108    ? normalizeList(options.extensions)
109    : []
110
111  this._index = options.index !== undefined
112    ? normalizeList(options.index)
113    : ['index.html']
114
115  this._lastModified = options.lastModified !== undefined
116    ? Boolean(options.lastModified)
117    : true
118
119  this._maxage = options.maxAge || options.maxage
120  this._maxage = typeof this._maxage === 'string'
121    ? ms(this._maxage)
122    : Number(this._maxage)
123  this._maxage = !isNaN(this._maxage)
124    ? Math.min(Math.max(0, this._maxage), maxMaxAge)
125    : 0
126
127  this._root = options.root
128    ? resolve(options.root)
129    : null
130
131  if (!this._root && options.from) {
132    this.from(options.from);
133  }
134}
135
136/**
137 * Inherits from `Stream.prototype`.
138 */
139
140SendStream.prototype.__proto__ = Stream.prototype;
141
142/**
143 * Enable or disable etag generation.
144 *
145 * @param {Boolean} val
146 * @return {SendStream}
147 * @api public
148 */
149
150SendStream.prototype.etag = deprecate.function(function etag(val) {
151  val = Boolean(val);
152  debug('etag %s', val);
153  this._etag = val;
154  return this;
155}, 'send.etag: pass etag as option');
156
157/**
158 * Enable or disable "hidden" (dot) files.
159 *
160 * @param {Boolean} path
161 * @return {SendStream}
162 * @api public
163 */
164
165SendStream.prototype.hidden = deprecate.function(function hidden(val) {
166  val = Boolean(val);
167  debug('hidden %s', val);
168  this._hidden = val;
169  this._dotfiles = undefined
170  return this;
171}, 'send.hidden: use dotfiles option');
172
173/**
174 * Set index `paths`, set to a falsy
175 * value to disable index support.
176 *
177 * @param {String|Boolean|Array} paths
178 * @return {SendStream}
179 * @api public
180 */
181
182SendStream.prototype.index = deprecate.function(function index(paths) {
183  var index = !paths ? [] : normalizeList(paths);
184  debug('index %o', paths);
185  this._index = index;
186  return this;
187}, 'send.index: pass index as option');
188
189/**
190 * Set root `path`.
191 *
192 * @param {String} path
193 * @return {SendStream}
194 * @api public
195 */
196
197SendStream.prototype.root = function(path){
198  path = String(path);
199  this._root = resolve(path)
200  return this;
201};
202
203SendStream.prototype.from = deprecate.function(SendStream.prototype.root,
204  'send.from: pass root as option');
205
206SendStream.prototype.root = deprecate.function(SendStream.prototype.root,
207  'send.root: pass root as option');
208
209/**
210 * Set max-age to `maxAge`.
211 *
212 * @param {Number} maxAge
213 * @return {SendStream}
214 * @api public
215 */
216
217SendStream.prototype.maxage = deprecate.function(function maxage(maxAge) {
218  maxAge = typeof maxAge === 'string'
219    ? ms(maxAge)
220    : Number(maxAge);
221  if (isNaN(maxAge)) maxAge = 0;
222  if (Infinity == maxAge) maxAge = 60 * 60 * 24 * 365 * 1000;
223  debug('max-age %d', maxAge);
224  this._maxage = maxAge;
225  return this;
226}, 'send.maxage: pass maxAge as option');
227
228/**
229 * Emit error with `status`.
230 *
231 * @param {Number} status
232 * @api private
233 */
234
235SendStream.prototype.error = function(status, err){
236  var res = this.res;
237  var msg = http.STATUS_CODES[status];
238
239  err = err || new Error(msg);
240  err.status = status;
241
242  // emit if listeners instead of responding
243  if (listenerCount(this, 'error') !== 0) {
244    return this.emit('error', err);
245  }
246
247  // wipe all existing headers
248  res._headers = undefined;
249
250  res.statusCode = err.status;
251  res.end(msg);
252};
253
254/**
255 * Check if the pathname ends with "/".
256 *
257 * @return {Boolean}
258 * @api private
259 */
260
261SendStream.prototype.hasTrailingSlash = function(){
262  return '/' == this.path[this.path.length - 1];
263};
264
265/**
266 * Check if this is a conditional GET request.
267 *
268 * @return {Boolean}
269 * @api private
270 */
271
272SendStream.prototype.isConditionalGET = function(){
273  return this.req.headers['if-none-match']
274    || this.req.headers['if-modified-since'];
275};
276
277/**
278 * Strip content-* header fields.
279 *
280 * @api private
281 */
282
283SendStream.prototype.removeContentHeaderFields = function(){
284  var res = this.res;
285  Object.keys(res._headers).forEach(function(field){
286    if (0 == field.indexOf('content')) {
287      res.removeHeader(field);
288    }
289  });
290};
291
292/**
293 * Respond with 304 not modified.
294 *
295 * @api private
296 */
297
298SendStream.prototype.notModified = function(){
299  var res = this.res;
300  debug('not modified');
301  this.removeContentHeaderFields();
302  res.statusCode = 304;
303  res.end();
304};
305
306/**
307 * Raise error that headers already sent.
308 *
309 * @api private
310 */
311
312SendStream.prototype.headersAlreadySent = function headersAlreadySent(){
313  var err = new Error('Can\'t set headers after they are sent.');
314  debug('headers already sent');
315  this.error(500, err);
316};
317
318/**
319 * Check if the request is cacheable, aka
320 * responded with 2xx or 304 (see RFC 2616 section 14.2{5,6}).
321 *
322 * @return {Boolean}
323 * @api private
324 */
325
326SendStream.prototype.isCachable = function(){
327  var res = this.res;
328  return (res.statusCode >= 200 && res.statusCode < 300) || 304 == res.statusCode;
329};
330
331/**
332 * Handle stat() error.
333 *
334 * @param {Error} err
335 * @api private
336 */
337
338SendStream.prototype.onStatError = function(err){
339  var notfound = ['ENOENT', 'ENAMETOOLONG', 'ENOTDIR'];
340  if (~notfound.indexOf(err.code)) return this.error(404, err);
341  this.error(500, err);
342};
343
344/**
345 * Check if the cache is fresh.
346 *
347 * @return {Boolean}
348 * @api private
349 */
350
351SendStream.prototype.isFresh = function(){
352  return fresh(this.req.headers, this.res._headers);
353};
354
355/**
356 * Check if the range is fresh.
357 *
358 * @return {Boolean}
359 * @api private
360 */
361
362SendStream.prototype.isRangeFresh = function isRangeFresh(){
363  var ifRange = this.req.headers['if-range'];
364
365  if (!ifRange) return true;
366
367  return ~ifRange.indexOf('"')
368    ? ~ifRange.indexOf(this.res._headers['etag'])
369    : Date.parse(this.res._headers['last-modified']) <= Date.parse(ifRange);
370};
371
372/**
373 * Redirect to `path`.
374 *
375 * @param {String} path
376 * @api private
377 */
378
379SendStream.prototype.redirect = function(path){
380  if (listenerCount(this, 'directory') !== 0) {
381    return this.emit('directory');
382  }
383
384  if (this.hasTrailingSlash()) return this.error(403);
385  var res = this.res;
386  path += '/';
387  res.statusCode = 301;
388  res.setHeader('Content-Type', 'text/html; charset=utf-8');
389  res.setHeader('Location', path);
390  res.end('Redirecting to <a href="' + escapeHtml(path) + '">' + escapeHtml(path) + '</a>\n');
391};
392
393/**
394 * Pipe to `res.
395 *
396 * @param {Stream} res
397 * @return {Stream} res
398 * @api public
399 */
400
401SendStream.prototype.pipe = function(res){
402  var self = this
403    , args = arguments
404    , root = this._root;
405
406  // references
407  this.res = res;
408
409  // decode the path
410  var path = decode(this.path)
411  if (path === -1) return this.error(400)
412
413  // null byte(s)
414  if (~path.indexOf('\0')) return this.error(400);
415
416  var parts
417  if (root !== null) {
418    // join / normalize from optional root dir
419    path = normalize(join(root, path))
420    root = normalize(root + sep)
421
422    // malicious path
423    if ((path + sep).substr(0, root.length) !== root) {
424      debug('malicious path "%s"', path)
425      return this.error(403)
426    }
427
428    // explode path parts
429    parts = path.substr(root.length).split(sep)
430  } else {
431    // ".." is malicious without "root"
432    if (upPathRegexp.test(path)) {
433      debug('malicious path "%s"', path)
434      return this.error(403)
435    }
436
437    // explode path parts
438    parts = normalize(path).split(sep)
439
440    // resolve the path
441    path = resolve(path)
442  }
443
444  // dotfile handling
445  if (containsDotFile(parts)) {
446    var access = this._dotfiles
447
448    // legacy support
449    if (access === undefined) {
450      access = parts[parts.length - 1][0] === '.'
451        ? (this._hidden ? 'allow' : 'ignore')
452        : 'allow'
453    }
454
455    debug('%s dotfile "%s"', access, path)
456    switch (access) {
457      case 'allow':
458        break
459      case 'deny':
460        return this.error(403)
461      case 'ignore':
462      default:
463        return this.error(404)
464    }
465  }
466
467  // index file support
468  if (this._index.length && this.path[this.path.length - 1] === '/') {
469    this.sendIndex(path);
470    return res;
471  }
472
473  this.sendFile(path);
474  return res;
475};
476
477/**
478 * Transfer `path`.
479 *
480 * @param {String} path
481 * @api public
482 */
483
484SendStream.prototype.send = function(path, stat){
485  var options = this.options;
486  var len = stat.size;
487  var res = this.res;
488  var req = this.req;
489  var ranges = req.headers.range;
490  var offset = options.start || 0;
491
492  if (res._header) {
493    // impossible to send now
494    return this.headersAlreadySent();
495  }
496
497  debug('pipe "%s"', path)
498
499  // set header fields
500  this.setHeader(path, stat);
501
502  // set content-type
503  this.type(path);
504
505  // conditional GET support
506  if (this.isConditionalGET()
507    && this.isCachable()
508    && this.isFresh()) {
509    return this.notModified();
510  }
511
512  // adjust len to start/end options
513  len = Math.max(0, len - offset);
514  if (options.end !== undefined) {
515    var bytes = options.end - offset + 1;
516    if (len > bytes) len = bytes;
517  }
518
519  // Range support
520  if (ranges) {
521    ranges = parseRange(len, ranges);
522
523    // If-Range support
524    if (!this.isRangeFresh()) {
525      debug('range stale');
526      ranges = -2;
527    }
528
529    // unsatisfiable
530    if (-1 == ranges) {
531      debug('range unsatisfiable');
532      res.setHeader('Content-Range', 'bytes */' + stat.size);
533      return this.error(416);
534    }
535
536    // valid (syntactically invalid/multiple ranges are treated as a regular response)
537    if (-2 != ranges && ranges.length === 1) {
538      debug('range %j', ranges);
539
540      options.start = offset + ranges[0].start;
541      options.end = offset + ranges[0].end;
542
543      // Content-Range
544      res.statusCode = 206;
545      res.setHeader('Content-Range', 'bytes '
546        + ranges[0].start
547        + '-'
548        + ranges[0].end
549        + '/'
550        + len);
551      len = options.end - options.start + 1;
552    }
553  }
554
555  // content-length
556  res.setHeader('Content-Length', len);
557
558  // HEAD support
559  if ('HEAD' == req.method) return res.end();
560
561  this.stream(path, options);
562};
563
564/**
565 * Transfer file for `path`.
566 *
567 * @param {String} path
568 * @api private
569 */
570SendStream.prototype.sendFile = function sendFile(path) {
571  var i = 0
572  var self = this
573
574  debug('stat "%s"', path);
575  fs.stat(path, function onstat(err, stat) {
576    if (err && err.code === 'ENOENT'
577      && !extname(path)
578      && path[path.length - 1] !== sep) {
579      // not found, check extensions
580      return next(err)
581    }
582    if (err) return self.onStatError(err)
583    if (stat.isDirectory()) return self.redirect(self.path)
584    self.emit('file', path, stat)
585    self.send(path, stat)
586  })
587
588  function next(err) {
589    if (self._extensions.length <= i) {
590      return err
591        ? self.onStatError(err)
592        : self.error(404)
593    }
594
595    var p = path + '.' + self._extensions[i++]
596
597    debug('stat "%s"', p)
598    fs.stat(p, function (err, stat) {
599      if (err) return next(err)
600      if (stat.isDirectory()) return next()
601      self.emit('file', p, stat)
602      self.send(p, stat)
603    })
604  }
605}
606
607/**
608 * Transfer index for `path`.
609 *
610 * @param {String} path
611 * @api private
612 */
613SendStream.prototype.sendIndex = function sendIndex(path){
614  var i = -1;
615  var self = this;
616
617  function next(err){
618    if (++i >= self._index.length) {
619      if (err) return self.onStatError(err);
620      return self.error(404);
621    }
622
623    var p = join(path, self._index[i]);
624
625    debug('stat "%s"', p);
626    fs.stat(p, function(err, stat){
627      if (err) return next(err);
628      if (stat.isDirectory()) return next();
629      self.emit('file', p, stat);
630      self.send(p, stat);
631    });
632  }
633
634  next();
635};
636
637/**
638 * Stream `path` to the response.
639 *
640 * @param {String} path
641 * @param {Object} options
642 * @api private
643 */
644
645SendStream.prototype.stream = function(path, options){
646  // TODO: this is all lame, refactor meeee
647  var finished = false;
648  var self = this;
649  var res = this.res;
650  var req = this.req;
651
652  // pipe
653  var stream = fs.createReadStream(path, options);
654  this.emit('stream', stream);
655  stream.pipe(res);
656
657  // response finished, done with the fd
658  onFinished(res, function onfinished(){
659    finished = true;
660    destroy(stream);
661  });
662
663  // error handling code-smell
664  stream.on('error', function onerror(err){
665    // request already finished
666    if (finished) return;
667
668    // clean up stream
669    finished = true;
670    destroy(stream);
671
672    // error
673    self.onStatError(err);
674  });
675
676  // end
677  stream.on('end', function onend(){
678    self.emit('end');
679  });
680};
681
682/**
683 * Set content-type based on `path`
684 * if it hasn't been explicitly set.
685 *
686 * @param {String} path
687 * @api private
688 */
689
690SendStream.prototype.type = function(path){
691  var res = this.res;
692  if (res.getHeader('Content-Type')) return;
693  var type = mime.lookup(path);
694  var charset = mime.charsets.lookup(type);
695  debug('content-type %s', type);
696  res.setHeader('Content-Type', type + (charset ? '; charset=' + charset : ''));
697};
698
699/**
700 * Set response header fields, most
701 * fields may be pre-defined.
702 *
703 * @param {String} path
704 * @param {Object} stat
705 * @api private
706 */
707
708SendStream.prototype.setHeader = function setHeader(path, stat){
709  var res = this.res;
710
711  this.emit('headers', res, path, stat);
712
713  if (!res.getHeader('Accept-Ranges')) res.setHeader('Accept-Ranges', 'bytes');
714  if (!res.getHeader('Date')) res.setHeader('Date', new Date().toUTCString());
715  if (!res.getHeader('Cache-Control')) res.setHeader('Cache-Control', 'public, max-age=' + Math.floor(this._maxage / 1000));
716
717  if (this._lastModified && !res.getHeader('Last-Modified')) {
718    var modified = stat.mtime.toUTCString()
719    debug('modified %s', modified)
720    res.setHeader('Last-Modified', modified)
721  }
722
723  if (this._etag && !res.getHeader('ETag')) {
724    var val = etag(stat)
725    debug('etag %s', val)
726    res.setHeader('ETag', val)
727  }
728};
729
730/**
731 * Determine if path parts contain a dotfile.
732 *
733 * @api private
734 */
735
736function containsDotFile(parts) {
737  for (var i = 0; i < parts.length; i++) {
738    if (parts[i][0] === '.') {
739      return true
740    }
741  }
742
743  return false
744}
745
746/**
747 * decodeURIComponent.
748 *
749 * Allows V8 to only deoptimize this fn instead of all
750 * of send().
751 *
752 * @param {String} path
753 * @api private
754 */
755
756function decode(path) {
757  try {
758    return decodeURIComponent(path)
759  } catch (err) {
760    return -1
761  }
762}
763
764/**
765 * Normalize the index option into an array.
766 *
767 * @param {boolean|string|array} val
768 * @api private
769 */
770
771function normalizeList(val){
772  return [].concat(val || [])
773}