forked from svrjs/svrjs
Add static file serving and directory listing functionality.
This commit is contained in:
parent
b5ad179097
commit
0ceded2256
4 changed files with 1019 additions and 1 deletions
22
package-lock.json
generated
22
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -23,5 +23,8 @@
|
|||
"globals": "^15.9.0",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"mime-types": "^2.1.35"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
992
src/middleware/staticFileServingAndDirectoryListings.js
Normal file
992
src/middleware/staticFileServingAndDirectoryListings.js
Normal 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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">") +
|
||||
'</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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">") +
|
||||
"</title>",
|
||||
)
|
||||
: res.head.replace(
|
||||
/<head>/i,
|
||||
"<head><title>Directory: " +
|
||||
decodeURIComponent(origHref)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">") +
|
||||
"</title>",
|
||||
)) +
|
||||
(!headerHasHTMLTag ? customDirListingHeader : "") +
|
||||
"<h1>Directory: " +
|
||||
decodeURIComponent(origHref)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">") +
|
||||
'</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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">") +
|
||||
(req.headers.host == undefined
|
||||
? ""
|
||||
: " on " +
|
||||
String(req.headers.host)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")) +
|
||||
"</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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">") +
|
||||
"</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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">") +
|
||||
"</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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">") +
|
||||
"</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();
|
||||
}
|
||||
});
|
||||
};
|
Reference in a new issue