index.js 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. /*!
  2. * serve-static
  3. * Copyright(c) 2010 Sencha Inc.
  4. * Copyright(c) 2011 TJ Holowaychuk
  5. * Copyright(c) 2014-2015 Douglas Christopher Wilson
  6. * MIT Licensed
  7. */
  8. 'use strict'
  9. /**
  10. * Module dependencies.
  11. * @private
  12. */
  13. var escapeHtml = require('escape-html')
  14. var parseUrl = require('parseurl')
  15. var resolve = require('path').resolve
  16. var send = require('send')
  17. var url = require('url')
  18. /**
  19. * Module exports.
  20. * @public
  21. */
  22. module.exports = serveStatic
  23. module.exports.mime = send.mime
  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[i] !== '/') {
  108. break
  109. }
  110. }
  111. return i > 1
  112. ? '/' + str.substr(i)
  113. : str
  114. }
  115. /**
  116. * Create a directory listener that just 404s.
  117. * @private
  118. */
  119. function createNotFoundDirectoryListener () {
  120. return function notFound () {
  121. this.error(404)
  122. }
  123. }
  124. /**
  125. * Create a directory listener that performs a redirect.
  126. * @private
  127. */
  128. function createRedirectDirectoryListener () {
  129. return function redirect () {
  130. if (this.hasTrailingSlash()) {
  131. this.error(404)
  132. return
  133. }
  134. // get original URL
  135. var originalUrl = parseUrl.original(this.req)
  136. // append trailing slash
  137. originalUrl.path = null
  138. originalUrl.pathname = collapseLeadingSlashes(originalUrl.pathname + '/')
  139. // reformat the URL
  140. var loc = url.format(originalUrl)
  141. var msg = 'Redirecting to <a href="' + escapeHtml(loc) + '">' + escapeHtml(loc) + '</a>\n'
  142. var res = this.res
  143. // send redirect response
  144. res.statusCode = 303
  145. res.setHeader('Content-Type', 'text/html; charset=UTF-8')
  146. res.setHeader('Content-Length', Buffer.byteLength(msg))
  147. res.setHeader('X-Content-Type-Options', 'nosniff')
  148. res.setHeader('Location', loc)
  149. res.end(msg)
  150. }
  151. }