index.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  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.charCodeAt(i) !== 0x2f /* / */) {
  109. break
  110. }
  111. }
  112. return i > 1
  113. ? '/' + str.substr(i)
  114. : str
  115. }
  116. /**
  117. * Create a minimal HTML document.
  118. *
  119. * @param {string} title
  120. * @param {string} body
  121. * @private
  122. */
  123. function createHtmlDocument (title, body) {
  124. return '<!DOCTYPE html>\n' +
  125. '<html lang="en">\n' +
  126. '<head>\n' +
  127. '<meta charset="utf-8">\n' +
  128. '<title>' + title + '</title>\n' +
  129. '</head>\n' +
  130. '<body>\n' +
  131. '<pre>' + body + '</pre>\n' +
  132. '</body>\n' +
  133. '</html>\n'
  134. }
  135. /**
  136. * Create a directory listener that just 404s.
  137. * @private
  138. */
  139. function createNotFoundDirectoryListener () {
  140. return function notFound () {
  141. this.error(404)
  142. }
  143. }
  144. /**
  145. * Create a directory listener that performs a redirect.
  146. * @private
  147. */
  148. function createRedirectDirectoryListener () {
  149. return function redirect (res) {
  150. if (this.hasTrailingSlash()) {
  151. this.error(404)
  152. return
  153. }
  154. // get original URL
  155. var originalUrl = parseUrl.original(this.req)
  156. // append trailing slash
  157. originalUrl.path = null
  158. originalUrl.pathname = collapseLeadingSlashes(originalUrl.pathname + '/')
  159. // reformat the URL
  160. var loc = encodeUrl(url.format(originalUrl))
  161. var doc = createHtmlDocument('Redirecting', 'Redirecting to <a href="' + escapeHtml(loc) + '">' +
  162. escapeHtml(loc) + '</a>')
  163. // send redirect response
  164. res.statusCode = 301
  165. res.setHeader('Content-Type', 'text/html; charset=UTF-8')
  166. res.setHeader('Content-Length', Buffer.byteLength(doc))
  167. res.setHeader('Content-Security-Policy', "default-src 'self'")
  168. res.setHeader('X-Content-Type-Options', 'nosniff')
  169. res.setHeader('Location', loc)
  170. res.end(doc)
  171. }
  172. }