1
0
Fork 0
forked from svrjs/svrjs

Add static file serving and directory listing functionality.

This commit is contained in:
Dorian Niemiec 2024-08-25 10:23:37 +02:00
parent b5ad179097
commit 0ceded2256
4 changed files with 1019 additions and 1 deletions

22
package-lock.json generated
View file

@ -7,6 +7,9 @@
"": {
"name": "svrjs-build",
"version": "0.0.0",
"dependencies": {
"mime-types": "^2.1.35"
},
"devDependencies": {
"@eslint/js": "^9.9.0",
"archiver": "^7.0.1",
@ -4710,6 +4713,25 @@
"node": ">=8.6"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mimic-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",

View file

@ -23,5 +23,8 @@
"globals": "^15.9.0",
"jest": "^29.7.0",
"prettier": "^3.3.3"
},
"dependencies": {
"mime-types": "^2.1.35"
}
}

View file

@ -211,7 +211,8 @@ let middleware = [
// TODO: SVR.JS mods go here
// TODO: default handler
require("./middleware/defaultHandlerChecks.js"),
require("./middleware/status.js")
require("./middleware/status.js"),
require("./middleware/staticFileServingAndDirectoryListings.js")
];
function addMiddleware(mw) {

View file

@ -0,0 +1,992 @@
const http = require("http");
const fs = require("fs");
const os = require("os");
const zlib = require("zlib");
const mime = require("mime-types");
const matchHostname = require("../utils/matchHostname.js");
const ipMatch = require("../utils/ipMatch.js");
const createRegex = require("../utils/createRegex.js");
const sha256 = require("../utils/sha256.js");
const sizify = require("../utils/sizify.js");
// ETag-related
let ETagDB = {};
const generateETag = (filePath, stat) => {
if (!ETagDB[filePath + "-" + stat.size + "-" + stat.mtime])
ETagDB[filePath + "-" + stat.size + "-" + stat.mtime] = sha256(
filePath + "-" + stat.size + "-" + stat.mtime,
);
return ETagDB[filePath + "-" + stat.size + "-" + stat.mtime];
};
module.exports = (req, res, logFacilities, config, next) => {
const checkPathLevel = (path) => {
// Split the path into an array of components based on "/"
const pathComponents = path.split("/");
// Initialize counters for level up (..) and level down (.)
let levelUpCount = 0;
let levelDownCount = 0;
// Loop through the path components
for (var i = 0; i < pathComponents.length; i++) {
// If the component is "..", decrement the levelUpCount
if (".." === pathComponents[i]) {
levelUpCount--;
}
// If the component is not "." or an empty string, increment the levelDownCount
else if ("." !== pathComponents[i] && "" !== pathComponents[i]) {
levelDownCount++;
}
}
// Calculate the overall level by subtracting levelUpCount from levelDownCount
const overallLevel = levelDownCount - levelUpCount;
// Return the overall level
return overallLevel;
};
const checkForEnabledDirectoryListing = (hostname, localAddress) => {
const main =
config.enableDirectoryListing ||
config.enableDirectoryListing === undefined;
if (!config.enableDirectoryListingVHost) return main;
let vhostP = null;
config.enableDirectoryListingVHost.every(function (vhost) {
if (
matchHostname(vhost.host, hostname) &&
ipMatch(vhost.ip, localAddress)
) {
vhostP = vhost;
return false;
} else {
return true;
}
});
if (!vhostP || vhostP.enabled === undefined) return main;
else return vhostP.enabled;
};
let href = req.parsedURL.pathname;
let origHref = req.originalParsedURL.pathname;
let ext = href.match(/[^\/]\.([^.]+)$/);
if (!ext) ext = "";
let dHref = "";
try {
dHref = decodeURIComponent(href);
} catch (err) {
res.error(400);
return;
}
let readFrom = "." + dHref;
let dirImagesMissing = false;
fs.stat(readFrom, (err, stats) => {
if (err) {
if (err.code == "ENOENT") {
if (
process.dirname != process.cwd() &&
dHref.match(/^\/\.dirimages\/(?:(?!\.png$).)+\.png$/)
) {
dirImagesMissing = true;
readFrom = process.dirname + dHref;
} else {
res.error(404);
logFacilities.errmessage("Resource not found.");
return;
}
} else if (err.code == "ENOTDIR") {
res.error(404); // Assume that file doesn't exist.
logFacilities.errmessage("Resource not found.");
return;
} else if (err.code == "EACCES") {
res.error(403);
logFacilities.errmessage("Access denied.");
return;
} else if (err.code == "ENAMETOOLONG") {
res.error(414);
return;
} else if (err.code == "EMFILE") {
res.error(503);
return;
} else if (err.code == "ELOOP") {
res.error(508); // The symbolic link loop is detected during file system operations.
logFacilities.errmessage("Symbolic link loop detected.");
return;
} else {
res.error(500, err);
return;
}
}
const properDirectoryListingAndStaticFileServe = () => {
if (stats.isFile()) {
let acceptEncoding = req.headers["accept-encoding"];
if (!acceptEncoding) acceptEncoding = "";
let filelen = stats.size;
// ETag code
let fileETag = undefined;
if (config.enableETag == undefined || config.enableETag) {
fileETag = generateETag(href, stats);
// Check if the client's request matches the ETag value (If-None-Match)
const clientETag = req.headers["if-none-match"];
if (clientETag === fileETag) {
res.writeHead(304, http.STATUS_CODES[304], {
ETag: clientETag,
});
res.end();
return;
}
// Check if the client's request doesn't match the ETag value (If-Match)
const ifMatchETag = req.headers["if-match"];
if (ifMatchETag && ifMatchETag !== "*" && ifMatchETag !== fileETag) {
res.error(412, {
ETag: clientETag,
});
return;
}
}
// Handle partial content request
if (req.headers["range"]) {
try {
let rhd = config.getCustomHeaders();
rhd["Accept-Ranges"] = "bytes";
rhd["Content-Range"] = "bytes */" + filelen;
const regexmatch = req.headers["range"].match(
/bytes=([0-9]*)-([0-9]*)/,
);
if (!regexmatch) {
res.error(416, rhd);
} else {
// Process the partial content request
const beginOrig = regexmatch[1];
const endOrig = regexmatch[2];
const maxEnd =
filelen -
1 +
(ext == "html" ? res.head.length + res.foot.length : 0);
let begin = 0;
let end = maxEnd;
if (beginOrig == "" && endOrig == "") {
res.error(416, rhd);
return;
} else if (beginOrig == "") {
begin = end - parseInt(endOrig) + 1;
} else {
begin = parseInt(beginOrig);
if (endOrig != "") end = parseInt(endOrig);
}
if (begin > end || begin < 0 || begin > maxEnd) {
res.error(416, rhd);
return;
}
if (end > maxEnd) end = maxEnd;
rhd["Content-Range"] =
"bytes " + begin + "-" + end + "/" + filelen;
rhd["Content-Length"] = end - begin + 1;
delete rhd["Content-Type"];
const mtype = mime.contentType(ext);
if (mtype && ext != "") rhd["Content-Type"] = mtype;
if (fileETag) rhd["ETag"] = fileETag;
if (req.method != "HEAD") {
if (
ext == "html" &&
begin < res.head.length &&
end - begin < res.head.length
) {
res.writeHead(206, http.STATUS_CODES[206], rhd);
res.end(res.head.substring(begin, end + 1));
return;
} else if (
ext == "html" &&
begin >= res.head.length + filelen
) {
res.writeHead(206, http.STATUS_CODES[206], rhd);
res.end(
res.foot.substring(
begin - res.head.length - filelen,
end - res.head.length - filelen + 1,
),
);
return;
}
let readStream = fs.createReadStream(readFrom, {
start:
ext == "html"
? Math.max(0, begin - res.head.length)
: begin,
end:
ext == "html"
? Math.min(filelen, end - res.head.length)
: end,
});
readStream
.on("error", (err) => {
if (err.code == "ENOENT") {
res.error(404);
logFacilities.errmessage("Resource not found.");
} else if (err.code == "ENOTDIR") {
res.error(404); // Assume that file doesn't exist.
logFacilities.errmessage("Resource not found.");
} else if (err.code == "EACCES") {
res.error(403);
logFacilities.errmessage("Access denied.");
} else if (err.code == "ENAMETOOLONG") {
res.error(414);
} else if (err.code == "EMFILE") {
res.error(503);
} else if (err.code == "ELOOP") {
res.error(508); // The symbolic link loop is detected during file system operations.
logFacilities.errmessage("Symbolic link loop detected.");
} else {
res.error(500, err);
}
})
.on("open", () => {
try {
if (ext == "html") {
const afterWriteCallback = () => {
if (
res.foot.length > 0 &&
end > res.head.length + filelen
) {
readStream.on("end", () => {
res.end(
res.foot.substring(
0,
end - res.head.length - filelen + 1,
),
);
});
}
readStream.pipe(res, {
end: !(
res.foot.length > 0 &&
end > res.head.length + filelen
),
});
};
res.writeHead(206, http.STATUS_CODES[206], rhd);
if (res.head.length == 0 || begin > res.head.length) {
afterWriteCallback();
} else if (
!res.write(
res.head.substring(begin, res.head.length - begin),
)
) {
res.on("drain", afterWriteCallback);
} else {
process.nextTick(afterWriteCallback);
}
} else {
res.writeHead(206, http.STATUS_CODES[206], rhd);
readStream.pipe(res);
}
logFacilities.resmessage(
"Client successfully received content.",
);
} catch (err) {
res.error(500, err);
}
});
} else {
res.writeHead(206, http.STATUS_CODES[206], rhd);
res.end();
}
}
} catch (err) {
res.error(500, err);
}
} else {
// Helper function to check if compression is allowed for the file
const canCompress = (path, list) => {
let canCompress = true;
for (var i = 0; i < list.length; i++) {
if (createRegex(list[i], true).test(path)) {
canCompress = false;
break;
}
}
return canCompress;
};
let useBrotli =
ext != "br" &&
filelen > 256 &&
zlib.createBrotliCompress &&
acceptEncoding.match(/\bbr\b/);
let useDeflate =
ext != "zip" &&
filelen > 256 &&
acceptEncoding.match(/\bdeflate\b/);
let useGzip =
ext != "gz" && filelen > 256 && acceptEncoding.match(/\bgzip\b/);
let isCompressable = true;
try {
// Check for files not to compressed and compression enabling setting. Also check for browser quirks and adjust compression accordingly
if (
(!useBrotli && !useDeflate && !useGzip) ||
config.enableCompression !== true ||
!canCompress(href, config.dontCompress)
) {
isCompressable = false; // Compression is disabled
} else if (
ext != "html" &&
ext != "htm" &&
ext != "xhtml" &&
ext != "xht" &&
ext != "shtml"
) {
if (
/^Mozilla\/4\.[0-9]+(( *\[[^)]*\] *| *)\([^)\]]*\))? *$/.test(
req.headers["user-agent"],
) &&
!/https?:\/\/|[bB][oO][tT]|[sS][pP][iI][dD][eE][rR]|[sS][uU][rR][vV][eE][yY]|MSIE/.test(
req.headers["user-agent"],
)
) {
isCompressable = false; // Netscape 4.x doesn't handle compressed data properly outside of HTML documents.
} else if (/^w3m\/[^ ]*$/.test(req.headers["user-agent"])) {
isCompressable = false; // w3m doesn't handle compressed data properly outside of HTML documents.
}
} else {
if (
/^Mozilla\/4\.0[6-8](( *\[[^)]*\] *| *)\([^)\]]*\))? *$/.test(
req.headers["user-agent"],
) &&
!/https?:\/\/|[bB][oO][tT]|[sS][pP][iI][dD][eE][rR]|[sS][uU][rR][vV][eE][yY]|MSIE/.test(
req.headers["user-agent"],
)
) {
isCompressable = false; // Netscape 4.06-4.08 doesn't handle compressed data properly.
}
}
} catch (err) {
res.error(500, err);
return;
}
// Bun 1.1 has definition for zlib.createBrotliCompress, but throws an error while invoking the function.
if (process.isBun && useBrotli && isCompressable) {
try {
zlib.createBrotliCompress();
} catch (err) {
useBrotli = false;
}
}
try {
let hdhds = {};
if (useBrotli && isCompressable) {
hdhds["Content-Encoding"] = "br";
} else if (useDeflate && isCompressable) {
hdhds["Content-Encoding"] = "deflate";
} else if (useGzip && isCompressable) {
hdhds["Content-Encoding"] = "gzip";
} else {
if (ext == "html") {
hdhds["Content-Length"] =
res.head.length + filelen + res.foot.length;
} else {
hdhds["Content-Length"] = filelen;
}
}
hdhds["Accept-Ranges"] = "bytes";
delete hdhds["Content-Type"];
const mtype = mime.contentType(ext);
if (mtype && ext != "") hdhds["Content-Type"] = mtype;
if (fileETag) hdhds["ETag"] = fileETag;
if (req.method != "HEAD") {
let readStream = fs.createReadStream(readFrom);
readStream
.on("error", (err) => {
if (err.code == "ENOENT") {
res.error(404);
logFacilities.errmessage("Resource not found.");
} else if (err.code == "ENOTDIR") {
res.error(404); // Assume that file doesn't exist.
logFacilities.errmessage("Resource not found.");
} else if (err.code == "EACCES") {
res.error(403);
logFacilities.errmessage("Access denied.");
} else if (err.code == "ENAMETOOLONG") {
res.error(414);
} else if (err.code == "EMFILE") {
res.error(503);
} else if (err.code == "ELOOP") {
res.error(508); // The symbolic link loop is detected during file system operations.
logFacilities.errmessage("Symbolic link loop detected.");
} else {
res.error(500, err);
}
})
.on("open", () => {
try {
var resStream = {};
if (useBrotli && isCompressable) {
resStream = zlib.createBrotliCompress();
resStream.pipe(res);
} else if (useDeflate && isCompressable) {
resStream = zlib.createDeflateRaw();
resStream.pipe(res);
} else if (useGzip && isCompressable) {
resStream = zlib.createGzip();
resStream.pipe(res);
} else {
resStream = res;
}
if (ext == "html") {
const afterWriteCallback = () => {
if (res.foot.length > 0) {
readStream.on("end", () => {
resStream.end(res.foot);
});
}
readStream.pipe(resStream, {
end: res.foot.length == 0,
});
};
res.writeHead(200, http.STATUS_CODES[200], hdhds);
if (res.head.length == 0) {
afterWriteCallback();
} else if (!resStream.write(res.head)) {
resStream.on("drain", afterWriteCallback);
} else {
process.nextTick(afterWriteCallback);
}
} else {
res.writeHead(200, http.STATUS_CODES[200], hdhds);
readStream.pipe(resStream);
}
logFacilities.resmessage(
"Client successfully received content.",
);
} catch (err) {
res.error(500, err);
}
});
} else {
res.writeHead(200, http.STATUS_CODES[200], hdhds);
res.end();
logFacilities.resmessage("Client successfully received content.");
}
} catch (err) {
res.error(500, err);
}
}
} else if (stats.isDirectory()) {
// Check if directory listing is enabled in the configuration
if (
checkForEnabledDirectoryListing(
req.headers.host,
req.socket ? req.socket.localAddress : undefined,
)
) {
let customDirListingHeader = "";
let customDirListingFooter = "";
const getCustomDirListingHeader = (callback) => {
fs.readFile(
("." + dHref + "/.dirhead").replace(/\/+/g, "/"),
(err, data) => {
if (err) {
if (err.code == "ENOENT" || err.code == "EISDIR") {
if (os.platform != "win32" || href != "/") {
fs.readFile(
("." + dHref + "/HEAD.html").replace(/\/+/g, "/"),
(err, data) => {
if (err) {
if (err.code == "ENOENT" || err.code == "EISDIR") {
callback();
} else {
res.error(500, err);
}
} else {
customDirListingHeader = data.toString();
callback();
}
},
);
} else {
callback();
}
} else {
res.error(500, err);
}
} else {
customDirListingHeader = data.toString();
callback();
}
},
);
};
const getCustomDirListingFooter = (callback) => {
fs.readFile(
("." + dHref + "/.dirfoot").replace(/\/+/g, "/"),
(err, data) => {
if (err) {
if (err.code == "ENOENT" || err.code == "EISDIR") {
if (os.platform != "win32" || href != "/") {
fs.readFile(
("." + dHref + "/FOOT.html").replace(/\/+/g, "/"),
(err, data) => {
if (err) {
if (err.code == "ENOENT" || err.code == "EISDIR") {
callback();
} else {
res.error(500, err);
}
} else {
customDirListingFooter = data.toString();
callback();
}
},
);
} else {
callback();
}
} else {
res.error(500, err);
}
} else {
customDirListingFooter = data.toString();
callback();
}
},
);
};
// Read custom header and footer content (if available)
getCustomDirListingHeader(() => {
getCustomDirListingFooter(() => {
// Check if custom header has HTML tag
const headerHasHTMLTag = customDirListingHeader
.replace(/<!--(?:(?:(?!--\>)[\s\S])*|)(?:-->|$)/g, "")
.match(
/<html(?![a-zA-Z0-9])(?:"(?:\\(?:[\s\S]|$)|[^\\"])*(?:"|$)|'(?:\\(?:[\s\S]|$)|[^\\'])*(?:'|$)|[^'">])*(?:>|$)/i,
);
// Generate HTML head and footer based on configuration and custom content
let htmlHead =
(!config.enableDirectoryListingWithDefaultHead || res.head == ""
? !headerHasHTMLTag
? "<!DOCTYPE html><html><head><title>Directory: " +
decodeURIComponent(origHref)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;") +
'</title><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><style>html{background-color:#dfffdf;color:#000000;font-family:FreeSans, Helvetica, Tahoma, Verdana, Arial, sans-serif;margin:0.75em}body{background-color:#ffffff;padding:0.5em 0.5em 0.1em;margin:0.5em auto;width:90%;max-width:800px;-webkit-box-shadow:0 5px 10px 0 rgba(0, 0, 0, 0.15);-moz-box-shadow:0 5px 10px 0 rgba(0, 0, 0, 0.15);box-shadow:0 5px 10px 0 rgba(0, 0, 0, 0.15)}h1{text-align:center;font-size:2.25em;margin:0.3em 0 0.5em}code{background-color:#dfffdf;-webkit-box-shadow:0 2px 4px 0 rgba(0, 0, 0, 0.1);-moz-box-shadow:0 2px 4px 0 rgba(0, 0, 0, 0.1);box-shadow:0 2px 4px 0 rgba(0, 0, 0, 0.1);display:block;padding:0.2em;font-family:"DejaVu Sans Mono", "Bitstream Vera Sans Mono", Hack, Menlo, Consolas, Monaco, monospace;font-size:0.85em;margin:auto;width:95%;max-width:600px}table{width:95%;border-collapse:collapse;margin:auto;overflow-wrap:break-word;word-wrap:break-word;word-break:break-all;word-break:break-word;position:relative;z-index:0}table tbody{background-color:#ffffff;color:#000000}table tbody:after{-webkit-box-shadow:0 4px 8px 0 rgba(0, 0, 0, 0.175);-moz-box-shadow:0 4px 8px 0 rgba(0, 0, 0, 0.175);box-shadow:0 4px 8px 0 rgba(0, 0, 0, 0.175);content:\' \';position:absolute;top:0;left:0;right:0;bottom:0;z-index:-1}table img{margin:0;display:inline}th,tr{padding:0.15em;text-align:center}th{background-color:#007000;color:#ffffff}th a{color:#ffffff}td,th{padding:0.225em}td{text-align:left}tr:nth-child(odd){background-color:#dfffdf}hr{color:#ffffff}@media screen and (prefers-color-scheme: dark){html{background-color:#002000;color:#ffffff}body{background-color:#000f00;-webkit-box-shadow:0 5px 10px 0 rgba(127, 127, 127, 0.15);-moz-box-shadow:0 5px 10px 0 rgba(127, 127, 127, 0.15);box-shadow:0 5px 10px 0 rgba(127, 127, 127, 0.15)}code{background-color:#002000;-webkit-box-shadow:0 2px 4px 0 rgba(127, 127, 127, 0.1);-moz-box-shadow:0 2px 4px 0 rgba(127, 127, 127, 0.1);box-shadow:0 2px 4px 0 rgba(127, 127, 127, 0.1)}a{color:#ffffff}a:hover{color:#00ff00}table tbody{background-color:#000f00;color:#ffffff}table tbody:after{-webkit-box-shadow:0 4px 8px 0 rgba(127, 127, 127, 0.175);-moz-box-shadow:0 4px 8px 0 rgba(127, 127, 127, 0.175);box-shadow:0 4px 8px 0 rgba(127, 127, 127, 0.175)}tr:nth-child(odd){background-color:#002000}}</style></head><body>'
: customDirListingHeader.replace(
/<head>/i,
"<head><title>Directory: " +
decodeURIComponent(origHref)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;") +
"</title>",
)
: res.head.replace(
/<head>/i,
"<head><title>Directory: " +
decodeURIComponent(origHref)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;") +
"</title>",
)) +
(!headerHasHTMLTag ? customDirListingHeader : "") +
"<h1>Directory: " +
decodeURIComponent(origHref)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;") +
'</h1><table id="directoryListing"> <tr> <th></th> <th>Filename</th> <th>Size</th> <th>Date</th> </tr>' +
(checkPathLevel(decodeURIComponent(origHref)) < 1
? ""
: '<tr><td style="width: 24px;"><img src="/.dirimages/return.png" width="24px" height="24px" alt="[RET]" /></td><td style="word-wrap: break-word; word-break: break-word; overflow-wrap: break-word;"><a href="' +
origHref.replace(/\/+/g, "/").replace(/\/[^\/]*\/?$/, "/") +
'">Return</a></td><td></td><td></td></tr>');
let htmlFoot =
"</table><p><i>" +
config
.generateServerString()
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;") +
(req.headers.host == undefined
? ""
: " on " +
String(req.headers.host)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")) +
"</i></p>" +
customDirListingFooter +
(!config.enableDirectoryListingWithDefaultHead || res.foot == ""
? "</body></html>"
: res.foot);
if (
fs.existsSync(
"." +
decodeURIComponent(href) +
"/.maindesc".replace(/\/+/g, "/"),
)
) {
htmlFoot =
"</table><hr/>" +
fs.readFileSync(
"." +
decodeURIComponent(href) +
"/.maindesc".replace(/\/+/g, "/"),
) +
htmlFoot;
}
fs.readdir(readFrom, (err, list) => {
try {
if (err) throw err;
list = list.sort();
// Function to get stats for all files in the directory
const getStatsForAllFilesI = (
fileList,
callback,
prefix,
pushArray,
index,
) => {
if (fileList.length == 0) {
callback(pushArray);
return;
}
fs.stat(
(prefix + "/" + fileList[index]).replace(/\/+/g, "/"),
(err, stats) => {
if (err) {
fs.lstat(
(prefix + "/" + fileList[index]).replace(
/\/+/g,
"/",
),
(err, stats) => {
pushArray.push({
name: fileList[index],
stats: err ? null : stats,
errored: true,
});
if (index < fileList.length - 1) {
getStatsForAllFilesI(
fileList,
callback,
prefix,
pushArray,
index + 1,
);
} else {
callback(pushArray);
}
},
);
} else {
pushArray.push({
name: fileList[index],
stats: stats,
errored: false,
});
if (index < fileList.length - 1) {
getStatsForAllFilesI(
fileList,
callback,
prefix,
pushArray,
index + 1,
);
} else {
callback(pushArray);
}
}
},
);
};
// Wrapper function to get stats for all files
const getStatsForAllFiles = (fileList, prefix, callback) => {
if (!prefix) prefix = "";
getStatsForAllFilesI(fileList, callback, prefix, [], 0);
};
// Get stats for all files in the directory and generate the listing
getStatsForAllFiles(list, readFrom, (filelist) => {
let directoryListingRows = [];
for (var i = 0; i < filelist.length; i++) {
if (filelist[i].name[0] !== ".") {
const estats = filelist[i].stats;
const ename = filelist[i].name;
let eext = ename.match(/\.([^.]+)$/);
eext = eext ? eext[1] : "";
const emime = eext ? mime.contentType(eext) : false;
if (filelist[i].errored) {
directoryListingRows.push(
'<tr><td style="width: 24px;"><img src="/.dirimages/bad.png" alt="[BAD]" width="24px" height="24px" /></td><td style="word-wrap: break-word; word-break: break-word; overflow-wrap: break-word;"><a href="' +
(href + "/" + encodeURI(ename)).replace(
/\/+/g,
"/",
) +
'">' +
ename
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;") +
"</a></td><td>-</td><td>" +
(estats ? estats.mtime.toDateString() : "-") +
"</td></tr>\r\n",
);
} else {
let entry =
'<tr><td style="width: 24px;"><img src="[img]" alt="[alt]" width="24px" height="24px" /></td><td style="word-wrap: break-word; word-break: break-word; overflow-wrap: break-word;"><a href="' +
(
origHref +
"/" +
encodeURIComponent(ename)
).replace(/\/+/g, "/") +
(estats.isDirectory() ? "/" : "") +
'">' +
ename
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;") +
"</a></td><td>" +
(estats.isDirectory()
? "-"
: sizify(estats.size.toString())) +
"</td><td>" +
estats.mtime.toDateString() +
"</td></tr>\r\n";
// Determine the file type and set the appropriate image and alt text
if (estats.isDirectory()) {
entry = entry
.replace("[img]", "/.dirimages/directory.png")
.replace("[alt]", "[DIR]");
} else if (!estats.isFile()) {
entry =
'<tr><td style="width: 24px;"><img src="[img]" alt="[alt]" width="24px" height="24px" /></td><td style="word-wrap: break-word; word-break: break-word; overflow-wrap: break-word;"><a href="' +
(
origHref +
"/" +
encodeURIComponent(ename)
).replace(/\/+/g, "/") +
'">' +
ename
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;") +
"</a></td><td>-</td><td>" +
estats.mtime.toDateString() +
"</td></tr>\r\n";
// Determine the special file types (block device, character device, etc.)
if (estats.isBlockDevice()) {
entry = entry
.replace("[img]", "/.dirimages/hwdevice.png")
.replace("[alt]", "[BLK]");
} else if (estats.isCharacterDevice()) {
entry = entry
.replace("[img]", "/.dirimages/hwdevice.png")
.replace("[alt]", "[CHR]");
} else if (estats.isFIFO()) {
entry = entry
.replace("[img]", "/.dirimages/fifo.png")
.replace("[alt]", "[FIF]");
} else if (estats.isSocket()) {
entry = entry
.replace("[img]", "/.dirimages/socket.png")
.replace("[alt]", "[SCK]");
}
} else if (ename.match(/README|LICEN[SC]E/i)) {
entry = entry
.replace("[img]", "/.dirimages/important.png")
.replace("[alt]", "[IMP]");
} else if (eext.match(/^(?:[xs]?html?|xml)$/i)) {
entry = entry
.replace("[img]", "/.dirimages/html.png")
.replace(
"[alt]",
eext == "xml" ? "[XML]" : "[HTM]",
);
} else if (eext == "js") {
entry = entry
.replace("[img]", "/.dirimages/javascript.png")
.replace("[alt]", "[JS ]");
} else if (eext == "php") {
entry = entry
.replace("[img]", "/.dirimages/php.png")
.replace("[alt]", "[PHP]");
} else if (eext == "css") {
entry = entry
.replace("[img]", "/.dirimages/css.png")
.replace("[alt]", "[CSS]");
} else if (emime && emime.split("/")[0] == "image") {
entry = entry
.replace("[img]", "/.dirimages/image.png")
.replace(
"[alt]",
eext == "ico" ? "[ICO]" : "[IMG]",
);
} else if (emime && emime.split("/")[0] == "font") {
entry = entry
.replace("[img]", "/.dirimages/font.png")
.replace("[alt]", "[FON]");
} else if (emime && emime.split("/")[0] == "audio") {
entry = entry
.replace("[img]", "/.dirimages/audio.png")
.replace("[alt]", "[AUD]");
} else if (
(emime && emime.split("/")[0] == "text") ||
eext == "json"
) {
entry = entry
.replace("[img]", "/.dirimages/text.png")
.replace(
"[alt]",
eext == "json" ? "[JSO]" : "[TXT]",
);
} else if (emime && emime.split("/")[0] == "video") {
entry = entry
.replace("[img]", "/.dirimages/video.png")
.replace("[alt]", "[VID]");
} else if (
eext.match(/^(?:zip|rar|bz2|[gb7x]z|lzma|tar)$/i)
) {
entry = entry
.replace("[img]", "/.dirimages/archive.png")
.replace("[alt]", "[ARC]");
} else if (eext.match(/^(?:[id]mg|iso|flp)$/i)) {
entry = entry
.replace("[img]", "/.dirimages/diskimage.png")
.replace("[alt]", "[DSK]");
} else {
entry = entry
.replace("[img]", "/.dirimages/other.png")
.replace("[alt]", "[OTH]");
}
directoryListingRows.push(entry);
}
}
}
// Push the information about empty directory
if (directoryListingRows.length == 0) {
directoryListingRows.push(
"<tr><td></td><td>No files found</td><td></td><td></td></tr>",
);
}
// Send the directory listing response
res.writeHead(200, http.STATUS_CODES[200], {
"Content-Type": "text/html; charset=utf-8",
});
res.end(
htmlHead + directoryListingRows.join("") + htmlFoot,
);
logFacilities.resmessage(
"Client successfully received content.",
);
});
} catch (err) {
if (err.code == "ENOENT") {
res.error(404);
logFacilities.errmessage("Resource not found.");
} else if (err.code == "ENOTDIR") {
res.error(404); // Assume that file doesn't exist.
logFacilities.errmessage("Resource not found.");
} else if (err.code == "EACCES") {
res.error(403);
logFacilities.errmessage("Access denied.");
} else if (err.code == "ENAMETOOLONG") {
res.error(414);
} else if (err.code == "EMFILE") {
res.error(503);
} else if (err.code == "ELOOP") {
res.error(508); // The symbolic link loop is detected during file system operations.
logFacilities.errmessage("Symbolic link loop detected.");
} else {
res.error(500, err);
}
}
});
});
});
} else {
// Directory listing is disabled, call 403 Forbidden error
res.error(403);
logFacilities.errmessage("Directory listing is disabled.");
}
} else {
res.error(501);
logFacilities.errmessage(
"SVR.JS doesn't support block devices, character devices, FIFOs nor sockets.",
);
return;
}
};
// Check if index file exists
if (!dirImagesMissing && (req.url == "/" || stats.isDirectory())) {
fs.stat((readFrom + "/index.html").replace(/\/+/g, "/"), (e, s) => {
if (e || !s.isFile()) {
fs.stat((readFrom + "/index.htm").replace(/\/+/g, "/"), (e, s) => {
if (e || !s.isFile()) {
fs.stat(
(readFrom + "/index.xhtml").replace(/\/+/g, "/"),
(e, s) => {
if (e || !s.isFile()) {
properDirectoryListingAndStaticFileServe();
} else {
stats = s;
ext = "xhtml";
readFrom = (readFrom + "/index.xhtml").replace(/\/+/g, "/");
properDirectoryListingAndStaticFileServe();
}
},
);
} else {
stats = s;
ext = "htm";
readFrom = (readFrom + "/index.htm").replace(/\/+/g, "/");
properDirectoryListingAndStaticFileServe();
}
});
} else {
stats = s;
ext = "html";
readFrom = (readFrom + "/index.html").replace(/\/+/g, "/");
properDirectoryListingAndStaticFileServe();
}
});
} else if (dirImagesMissing) {
fs.stat(readFrom, (e, s) => {
if (e || !s.isFile()) {
properDirectoryListingAndStaticFileServe();
} else {
stats = s;
properDirectoryListingAndStaticFileServe();
}
});
} else {
properDirectoryListingAndStaticFileServe();
}
});
};