pax_global_header00006660000000000000000000000064124106115700014507gustar00rootroot0000000000000052 comment=3dbf47379d9077502208d8057022babcfc2f7cbc send-0.9.4/000077500000000000000000000000001241061157000124525ustar00rootroot00000000000000send-0.9.4/.gitignore000066400000000000000000000000461241061157000144420ustar00rootroot00000000000000coverage/ node_modules/ npm-debug.log send-0.9.4/.travis.yml000066400000000000000000000003711241061157000145640ustar00rootroot00000000000000language: node_js node_js: - "0.8" - "0.10" - "0.11" matrix: allow_failures: - node_js: "0.11" fast_finish: true script: "npm run-script test-travis" after_script: "npm install coveralls@2.10.0 && cat ./coverage/lcov.info | coveralls" send-0.9.4/History.md000066400000000000000000000104621241061157000144400ustar00rootroot000000000000000.9.3 / 2014-09-24 ================== * deps: etag@~1.4.0 - Support "fake" stats objects 0.9.2 / 2014-09-15 ================== * deps: depd@0.4.5 * deps: etag@~1.3.1 * deps: range-parser@~1.0.2 0.9.1 / 2014-09-07 ================== * deps: fresh@0.2.4 0.9.0 / 2014-09-07 ================== * Add `lastModified` option * Use `etag` to generate `ETag` header * deps: debug@~2.0.0 0.8.5 / 2014-09-04 ================== * Fix malicious path detection for empty string path 0.8.4 / 2014-09-04 ================== * Fix a path traversal issue when using `root` 0.8.3 / 2014-08-16 ================== * deps: destroy@1.0.3 - renamed from dethroy * deps: on-finished@2.1.0 0.8.2 / 2014-08-14 ================== * Work around `fd` leak in Node.js 0.10 for `fs.ReadStream` * deps: dethroy@1.0.2 0.8.1 / 2014-08-05 ================== * Fix `extensions` behavior when file already has extension 0.8.0 / 2014-08-05 ================== * Add `extensions` option 0.7.4 / 2014-08-04 ================== * Fix serving index files without root dir 0.7.3 / 2014-07-29 ================== * Fix incorrect 403 on Windows and Node.js 0.11 0.7.2 / 2014-07-27 ================== * deps: depd@0.4.4 - Work-around v8 generating empty stack traces 0.7.1 / 2014-07-26 ================== * deps: depd@0.4.3 - Fix exception when global `Error.stackTraceLimit` is too low 0.7.0 / 2014-07-20 ================== * Deprecate `hidden` option; use `dotfiles` option * Add `dotfiles` option * deps: debug@1.0.4 * deps: depd@0.4.2 - Add `TRACE_DEPRECATION` environment variable - Remove non-standard grey color from color output - Support `--no-deprecation` argument - Support `--trace-deprecation` argument 0.6.0 / 2014-07-11 ================== * Deprecate `from` option; use `root` option * Deprecate `send.etag()` -- use `etag` in `options` * Deprecate `send.hidden()` -- use `hidden` in `options` * Deprecate `send.index()` -- use `index` in `options` * Deprecate `send.maxage()` -- use `maxAge` in `options` * Deprecate `send.root()` -- use `root` in `options` * Cap `maxAge` value to 1 year * deps: debug@1.0.3 - Add support for multiple wildcards in namespaces 0.5.0 / 2014-06-28 ================== * Accept string for `maxAge` (converted by `ms`) * Add `headers` event * Include link in default redirect response * Use `EventEmitter.listenerCount` to count listeners 0.4.3 / 2014-06-11 ================== * Do not throw un-catchable error on file open race condition * Use `escape-html` for HTML escaping * deps: debug@1.0.2 - fix some debugging output colors on node.js 0.8 * deps: finished@1.2.2 * deps: fresh@0.2.2 0.4.2 / 2014-06-09 ================== * fix "event emitter leak" warnings * deps: debug@1.0.1 * deps: finished@1.2.1 0.4.1 / 2014-06-02 ================== * Send `max-age` in `Cache-Control` in correct format 0.4.0 / 2014-05-27 ================== * Calculate ETag with md5 for reduced collisions * Fix wrong behavior when index file matches directory * Ignore stream errors after request ends - Goodbye `EBADF, read` * Skip directories in index file search * deps: debug@0.8.1 0.3.0 / 2014-04-24 ================== * Fix sending files with dots without root set * Coerce option types * Accept API options in options object * Set etags to "weak" * Include file path in etag * Make "Can't set headers after they are sent." catchable * Send full entity-body for multi range requests * Default directory access to 403 when index disabled * Support multiple index paths * Support "If-Range" header * Control whether to generate etags * deps: mime@1.2.11 0.2.0 / 2014-01-29 ================== * update range-parser and fresh 0.1.4 / 2013-08-11 ================== * update fresh 0.1.3 / 2013-07-08 ================== * Revert "Fix fd leak" 0.1.2 / 2013-07-03 ================== * Fix fd leak 0.1.0 / 2012-08-25 ================== * add options parameter to send() that is passed to fs.createReadStream() [kanongil] 0.0.4 / 2012-08-16 ================== * allow custom "Accept-Ranges" definition 0.0.3 / 2012-07-16 ================== * fix normalization of the root directory. Closes #3 0.0.2 / 2012-07-09 ================== * add passing of req explicitly for now (YUCK) 0.0.1 / 2010-01-03 ================== * Initial release send-0.9.4/LICENSE000066400000000000000000000021431241061157000134570ustar00rootroot00000000000000(The MIT License) Copyright (c) 2012 TJ Holowaychuk Copyright (c) 2014 Douglas Christopher Wilson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. send-0.9.4/Readme.md000066400000000000000000000123021241061157000141670ustar00rootroot00000000000000# send [![NPM Version][npm-image]][npm-url] [![NPM Downloads][downloads-image]][downloads-url] [![Build Status][travis-image]][travis-url] [![Test Coverage][coveralls-image]][coveralls-url] [![Gittip][gittip-image]][gittip-url] Send is Connect's `static()` extracted for generalized use, a streaming static file server supporting partial responses (Ranges), conditional-GET negotiation, high test coverage, and granular events which may be leveraged to take appropriate actions in your application or framework. ## Installation ```bash $ npm install send ``` ## API ```js var send = require('send') ``` ### send(req, path, [options]) Create a new `SendStream` for the given path to send to a `res`. The `req` is the Node.js HTTP request and the `path` is a urlencoded path to send (urlencoded, not the actual file-system path). #### Options ##### dotfiles Set how "dotfiles" are treated when encountered. A dotfile is a file or directory that begins with a dot ("."). Note this check is done on the path itself without checking if the path actually exists on the disk. If `root` is specified, only the dotfiles above the root are checked (i.e. the root itself can be within a dotfile when when set to "deny"). The default value is `'ignore'`. - `'allow'` No special treatment for dotfiles. - `'deny'` Send a 403 for any request for a dotfile. - `'ignore'` Pretend like the dotfile does not exist and 404. ##### etag Enable or disable etag generation, defaults to true. ##### extensions If a given file doesn't exist, try appending one of the given extensions, in the given order. By default, this is disabled (set to `false`). An example value that will serve extension-less HTML files: `['html', 'htm']`. This is skipped if the requested file already has an extension. ##### index By default send supports "index.html" files, to disable this set `false` or to supply a new index pass a string or an array in preferred order. ##### lastModified Enable or disable `Last-Modified` header, defaults to true. Uses the file system's last modified value. ##### maxAge Provide a max-age in milliseconds for http caching, defaults to 0. This can also be a string accepted by the [ms](https://www.npmjs.org/package/ms#readme) module. ##### root Serve files relative to `path`. ### Events The `SendStream` is an event emitter and will emit the following events: - `error` an error occurred `(err)` - `directory` a directory was requested - `file` a file was requested `(path, stat)` - `headers` the headers are about to be set on a file `(res, path, stat)` - `stream` file streaming has started `(stream)` - `end` streaming has completed ### .pipe The `pipe` method is used to pipe the response into the Node.js HTTP response object, typically `send(req, path, options).pipe(res)`. ## Error-handling By default when no `error` listeners are present an automatic response will be made, otherwise you have full control over the response, aka you may show a 5xx page etc. ## Caching It does _not_ perform internal caching, you should use a reverse proxy cache such as Varnish for this, or those fancy things called CDNs. If your application is small enough that it would benefit from single-node memory caching, it's small enough that it does not need caching at all ;). ## Debugging To enable `debug()` instrumentation output export __DEBUG__: ``` $ DEBUG=send node app ``` ## Running tests ``` $ npm install $ npm test ``` ## Examples Small: ```js var http = require('http'); var send = require('send'); var app = http.createServer(function(req, res){ send(req, req.url).pipe(res); }).listen(3000); ``` Serving from a root directory with custom error-handling: ```js var http = require('http'); var send = require('send'); var url = require('url'); var app = http.createServer(function(req, res){ // your custom error-handling logic: function error(err) { res.statusCode = err.status || 500; res.end(err.message); } // your custom headers function headers(res, path, stat) { // serve all files for download res.setHeader('Content-Disposition', 'attachment'); } // your custom directory handling logic: function redirect() { res.statusCode = 301; res.setHeader('Location', req.url + '/'); res.end('Redirecting to ' + req.url + '/'); } // transfer arbitrary files from within // /www/example.com/public/* send(req, url.parse(req.url).pathname, {root: '/www/example.com/public'}) .on('error', error) .on('directory', redirect) .on('headers', headers) .pipe(res); }).listen(3000); ``` ## License [MIT](LICENSE) [npm-image]: https://img.shields.io/npm/v/send.svg?style=flat [npm-url]: https://npmjs.org/package/send [travis-image]: https://img.shields.io/travis/visionmedia/send.svg?style=flat [travis-url]: https://travis-ci.org/visionmedia/send [coveralls-image]: https://img.shields.io/coveralls/visionmedia/send.svg?style=flat [coveralls-url]: https://coveralls.io/r/visionmedia/send?branch=master [downloads-image]: https://img.shields.io/npm/dm/send.svg?style=flat [downloads-url]: https://npmjs.org/package/send [gittip-image]: https://img.shields.io/gittip/dougwilson.svg?style=flat [gittip-url]: https://www.gittip.com/dougwilson/ send-0.9.4/index.js000066400000000000000000000400671241061157000141260ustar00rootroot00000000000000 /** * Module dependencies. */ var debug = require('debug')('send') var deprecate = require('depd')('send') var destroy = require('destroy') var escapeHtml = require('escape-html') , parseRange = require('range-parser') , Stream = require('stream') , mime = require('mime') , fresh = require('fresh') , path = require('path') , http = require('http') , fs = require('fs') , normalize = path.normalize , join = path.join var etag = require('etag') var EventEmitter = require('events').EventEmitter; var ms = require('ms'); var onFinished = require('on-finished') /** * Variables. */ var extname = path.extname var maxMaxAge = 60 * 60 * 24 * 365 * 1000; // 1 year var resolve = path.resolve var sep = path.sep var toString = Object.prototype.toString var upPathRegexp = /(?:^|[\\\/])\.\.(?:[\\\/]|$)/ /** * Expose `send`. */ exports = module.exports = send; /** * Expose mime module. */ exports.mime = mime; /** * Shim EventEmitter.listenerCount for node.js < 0.10 */ /* istanbul ignore next */ var listenerCount = EventEmitter.listenerCount || function(emitter, type){ return emitter.listeners(type).length; }; /** * 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`. * * @param {Request} req * @param {String} path * @param {Object} options * @api private */ function SendStream(req, path, options) { var self = this; options = options || {}; this.req = req; this.path = path; this.options = options; this._etag = options.etag !== undefined ? Boolean(options.etag) : true this._dotfiles = options.dotfiles !== undefined ? options.dotfiles : 'ignore' if (['allow', 'deny', 'ignore'].indexOf(this._dotfiles) === -1) { throw new TypeError('dotfiles option must be "allow", "deny", or "ignore"') } this._hidden = Boolean(options.hidden) if ('hidden' in options) { deprecate('hidden: use dotfiles: \'' + (this._hidden ? 'allow' : 'ignore') + '\' instead') } // legacy support if (!('dotfiles' in options)) { this._dotfiles = undefined } this._extensions = options.extensions !== undefined ? normalizeList(options.extensions) : [] this._index = options.index !== undefined ? normalizeList(options.index) : ['index.html'] this._lastModified = options.lastModified !== undefined ? Boolean(options.lastModified) : true this._maxage = options.maxAge || options.maxage this._maxage = typeof this._maxage === 'string' ? ms(this._maxage) : Number(this._maxage) this._maxage = !isNaN(this._maxage) ? Math.min(Math.max(0, this._maxage), maxMaxAge) : 0 this._root = options.root ? resolve(options.root) : null if (!this._root && options.from) { this.from(options.from); } } /** * Inherits from `Stream.prototype`. */ SendStream.prototype.__proto__ = Stream.prototype; /** * Enable or disable etag generation. * * @param {Boolean} val * @return {SendStream} * @api public */ SendStream.prototype.etag = deprecate.function(function etag(val) { val = Boolean(val); debug('etag %s', val); this._etag = val; return this; }, 'send.etag: pass etag as option'); /** * Enable or disable "hidden" (dot) files. * * @param {Boolean} path * @return {SendStream} * @api public */ SendStream.prototype.hidden = deprecate.function(function hidden(val) { val = Boolean(val); debug('hidden %s', val); this._hidden = val; this._dotfiles = undefined return this; }, 'send.hidden: use dotfiles option'); /** * Set index `paths`, set to a falsy * value to disable index support. * * @param {String|Boolean|Array} paths * @return {SendStream} * @api public */ SendStream.prototype.index = deprecate.function(function index(paths) { var index = !paths ? [] : normalizeList(paths); debug('index %o', paths); this._index = index; return this; }, 'send.index: pass index as option'); /** * Set root `path`. * * @param {String} path * @return {SendStream} * @api public */ SendStream.prototype.root = function(path){ path = String(path); this._root = resolve(path) return this; }; SendStream.prototype.from = deprecate.function(SendStream.prototype.root, 'send.from: pass root as option'); SendStream.prototype.root = deprecate.function(SendStream.prototype.root, 'send.root: pass root as option'); /** * Set max-age to `maxAge`. * * @param {Number} maxAge * @return {SendStream} * @api public */ SendStream.prototype.maxage = deprecate.function(function maxage(maxAge) { maxAge = typeof maxAge === 'string' ? ms(maxAge) : Number(maxAge); if (isNaN(maxAge)) maxAge = 0; if (Infinity == maxAge) maxAge = 60 * 60 * 24 * 365 * 1000; debug('max-age %d', maxAge); this._maxage = maxAge; return this; }, 'send.maxage: pass maxAge as option'); /** * 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; // emit if listeners instead of responding if (listenerCount(this, 'error') !== 0) { return this.emit('error', err); } // wipe all existing headers res._headers = undefined; res.statusCode = err.status; res.end(msg); }; /** * Check if the pathname ends with "/". * * @return {Boolean} * @api private */ SendStream.prototype.hasTrailingSlash = function(){ return '/' == this.path[this.path.length - 1]; }; /** * 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(); }; /** * Raise error that headers already sent. * * @api private */ SendStream.prototype.headersAlreadySent = function headersAlreadySent(){ var err = new Error('Can\'t set headers after they are sent.'); debug('headers already sent'); this.error(500, err); }; /** * 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); }; /** * Check if the range is fresh. * * @return {Boolean} * @api private */ SendStream.prototype.isRangeFresh = function isRangeFresh(){ var ifRange = this.req.headers['if-range']; if (!ifRange) return true; return ~ifRange.indexOf('"') ? ~ifRange.indexOf(this.res._headers['etag']) : Date.parse(this.res._headers['last-modified']) <= Date.parse(ifRange); }; /** * Redirect to `path`. * * @param {String} path * @api private */ SendStream.prototype.redirect = function(path){ if (listenerCount(this, 'directory') !== 0) { return this.emit('directory'); } if (this.hasTrailingSlash()) return this.error(403); var res = this.res; path += '/'; res.statusCode = 301; res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.setHeader('Location', path); res.end('Redirecting to ' + escapeHtml(path) + '\n'); }; /** * Pipe to `res. * * @param {Stream} res * @return {Stream} res * @api public */ SendStream.prototype.pipe = function(res){ var self = this , args = arguments , root = this._root; // references this.res = res; // decode the path var path = decode(this.path) if (path === -1) return this.error(400) // null byte(s) if (~path.indexOf('\0')) return this.error(400); var parts if (root !== null) { // join / normalize from optional root dir path = normalize(join(root, path)) root = normalize(root + sep) // malicious path if ((path + sep).substr(0, root.length) !== root) { debug('malicious path "%s"', path) return this.error(403) } // explode path parts parts = path.substr(root.length).split(sep) } else { // ".." is malicious without "root" if (upPathRegexp.test(path)) { debug('malicious path "%s"', path) return this.error(403) } // explode path parts parts = normalize(path).split(sep) // resolve the path path = resolve(path) } // dotfile handling if (containsDotFile(parts)) { var access = this._dotfiles // legacy support if (access === undefined) { access = parts[parts.length - 1][0] === '.' ? (this._hidden ? 'allow' : 'ignore') : 'allow' } debug('%s dotfile "%s"', access, path) switch (access) { case 'allow': break case 'deny': return this.error(403) case 'ignore': default: return this.error(404) } } // index file support if (this._index.length && this.path[this.path.length - 1] === '/') { this.sendIndex(path); return res; } this.sendFile(path); 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; if (res._header) { // impossible to send now return this.headersAlreadySent(); } debug('pipe "%s"', path) // set header fields this.setHeader(path, 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); // If-Range support if (!this.isRangeFresh()) { debug('range stale'); ranges = -2; } // unsatisfiable if (-1 == ranges) { debug('range unsatisfiable'); res.setHeader('Content-Range', 'bytes */' + stat.size); return this.error(416); } // valid (syntactically invalid/multiple ranges are treated as a regular response) if (-2 != ranges && ranges.length === 1) { debug('range %j', 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); }; /** * Transfer file for `path`. * * @param {String} path * @api private */ SendStream.prototype.sendFile = function sendFile(path) { var i = 0 var self = this debug('stat "%s"', path); fs.stat(path, function onstat(err, stat) { if (err && err.code === 'ENOENT' && !extname(path) && path[path.length - 1] !== sep) { // not found, check extensions return next(err) } if (err) return self.onStatError(err) if (stat.isDirectory()) return self.redirect(self.path) self.emit('file', path, stat) self.send(path, stat) }) function next(err) { if (self._extensions.length <= i) { return err ? self.onStatError(err) : self.error(404) } var p = path + '.' + self._extensions[i++] debug('stat "%s"', p) fs.stat(p, function (err, stat) { if (err) return next(err) if (stat.isDirectory()) return next() self.emit('file', p, stat) self.send(p, stat) }) } } /** * Transfer index for `path`. * * @param {String} path * @api private */ SendStream.prototype.sendIndex = function sendIndex(path){ var i = -1; var self = this; function next(err){ if (++i >= self._index.length) { if (err) return self.onStatError(err); return self.error(404); } var p = join(path, self._index[i]); debug('stat "%s"', p); fs.stat(p, function(err, stat){ if (err) return next(err); if (stat.isDirectory()) return next(); self.emit('file', p, stat); self.send(p, stat); }); } next(); }; /** * 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 finished = false; 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); // response finished, done with the fd onFinished(res, function onfinished(){ finished = true; destroy(stream); }); // error handling code-smell stream.on('error', function onerror(err){ // request already finished if (finished) return; // clean up stream finished = true; destroy(stream); // error self.onStatError(err); }); // end stream.on('end', function onend(){ 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 response header fields, most * fields may be pre-defined. * * @param {String} path * @param {Object} stat * @api private */ SendStream.prototype.setHeader = function setHeader(path, stat){ var res = this.res; this.emit('headers', res, path, stat); if (!res.getHeader('Accept-Ranges')) res.setHeader('Accept-Ranges', 'bytes'); if (!res.getHeader('Date')) res.setHeader('Date', new Date().toUTCString()); if (!res.getHeader('Cache-Control')) res.setHeader('Cache-Control', 'public, max-age=' + Math.floor(this._maxage / 1000)); if (this._lastModified && !res.getHeader('Last-Modified')) { var modified = stat.mtime.toUTCString() debug('modified %s', modified) res.setHeader('Last-Modified', modified) } if (this._etag && !res.getHeader('ETag')) { var val = etag(stat) debug('etag %s', val) res.setHeader('ETag', val) } }; /** * Determine if path parts contain a dotfile. * * @api private */ function containsDotFile(parts) { for (var i = 0; i < parts.length; i++) { if (parts[i][0] === '.') { return true } } return false } /** * decodeURIComponent. * * Allows V8 to only deoptimize this fn instead of all * of send(). * * @param {String} path * @api private */ function decode(path) { try { return decodeURIComponent(path) } catch (err) { return -1 } } /** * Normalize the index option into an array. * * @param {boolean|string|array} val * @api private */ function normalizeList(val){ return [].concat(val || []) } send-0.9.4/package.json000066400000000000000000000022331241061157000147400ustar00rootroot00000000000000{ "name": "send", "description": "Better streaming static file server with Range and conditional-GET support", "version": "0.9.3", "author": "TJ Holowaychuk ", "contributors": [ "Douglas Christopher Wilson " ], "license": "MIT", "repository": "visionmedia/send", "keywords": [ "static", "file", "server" ], "dependencies": { "debug": "~2.0.0", "depd": "0.4.5", "destroy": "1.0.3", "escape-html": "1.0.1", "etag": "~1.4.0", "fresh": "0.2.4", "mime": "1.2.11", "ms": "0.6.2", "on-finished": "2.1.0", "range-parser": "~1.0.2" }, "devDependencies": { "istanbul": "0.3.2", "mocha": "~1.21.0", "should": "~4.0.0", "supertest": "~0.13.0" }, "files": [ "History.md", "LICENSE", "index.js" ], "engines": { "node": ">= 0.8.0" }, "scripts": { "test": "mocha --check-leaks --reporter spec --bail", "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --check-leaks --reporter dot", "test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --check-leaks --reporter spec" } } send-0.9.4/test/000077500000000000000000000000001241061157000134315ustar00rootroot00000000000000send-0.9.4/test/fixtures/000077500000000000000000000000001241061157000153025ustar00rootroot00000000000000send-0.9.4/test/fixtures/.hidden000066400000000000000000000000071241061157000165330ustar00rootroot00000000000000secret send-0.9.4/test/fixtures/.mine/000077500000000000000000000000001241061157000163105ustar00rootroot00000000000000send-0.9.4/test/fixtures/.mine/name.txt000066400000000000000000000000041241061157000177630ustar00rootroot00000000000000tobisend-0.9.4/test/fixtures/do..ts.txt000066400000000000000000000000031241061157000171410ustar00rootroot00000000000000...send-0.9.4/test/fixtures/name.d/000077500000000000000000000000001241061157000164445ustar00rootroot00000000000000send-0.9.4/test/fixtures/name.d/name.txt000066400000000000000000000000041241061157000201170ustar00rootroot00000000000000lokisend-0.9.4/test/fixtures/name.dir/000077500000000000000000000000001241061157000167775ustar00rootroot00000000000000send-0.9.4/test/fixtures/name.dir/name.txt000066400000000000000000000000041241061157000204520ustar00rootroot00000000000000tobisend-0.9.4/test/fixtures/name.html000066400000000000000000000000131241061157000171020ustar00rootroot00000000000000

tobi

send-0.9.4/test/fixtures/name.txt000066400000000000000000000000041241061157000167550ustar00rootroot00000000000000tobisend-0.9.4/test/fixtures/nums000066400000000000000000000000111241061157000161770ustar00rootroot00000000000000123456789send-0.9.4/test/fixtures/pets/000077500000000000000000000000001241061157000162555ustar00rootroot00000000000000send-0.9.4/test/fixtures/pets/index.html000066400000000000000000000000161241061157000202470ustar00rootroot00000000000000tobi loki janesend-0.9.4/test/fixtures/some thing.txt000066400000000000000000000000031241061157000200710ustar00rootroot00000000000000heysend-0.9.4/test/fixtures/thing.html.html000066400000000000000000000000141241061157000202370ustar00rootroot00000000000000

trap!

send-0.9.4/test/fixtures/tobi.html000066400000000000000000000000131241061157000171170ustar00rootroot00000000000000

tobi

send-0.9.4/test/send.js000066400000000000000000001003451241061157000147230ustar00rootroot00000000000000 process.env.NO_DEPRECATION = 'send'; var assert = require('assert'); var fs = require('fs'); var http = require('http'); var path = require('path'); var request = require('supertest'); var send = require('..') var should = require('should'); // test server var dateRegExp = /^\w{3}, \d+ \w+ \d+ \d+:\d+:\d+ \w+$/; var fixtures = path.join(__dirname, 'fixtures'); var app = http.createServer(function(req, res){ function error(err) { res.statusCode = err.status; res.end(http.STATUS_CODES[err.status]); } function redirect() { res.statusCode = 301; res.setHeader('Location', req.url + '/'); res.end('Redirecting to ' + req.url + '/'); } send(req, req.url, {root: fixtures}) .on('error', error) .on('directory', redirect) .pipe(res); }); describe('send.mime', function(){ it('should be exposed', function(){ assert(send.mime); }) }) describe('send(file).pipe(res)', function(){ it('should stream the file contents', function(done){ request(app) .get('/name.txt') .expect('Content-Length', '4') .expect(200, 'tobi', done) }) it('should decode the given path as a URI', function(done){ request(app) .get('/some%20thing.txt') .expect(200, 'hey', done) }) it('should serve files with dots in name', function(done){ request(app) .get('/do..ts.txt') .expect(200, '...', done) }) it('should treat a malformed URI as a bad request', function(done){ request(app) .get('/some%99thing.txt') .expect(400, 'Bad Request', done) }) it('should 400 on NULL bytes', function(done){ request(app) .get('/some%00thing.txt') .expect(400, 'Bad Request', done) }) it('should treat an ENAMETOOLONG as a 404', function(done){ var path = Array(100).join('foobar'); request(app) .get('/' + path) .expect(404, done); }) it('should handle headers already sent error', function(done){ var app = http.createServer(function(req, res){ res.write('0'); send(req, req.url, {root: fixtures}) .on('error', function(err){ res.end(' - ' + err.message) }) .pipe(res); }); request(app) .get('/nums') .expect(200, '0 - Can\'t set headers after they are sent.', done); }) it('should support HEAD', function(done){ request(app) .head('/name.txt') .expect('Content-Length', '4') .expect(200, '', done) }) it('should add an ETag header field', function(done){ request(app) .get('/name.txt') .expect('etag', /^W\/"[^"]+"$/) .end(done); }) it('should add a Date header field', function(done){ request(app) .get('/name.txt') .expect('date', dateRegExp, done) }) it('should add a Last-Modified header field', function(done){ request(app) .get('/name.txt') .expect('last-modified', dateRegExp, done) }) it('should add a Accept-Ranges header field', function(done){ request(app) .get('/name.txt') .expect('Accept-Ranges', 'bytes', done) }) it('should 404 if the file does not exist', function(done){ request(app) .get('/meow') .expect(404, 'Not Found', done) }) it('should 301 if the directory exists', function(done){ request(app) .get('/pets') .expect('Location', '/pets/') .expect(301, 'Redirecting to /pets/', done) }) it('should not override content-type', function(done){ var app = http.createServer(function(req, res){ res.setHeader('Content-Type', 'application/x-custom') send(req, req.url, {root: fixtures}).pipe(res) }); request(app) .get('/nums') .expect('Content-Type', 'application/x-custom', done); }) it('should set Content-Type via mime map', function(done){ request(app) .get('/name.txt') .expect('Content-Type', 'text/plain; charset=UTF-8') .expect(200, function(err){ if (err) return done(err) request(app) .get('/tobi.html') .expect('Content-Type', 'text/html; charset=UTF-8') .expect(200, done) }); }) it('should 404 if file disappears after stat, before open', function(done){ var app = http.createServer(function(req, res){ send(req, req.url, {root: 'test/fixtures'}) .on('file', function(){ // simulate file ENOENT after on open, after stat var fn = this.send; this.send = function(path, stat){ path += '__xxx_no_exist'; fn.call(this, path, stat); }; }) .pipe(res); }); request(app) .get('/name.txt') .expect(404, done); }) it('should 500 on file stream error', function(done){ var app = http.createServer(function(req, res){ send(req, req.url, {root: 'test/fixtures'}) .on('stream', function(stream){ // simulate file error process.nextTick(function(){ stream.emit('error', new Error('boom!')); }); }) .pipe(res); }); request(app) .get('/name.txt') .expect(500, done); }) describe('"headers" event', function () { var args var fn var headers var server before(function () { server = http.createServer(function (req, res) { send(req, req.url, {root: fixtures}) .on('headers', function () { args = arguments headers = true fn && fn.apply(this, arguments) }) .pipe(res) }) }) beforeEach(function () { args = undefined fn = undefined headers = false }) it('should fire when sending file', function (done) { request(server) .get('/nums') .expect(200, '123456789', function (err, res) { if (err) return done(err) headers.should.be.true done() }) }) it('should not fire on 404', function (done) { request(server) .get('/bogus') .expect(404, function (err, res) { if (err) return done(err) headers.should.be.false done() }) }) it('should fire on index', function (done) { request(server) .get('/pets/') .expect(200, /tobi/, function (err, res) { if (err) return done(err) headers.should.be.true done() }) }) it('should not fire on redirect', function (done) { request(server) .get('/pets') .expect(301, function (err, res) { if (err) return done(err) headers.should.be.false done() }) }) it('should provide path', function (done) { request(server) .get('/nums') .expect(200, '123456789', function (err, res) { if (err) return done(err) headers.should.be.true args[1].should.endWith('nums') done() }) }) it('should provide stat', function (done) { request(server) .get('/nums') .expect(200, '123456789', function (err, res) { if (err) return done(err) headers.should.be.true args[2].should.have.property('mtime') done() }) }) it('should allow altering headers', function (done) { fn = function (res, path, stat) { res.setHeader('Cache-Control', 'no-cache') res.setHeader('Content-Type', 'text/x-custom') res.setHeader('ETag', 'W/"everything"') res.setHeader('X-Created', stat.ctime.toUTCString()) } request(server) .get('/nums') .expect('Cache-Control', 'no-cache') .expect('Content-Type', 'text/x-custom') .expect('ETag', 'W/"everything"') .expect('X-Created', dateRegExp) .expect(200, '123456789', done) }) }) describe('when no "directory" listeners are present', function(){ var server before(function(){ server = http.createServer(function(req, res){ send(req, req.url, {root: 'test/fixtures'}) .pipe(res) }) }) it('should respond with an HTML redirect', function(done){ request(server) .get('/pets') .expect('Location', '/pets/') .expect('Content-Type', 'text/html; charset=utf-8') .expect(301, 'Redirecting to /pets/\n', done) }) }) describe('when no "error" listeners are present', function(){ it('should respond to errors directly', function(done){ var app = http.createServer(function(req, res){ send(req, 'test/fixtures' + req.url).pipe(res); }); request(app) .get('/foobar') .expect(404, 'Not Found', done) }) }) describe('with conditional-GET', function(){ it('should respond with 304 on a match', function(done){ request(app) .get('/name.txt') .expect(200, function(err, res){ if (err) return done(err) request(app) .get('/name.txt') .set('If-None-Match', res.headers.etag) .expect(304, function(err, res){ if (err) return done(err) res.headers.should.not.have.property('content-type'); res.headers.should.not.have.property('content-length'); done(); }); }) }) it('should respond with 200 otherwise', function(done){ request(app) .get('/name.txt') .expect(200, function(err, res){ if (err) return done(err) request(app) .get('/name.txt') .set('If-None-Match', '"123"') .expect(200, 'tobi', done) }) }) }) describe('with Range request', function(){ it('should support byte ranges', function(done){ request(app) .get('/nums') .set('Range', 'bytes=0-4') .expect(206, '12345', done); }) it('should be inclusive', function(done){ request(app) .get('/nums') .set('Range', 'bytes=0-0') .expect(206, '1', done); }) it('should set Content-Range', function(done){ request(app) .get('/nums') .set('Range', 'bytes=2-5') .expect('Content-Range', 'bytes 2-5/9') .expect(206, done); }) it('should support -n', function(done){ request(app) .get('/nums') .set('Range', 'bytes=-3') .expect(206, '789', done); }) it('should support n-', function(done){ request(app) .get('/nums') .set('Range', 'bytes=3-') .expect(206, '456789', done); }) it('should respond with 206 "Partial Content"', function(done){ request(app) .get('/nums') .set('Range', 'bytes=0-4') .expect(206, done); }) it('should set Content-Length to the # of octets transferred', function(done){ request(app) .get('/nums') .set('Range', 'bytes=2-3') .expect('Content-Length', '2') .expect(206, '34', done); }) describe('when last-byte-pos of the range is greater the length', function(){ it('is taken to be equal to one less than the length', function(done){ request(app) .get('/nums') .set('Range', 'bytes=2-50') .expect('Content-Range', 'bytes 2-8/9') .expect(206, done); }) it('should adapt the Content-Length accordingly', function(done){ request(app) .get('/nums') .set('Range', 'bytes=2-50') .expect('Content-Length', '7') .expect(206, done); }) }) describe('when the first- byte-pos of the range is greater length', function(){ it('should respond with 416', function(done){ request(app) .get('/nums') .set('Range', 'bytes=9-50') .expect('Content-Range', 'bytes */9') .expect(416, done); }) }) describe('when syntactically invalid', function(){ it('should respond with 200 and the entire contents', function(done){ request(app) .get('/nums') .set('Range', 'asdf') .expect(200, '123456789', done); }) }) describe('when multiple ranges', function(){ it('should respond with 200 and the entire contents', function(done){ request(app) .get('/nums') .set('Range', 'bytes=1-1,3-') .expect(200, '123456789', done); }) }) describe('when if-range present', function(){ it('should respond with parts when etag unchanged', function(done){ request(app) .get('/nums') .expect(200, function(err, res){ if (err) return done(err); var etag = res.headers.etag; request(app) .get('/nums') .set('If-Range', etag) .set('Range', 'bytes=0-0') .expect(206, '1', done); }) }) it('should respond with 200 when etag changed', function(done){ request(app) .get('/nums') .expect(200, function(err, res){ if (err) return done(err); var etag = res.headers.etag.replace(/"(.)/, '"0$1'); request(app) .get('/nums') .set('If-Range', etag) .set('Range', 'bytes=0-0') .expect(200, '123456789', done); }) }) it('should respond with parts when modified unchanged', function(done){ request(app) .get('/nums') .expect(200, function(err, res){ if (err) return done(err); var modified = res.headers['last-modified'] request(app) .get('/nums') .set('If-Range', modified) .set('Range', 'bytes=0-0') .expect(206, '1', done); }) }) it('should respond with 200 when modified changed', function(done){ request(app) .get('/nums') .expect(200, function(err, res){ if (err) return done(err); var modified = Date.parse(res.headers['last-modified']) - 20000; request(app) .get('/nums') .set('If-Range', new Date(modified).toUTCString()) .set('Range', 'bytes=0-0') .expect(200, '123456789', done); }) }) }) }) describe('when "options" is specified', function(){ it('should support start/end', function(done){ var app = http.createServer(function(req, res){ var i = parseInt(req.url.slice(1)); send(req, 'test/fixtures/nums', { start:i*3, end:i*3+2 }).pipe(res); }); request(app) .get('/1') .expect(200, '456', done); }) it('should adjust too large end', function(done){ var app = http.createServer(function(req, res){ var i = parseInt(req.url.slice(1)); send(req, 'test/fixtures/nums', { start:i*3, end:90 }).pipe(res); }); request(app) .get('/1') .expect(200, '456789', done); }) it('should support start/end with Range request', function(done){ var app = http.createServer(function(req, res){ var i = parseInt(req.url.slice(1)); send(req, 'test/fixtures/nums', { start:i*3, end:i*3+2 }).pipe(res); }); request(app) .get('/0') .set('Range', 'bytes=-2') .expect(206, '23', done) }) }) describe('.etag()', function(){ it('should support disabling etags', function(done){ var app = http.createServer(function(req, res){ send(req, req.url, {root: fixtures}) .etag(false) .pipe(res); }); request(app) .get('/nums') .expect(200, function(err, res){ if (err) return done(err); res.headers.should.not.have.property('etag'); done(); }); }) }) describe('.from()', function(){ it('should set with deprecated from', function(done){ var app = http.createServer(function(req, res){ send(req, req.url) .from(__dirname + '/fixtures') .pipe(res); }); request(app) .get('/pets/../name.txt') .expect(200, 'tobi', done) }) }) describe('.hidden()', function(){ it('should default support sending hidden files', function(done){ var app = http.createServer(function(req, res){ send(req, req.url, {root: fixtures}) .hidden(true) .pipe(res); }); request(app) .get('/.hidden') .expect(200, /secret/, done); }) }) describe('.index()', function(){ it('should be configurable', function(done){ var app = http.createServer(function(req, res){ send(req, req.url, {root: fixtures}) .index('tobi.html') .pipe(res); }); request(app) .get('/') .expect(200, '

tobi

', done); }) it('should support disabling', function(done){ var app = http.createServer(function(req, res){ send(req, req.url, {root: fixtures}) .index(false) .pipe(res); }); request(app) .get('/pets/') .expect(403, done); }) it('should support fallbacks', function(done){ var app = http.createServer(function(req, res){ send(req, req.url, {root: fixtures}) .index(['default.htm', 'index.html']) .pipe(res); }); request(app) .get('/pets/') .expect(200, fs.readFileSync(path.join(fixtures, 'pets', 'index.html'), 'utf8'), done) }) }) describe('.maxage()', function(){ it('should default to 0', function(done){ var app = http.createServer(function(req, res){ send(req, 'test/fixtures/name.txt') .maxage(undefined) .pipe(res); }); request(app) .get('/name.txt') .expect('Cache-Control', 'public, max-age=0', done) }) it('should floor to integer', function(done){ var app = http.createServer(function(req, res){ send(req, 'test/fixtures/name.txt') .maxage(1234) .pipe(res); }); request(app) .get('/name.txt') .expect('Cache-Control', 'public, max-age=1', done) }) it('should accept string', function(done){ var app = http.createServer(function(req, res){ send(req, 'test/fixtures/name.txt') .maxage('30d') .pipe(res); }); request(app) .get('/name.txt') .expect('Cache-Control', 'public, max-age=2592000', done) }) it('should max at 1 year', function(done){ var app = http.createServer(function(req, res){ send(req, 'test/fixtures/name.txt') .maxage(Infinity) .pipe(res); }); request(app) .get('/name.txt') .expect('Cache-Control', 'public, max-age=31536000', done) }) }) describe('.root()', function(){ it('should set root', function(done){ var app = http.createServer(function(req, res){ send(req, req.url) .root(__dirname + '/fixtures') .pipe(res); }); request(app) .get('/pets/../name.txt') .expect(200, 'tobi', done) }) }) }) describe('send(file, options)', function(){ describe('etag', function(){ it('should support disabling etags', function(done){ var app = http.createServer(function(req, res){ send(req, req.url, {etag: false, root: fixtures}) .pipe(res); }); request(app) .get('/nums') .expect(200, function(err, res){ if (err) return done(err); res.headers.should.not.have.property('etag'); done(); }); }) }) describe('extensions', function () { it('should be not be enabled by default', function (done) { var server = createServer({root: fixtures}); request(server) .get('/tobi') .expect(404, done) }) it('should be configurable', function (done) { var server = createServer({extensions: 'txt', root: fixtures}) request(server) .get('/name') .expect(200, 'tobi', done) }) it('should support disabling extensions', function (done) { var server = createServer({extensions: false, root: fixtures}) request(server) .get('/name') .expect(404, done) }) it('should support fallbacks', function (done) { var server = createServer({extensions: ['htm', 'html', 'txt'], root: fixtures}) request(server) .get('/name') .expect(200, '

tobi

', done) }) it('should 404 if nothing found', function (done) { var server = createServer({extensions: ['htm', 'html', 'txt'], root: fixtures}) request(server) .get('/bob') .expect(404, done) }) it('should skip directories', function (done) { var server = createServer({extensions: ['file', 'dir'], root: fixtures}) request(server) .get('/name') .expect(404, done) }) it('should not search if file has extension', function (done) { var server = createServer({extensions: 'html', root: fixtures}) request(server) .get('/thing.html') .expect(404, done) }) }) describe('lastModified', function () { it('should support disabling last-modified', function (done) { var app = http.createServer(function(req, res){ send(req, req.url, {lastModified: false, root: fixtures}) .pipe(res) }) request(app) .get('/nums') .expect(200, function (err, res) { if (err) return done(err) res.headers.should.not.have.property('last-modified') done() }) }) }) describe('from', function(){ it('should set with deprecated from', function(done){ var app = http.createServer(function(req, res){ send(req, req.url, {from: __dirname + '/fixtures'}) .pipe(res) }); request(app) .get('/pets/../name.txt') .expect(200, 'tobi', done) }) }) describe('dotfiles', function () { it('should default to "ignore"', function (done) { request(createServer({root: fixtures})) .get('/.hidden') .expect(404, done) }) it('should allow file within dotfile directory for back-compat', function (done) { request(createServer({root: fixtures})) .get('/.mine/name.txt') .expect(200, /tobi/, done) }) it('should reject bad value', function (done) { request(createServer({dotfiles: 'bogus'})) .get('/nums') .expect(500, /dotfiles/, done) }) describe('when "allow"', function (done) { it('should send dotfile', function (done) { request(createServer({dotfiles: 'allow', root: fixtures})) .get('/.hidden') .expect(200, /secret/, done) }) it('should send within dotfile directory', function (done) { request(createServer({dotfiles: 'allow', root: fixtures})) .get('/.mine/name.txt') .expect(200, /tobi/, done) }) it('should 404 for non-existent dotfile', function (done) { request(createServer({dotfiles: 'allow', root: fixtures})) .get('/.nothere') .expect(404, done) }) }) describe('when "deny"', function (done) { it('should 403 for dotfile', function (done) { request(createServer({dotfiles: 'deny', root: fixtures})) .get('/.hidden') .expect(403, done) }) it('should 403 for dotfile directory', function (done) { request(createServer({dotfiles: 'deny', root: fixtures})) .get('/.mine') .expect(403, done) }) it('should 403 for dotfile directory with trailing slash', function (done) { request(createServer({dotfiles: 'deny', root: fixtures})) .get('/.mine/') .expect(403, done) }) it('should 403 for file within dotfile directory', function (done) { request(createServer({dotfiles: 'deny', root: fixtures})) .get('/.mine/name.txt') .expect(403, done) }) it('should 403 for non-existent dotfile', function (done) { request(createServer({dotfiles: 'deny', root: fixtures})) .get('/.nothere') .expect(403, done) }) it('should 403 for non-existent dotfile directory', function (done) { request(createServer({dotfiles: 'deny', root: fixtures})) .get('/.what/name.txt') .expect(403, done) }) it('should send files in root dotfile directory', function (done) { request(createServer({dotfiles: 'deny', root: path.join(fixtures, '.mine')})) .get('/name.txt') .expect(200, /tobi/, done) }) it('should 403 for dotfile without root', function (done) { var server = http.createServer(function onRequest(req, res) { send(req, fixtures + '/.mine' + req.url, {dotfiles: 'deny'}).pipe(res) }) request(server) .get('/name.txt') .expect(403, done) }) }) describe('when "ignore"', function (done) { it('should 404 for dotfile', function (done) { request(createServer({dotfiles: 'ignore', root: fixtures})) .get('/.hidden') .expect(404, done) }) it('should 404 for dotfile directory', function (done) { request(createServer({dotfiles: 'ignore', root: fixtures})) .get('/.mine') .expect(404, done) }) it('should 404 for dotfile directory with trailing slash', function (done) { request(createServer({dotfiles: 'ignore', root: fixtures})) .get('/.mine/') .expect(404, done) }) it('should 404 for file within dotfile directory', function (done) { request(createServer({dotfiles: 'ignore', root: fixtures})) .get('/.mine/name.txt') .expect(404, done) }) it('should 404 for non-existent dotfile', function (done) { request(createServer({dotfiles: 'ignore', root: fixtures})) .get('/.nothere') .expect(404, done) }) it('should 404 for non-existent dotfile directory', function (done) { request(createServer({dotfiles: 'ignore', root: fixtures})) .get('/.what/name.txt') .expect(404, done) }) it('should send files in root dotfile directory', function (done) { request(createServer({dotfiles: 'ignore', root: path.join(fixtures, '.mine')})) .get('/name.txt') .expect(200, /tobi/, done) }) it('should 404 for dotfile without root', function (done) { var server = http.createServer(function onRequest(req, res) { send(req, fixtures + '/.mine' + req.url, {dotfiles: 'ignore'}).pipe(res) }) request(server) .get('/name.txt') .expect(404, done) }) }) }) describe('hidden', function(){ it('should default to false', function(done){ request(app) .get('/.hidden') .expect(404, 'Not Found', done) }) it('should default support sending hidden files', function(done){ var app = http.createServer(function(req, res){ send(req, req.url, {hidden: true, root: fixtures}) .pipe(res); }); request(app) .get('/.hidden') .expect(200, /secret/, done) }) }) describe('maxAge', function(){ it('should default to 0', function(done){ request(app) .get('/name.txt') .expect('Cache-Control', 'public, max-age=0', done) }) it('should floor to integer', function(done){ var app = http.createServer(function(req, res){ send(req, 'test/fixtures/name.txt', {maxAge: 123956}) .pipe(res); }); request(app) .get('/name.txt') .expect('Cache-Control', 'public, max-age=123', done) }) it('should accept string', function(done){ var app = http.createServer(function(req, res){ send(req, 'test/fixtures/name.txt', {maxAge: '30d'}) .pipe(res); }); request(app) .get('/name.txt') .expect('Cache-Control', 'public, max-age=2592000', done) }) it('should max at 1 year', function(done){ var app = http.createServer(function(req, res){ send(req, 'test/fixtures/name.txt', {maxAge: Infinity}) .pipe(res); }); request(app) .get('/name.txt') .expect('Cache-Control', 'public, max-age=31536000', done) }) }) describe('index', function(){ it('should default to index.html', function(done){ request(app) .get('/pets/') .expect(fs.readFileSync(path.join(fixtures, 'pets', 'index.html'), 'utf8'), done) }) it('should be configurable', function(done){ var app = http.createServer(function(req, res){ send(req, req.url, {root: fixtures, index: 'tobi.html'}) .pipe(res); }); request(app) .get('/') .expect(200, '

tobi

', done); }) it('should support disabling', function(done){ var app = http.createServer(function(req, res){ send(req, req.url, {root: fixtures, index: false}) .pipe(res); }); request(app) .get('/pets/') .expect(403, done); }) it('should support fallbacks', function(done){ var app = http.createServer(function(req, res){ send(req, req.url, {root: fixtures, index: ['default.htm', 'index.html']}) .pipe(res); }); request(app) .get('/pets/') .expect(200, fs.readFileSync(path.join(fixtures, 'pets', 'index.html'), 'utf8'), done) }) it('should 404 if no index file found (file)', function(done){ var app = http.createServer(function(req, res){ send(req, req.url, {root: fixtures, index: 'default.htm'}) .pipe(res); }); request(app) .get('/pets/') .expect(404, done) }) it('should 404 if no index file found (dir)', function(done){ var app = http.createServer(function(req, res){ send(req, req.url, {root: fixtures, index: 'pets'}) .pipe(res); }); request(app) .get('/') .expect(404, done) }) it('should not follow directories', function(done){ var app = http.createServer(function(req, res){ send(req, req.url, {root: fixtures, index: ['pets', 'name.txt']}) .pipe(res); }); request(app) .get('/') .expect(200, 'tobi', done) }) it('should work without root', function (done) { var server = http.createServer(function(req, res){ var p = path.join(fixtures, 'pets').replace(/\\/g, '/') + '/'; send(req, p, {index: ['index.html']}) .pipe(res); }); request(server) .get('/') .expect(200, /tobi/, done) }) }) describe('root', function(){ describe('when given', function(){ it('should join root', function(done){ var app = http.createServer(function(req, res){ send(req, req.url, {root: __dirname + '/fixtures'}) .pipe(res); }); request(app) .get('/pets/../name.txt') .expect(200, 'tobi', done) }) it('should work with trailing slash', function(done){ var app = http.createServer(function(req, res){ send(req, req.url, {root: __dirname + '/fixtures/'}) .pipe(res); }); request(app) .get('/name.txt') .expect(200, 'tobi', done) }) it('should work with empty path', function(done){ var app = http.createServer(function(req, res){ send(req, '', {root: __dirname + '/fixtures'}) .pipe(res); }); request(app) .get('/name.txt') .expect(301, /Redirecting to/, done) }) it('should restrict paths to within root', function(done){ var app = http.createServer(function(req, res){ send(req, req.url, {root: __dirname + '/fixtures'}) .pipe(res); }); request(app) .get('/pets/../../send.js') .expect(403, done) }) it('should allow .. in root', function(done){ var app = http.createServer(function(req, res){ send(req, req.url, {root: __dirname + '/fixtures/../fixtures'}) .pipe(res); }); request(app) .get('/pets/../../send.js') .expect(403, done) }) it('should not allow root transversal', function(done){ var app = http.createServer(function(req, res){ send(req, req.url, {root: __dirname + '/fixtures/name.d'}) .pipe(res); }); request(app) .get('/../name.dir/name.txt') .expect(403, done) }) }) describe('when missing', function(){ it('should consider .. malicious', function(done){ var app = http.createServer(function(req, res){ send(req, fixtures + req.url) .pipe(res); }); request(app) .get('/../send.js') .expect(403, done) }) it('should still serve files with dots in name', function(done){ var app = http.createServer(function(req, res){ send(req, fixtures + req.url) .pipe(res); }); request(app) .get('/do..ts.txt') .expect(200, '...', done); }) }) }) }) function createServer(opts) { return http.createServer(function onRequest(req, res) { try { send(req, req.url, opts).pipe(res) } catch (err) { res.statusCode = 500 res.end(err.message) } }) }