index.js
1 /*! 2 * send 3 * Copyright(c) 2012 TJ Holowaychuk 4 * Copyright(c) 2014-2022 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 encodeUrl = require('encodeurl') 18 var escapeHtml = require('escape-html') 19 var etag = require('etag') 20 var fresh = require('fresh') 21 var fs = require('fs') 22 var mime = require('mime-types') 23 var ms = require('ms') 24 var onFinished = require('on-finished') 25 var parseRange = require('range-parser') 26 var path = require('path') 27 var statuses = require('statuses') 28 var Stream = require('stream') 29 var util = require('util') 30 31 /** 32 * Path function references. 33 * @private 34 */ 35 36 var extname = path.extname 37 var join = path.join 38 var normalize = path.normalize 39 var resolve = path.resolve 40 var sep = path.sep 41 42 /** 43 * Regular expression for identifying a bytes Range header. 44 * @private 45 */ 46 47 var BYTES_RANGE_REGEXP = /^ *bytes=/ 48 49 /** 50 * Maximum value allowed for the max age. 51 * @private 52 */ 53 54 var MAX_MAXAGE = 60 * 60 * 24 * 365 * 1000 // 1 year 55 56 /** 57 * Regular expression to match a path with a directory up component. 58 * @private 59 */ 60 61 var UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/ 62 63 /** 64 * Module exports. 65 * @public 66 */ 67 68 module.exports = send 69 70 /** 71 * Return a `SendStream` for `req` and `path`. 72 * 73 * @param {object} req 74 * @param {string} path 75 * @param {object} [options] 76 * @return {SendStream} 77 * @public 78 */ 79 80 function send (req, path, options) { 81 return new SendStream(req, path, options) 82 } 83 84 /** 85 * Initialize a `SendStream` with the given `path`. 86 * 87 * @param {Request} req 88 * @param {String} path 89 * @param {object} [options] 90 * @private 91 */ 92 93 function SendStream (req, path, options) { 94 Stream.call(this) 95 96 var opts = options || {} 97 98 this.options = opts 99 this.path = path 100 this.req = req 101 102 this._acceptRanges = opts.acceptRanges !== undefined 103 ? Boolean(opts.acceptRanges) 104 : true 105 106 this._cacheControl = opts.cacheControl !== undefined 107 ? Boolean(opts.cacheControl) 108 : true 109 110 this._etag = opts.etag !== undefined 111 ? Boolean(opts.etag) 112 : true 113 114 this._dotfiles = opts.dotfiles !== undefined 115 ? opts.dotfiles 116 : 'ignore' 117 118 if (this._dotfiles !== 'ignore' && this._dotfiles !== 'allow' && this._dotfiles !== 'deny') { 119 throw new TypeError('dotfiles option must be "allow", "deny", or "ignore"') 120 } 121 122 this._extensions = opts.extensions !== undefined 123 ? normalizeList(opts.extensions, 'extensions option') 124 : [] 125 126 this._immutable = opts.immutable !== undefined 127 ? Boolean(opts.immutable) 128 : false 129 130 this._index = opts.index !== undefined 131 ? normalizeList(opts.index, 'index option') 132 : ['index.html'] 133 134 this._lastModified = opts.lastModified !== undefined 135 ? Boolean(opts.lastModified) 136 : true 137 138 this._maxage = opts.maxAge || opts.maxage 139 this._maxage = typeof this._maxage === 'string' 140 ? ms(this._maxage) 141 : Number(this._maxage) 142 this._maxage = !isNaN(this._maxage) 143 ? Math.min(Math.max(0, this._maxage), MAX_MAXAGE) 144 : 0 145 146 this._root = opts.root 147 ? resolve(opts.root) 148 : null 149 } 150 151 /** 152 * Inherits from `Stream`. 153 */ 154 155 util.inherits(SendStream, Stream) 156 157 /** 158 * Emit error with `status`. 159 * 160 * @param {number} status 161 * @param {Error} [err] 162 * @private 163 */ 164 165 SendStream.prototype.error = function error (status, err) { 166 // emit if listeners instead of responding 167 if (hasListeners(this, 'error')) { 168 return this.emit('error', createHttpError(status, err)) 169 } 170 171 var res = this.res 172 var msg = statuses.message[status] || String(status) 173 var doc = createHtmlDocument('Error', escapeHtml(msg)) 174 175 // clear existing headers 176 clearHeaders(res) 177 178 // add error headers 179 if (err && err.headers) { 180 setHeaders(res, err.headers) 181 } 182 183 // send basic response 184 res.statusCode = status 185 res.setHeader('Content-Type', 'text/html; charset=UTF-8') 186 res.setHeader('Content-Length', Buffer.byteLength(doc)) 187 res.setHeader('Content-Security-Policy', "default-src 'none'") 188 res.setHeader('X-Content-Type-Options', 'nosniff') 189 res.end(doc) 190 } 191 192 /** 193 * Check if the pathname ends with "/". 194 * 195 * @return {boolean} 196 * @private 197 */ 198 199 SendStream.prototype.hasTrailingSlash = function hasTrailingSlash () { 200 return this.path[this.path.length - 1] === '/' 201 } 202 203 /** 204 * Check if this is a conditional GET request. 205 * 206 * @return {Boolean} 207 * @api private 208 */ 209 210 SendStream.prototype.isConditionalGET = function isConditionalGET () { 211 return this.req.headers['if-match'] || 212 this.req.headers['if-unmodified-since'] || 213 this.req.headers['if-none-match'] || 214 this.req.headers['if-modified-since'] 215 } 216 217 /** 218 * Check if the request preconditions failed. 219 * 220 * @return {boolean} 221 * @private 222 */ 223 224 SendStream.prototype.isPreconditionFailure = function isPreconditionFailure () { 225 var req = this.req 226 var res = this.res 227 228 // if-match 229 var match = req.headers['if-match'] 230 if (match) { 231 var etag = res.getHeader('ETag') 232 return !etag || (match !== '*' && parseTokenList(match).every(function (match) { 233 return match !== etag && match !== 'W/' + etag && 'W/' + match !== etag 234 })) 235 } 236 237 // if-unmodified-since 238 var unmodifiedSince = parseHttpDate(req.headers['if-unmodified-since']) 239 if (!isNaN(unmodifiedSince)) { 240 var lastModified = parseHttpDate(res.getHeader('Last-Modified')) 241 return isNaN(lastModified) || lastModified > unmodifiedSince 242 } 243 244 return false 245 } 246 247 /** 248 * Strip various content header fields for a change in entity. 249 * 250 * @private 251 */ 252 253 SendStream.prototype.removeContentHeaderFields = function removeContentHeaderFields () { 254 var res = this.res 255 256 res.removeHeader('Content-Encoding') 257 res.removeHeader('Content-Language') 258 res.removeHeader('Content-Length') 259 res.removeHeader('Content-Range') 260 res.removeHeader('Content-Type') 261 } 262 263 /** 264 * Respond with 304 not modified. 265 * 266 * @api private 267 */ 268 269 SendStream.prototype.notModified = function notModified () { 270 var res = this.res 271 debug('not modified') 272 this.removeContentHeaderFields() 273 res.statusCode = 304 274 res.end() 275 } 276 277 /** 278 * Raise error that headers already sent. 279 * 280 * @api private 281 */ 282 283 SendStream.prototype.headersAlreadySent = function headersAlreadySent () { 284 var err = new Error('Can\'t set headers after they are sent.') 285 debug('headers already sent') 286 this.error(500, err) 287 } 288 289 /** 290 * Check if the request is cacheable, aka 291 * responded with 2xx or 304 (see RFC 2616 section 14.2{5,6}). 292 * 293 * @return {Boolean} 294 * @api private 295 */ 296 297 SendStream.prototype.isCachable = function isCachable () { 298 var statusCode = this.res.statusCode 299 return (statusCode >= 200 && statusCode < 300) || 300 statusCode === 304 301 } 302 303 /** 304 * Handle stat() error. 305 * 306 * @param {Error} error 307 * @private 308 */ 309 310 SendStream.prototype.onStatError = function onStatError (error) { 311 switch (error.code) { 312 case 'ENAMETOOLONG': 313 case 'ENOENT': 314 case 'ENOTDIR': 315 this.error(404, error) 316 break 317 default: 318 this.error(500, error) 319 break 320 } 321 } 322 323 /** 324 * Check if the cache is fresh. 325 * 326 * @return {Boolean} 327 * @api private 328 */ 329 330 SendStream.prototype.isFresh = function isFresh () { 331 return fresh(this.req.headers, { 332 etag: this.res.getHeader('ETag'), 333 'last-modified': this.res.getHeader('Last-Modified') 334 }) 335 } 336 337 /** 338 * Check if the range is fresh. 339 * 340 * @return {Boolean} 341 * @api private 342 */ 343 344 SendStream.prototype.isRangeFresh = function isRangeFresh () { 345 var ifRange = this.req.headers['if-range'] 346 347 if (!ifRange) { 348 return true 349 } 350 351 // if-range as etag 352 if (ifRange.indexOf('"') !== -1) { 353 var etag = this.res.getHeader('ETag') 354 return Boolean(etag && ifRange.indexOf(etag) !== -1) 355 } 356 357 // if-range as modified date 358 var lastModified = this.res.getHeader('Last-Modified') 359 return parseHttpDate(lastModified) <= parseHttpDate(ifRange) 360 } 361 362 /** 363 * Redirect to path. 364 * 365 * @param {string} path 366 * @private 367 */ 368 369 SendStream.prototype.redirect = function redirect (path) { 370 var res = this.res 371 372 if (hasListeners(this, 'directory')) { 373 this.emit('directory', res, path) 374 return 375 } 376 377 if (this.hasTrailingSlash()) { 378 this.error(403) 379 return 380 } 381 382 var loc = encodeUrl(collapseLeadingSlashes(this.path + '/')) 383 var doc = createHtmlDocument('Redirecting', 'Redirecting to ' + escapeHtml(loc)) 384 385 // redirect 386 res.statusCode = 301 387 res.setHeader('Content-Type', 'text/html; charset=UTF-8') 388 res.setHeader('Content-Length', Buffer.byteLength(doc)) 389 res.setHeader('Content-Security-Policy', "default-src 'none'") 390 res.setHeader('X-Content-Type-Options', 'nosniff') 391 res.setHeader('Location', loc) 392 res.end(doc) 393 } 394 395 /** 396 * Pipe to `res. 397 * 398 * @param {Stream} res 399 * @return {Stream} res 400 * @api public 401 */ 402 403 SendStream.prototype.pipe = function pipe (res) { 404 // root path 405 var root = this._root 406 407 // references 408 this.res = res 409 410 // decode the path 411 var path = decode(this.path) 412 if (path === -1) { 413 this.error(400) 414 return res 415 } 416 417 // null byte(s) 418 if (~path.indexOf('\0')) { 419 this.error(400) 420 return res 421 } 422 423 var parts 424 if (root !== null) { 425 // normalize 426 if (path) { 427 path = normalize('.' + sep + path) 428 } 429 430 // malicious path 431 if (UP_PATH_REGEXP.test(path)) { 432 debug('malicious path "%s"', path) 433 this.error(403) 434 return res 435 } 436 437 // explode path parts 438 parts = path.split(sep) 439 440 // join / normalize from optional root dir 441 path = normalize(join(root, path)) 442 } else { 443 // ".." is malicious without "root" 444 if (UP_PATH_REGEXP.test(path)) { 445 debug('malicious path "%s"', path) 446 this.error(403) 447 return res 448 } 449 450 // explode path parts 451 parts = normalize(path).split(sep) 452 453 // resolve the path 454 path = resolve(path) 455 } 456 457 // dotfile handling 458 if (containsDotFile(parts)) { 459 debug('%s dotfile "%s"', this._dotfiles, path) 460 switch (this._dotfiles) { 461 case 'allow': 462 break 463 case 'deny': 464 this.error(403) 465 return res 466 case 'ignore': 467 default: 468 this.error(404) 469 return res 470 } 471 } 472 473 // index file support 474 if (this._index.length && this.hasTrailingSlash()) { 475 this.sendIndex(path) 476 return res 477 } 478 479 this.sendFile(path) 480 return res 481 } 482 483 /** 484 * Transfer `path`. 485 * 486 * @param {String} path 487 * @api public 488 */ 489 490 SendStream.prototype.send = function send (path, stat) { 491 var len = stat.size 492 var options = this.options 493 var opts = {} 494 var res = this.res 495 var req = this.req 496 var ranges = req.headers.range 497 var offset = options.start || 0 498 499 if (res.headersSent) { 500 // impossible to send now 501 this.headersAlreadySent() 502 return 503 } 504 505 debug('pipe "%s"', path) 506 507 // set header fields 508 this.setHeader(path, stat) 509 510 // set content-type 511 this.type(path) 512 513 // conditional GET support 514 if (this.isConditionalGET()) { 515 if (this.isPreconditionFailure()) { 516 this.error(412) 517 return 518 } 519 520 if (this.isCachable() && this.isFresh()) { 521 this.notModified() 522 return 523 } 524 } 525 526 // adjust len to start/end options 527 len = Math.max(0, len - offset) 528 if (options.end !== undefined) { 529 var bytes = options.end - offset + 1 530 if (len > bytes) len = bytes 531 } 532 533 // Range support 534 if (this._acceptRanges && BYTES_RANGE_REGEXP.test(ranges)) { 535 // parse 536 ranges = parseRange(len, ranges, { 537 combine: true 538 }) 539 540 // If-Range support 541 if (!this.isRangeFresh()) { 542 debug('range stale') 543 ranges = -2 544 } 545 546 // unsatisfiable 547 if (ranges === -1) { 548 debug('range unsatisfiable') 549 550 // Content-Range 551 res.setHeader('Content-Range', contentRange('bytes', len)) 552 553 // 416 Requested Range Not Satisfiable 554 return this.error(416, { 555 headers: { 'Content-Range': res.getHeader('Content-Range') } 556 }) 557 } 558 559 // valid (syntactically invalid/multiple ranges are treated as a regular response) 560 if (ranges !== -2 && ranges.length === 1) { 561 debug('range %j', ranges) 562 563 // Content-Range 564 res.statusCode = 206 565 res.setHeader('Content-Range', contentRange('bytes', len, ranges[0])) 566 567 // adjust for requested range 568 offset += ranges[0].start 569 len = ranges[0].end - ranges[0].start + 1 570 } 571 } 572 573 // clone options 574 for (var prop in options) { 575 opts[prop] = options[prop] 576 } 577 578 // set read options 579 opts.start = offset 580 opts.end = Math.max(offset, offset + len - 1) 581 582 // content-length 583 res.setHeader('Content-Length', len) 584 585 // HEAD support 586 if (req.method === 'HEAD') { 587 res.end() 588 return 589 } 590 591 this.stream(path, opts) 592 } 593 594 /** 595 * Transfer file for `path`. 596 * 597 * @param {String} path 598 * @api private 599 */ 600 SendStream.prototype.sendFile = function sendFile (path) { 601 var i = 0 602 var self = this 603 604 debug('stat "%s"', path) 605 fs.stat(path, function onstat (err, stat) { 606 var pathEndsWithSep = path[path.length - 1] === sep 607 if (err && err.code === 'ENOENT' && !extname(path) && !pathEndsWithSep) { 608 // not found, check extensions 609 return next(err) 610 } 611 if (err) return self.onStatError(err) 612 if (stat.isDirectory()) return self.redirect(path) 613 if (pathEndsWithSep) return self.error(404) 614 self.emit('file', path, stat) 615 self.send(path, stat) 616 }) 617 618 function next (err) { 619 if (self._extensions.length <= i) { 620 return err 621 ? self.onStatError(err) 622 : self.error(404) 623 } 624 625 var p = path + '.' + self._extensions[i++] 626 627 debug('stat "%s"', p) 628 fs.stat(p, function (err, stat) { 629 if (err) return next(err) 630 if (stat.isDirectory()) return next() 631 self.emit('file', p, stat) 632 self.send(p, stat) 633 }) 634 } 635 } 636 637 /** 638 * Transfer index for `path`. 639 * 640 * @param {String} path 641 * @api private 642 */ 643 SendStream.prototype.sendIndex = function sendIndex (path) { 644 var i = -1 645 var self = this 646 647 function next (err) { 648 if (++i >= self._index.length) { 649 if (err) return self.onStatError(err) 650 return self.error(404) 651 } 652 653 var p = join(path, self._index[i]) 654 655 debug('stat "%s"', p) 656 fs.stat(p, function (err, stat) { 657 if (err) return next(err) 658 if (stat.isDirectory()) return next() 659 self.emit('file', p, stat) 660 self.send(p, stat) 661 }) 662 } 663 664 next() 665 } 666 667 /** 668 * Stream `path` to the response. 669 * 670 * @param {String} path 671 * @param {Object} options 672 * @api private 673 */ 674 675 SendStream.prototype.stream = function stream (path, options) { 676 var self = this 677 var res = this.res 678 679 // pipe 680 var stream = fs.createReadStream(path, options) 681 this.emit('stream', stream) 682 stream.pipe(res) 683 684 // cleanup 685 function cleanup () { 686 stream.destroy() 687 } 688 689 // response finished, cleanup 690 onFinished(res, cleanup) 691 692 // error handling 693 stream.on('error', function onerror (err) { 694 // clean up stream early 695 cleanup() 696 697 // error 698 self.onStatError(err) 699 }) 700 701 // end 702 stream.on('end', function onend () { 703 self.emit('end') 704 }) 705 } 706 707 /** 708 * Set content-type based on `path` 709 * if it hasn't been explicitly set. 710 * 711 * @param {String} path 712 * @api private 713 */ 714 715 SendStream.prototype.type = function type (path) { 716 var res = this.res 717 718 if (res.getHeader('Content-Type')) return 719 720 var ext = extname(path) 721 var type = mime.contentType(ext) || 'application/octet-stream' 722 723 debug('content-type %s', type) 724 res.setHeader('Content-Type', type) 725 } 726 727 /** 728 * Set response header fields, most 729 * fields may be pre-defined. 730 * 731 * @param {String} path 732 * @param {Object} stat 733 * @api private 734 */ 735 736 SendStream.prototype.setHeader = function setHeader (path, stat) { 737 var res = this.res 738 739 this.emit('headers', res, path, stat) 740 741 if (this._acceptRanges && !res.getHeader('Accept-Ranges')) { 742 debug('accept ranges') 743 res.setHeader('Accept-Ranges', 'bytes') 744 } 745 746 if (this._cacheControl && !res.getHeader('Cache-Control')) { 747 var cacheControl = 'public, max-age=' + Math.floor(this._maxage / 1000) 748 749 if (this._immutable) { 750 cacheControl += ', immutable' 751 } 752 753 debug('cache-control %s', cacheControl) 754 res.setHeader('Cache-Control', cacheControl) 755 } 756 757 if (this._lastModified && !res.getHeader('Last-Modified')) { 758 var modified = stat.mtime.toUTCString() 759 debug('modified %s', modified) 760 res.setHeader('Last-Modified', modified) 761 } 762 763 if (this._etag && !res.getHeader('ETag')) { 764 var val = etag(stat) 765 debug('etag %s', val) 766 res.setHeader('ETag', val) 767 } 768 } 769 770 /** 771 * Clear all headers from a response. 772 * 773 * @param {object} res 774 * @private 775 */ 776 777 function clearHeaders (res) { 778 for (const header of res.getHeaderNames()) { 779 res.removeHeader(header) 780 } 781 } 782 783 /** 784 * Collapse all leading slashes into a single slash 785 * 786 * @param {string} str 787 * @private 788 */ 789 function collapseLeadingSlashes (str) { 790 for (var i = 0; i < str.length; i++) { 791 if (str[i] !== '/') { 792 break 793 } 794 } 795 796 return i > 1 797 ? '/' + str.substr(i) 798 : str 799 } 800 801 /** 802 * Determine if path parts contain a dotfile. 803 * 804 * @api private 805 */ 806 807 function containsDotFile (parts) { 808 for (var i = 0; i < parts.length; i++) { 809 var part = parts[i] 810 if (part.length > 1 && part[0] === '.') { 811 return true 812 } 813 } 814 815 return false 816 } 817 818 /** 819 * Create a Content-Range header. 820 * 821 * @param {string} type 822 * @param {number} size 823 * @param {array} [range] 824 */ 825 826 function contentRange (type, size, range) { 827 return type + ' ' + (range ? range.start + '-' + range.end : '*') + '/' + size 828 } 829 830 /** 831 * Create a minimal HTML document. 832 * 833 * @param {string} title 834 * @param {string} body 835 * @private 836 */ 837 838 function createHtmlDocument (title, body) { 839 return '<!DOCTYPE html>\n' + 840 '<html lang="en">\n' + 841 '<head>\n' + 842 '<meta charset="utf-8">\n' + 843 '<title>' + title + '</title>\n' + 844 '</head>\n' + 845 '<body>\n' + 846 '<pre>' + body + '</pre>\n' + 847 '</body>\n' + 848 '</html>\n' 849 } 850 851 /** 852 * Create a HttpError object from simple arguments. 853 * 854 * @param {number} status 855 * @param {Error|object} err 856 * @private 857 */ 858 859 function createHttpError (status, err) { 860 if (!err) { 861 return createError(status) 862 } 863 864 return err instanceof Error 865 ? createError(status, err, { expose: false }) 866 : createError(status, err) 867 } 868 869 /** 870 * decodeURIComponent. 871 * 872 * Allows V8 to only deoptimize this fn instead of all 873 * of send(). 874 * 875 * @param {String} path 876 * @api private 877 */ 878 879 function decode (path) { 880 try { 881 return decodeURIComponent(path) 882 } catch (err) { 883 return -1 884 } 885 } 886 887 /** 888 * Determine if emitter has listeners of a given type. 889 * 890 * The way to do this check is done three different ways in Node.js >= 0.10 891 * so this consolidates them into a minimal set using instance methods. 892 * 893 * @param {EventEmitter} emitter 894 * @param {string} type 895 * @returns {boolean} 896 * @private 897 */ 898 899 function hasListeners (emitter, type) { 900 var count = typeof emitter.listenerCount !== 'function' 901 ? emitter.listeners(type).length 902 : emitter.listenerCount(type) 903 904 return count > 0 905 } 906 907 /** 908 * Normalize the index option into an array. 909 * 910 * @param {boolean|string|array} val 911 * @param {string} name 912 * @private 913 */ 914 915 function normalizeList (val, name) { 916 var list = [].concat(val || []) 917 918 for (var i = 0; i < list.length; i++) { 919 if (typeof list[i] !== 'string') { 920 throw new TypeError(name + ' must be array of strings or false') 921 } 922 } 923 924 return list 925 } 926 927 /** 928 * Parse an HTTP Date into a number. 929 * 930 * @param {string} date 931 * @private 932 */ 933 934 function parseHttpDate (date) { 935 var timestamp = date && Date.parse(date) 936 937 return typeof timestamp === 'number' 938 ? timestamp 939 : NaN 940 } 941 942 /** 943 * Parse a HTTP token list. 944 * 945 * @param {string} str 946 * @private 947 */ 948 949 function parseTokenList (str) { 950 var end = 0 951 var list = [] 952 var start = 0 953 954 // gather tokens 955 for (var i = 0, len = str.length; i < len; i++) { 956 switch (str.charCodeAt(i)) { 957 case 0x20: /* */ 958 if (start === end) { 959 start = end = i + 1 960 } 961 break 962 case 0x2c: /* , */ 963 if (start !== end) { 964 list.push(str.substring(start, end)) 965 } 966 start = end = i + 1 967 break 968 default: 969 end = i + 1 970 break 971 } 972 } 973 974 // final token 975 if (start !== end) { 976 list.push(str.substring(start, end)) 977 } 978 979 return list 980 } 981 982 /** 983 * Set an object of headers on a response. 984 * 985 * @param {object} res 986 * @param {object} headers 987 * @private 988 */ 989 990 function setHeaders (res, headers) { 991 var keys = Object.keys(headers) 992 993 for (var i = 0; i < keys.length; i++) { 994 var key = keys[i] 995 res.setHeader(key, headers[key]) 996 } 997 }