3 * Copyright(c) 2012 TJ Holowaychuk
4 * Copyright(c) 2014-2016 Douglas Christopher Wilson
11 * Module dependencies.
15 var createError = require('http-errors')
16 var debug = require('debug')('send')
17 var deprecate = require('depd')('send')
18 var destroy = require('destroy')
19 var encodeUrl = require('encodeurl')
20 var escapeHtml = require('escape-html')
21 var etag = require('etag')
22 var fresh = require('fresh')
23 var fs = require('fs')
24 var mime = require('mime')
25 var ms = require('ms')
26 var onFinished = require('on-finished')
27 var parseRange = require('range-parser')
28 var path = require('path')
29 var statuses = require('statuses')
30 var Stream = require('stream')
31 var util = require('util')
34 * Path function references.
38 var extname = path.extname
40 var normalize = path.normalize
41 var resolve = path.resolve
45 * Regular expression for identifying a bytes Range header.
49 var BYTES_RANGE_REGEXP = /^ *bytes=/
52 * Maximum value allowed for the max age.
56 var MAX_MAXAGE = 60 * 60 * 24 * 365 * 1000 // 1 year
59 * Regular expression to match a path with a directory up component.
63 var UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/
71 module.exports.mime = mime
74 * Return a `SendStream` for `req` and `path`.
77 * @param {string} path
78 * @param {object} [options]
79 * @return {SendStream}
83 function send (req, path, options) {
84 return new SendStream(req, path, options)
88 * Initialize a `SendStream` with the given `path`.
90 * @param {Request} req
91 * @param {String} path
92 * @param {object} [options]
96 function SendStream (req, path, options) {
99 var opts = options || {}
105 this._acceptRanges = opts.acceptRanges !== undefined
106 ? Boolean(opts.acceptRanges)
109 this._cacheControl = opts.cacheControl !== undefined
110 ? Boolean(opts.cacheControl)
113 this._etag = opts.etag !== undefined
117 this._dotfiles = opts.dotfiles !== undefined
121 if (this._dotfiles !== 'ignore' && this._dotfiles !== 'allow' && this._dotfiles !== 'deny') {
122 throw new TypeError('dotfiles option must be "allow", "deny", or "ignore"')
125 this._hidden = Boolean(opts.hidden)
127 if (opts.hidden !== undefined) {
128 deprecate('hidden: use dotfiles: \'' + (this._hidden ? 'allow' : 'ignore') + '\' instead')
132 if (opts.dotfiles === undefined) {
133 this._dotfiles = undefined
136 this._extensions = opts.extensions !== undefined
137 ? normalizeList(opts.extensions, 'extensions option')
140 this._immutable = opts.immutable !== undefined
141 ? Boolean(opts.immutable)
144 this._index = opts.index !== undefined
145 ? normalizeList(opts.index, 'index option')
148 this._lastModified = opts.lastModified !== undefined
149 ? Boolean(opts.lastModified)
152 this._maxage = opts.maxAge || opts.maxage
153 this._maxage = typeof this._maxage === 'string'
155 : Number(this._maxage)
156 this._maxage = !isNaN(this._maxage)
157 ? Math.min(Math.max(0, this._maxage), MAX_MAXAGE)
160 this._root = opts.root
164 if (!this._root && opts.from) {
170 * Inherits from `Stream`.
173 util.inherits(SendStream, Stream)
176 * Enable or disable etag generation.
178 * @param {Boolean} val
179 * @return {SendStream}
183 SendStream.prototype.etag = deprecate.function(function etag (val) {
184 this._etag = Boolean(val)
185 debug('etag %s', this._etag)
187 }, 'send.etag: pass etag as option')
190 * Enable or disable "hidden" (dot) files.
192 * @param {Boolean} path
193 * @return {SendStream}
197 SendStream.prototype.hidden = deprecate.function(function hidden (val) {
198 this._hidden = Boolean(val)
199 this._dotfiles = undefined
200 debug('hidden %s', this._hidden)
202 }, 'send.hidden: use dotfiles option')
205 * Set index `paths`, set to a falsy
206 * value to disable index support.
208 * @param {String|Boolean|Array} paths
209 * @return {SendStream}
213 SendStream.prototype.index = deprecate.function(function index (paths) {
214 var index = !paths ? [] : normalizeList(paths, 'paths argument')
215 debug('index %o', paths)
218 }, 'send.index: pass index as option')
223 * @param {String} path
224 * @return {SendStream}
228 SendStream.prototype.root = function root (path) {
229 this._root = resolve(String(path))
230 debug('root %s', this._root)
234 SendStream.prototype.from = deprecate.function(SendStream.prototype.root,
235 'send.from: pass root as option')
237 SendStream.prototype.root = deprecate.function(SendStream.prototype.root,
238 'send.root: pass root as option')
241 * Set max-age to `maxAge`.
243 * @param {Number} maxAge
244 * @return {SendStream}
248 SendStream.prototype.maxage = deprecate.function(function maxage (maxAge) {
249 this._maxage = typeof maxAge === 'string'
252 this._maxage = !isNaN(this._maxage)
253 ? Math.min(Math.max(0, this._maxage), MAX_MAXAGE)
255 debug('max-age %d', this._maxage)
257 }, 'send.maxage: pass maxAge as option')
260 * Emit error with `status`.
262 * @param {number} status
263 * @param {Error} [err]
267 SendStream.prototype.error = function error (status, err) {
268 // emit if listeners instead of responding
269 if (hasListeners(this, 'error')) {
270 return this.emit('error', createError(status, err, {
276 var msg = statuses[status] || String(status)
277 var doc = createHtmlDocument('Error', escapeHtml(msg))
279 // clear existing headers
283 if (err && err.headers) {
284 setHeaders(res, err.headers)
287 // send basic response
288 res.statusCode = status
289 res.setHeader('Content-Type', 'text/html; charset=UTF-8')
290 res.setHeader('Content-Length', Buffer.byteLength(doc))
291 res.setHeader('Content-Security-Policy', "default-src 'self'")
292 res.setHeader('X-Content-Type-Options', 'nosniff')
297 * Check if the pathname ends with "/".
303 SendStream.prototype.hasTrailingSlash = function hasTrailingSlash () {
304 return this.path[this.path.length - 1] === '/'
308 * Check if this is a conditional GET request.
314 SendStream.prototype.isConditionalGET = function isConditionalGET () {
315 return this.req.headers['if-match'] ||
316 this.req.headers['if-unmodified-since'] ||
317 this.req.headers['if-none-match'] ||
318 this.req.headers['if-modified-since']
322 * Check if the request preconditions failed.
328 SendStream.prototype.isPreconditionFailure = function isPreconditionFailure () {
333 var match = req.headers['if-match']
335 var etag = res.getHeader('ETag')
336 return !etag || (match !== '*' && parseTokenList(match).every(function (match) {
337 return match !== etag && match !== 'W/' + etag && 'W/' + match !== etag
341 // if-unmodified-since
342 var unmodifiedSince = parseHttpDate(req.headers['if-unmodified-since'])
343 if (!isNaN(unmodifiedSince)) {
344 var lastModified = parseHttpDate(res.getHeader('Last-Modified'))
345 return isNaN(lastModified) || lastModified > unmodifiedSince
352 * Strip content-* header fields.
357 SendStream.prototype.removeContentHeaderFields = function removeContentHeaderFields () {
359 var headers = getHeaderNames(res)
361 for (var i = 0; i < headers.length; i++) {
362 var header = headers[i]
363 if (header.substr(0, 8) === 'content-' && header !== 'content-location') {
364 res.removeHeader(header)
370 * Respond with 304 not modified.
375 SendStream.prototype.notModified = function notModified () {
377 debug('not modified')
378 this.removeContentHeaderFields()
384 * Raise error that headers already sent.
389 SendStream.prototype.headersAlreadySent = function headersAlreadySent () {
390 var err = new Error('Can\'t set headers after they are sent.')
391 debug('headers already sent')
396 * Check if the request is cacheable, aka
397 * responded with 2xx or 304 (see RFC 2616 section 14.2{5,6}).
403 SendStream.prototype.isCachable = function isCachable () {
404 var statusCode = this.res.statusCode
405 return (statusCode >= 200 && statusCode < 300) ||
410 * Handle stat() error.
412 * @param {Error} error
416 SendStream.prototype.onStatError = function onStatError (error) {
417 switch (error.code) {
421 this.error(404, error)
424 this.error(500, error)
430 * Check if the cache is fresh.
436 SendStream.prototype.isFresh = function isFresh () {
437 return fresh(this.req.headers, {
438 'etag': this.res.getHeader('ETag'),
439 'last-modified': this.res.getHeader('Last-Modified')
444 * Check if the range is fresh.
450 SendStream.prototype.isRangeFresh = function isRangeFresh () {
451 var ifRange = this.req.headers['if-range']
458 if (ifRange.indexOf('"') !== -1) {
459 var etag = this.res.getHeader('ETag')
460 return Boolean(etag && ifRange.indexOf(etag) !== -1)
463 // if-range as modified date
464 var lastModified = this.res.getHeader('Last-Modified')
465 return parseHttpDate(lastModified) <= parseHttpDate(ifRange)
471 * @param {string} path
475 SendStream.prototype.redirect = function redirect (path) {
478 if (hasListeners(this, 'directory')) {
479 this.emit('directory', res, path)
483 if (this.hasTrailingSlash()) {
488 var loc = encodeUrl(collapseLeadingSlashes(this.path + '/'))
489 var doc = createHtmlDocument('Redirecting', 'Redirecting to <a href="' + escapeHtml(loc) + '">' +
490 escapeHtml(loc) + '</a>')
494 res.setHeader('Content-Type', 'text/html; charset=UTF-8')
495 res.setHeader('Content-Length', Buffer.byteLength(doc))
496 res.setHeader('Content-Security-Policy', "default-src 'self'")
497 res.setHeader('X-Content-Type-Options', 'nosniff')
498 res.setHeader('Location', loc)
505 * @param {Stream} res
506 * @return {Stream} res
510 SendStream.prototype.pipe = function pipe (res) {
512 var root = this._root
518 var path = decode(this.path)
525 if (~path.indexOf('\0')) {
534 path = normalize('.' + sep + path)
538 if (UP_PATH_REGEXP.test(path)) {
539 debug('malicious path "%s"', path)
544 // explode path parts
545 parts = path.split(sep)
547 // join / normalize from optional root dir
548 path = normalize(join(root, path))
549 root = normalize(root + sep)
551 // ".." is malicious without "root"
552 if (UP_PATH_REGEXP.test(path)) {
553 debug('malicious path "%s"', path)
558 // explode path parts
559 parts = normalize(path).split(sep)
566 if (containsDotFile(parts)) {
567 var access = this._dotfiles
570 if (access === undefined) {
571 access = parts[parts.length - 1][0] === '.'
572 ? (this._hidden ? 'allow' : 'ignore')
576 debug('%s dotfile "%s"', access, path)
590 // index file support
591 if (this._index.length && this.hasTrailingSlash()) {
603 * @param {String} path
607 SendStream.prototype.send = function send (path, stat) {
609 var options = this.options
613 var ranges = req.headers.range
614 var offset = options.start || 0
616 if (headersSent(res)) {
617 // impossible to send now
618 this.headersAlreadySent()
622 debug('pipe "%s"', path)
625 this.setHeader(path, stat)
630 // conditional GET support
631 if (this.isConditionalGET()) {
632 if (this.isPreconditionFailure()) {
637 if (this.isCachable() && this.isFresh()) {
643 // adjust len to start/end options
644 len = Math.max(0, len - offset)
645 if (options.end !== undefined) {
646 var bytes = options.end - offset + 1
647 if (len > bytes) len = bytes
651 if (this._acceptRanges && BYTES_RANGE_REGEXP.test(ranges)) {
653 ranges = parseRange(len, ranges, {
658 if (!this.isRangeFresh()) {
665 debug('range unsatisfiable')
668 res.setHeader('Content-Range', contentRange('bytes', len))
670 // 416 Requested Range Not Satisfiable
671 return this.error(416, {
672 headers: {'Content-Range': res.getHeader('Content-Range')}
676 // valid (syntactically invalid/multiple ranges are treated as a regular response)
677 if (ranges !== -2 && ranges.length === 1) {
678 debug('range %j', ranges)
682 res.setHeader('Content-Range', contentRange('bytes', len, ranges[0]))
684 // adjust for requested range
685 offset += ranges[0].start
686 len = ranges[0].end - ranges[0].start + 1
691 for (var prop in options) {
692 opts[prop] = options[prop]
697 opts.end = Math.max(offset, offset + len - 1)
700 res.setHeader('Content-Length', len)
703 if (req.method === 'HEAD') {
708 this.stream(path, opts)
712 * Transfer file for `path`.
714 * @param {String} path
717 SendStream.prototype.sendFile = function sendFile (path) {
721 debug('stat "%s"', path)
722 fs.stat(path, function onstat (err, stat) {
723 if (err && err.code === 'ENOENT' && !extname(path) && path[path.length - 1] !== sep) {
724 // not found, check extensions
727 if (err) return self.onStatError(err)
728 if (stat.isDirectory()) return self.redirect(path)
729 self.emit('file', path, stat)
730 self.send(path, stat)
733 function next (err) {
734 if (self._extensions.length <= i) {
736 ? self.onStatError(err)
740 var p = path + '.' + self._extensions[i++]
742 debug('stat "%s"', p)
743 fs.stat(p, function (err, stat) {
744 if (err) return next(err)
745 if (stat.isDirectory()) return next()
746 self.emit('file', p, stat)
753 * Transfer index for `path`.
755 * @param {String} path
758 SendStream.prototype.sendIndex = function sendIndex (path) {
762 function next (err) {
763 if (++i >= self._index.length) {
764 if (err) return self.onStatError(err)
765 return self.error(404)
768 var p = join(path, self._index[i])
770 debug('stat "%s"', p)
771 fs.stat(p, function (err, stat) {
772 if (err) return next(err)
773 if (stat.isDirectory()) return next()
774 self.emit('file', p, stat)
783 * Stream `path` to the response.
785 * @param {String} path
786 * @param {Object} options
790 SendStream.prototype.stream = function stream (path, options) {
791 // TODO: this is all lame, refactor meeee
797 var stream = fs.createReadStream(path, options)
798 this.emit('stream', stream)
801 // response finished, done with the fd
802 onFinished(res, function onfinished () {
807 // error handling code-smell
808 stream.on('error', function onerror (err) {
809 // request already finished
817 self.onStatError(err)
821 stream.on('end', function onend () {
827 * Set content-type based on `path`
828 * if it hasn't been explicitly set.
830 * @param {String} path
834 SendStream.prototype.type = function type (path) {
837 if (res.getHeader('Content-Type')) return
839 var type = mime.lookup(path)
842 debug('no content-type')
846 var charset = mime.charsets.lookup(type)
848 debug('content-type %s', type)
849 res.setHeader('Content-Type', type + (charset ? '; charset=' + charset : ''))
853 * Set response header fields, most
854 * fields may be pre-defined.
856 * @param {String} path
857 * @param {Object} stat
861 SendStream.prototype.setHeader = function setHeader (path, stat) {
864 this.emit('headers', res, path, stat)
866 if (this._acceptRanges && !res.getHeader('Accept-Ranges')) {
867 debug('accept ranges')
868 res.setHeader('Accept-Ranges', 'bytes')
871 if (this._cacheControl && !res.getHeader('Cache-Control')) {
872 var cacheControl = 'public, max-age=' + Math.floor(this._maxage / 1000)
874 if (this._immutable) {
875 cacheControl += ', immutable'
878 debug('cache-control %s', cacheControl)
879 res.setHeader('Cache-Control', cacheControl)
882 if (this._lastModified && !res.getHeader('Last-Modified')) {
883 var modified = stat.mtime.toUTCString()
884 debug('modified %s', modified)
885 res.setHeader('Last-Modified', modified)
888 if (this._etag && !res.getHeader('ETag')) {
890 debug('etag %s', val)
891 res.setHeader('ETag', val)
896 * Clear all headers from a response.
898 * @param {object} res
902 function clearHeaders (res) {
903 var headers = getHeaderNames(res)
905 for (var i = 0; i < headers.length; i++) {
906 res.removeHeader(headers[i])
911 * Collapse all leading slashes into a single slash
913 * @param {string} str
916 function collapseLeadingSlashes (str) {
917 for (var i = 0; i < str.length; i++) {
918 if (str[i] !== '/') {
924 ? '/' + str.substr(i)
929 * Determine if path parts contain a dotfile.
934 function containsDotFile (parts) {
935 for (var i = 0; i < parts.length; i++) {
937 if (part.length > 1 && part[0] === '.') {
946 * Create a Content-Range header.
948 * @param {string} type
949 * @param {number} size
950 * @param {array} [range]
953 function contentRange (type, size, range) {
954 return type + ' ' + (range ? range.start + '-' + range.end : '*') + '/' + size
958 * Create a minimal HTML document.
960 * @param {string} title
961 * @param {string} body
965 function createHtmlDocument (title, body) {
966 return '<!DOCTYPE html>\n' +
967 '<html lang="en">\n' +
969 '<meta charset="utf-8">\n' +
970 '<title>' + title + '</title>\n' +
973 '<pre>' + body + '</pre>\n' +
979 * decodeURIComponent.
981 * Allows V8 to only deoptimize this fn instead of all
984 * @param {String} path
988 function decode (path) {
990 return decodeURIComponent(path)
997 * Get the header names on a respnse.
999 * @param {object} res
1000 * @returns {array[string]}
1004 function getHeaderNames (res) {
1005 return typeof res.getHeaderNames !== 'function'
1006 ? Object.keys(res._headers || {})
1007 : res.getHeaderNames()
1011 * Determine if emitter has listeners of a given type.
1013 * The way to do this check is done three different ways in Node.js >= 0.8
1014 * so this consolidates them into a minimal set using instance methods.
1016 * @param {EventEmitter} emitter
1017 * @param {string} type
1018 * @returns {boolean}
1022 function hasListeners (emitter, type) {
1023 var count = typeof emitter.listenerCount !== 'function'
1024 ? emitter.listeners(type).length
1025 : emitter.listenerCount(type)
1031 * Determine if the response headers have been sent.
1033 * @param {object} res
1034 * @returns {boolean}
1038 function headersSent (res) {
1039 return typeof res.headersSent !== 'boolean'
1040 ? Boolean(res._header)
1045 * Normalize the index option into an array.
1047 * @param {boolean|string|array} val
1048 * @param {string} name
1052 function normalizeList (val, name) {
1053 var list = [].concat(val || [])
1055 for (var i = 0; i < list.length; i++) {
1056 if (typeof list[i] !== 'string') {
1057 throw new TypeError(name + ' must be array of strings or false')
1065 * Parse an HTTP Date into a number.
1067 * @param {string} date
1071 function parseHttpDate (date) {
1072 var timestamp = date && Date.parse(date)
1074 return typeof timestamp === 'number'
1080 * Parse a HTTP token list.
1082 * @param {string} str
1086 function parseTokenList (str) {
1092 for (var i = 0, len = str.length; i < len; i++) {
1093 switch (str.charCodeAt(i)) {
1095 if (start === end) {
1100 list.push(str.substring(start, end))
1110 list.push(str.substring(start, end))
1116 * Set an object of headers on a response.
1118 * @param {object} res
1119 * @param {object} headers
1123 function setHeaders (res, headers) {
1124 var keys = Object.keys(headers)
1126 for (var i = 0; i < keys.length; i++) {
1128 res.setHeader(key, headers[key])