From 0ceded22564f657e0f14849a37c22e303987738d Mon Sep 17 00:00:00 2001 From: Dorian Niemiec Date: Sun, 25 Aug 2024 10:23:37 +0200 Subject: [PATCH] Add static file serving and directory listing functionality. --- package-lock.json | 22 + package.json | 3 + src/index.js | 3 +- .../staticFileServingAndDirectoryListings.js | 992 ++++++++++++++++++ 4 files changed, 1019 insertions(+), 1 deletion(-) create mode 100644 src/middleware/staticFileServingAndDirectoryListings.js diff --git a/package-lock.json b/package-lock.json index fef9421..68d5d3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index d1516c5..51e78f9 100644 --- a/package.json +++ b/package.json @@ -23,5 +23,8 @@ "globals": "^15.9.0", "jest": "^29.7.0", "prettier": "^3.3.3" + }, + "dependencies": { + "mime-types": "^2.1.35" } } diff --git a/src/index.js b/src/index.js index e101548..f506259 100644 --- a/src/index.js +++ b/src/index.js @@ -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) { diff --git a/src/middleware/staticFileServingAndDirectoryListings.js b/src/middleware/staticFileServingAndDirectoryListings.js new file mode 100644 index 0000000..b7da8bd --- /dev/null +++ b/src/middleware/staticFileServingAndDirectoryListings.js @@ -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(/|$)/g, "") + .match( + /])*(?:>|$)/i, + ); + + // Generate HTML head and footer based on configuration and custom content + let htmlHead = + (!config.enableDirectoryListingWithDefaultHead || res.head == "" + ? !headerHasHTMLTag + ? "Directory: " + + decodeURIComponent(origHref) + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + + '' + : customDirListingHeader.replace( + //i, + "Directory: " + + decodeURIComponent(origHref) + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + + "", + ) + : res.head.replace( + //i, + "Directory: " + + decodeURIComponent(origHref) + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + + "", + )) + + (!headerHasHTMLTag ? customDirListingHeader : "") + + "

Directory: " + + decodeURIComponent(origHref) + .replace(/&/g, "&") + .replace(//g, ">") + + '

' + + (checkPathLevel(decodeURIComponent(origHref)) < 1 + ? "" + : ''); + + let htmlFoot = + "
Filename Size Date
[RET]Return

" + + config + .generateServerString() + .replace(/&/g, "&") + .replace(//g, ">") + + (req.headers.host == undefined + ? "" + : " on " + + String(req.headers.host) + .replace(/&/g, "&") + .replace(//g, ">")) + + "

" + + customDirListingFooter + + (!config.enableDirectoryListingWithDefaultHead || res.foot == "" + ? "" + : res.foot); + + if ( + fs.existsSync( + "." + + decodeURIComponent(href) + + "/.maindesc".replace(/\/+/g, "/"), + ) + ) { + htmlFoot = + "
" + + 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( + '[BAD]' + + ename + .replace(/&/g, "&") + .replace(//g, ">") + + "-" + + (estats ? estats.mtime.toDateString() : "-") + + "\r\n", + ); + } else { + let entry = + '[alt]' + + ename + .replace(/&/g, "&") + .replace(//g, ">") + + "" + + (estats.isDirectory() + ? "-" + : sizify(estats.size.toString())) + + "" + + estats.mtime.toDateString() + + "\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 = + '[alt]' + + ename + .replace(/&/g, "&") + .replace(//g, ">") + + "-" + + estats.mtime.toDateString() + + "\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( + "No files found", + ); + } + + // 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(); + } + }); +};