index.js 20 KB

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