yeet source viewer; replace with gitea
This commit is contained in:
320
index.js
320
index.js
@@ -60,36 +60,6 @@ const loopHourly = (something, skipFirst = false) => {
|
||||
}, 3600000 - new Date().getTime() % 3600000);
|
||||
}
|
||||
|
||||
function sendSource(req, res, filePath) {
|
||||
try {
|
||||
// This will throw an error, which I catch, if the file isnt found
|
||||
// Use lstatSync to have a sync read while also not going through symlinks (to prevent arbritrary reads via symlinks)
|
||||
// Further restricted by my use of the --permission commandline flag
|
||||
// I also added a manual check to block any file reads outside the base dir (/prod/portfolio as of the time of writing this)
|
||||
const stat = fs.lstatSync(filePath);
|
||||
|
||||
if (!stat.isFile()) {
|
||||
// If it's not a file, error
|
||||
res.status(403).send('<pre>Not a file</pre>');
|
||||
} else {
|
||||
// If it's a file, send the content
|
||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||
res.render('sourceviewer', { faviconb64, filePath, fileContent });
|
||||
}
|
||||
} catch (err) {
|
||||
// Legit why are you doing this
|
||||
// If it's whitelisted, it's listed under the index page
|
||||
// If it's not whitelisted, you cant access it anyway :P
|
||||
res.status(404).send('File not found');
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
// - Below is black magic. I have no idea how it works. -
|
||||
// - I have either stolen it from somewhere or completely forgotten how it works. -
|
||||
// - If you know how it works please explain it to me PLEASE I BEG OF YOU HALP :AAAAAA: -
|
||||
// --------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Pulls all the pictures for the songs and stores them in a base64 key:value array where the key is the song id and the value is the thumbnail
|
||||
* @param {*} song
|
||||
@@ -128,246 +98,6 @@ async function updateSongs() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array} pathPattern This thingy is the indexPaths below this function
|
||||
* @returns {Array} an array called `results`
|
||||
*/
|
||||
function getDirectoryContents(pathPattern) {
|
||||
const results = [];
|
||||
|
||||
// Handle wildcards (including multiple wildcards like blog/*/*)
|
||||
if (pathPattern.includes('*')) {
|
||||
const segments = pathPattern.split('/').filter(s => s);
|
||||
|
||||
/**
|
||||
* Recursively resolves the pattern segments against the file system.
|
||||
* @param {Array} remainingSegments - The parts of the path left to match (e.g., ['*']).
|
||||
* @param {String} currentAbsPath - The absolute path on disk we are currently checking.
|
||||
* @returns {Array} - Array of matched file/directory objects.
|
||||
*/
|
||||
function traverse(remainingSegments, currentAbsPath) {
|
||||
// Base case: We have matched all segments. Return the node at this location.
|
||||
if (remainingSegments.length === 0) {
|
||||
return [createNode(currentAbsPath)];
|
||||
}
|
||||
|
||||
const segment = remainingSegments[0];
|
||||
const rest = remainingSegments.slice(1);
|
||||
|
||||
if (segment === '*') {
|
||||
// Wildcard: Read current directory and recurse for all children
|
||||
try {
|
||||
const files = fs.readdirSync(currentAbsPath, { withFileTypes: true });
|
||||
const matches = [];
|
||||
|
||||
files.forEach(file => {
|
||||
const fullPath = path.join(currentAbsPath, file.name);
|
||||
// Recurse with the remaining segments
|
||||
const childMatches = traverse(rest, fullPath);
|
||||
matches.push(...childMatches);
|
||||
});
|
||||
|
||||
return matches;
|
||||
} catch (err) {
|
||||
console.error(`Error reading directory ${currentAbsPath}:`, err);
|
||||
return [];
|
||||
}
|
||||
} else {
|
||||
// Exact segment: Join path and recurse
|
||||
const fullPath = path.join(currentAbsPath, segment);
|
||||
return traverse(rest, fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the result object for a specific path.
|
||||
* If it's a directory, it recursively fetches children.
|
||||
*/
|
||||
function createNode(fullPath) {
|
||||
try {
|
||||
const stat = fs.statSync(fullPath);
|
||||
const relativePath = path.relative(baseDir, fullPath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
// Recursively get children using the main function
|
||||
// We append '/*' to get the immediate contents of this directory
|
||||
const children = getDirectoryContents(`${relativePath}/*`);
|
||||
return {
|
||||
name: path.basename(fullPath),
|
||||
path: relativePath,
|
||||
type: 'directory',
|
||||
children: children
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
name: path.basename(fullPath),
|
||||
path: relativePath,
|
||||
type: 'file'
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error reading path ${fullPath}:`, err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Start traversal from the base directory
|
||||
const nodes = traverse(segments, baseDir);
|
||||
// Filter out any null results from errors
|
||||
nodes.forEach(node => {
|
||||
if (node) results.push(node);
|
||||
});
|
||||
|
||||
} else {
|
||||
// Handle exact paths
|
||||
|
||||
// Thanks past me, for the extremely helpful comment above
|
||||
// I now understand exactly what this does /s
|
||||
|
||||
const fullPath = path.join(baseDir, pathPattern);
|
||||
try {
|
||||
const stat = fs.statSync(fullPath);
|
||||
const relativePath = path.relative(baseDir, fullPath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
const files = fs.readdirSync(fullPath, { withFileTypes: true });
|
||||
files.forEach(file => {
|
||||
const childPath = path.join(fullPath, file.name);
|
||||
const childRelativePath = path.relative(baseDir, childPath);
|
||||
|
||||
if (file.isDirectory()) {
|
||||
const children = getDirectoryContents(`${childRelativePath}/*`);
|
||||
results.push({
|
||||
name: file.name,
|
||||
path: childRelativePath,
|
||||
type: 'directory',
|
||||
children: children
|
||||
});
|
||||
} else {
|
||||
results.push({
|
||||
name: file.name,
|
||||
path: childRelativePath,
|
||||
type: 'file'
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
results.push({
|
||||
name: path.basename(fullPath),
|
||||
path: relativePath,
|
||||
type: 'file'
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error reading path ${fullPath}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* An array of the whitelisted directories and their contents
|
||||
*/
|
||||
let whitelistedFiles = [];
|
||||
|
||||
/**
|
||||
* Whitelisted paths for the source code (to prevent arbritrary reads and stuffs),
|
||||
* Also referred to as `pathPattern` in whatever the hell `getDirectoryContents()` is
|
||||
*/
|
||||
const indexPaths = [
|
||||
'blog/*',
|
||||
'blog/*/*',
|
||||
'public/*',
|
||||
'public/piskelfile/*',
|
||||
'views/*',
|
||||
'views/partials/*',
|
||||
'index.js'
|
||||
];
|
||||
|
||||
/**
|
||||
* directory tree object (built in the func buildDirectoryTree below)
|
||||
*/
|
||||
let directoryTree;
|
||||
|
||||
/**
|
||||
* sorted result
|
||||
*/
|
||||
let sortedResult;
|
||||
|
||||
// Build directory tree
|
||||
const buildDirectoryTree = (items) => {
|
||||
const tree = {};
|
||||
items.forEach(item => {
|
||||
const parts = item.path.split(path.sep).filter(part => part !== '');
|
||||
let currentLevel = tree;
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
if (!currentLevel[part]) {
|
||||
currentLevel[part] = {
|
||||
name: part,
|
||||
path: parts.slice(0, i + 1).join(path.sep),
|
||||
type: i === parts.length - 1 ? item.type : 'directory',
|
||||
children: {}
|
||||
};
|
||||
}
|
||||
currentLevel = currentLevel[part].children;
|
||||
}
|
||||
if (item.type === 'file') {
|
||||
currentLevel[parts[parts.length - 1]] = {
|
||||
...currentLevel[parts[parts.length - 1]],
|
||||
type: 'file'
|
||||
};
|
||||
}
|
||||
});
|
||||
return tree;
|
||||
};
|
||||
|
||||
// Convert the tree to an array for easier rendering
|
||||
const treeToArray = (node) => {
|
||||
const result = [];
|
||||
Object.values(node).forEach(item => {
|
||||
if (item.type === 'directory') {
|
||||
const directoryItem = {
|
||||
name: item.name,
|
||||
path: item.path,
|
||||
type: 'directory',
|
||||
children: treeToArray(item.children)
|
||||
};
|
||||
result.push(directoryItem);
|
||||
} else {
|
||||
result.push({
|
||||
name: item.name,
|
||||
path: item.path,
|
||||
type: 'file'
|
||||
});
|
||||
}
|
||||
});
|
||||
return result.sort((a, b) => {
|
||||
if (a.type === 'directory' && b.type === 'file') return -1;
|
||||
if (a.type === 'file' && b.type === 'directory') return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
};
|
||||
|
||||
async function updateFileIndex() {
|
||||
// Clear whitelisted files to prevent memory leak
|
||||
whitelistedFiles = [];
|
||||
|
||||
indexPaths.forEach(path => {
|
||||
let contents = getDirectoryContents(path);
|
||||
whitelistedFiles.push(...contents);
|
||||
});
|
||||
|
||||
directoryTree = buildDirectoryTree(whitelistedFiles);
|
||||
sortedResult = treeToArray(directoryTree);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// - End black magic -
|
||||
// - I am pretty sure I know what is going on below here :p -
|
||||
// ----------------------------------------------------------
|
||||
|
||||
var blog; // Setup the blog variable
|
||||
|
||||
// Blog stuffs
|
||||
@@ -445,45 +175,6 @@ app.get('/art', (req, res) => {
|
||||
res.render('art', { faviconb64 });
|
||||
});
|
||||
|
||||
// Source code page
|
||||
app.get('/source', (req, res) => {
|
||||
res.render('source', { faviconb64, paths: sortedResult });
|
||||
});
|
||||
|
||||
// Route to serve indexed files
|
||||
app.get(/^\/source\/.*/, (req, res) => {
|
||||
const requestedPath = req.path.split('/source/')[1];
|
||||
|
||||
// Check if the requested file is whitelisted
|
||||
const isIndexed = indexPaths.some(pattern => {
|
||||
if (pattern.toString().includes("*")) {
|
||||
return whitelistedFiles.some(pattern => { return requestedPath.startsWith(pattern.path) && !(requestedPath.split(pattern.path).some(pattern => {return pattern.includes("/");}));});
|
||||
}
|
||||
return requestedPath === pattern;
|
||||
});
|
||||
|
||||
if (!isIndexed) {
|
||||
return res.status(403).send('<pre>File not whitelisted</pre>');
|
||||
}
|
||||
|
||||
const filePath = path.join(baseDir, requestedPath);
|
||||
|
||||
// Completely prevent ../ attacks (hopefully)
|
||||
// Well I mean it wont prevent bind mounts but if you can bind mount I'm fucked anyways cause you're root
|
||||
// Symlinks might (?) bypass this??
|
||||
// Hardlinks defo bypass this afaik.
|
||||
// because i pass the --permission flag and only pass --allow-fs-read=/prod/portfolio/*
|
||||
// you can't write via NJS at all
|
||||
// nor read outside /prod/portfolio
|
||||
// you'd need to break outta nodejs and start a native process
|
||||
// and my filesystem permissions wont letchu write stuff anyway
|
||||
if (!filePath.startsWith(baseDir)) {
|
||||
return res.status(403).send('<pre>File not whitelisted</pre>');
|
||||
}
|
||||
|
||||
sendSource(req, res, filePath);
|
||||
});
|
||||
|
||||
// Moosic page
|
||||
app.get('/music', (req, res) => {
|
||||
res.render('moosic', { faviconb64, playlistId, playlistSongs, thumbnails });
|
||||
@@ -551,7 +242,6 @@ function printHelp() {
|
||||
rl.write("\n\n-------------------------------HELP-----------------------------------------\n");
|
||||
rl.write("> help [?] - prints this message\n");
|
||||
rl.write("> updateMusic [um] - runs updateSongs()\n");
|
||||
rl.write("> updateFileIndex [ufi] - runs updateFileIndex()\n");
|
||||
rl.write("> eval [exec] - runs raw JS code, returns output (if any)\n");
|
||||
rl.write("> quit [exit] - stops the server, and quits\n");
|
||||
rl.write("-------------------------------HELP-----------------------------------------\n\n")
|
||||
@@ -581,10 +271,6 @@ async function customConsole() {
|
||||
case "updateMusic":
|
||||
updateSongs();
|
||||
break;
|
||||
case "ufi":
|
||||
case "updateFileIndex":
|
||||
updateFileIndex();
|
||||
break;
|
||||
case "exec":
|
||||
case "eval":
|
||||
// TODO: Fix this not actually using the correct context for whatever reason it just evals in an empty context
|
||||
@@ -623,9 +309,6 @@ async function main() {
|
||||
// Populate the blog index
|
||||
await generateBlogIndex();
|
||||
|
||||
// Populate file index
|
||||
await updateFileIndex();
|
||||
|
||||
server = http.createServer(app);
|
||||
|
||||
await (async () => {
|
||||
@@ -634,9 +317,6 @@ async function main() {
|
||||
|
||||
// Start hourly loop to update playlist
|
||||
loopHourly(async () => await updateSongs());
|
||||
|
||||
// Start hourly loop to update file index
|
||||
loopHourly(async () => await updateFileIndex());
|
||||
});
|
||||
})();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user