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