index.js 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  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. /**
  25. * @param {string} root
  26. * @param {object} [options]
  27. * @return {function}
  28. * @public
  29. */
  30. function serveStatic (root, options) {
  31. if (!root) {
  32. throw new TypeError('root path required')
  33. }
  34. if (typeof root !== 'string') {
  35. throw new TypeError('root path must be a string')
  36. }
  37. // copy options object
  38. var opts = Object.create(options || null)
  39. // fall-though
  40. var fallthrough = opts.fallthrough !== false
  41. // default redirect
  42. var redirect = opts.redirect !== false
  43. // headers listener
  44. var setHeaders = opts.setHeaders
  45. if (setHeaders && typeof setHeaders !== 'function') {
  46. throw new TypeError('option setHeaders must be function')
  47. }
  48. // setup options for send
  49. opts.maxage = opts.maxage || opts.maxAge || 0
  50. opts.root = resolve(root)
  51. // construct directory listener
  52. var onDirectory = redirect
  53. ? createRedirectDirectoryListener()
  54. : createNotFoundDirectoryListener()
  55. return function serveStatic (req, res, next) {
  56. if (req.method !== 'GET' && req.method !== 'HEAD') {
  57. if (fallthrough) {
  58. return next()
  59. }
  60. // method not allowed
  61. res.statusCode = 405
  62. res.setHeader('Allow', 'GET, HEAD')
  63. res.setHeader('Content-Length', '0')
  64. res.end()
  65. return
  66. }
  67. var forwardError = !fallthrough
  68. var originalUrl = parseUrl.original(req)
  69. var path = parseUrl(req).pathname
  70. // make sure redirect occurs at mount
  71. if (path === '/' && originalUrl.pathname.substr(-1) !== '/') {
  72. path = ''
  73. }
  74. // create send stream
  75. var stream = send(req, path, opts)
  76. // add directory handler
  77. stream.on('directory', onDirectory)
  78. // add headers listener
  79. if (setHeaders) {
  80. stream.on('headers', setHeaders)
  81. }
  82. // add file listener for fallthrough
  83. if (fallthrough) {
  84. stream.on('file', function onFile () {
  85. // once file is determined, always forward error
  86. forwardError = true
  87. })
  88. }
  89. // forward errors
  90. stream.on('error', function error (err) {
  91. if (forwardError || !(err.statusCode < 500)) {
  92. next(err)
  93. return
  94. }
  95. next()
  96. })
  97. // pipe
  98. stream.pipe(res)
  99. }
  100. }
  101. /**
  102. * Collapse all leading slashes into a single slash
  103. * @private
  104. */
  105. function collapseLeadingSlashes (str) {
  106. for (var i = 0; i < str.length; i++) {
  107. if (str.charCodeAt(i) !== 0x2f /* / */) {
  108. break
  109. }
  110. }
  111. return i > 1
  112. ? '/' + str.substr(i)
  113. : str
  114. }
  115. /**
  116. * Create a minimal HTML document.
  117. *
  118. * @param {string} title
  119. * @param {string} body
  120. * @private
  121. */
  122. function createHtmlDocument (title, body) {
  123. return '<!DOCTYPE html>\n' +
  124. '<html lang="en">\n' +
  125. '<head>\n' +
  126. '<meta charset="utf-8">\n' +
  127. '<title>' + title + '</title>\n' +
  128. '</head>\n' +
  129. '<body>\n' +
  130. '<pre>' + body + '</pre>\n' +
  131. '</body>\n' +
  132. '</html>\n'
  133. }
  134. /**
  135. * Create a directory listener that just 404s.
  136. * @private
  137. */
  138. function createNotFoundDirectoryListener () {
  139. return function notFound () {
  140. this.error(404)
  141. }
  142. }
  143. /**
  144. * Create a directory listener that performs a redirect.
  145. * @private
  146. */
  147. function createRedirectDirectoryListener () {
  148. return function redirect (res) {
  149. if (this.hasTrailingSlash()) {
  150. this.error(404)
  151. return
  152. }
  153. // get original URL
  154. var originalUrl = parseUrl.original(this.req)
  155. // append trailing slash
  156. originalUrl.path = null
  157. originalUrl.pathname = collapseLeadingSlashes(originalUrl.pathname + '/')
  158. // reformat the URL
  159. var loc = encodeUrl(url.format(originalUrl))
  160. var doc = createHtmlDocument('Redirecting', 'Redirecting to ' + escapeHtml(loc))
  161. // send redirect response
  162. res.statusCode = 301
  163. res.setHeader('Content-Type', 'text/html; charset=UTF-8')
  164. res.setHeader('Content-Length', Buffer.byteLength(doc))
  165. res.setHeader('Content-Security-Policy', "default-src 'none'")
  166. res.setHeader('X-Content-Type-Options', 'nosniff')
  167. res.setHeader('Location', loc)
  168. res.end(doc)
  169. }
  170. }