218 lines
4.5 KiB
JavaScript
218 lines
4.5 KiB
JavaScript
/*!
|
|
* 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 ' + escapeHtml(loc)
|
|
);
|
|
|
|
// 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);
|
|
};
|
|
}
|