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