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