index.js 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. /*!
  2. * serve-static
  3. * Copyright(c) 2010 Sencha Inc.
  4. * Copyright(c) 2011 TJ Holowaychuk
  5. * Copyright(c) 2014-2016 Douglas Christopher Wilson
  6. * MIT Licensed
  7. */
  8. 'use strict'
  9. /**
  10. * Module dependencies.
  11. * @private
  12. */
  13. var encodeUrl = require('encodeurl')
  14. var escapeHtml = require('escape-html')
  15. var parseUrl = require('parseurl')
  16. var resolve = require('path').resolve
  17. var send = require('send')
  18. var url = require('url')
  19. /**
  20. * Module exports.
  21. * @public
  22. */
  23. module.exports = serveStatic
  24. module.exports.mime = send.mime
  25. /**
  26. * @param {string} root
  27. * @param {object} [options]
  28. * @return {function}
  29. * @public
  30. */
  31. function serveStatic (root, options) {
  32. if (!root) {
  33. throw new TypeError('root path required')
  34. }
  35. if (typeof root !== 'string') {
  36. throw new TypeError('root path must be a string')
  37. }
  38. // copy options object
  39. var opts = Object.create(options || null)
  40. // fall-though
  41. var fallthrough = opts.fallthrough !== false
  42. // default redirect
  43. var redirect = opts.redirect !== false
  44. // headers listener
  45. var setHeaders = opts.setHeaders
  46. if (setHeaders && typeof setHeaders !== 'function') {
  47. throw new TypeError('option setHeaders must be function')
  48. }
  49. // setup options for send
  50. opts.maxage = opts.maxage || opts.maxAge || 0
  51. opts.root = resolve(root)
  52. // construct directory listener
  53. var onDirectory = redirect
  54. ? createRedirectDirectoryListener()
  55. : createNotFoundDirectoryListener()
  56. return function serveStatic (req, res, next) {
  57. if (req.method !== 'GET' && req.method !== 'HEAD') {
  58. if (fallthrough) {
  59. return next()
  60. }
  61. // method not allowed
  62. res.statusCode = 405
  63. res.setHeader('Allow', 'GET, HEAD')
  64. res.setHeader('Content-Length', '0')
  65. res.end()
  66. return
  67. }
  68. var forwardError = !fallthrough
  69. var originalUrl = parseUrl.original(req)
  70. var path = parseUrl(req).pathname
  71. // make sure redirect occurs at mount
  72. if (path === '/' && originalUrl.pathname.substr(-1) !== '/') {
  73. path = ''
  74. }
  75. // create send stream
  76. var stream = send(req, path, opts)
  77. // add directory handler
  78. stream.on('directory', onDirectory)
  79. // add headers listener
  80. if (setHeaders) {
  81. stream.on('headers', setHeaders)
  82. }
  83. // add file listener for fallthrough
  84. if (fallthrough) {
  85. stream.on('file', function onFile () {
  86. // once file is determined, always forward error
  87. forwardError = true
  88. })
  89. }
  90. // forward errors
  91. stream.on('error', function error (err) {
  92. if (forwardError || !(err.statusCode < 500)) {
  93. next(err)
  94. return
  95. }
  96. next()
  97. })
  98. // pipe
  99. stream.pipe(res)
  100. }
  101. }
  102. /**
  103. * Collapse all leading slashes into a single slash
  104. * @private
  105. */
  106. function collapseLeadingSlashes (str) {
  107. for (var i = 0; i < str.length; i++) {
  108. if (str[i] !== '/') {
  109. break
  110. }
  111. }
  112. return i > 1
  113. ? '/' + str.substr(i)
  114. : str
  115. }
  116. /**
  117. * Create a directory listener that just 404s.
  118. * @private
  119. */
  120. function createNotFoundDirectoryListener () {
  121. return function notFound () {
  122. this.error(404)
  123. }
  124. }
  125. /**
  126. * Create a directory listener that performs a redirect.
  127. * @private
  128. */
  129. function createRedirectDirectoryListener () {
  130. return function redirect () {
  131. if (this.hasTrailingSlash()) {
  132. this.error(404)
  133. return
  134. }
  135. // get original URL
  136. var originalUrl = parseUrl.original(this.req)
  137. // append trailing slash
  138. originalUrl.path = null
  139. originalUrl.pathname = collapseLeadingSlashes(originalUrl.pathname + '/')
  140. // reformat the URL
  141. var loc = encodeUrl(url.format(originalUrl))
  142. var msg = 'Redirecting to <a href="' + escapeHtml(loc) + '">' + escapeHtml(loc) + '</a>\n'
  143. var res = this.res
  144. // send redirect response
  145. res.statusCode = 301
  146. res.setHeader('Content-Type', 'text/html; charset=UTF-8')
  147. res.setHeader('Content-Length', Buffer.byteLength(msg))
  148. res.setHeader('X-Content-Type-Options', 'nosniff')
  149. res.setHeader('Location', loc)
  150. res.end(msg)
  151. }
  152. }