github.com/solo-io/unik@v0.0.0-20190717152701-a58d3e8e33b7/docs/examples/example-nodejs-fileserver/node_modules/express/node_modules/send/index.js (about)

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