3 * Copyright(c) 2010 Sencha Inc.
4 * Copyright(c) 2011 TJ Holowaychuk
5 * Copyright(c) 2014-2016 Douglas Christopher Wilson
12 * Module dependencies.
16 var encodeUrl = require('encodeurl')
17 var escapeHtml = require('escape-html')
18 var parseUrl = require('parseurl')
19 var resolve = require('path').resolve
20 var send = require('send')
21 var url = require('url')
28 module.exports = serveStatic
29 module.exports.mime = send.mime
32 * @param {string} root
33 * @param {object} [options]
38 function serveStatic (root, options) {
40 throw new TypeError('root path required')
43 if (typeof root !== 'string') {
44 throw new TypeError('root path must be a string')
47 // copy options object
48 var opts = Object.create(options || null)
51 var fallthrough = opts.fallthrough !== false
54 var redirect = opts.redirect !== false
57 var setHeaders = opts.setHeaders
59 if (setHeaders && typeof setHeaders !== 'function') {
60 throw new TypeError('option setHeaders must be function')
63 // setup options for send
64 opts.maxage = opts.maxage || opts.maxAge || 0
65 opts.root = resolve(root)
67 // construct directory listener
68 var onDirectory = redirect
69 ? createRedirectDirectoryListener()
70 : createNotFoundDirectoryListener()
72 return function serveStatic (req, res, next) {
73 if (req.method !== 'GET' && req.method !== 'HEAD') {
80 res.setHeader('Allow', 'GET, HEAD')
81 res.setHeader('Content-Length', '0')
86 var forwardError = !fallthrough
87 var originalUrl = parseUrl.original(req)
88 var path = parseUrl(req).pathname
90 // make sure redirect occurs at mount
91 if (path === '/' && originalUrl.pathname.substr(-1) !== '/') {
96 var stream = send(req, path, opts)
98 // add directory handler
99 stream.on('directory', onDirectory)
101 // add headers listener
103 stream.on('headers', setHeaders)
106 // add file listener for fallthrough
108 stream.on('file', function onFile () {
109 // once file is determined, always forward error
115 stream.on('error', function error (err) {
116 if (forwardError || !(err.statusCode < 500)) {
130 * Collapse all leading slashes into a single slash
133 function collapseLeadingSlashes (str) {
134 for (var i = 0; i < str.length; i++) {
135 if (str.charCodeAt(i) !== 0x2f /* / */) {
141 ? '/' + str.substr(i)
146 * Create a minimal HTML document.
148 * @param {string} title
149 * @param {string} body
153 function createHtmlDocument (title, body) {
154 return '<!DOCTYPE html>\n' +
155 '<html lang="en">\n' +
157 '<meta charset="utf-8">\n' +
158 '<title>' + title + '</title>\n' +
161 '<pre>' + body + '</pre>\n' +
167 * Create a directory listener that just 404s.
171 function createNotFoundDirectoryListener () {
172 return function notFound () {
178 * Create a directory listener that performs a redirect.
182 function createRedirectDirectoryListener () {
183 return function redirect (res) {
184 if (this.hasTrailingSlash()) {
190 var originalUrl = parseUrl.original(this.req)
192 // append trailing slash
193 originalUrl.path = null
194 originalUrl.pathname = collapseLeadingSlashes(originalUrl.pathname + '/')
197 var loc = encodeUrl(url.format(originalUrl))
198 var doc = createHtmlDocument('Redirecting', 'Redirecting to <a href="' + escapeHtml(loc) + '">' +
199 escapeHtml(loc) + '</a>')
201 // send redirect response
203 res.setHeader('Content-Type', 'text/html; charset=UTF-8')
204 res.setHeader('Content-Length', Buffer.byteLength(doc))
205 res.setHeader('Content-Security-Policy', "default-src 'self'")
206 res.setHeader('X-Content-Type-Options', 'nosniff')
207 res.setHeader('Location', loc)