You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
210 lines
4.5 KiB
210 lines
4.5 KiB
/*! |
|
* serve-static |
|
* Copyright(c) 2010 Sencha Inc. |
|
* Copyright(c) 2011 TJ Holowaychuk |
|
* Copyright(c) 2014-2016 Douglas Christopher Wilson |
|
* MIT Licensed |
|
*/ |
|
|
|
'use strict' |
|
|
|
/** |
|
* Module dependencies. |
|
* @private |
|
*/ |
|
|
|
var encodeUrl = require('encodeurl') |
|
var escapeHtml = require('escape-html') |
|
var parseUrl = require('parseurl') |
|
var resolve = require('path').resolve |
|
var send = require('send') |
|
var url = require('url') |
|
|
|
/** |
|
* Module exports. |
|
* @public |
|
*/ |
|
|
|
module.exports = serveStatic |
|
module.exports.mime = send.mime |
|
|
|
/** |
|
* @param {string} root |
|
* @param {object} [options] |
|
* @return {function} |
|
* @public |
|
*/ |
|
|
|
function serveStatic (root, options) { |
|
if (!root) { |
|
throw new TypeError('root path required') |
|
} |
|
|
|
if (typeof root !== 'string') { |
|
throw new TypeError('root path must be a string') |
|
} |
|
|
|
// copy options object |
|
var opts = Object.create(options || null) |
|
|
|
// fall-though |
|
var fallthrough = opts.fallthrough !== false |
|
|
|
// default redirect |
|
var redirect = opts.redirect !== false |
|
|
|
// headers listener |
|
var setHeaders = opts.setHeaders |
|
|
|
if (setHeaders && typeof setHeaders !== 'function') { |
|
throw new TypeError('option setHeaders must be function') |
|
} |
|
|
|
// setup options for send |
|
opts.maxage = opts.maxage || opts.maxAge || 0 |
|
opts.root = resolve(root) |
|
|
|
// construct directory listener |
|
var onDirectory = redirect |
|
? createRedirectDirectoryListener() |
|
: createNotFoundDirectoryListener() |
|
|
|
return function serveStatic (req, res, next) { |
|
if (req.method !== 'GET' && req.method !== 'HEAD') { |
|
if (fallthrough) { |
|
return next() |
|
} |
|
|
|
// method not allowed |
|
res.statusCode = 405 |
|
res.setHeader('Allow', 'GET, HEAD') |
|
res.setHeader('Content-Length', '0') |
|
res.end() |
|
return |
|
} |
|
|
|
var forwardError = !fallthrough |
|
var originalUrl = parseUrl.original(req) |
|
var path = parseUrl(req).pathname |
|
|
|
// make sure redirect occurs at mount |
|
if (path === '/' && originalUrl.pathname.substr(-1) !== '/') { |
|
path = '' |
|
} |
|
|
|
// create send stream |
|
var stream = send(req, path, opts) |
|
|
|
// add directory handler |
|
stream.on('directory', onDirectory) |
|
|
|
// add headers listener |
|
if (setHeaders) { |
|
stream.on('headers', setHeaders) |
|
} |
|
|
|
// add file listener for fallthrough |
|
if (fallthrough) { |
|
stream.on('file', function onFile () { |
|
// once file is determined, always forward error |
|
forwardError = true |
|
}) |
|
} |
|
|
|
// forward errors |
|
stream.on('error', function error (err) { |
|
if (forwardError || !(err.statusCode < 500)) { |
|
next(err) |
|
return |
|
} |
|
|
|
next() |
|
}) |
|
|
|
// pipe |
|
stream.pipe(res) |
|
} |
|
} |
|
|
|
/** |
|
* Collapse all leading slashes into a single slash |
|
* @private |
|
*/ |
|
function collapseLeadingSlashes (str) { |
|
for (var i = 0; i < str.length; i++) { |
|
if (str.charCodeAt(i) !== 0x2f /* / */) { |
|
break |
|
} |
|
} |
|
|
|
return i > 1 |
|
? '/' + str.substr(i) |
|
: str |
|
} |
|
|
|
/** |
|
* Create a minimal HTML document. |
|
* |
|
* @param {string} title |
|
* @param {string} body |
|
* @private |
|
*/ |
|
|
|
function createHtmlDocument (title, body) { |
|
return '<!DOCTYPE html>\n' + |
|
'<html lang="en">\n' + |
|
'<head>\n' + |
|
'<meta charset="utf-8">\n' + |
|
'<title>' + title + '</title>\n' + |
|
'</head>\n' + |
|
'<body>\n' + |
|
'<pre>' + body + '</pre>\n' + |
|
'</body>\n' + |
|
'</html>\n' |
|
} |
|
|
|
/** |
|
* Create a directory listener that just 404s. |
|
* @private |
|
*/ |
|
|
|
function createNotFoundDirectoryListener () { |
|
return function notFound () { |
|
this.error(404) |
|
} |
|
} |
|
|
|
/** |
|
* Create a directory listener that performs a redirect. |
|
* @private |
|
*/ |
|
|
|
function createRedirectDirectoryListener () { |
|
return function redirect (res) { |
|
if (this.hasTrailingSlash()) { |
|
this.error(404) |
|
return |
|
} |
|
|
|
// get original URL |
|
var originalUrl = parseUrl.original(this.req) |
|
|
|
// append trailing slash |
|
originalUrl.path = null |
|
originalUrl.pathname = collapseLeadingSlashes(originalUrl.pathname + '/') |
|
|
|
// reformat the URL |
|
var loc = encodeUrl(url.format(originalUrl)) |
|
var doc = createHtmlDocument('Redirecting', 'Redirecting to <a href="' + escapeHtml(loc) + '">' + |
|
escapeHtml(loc) + '</a>') |
|
|
|
// send redirect response |
|
res.statusCode = 301 |
|
res.setHeader('Content-Type', 'text/html; charset=UTF-8') |
|
res.setHeader('Content-Length', Buffer.byteLength(doc)) |
|
res.setHeader('Content-Security-Policy', "default-src 'none'") |
|
res.setHeader('X-Content-Type-Options', 'nosniff') |
|
res.setHeader('Location', loc) |
|
res.end(doc) |
|
} |
|
}
|
|
|