index.js 22 KB

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