aboutsummaryrefslogtreecommitdiffstats
path: root/node_modules/express/node_modules/send/lib/send.js
diff options
context:
space:
mode:
Diffstat (limited to 'node_modules/express/node_modules/send/lib/send.js')
-rw-r--r--node_modules/express/node_modules/send/lib/send.js473
1 files changed, 473 insertions, 0 deletions
diff --git a/node_modules/express/node_modules/send/lib/send.js b/node_modules/express/node_modules/send/lib/send.js
new file mode 100644
index 00000000..de721466
--- /dev/null
+++ b/node_modules/express/node_modules/send/lib/send.js
@@ -0,0 +1,473 @@
+
+/**
+ * Module dependencies.
+ */
+
+var debug = require('debug')('send')
+ , parseRange = require('range-parser')
+ , Stream = require('stream')
+ , mime = require('mime')
+ , fresh = require('fresh')
+ , path = require('path')
+ , http = require('http')
+ , fs = require('fs')
+ , basename = path.basename
+ , normalize = path.normalize
+ , join = path.join
+ , utils = require('./utils');
+
+/**
+ * Expose `send`.
+ */
+
+exports = module.exports = send;
+
+/**
+ * Expose mime module.
+ */
+
+exports.mime = mime;
+
+/**
+ * Return a `SendStream` for `req` and `path`.
+ *
+ * @param {Request} req
+ * @param {String} path
+ * @param {Object} options
+ * @return {SendStream}
+ * @api public
+ */
+
+function send(req, path, options) {
+ return new SendStream(req, path, options);
+}
+
+/**
+ * Initialize a `SendStream` with the given `path`.
+ *
+ * Events:
+ *
+ * - `error` an error occurred
+ * - `stream` file streaming has started
+ * - `end` streaming has completed
+ * - `directory` a directory was requested
+ *
+ * @param {Request} req
+ * @param {String} path
+ * @param {Object} options
+ * @api private
+ */
+
+function SendStream(req, path, options) {
+ var self = this;
+ this.req = req;
+ this.path = path;
+ this.options = options || {};
+ this.maxage(0);
+ this.hidden(false);
+ this.index('index.html');
+}
+
+/**
+ * Inherits from `Stream.prototype`.
+ */
+
+SendStream.prototype.__proto__ = Stream.prototype;
+
+/**
+ * Enable or disable "hidden" (dot) files.
+ *
+ * @param {Boolean} path
+ * @return {SendStream}
+ * @api public
+ */
+
+SendStream.prototype.hidden = function(val){
+ debug('hidden %s', val);
+ this._hidden = val;
+ return this;
+};
+
+/**
+ * Set index `path`, set to a falsy
+ * value to disable index support.
+ *
+ * @param {String|Boolean} path
+ * @return {SendStream}
+ * @api public
+ */
+
+SendStream.prototype.index = function(path){
+ debug('index %s', path);
+ this._index = path;
+ return this;
+};
+
+/**
+ * Set root `path`.
+ *
+ * @param {String} path
+ * @return {SendStream}
+ * @api public
+ */
+
+SendStream.prototype.root =
+SendStream.prototype.from = function(path){
+ this._root = normalize(path);
+ return this;
+};
+
+/**
+ * Set max-age to `ms`.
+ *
+ * @param {Number} ms
+ * @return {SendStream}
+ * @api public
+ */
+
+SendStream.prototype.maxage = function(ms){
+ if (Infinity == ms) ms = 60 * 60 * 24 * 365 * 1000;
+ debug('max-age %d', ms);
+ this._maxage = ms;
+ return this;
+};
+
+/**
+ * Emit error with `status`.
+ *
+ * @param {Number} status
+ * @api private
+ */
+
+SendStream.prototype.error = function(status, err){
+ var res = this.res;
+ var msg = http.STATUS_CODES[status];
+ err = err || new Error(msg);
+ err.status = status;
+ if (this.listeners('error').length) return this.emit('error', err);
+ res.statusCode = err.status;
+ res.end(msg);
+};
+
+/**
+ * Check if the pathname is potentially malicious.
+ *
+ * @return {Boolean}
+ * @api private
+ */
+
+SendStream.prototype.isMalicious = function(){
+ return !this._root && ~this.path.indexOf('..');
+};
+
+/**
+ * Check if the pathname ends with "/".
+ *
+ * @return {Boolean}
+ * @api private
+ */
+
+SendStream.prototype.hasTrailingSlash = function(){
+ return '/' == this.path[this.path.length - 1];
+};
+
+/**
+ * Check if the basename leads with ".".
+ *
+ * @return {Boolean}
+ * @api private
+ */
+
+SendStream.prototype.hasLeadingDot = function(){
+ return '.' == basename(this.path)[0];
+};
+
+/**
+ * Check if this is a conditional GET request.
+ *
+ * @return {Boolean}
+ * @api private
+ */
+
+SendStream.prototype.isConditionalGET = function(){
+ return this.req.headers['if-none-match']
+ || this.req.headers['if-modified-since'];
+};
+
+/**
+ * Strip content-* header fields.
+ *
+ * @api private
+ */
+
+SendStream.prototype.removeContentHeaderFields = function(){
+ var res = this.res;
+ Object.keys(res._headers).forEach(function(field){
+ if (0 == field.indexOf('content')) {
+ res.removeHeader(field);
+ }
+ });
+};
+
+/**
+ * Respond with 304 not modified.
+ *
+ * @api private
+ */
+
+SendStream.prototype.notModified = function(){
+ var res = this.res;
+ debug('not modified');
+ this.removeContentHeaderFields();
+ res.statusCode = 304;
+ res.end();
+};
+
+/**
+ * Check if the request is cacheable, aka
+ * responded with 2xx or 304 (see RFC 2616 section 14.2{5,6}).
+ *
+ * @return {Boolean}
+ * @api private
+ */
+
+SendStream.prototype.isCachable = function(){
+ var res = this.res;
+ return (res.statusCode >= 200 && res.statusCode < 300) || 304 == res.statusCode;
+};
+
+/**
+ * Handle stat() error.
+ *
+ * @param {Error} err
+ * @api private
+ */
+
+SendStream.prototype.onStatError = function(err){
+ var notfound = ['ENOENT', 'ENAMETOOLONG', 'ENOTDIR'];
+ if (~notfound.indexOf(err.code)) return this.error(404, err);
+ this.error(500, err);
+};
+
+/**
+ * Check if the cache is fresh.
+ *
+ * @return {Boolean}
+ * @api private
+ */
+
+SendStream.prototype.isFresh = function(){
+ return fresh(this.req.headers, this.res._headers);
+};
+
+/**
+ * Redirect to `path`.
+ *
+ * @param {String} path
+ * @api private
+ */
+
+SendStream.prototype.redirect = function(path){
+ if (this.listeners('directory').length) return this.emit('directory');
+ var res = this.res;
+ path += '/';
+ res.statusCode = 301;
+ res.setHeader('Location', path);
+ res.end('Redirecting to ' + utils.escape(path));
+};
+
+/**
+ * Pipe to `res.
+ *
+ * @param {Stream} res
+ * @return {Stream} res
+ * @api public
+ */
+
+SendStream.prototype.pipe = function(res){
+ var self = this
+ , args = arguments
+ , path = this.path
+ , root = this._root;
+
+ // references
+ this.res = res;
+
+ // invalid request uri
+ path = utils.decode(path);
+ if (-1 == path) return this.error(400);
+
+ // null byte(s)
+ if (~path.indexOf('\0')) return this.error(400);
+
+ // join / normalize from optional root dir
+ if (root) path = normalize(join(this._root, path));
+
+ // ".." is malicious without "root"
+ if (this.isMalicious()) return this.error(403);
+
+ // malicious path
+ if (root && 0 != path.indexOf(root)) return this.error(403);
+
+ // hidden file support
+ if (!this._hidden && this.hasLeadingDot()) return this.error(404);
+
+ // index file support
+ if (this._index && this.hasTrailingSlash()) path += this._index;
+
+ debug('stat "%s"', path);
+ fs.stat(path, function(err, stat){
+ if (err) return self.onStatError(err);
+ if (stat.isDirectory()) return self.redirect(self.path);
+ self.send(path, stat);
+ });
+
+ return res;
+};
+
+/**
+ * Transfer `path`.
+ *
+ * @param {String} path
+ * @api public
+ */
+
+SendStream.prototype.send = function(path, stat){
+ var options = this.options;
+ var len = stat.size;
+ var res = this.res;
+ var req = this.req;
+ var ranges = req.headers.range;
+ var offset = options.start || 0;
+
+ // set header fields
+ this.setHeader(stat);
+
+ // set content-type
+ this.type(path);
+
+ // conditional GET support
+ if (this.isConditionalGET()
+ && this.isCachable()
+ && this.isFresh()) {
+ return this.notModified();
+ }
+
+ // adjust len to start/end options
+ len = Math.max(0, len - offset);
+ if (options.end !== undefined) {
+ var bytes = options.end - offset + 1;
+ if (len > bytes) len = bytes;
+ }
+
+ // Range support
+ if (ranges) {
+ ranges = parseRange(len, ranges);
+
+ // unsatisfiable
+ if (-1 == ranges) {
+ res.setHeader('Content-Range', 'bytes */' + stat.size);
+ return this.error(416);
+ }
+
+ // valid (syntactically invalid ranges are treated as a regular response)
+ if (-2 != ranges) {
+ options.start = offset + ranges[0].start;
+ options.end = offset + ranges[0].end;
+
+ // Content-Range
+ res.statusCode = 206;
+ res.setHeader('Content-Range', 'bytes '
+ + ranges[0].start
+ + '-'
+ + ranges[0].end
+ + '/'
+ + len);
+ len = options.end - options.start + 1;
+ }
+ }
+
+ // content-length
+ res.setHeader('Content-Length', len);
+
+ // HEAD support
+ if ('HEAD' == req.method) return res.end();
+
+ this.stream(path, options);
+};
+
+/**
+ * Stream `path` to the response.
+ *
+ * @param {String} path
+ * @param {Object} options
+ * @api private
+ */
+
+SendStream.prototype.stream = function(path, options){
+ // TODO: this is all lame, refactor meeee
+ var self = this;
+ var res = this.res;
+ var req = this.req;
+
+ // pipe
+ var stream = fs.createReadStream(path, options);
+ this.emit('stream', stream);
+ stream.pipe(res);
+
+ // socket closed, done with the fd
+ req.on('close', stream.destroy.bind(stream));
+
+ // error handling code-smell
+ stream.on('error', function(err){
+ // no hope in responding
+ if (res._header) {
+ console.error(err.stack);
+ req.destroy();
+ return;
+ }
+
+ // 500
+ err.status = 500;
+ self.emit('error', err);
+ });
+
+ // end
+ stream.on('end', function(){
+ self.emit('end');
+ });
+};
+
+/**
+ * Set content-type based on `path`
+ * if it hasn't been explicitly set.
+ *
+ * @param {String} path
+ * @api private
+ */
+
+SendStream.prototype.type = function(path){
+ var res = this.res;
+ if (res.getHeader('Content-Type')) return;
+ var type = mime.lookup(path);
+ var charset = mime.charsets.lookup(type);
+ debug('content-type %s', type);
+ res.setHeader('Content-Type', type + (charset ? '; charset=' + charset : ''));
+};
+
+/**
+ * Set reaponse header fields, most
+ * fields may be pre-defined.
+ *
+ * @param {Object} stat
+ * @api private
+ */
+
+SendStream.prototype.setHeader = function(stat){
+ var res = this.res;
+ if (!res.getHeader('Accept-Ranges')) res.setHeader('Accept-Ranges', 'bytes');
+ if (!res.getHeader('ETag')) res.setHeader('ETag', utils.etag(stat));
+ if (!res.getHeader('Date')) res.setHeader('Date', new Date().toUTCString());
+ if (!res.getHeader('Cache-Control')) res.setHeader('Cache-Control', 'public, max-age=' + (this._maxage / 1000));
+ if (!res.getHeader('Last-Modified')) res.setHeader('Last-Modified', stat.mtime.toUTCString());
+};