From cff8e4445e5af12f552ed4bd85864791cf615a94 Mon Sep 17 00:00:00 2001 From: Ambrose Chua Date: Fri, 7 May 2021 22:42:20 +0800 Subject: [PATCH] Fix #6 --- Dockerfile | 2 + Dockerfile.env | 1 + index.js | 841 +++++++++++++++++++++--------------------- views/list.handlebars | 4 + 4 files changed, 428 insertions(+), 420 deletions(-) diff --git a/Dockerfile b/Dockerfile index 103b12e..1a22a83 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,4 +13,6 @@ RUN cd /usr/local/share/file-manager \ VOLUME /data WORKDIR /data +ENV NODE_ENV=production + CMD ["node", "/usr/local/share/file-manager/index.js"] diff --git a/Dockerfile.env b/Dockerfile.env index 3974052..3d7aef3 100644 --- a/Dockerfile.env +++ b/Dockerfile.env @@ -7,5 +7,6 @@ RUN cd /usr/local/share/file-manager \ USER ambrose ENV SHELL="zsh -l" +ENV NODE_ENV=production CMD ["node", "/usr/local/share/file-manager/index.js"] diff --git a/index.js b/index.js index 753d5f9..dbb19de 100755 --- a/index.js +++ b/index.js @@ -30,40 +30,40 @@ let http = app.listen(process.env.PORT || 8080); app.set("views", path.join(__dirname, "views")); app.engine("handlebars", hbs({ - partialsDir: path.join(__dirname, "views", "partials"), - layoutsDir: path.join(__dirname, "views", "layouts"), - defaultLayout: "main", - helpers: { + partialsDir: path.join(__dirname, "views", "partials"), + layoutsDir: path.join(__dirname, "views", "layouts"), + defaultLayout: "main", + helpers: { either: (a, b, options) => { if (a || b) { return options.fn(this); } }, filesize: filesize, - octicon: (i, options) => { - if (!octicons[i]) { - return new handlebars.SafeString(octicons.question.toSVG()); - } - return new handlebars.SafeString(octicons[i].toSVG()); - }, - eachpath: (path, options) => { - if (typeof path != "string") { - return ""; - } - let out = ""; - path = path.split("/"); - path.splice(path.length - 1, 1); - path.unshift(""); - path.forEach((folder, index) => { - out += options.fn({ - name: folder + "/", - path: "/" + path.slice(1, index + 1).join("/"), - current: index === path.length - 1 - }); - }); - return out; - }, - } + octicon: (i, options) => { + if (!octicons[i]) { + return new handlebars.SafeString(octicons.question.toSVG()); + } + return new handlebars.SafeString(octicons[i].toSVG()); + }, + eachpath: (path, options) => { + if (typeof path != "string") { + return ""; + } + let out = ""; + path = path.split("/"); + path.splice(path.length - 1, 1); + path.unshift(""); + path.forEach((folder, index) => { + out += options.fn({ + name: folder + "/", + path: "/" + path.slice(1, index + 1).join("/"), + current: index === path.length - 1 + }); + }); + return out; + }, + } })); app.set("view engine", "handlebars"); @@ -77,7 +77,7 @@ app.use("/@assets/xterm-addon-attach", express.static(path.join(__dirname, "node app.use("/@assets/xterm-addon-fit", express.static(path.join(__dirname, "node_modules/xterm-addon-fit"))); app.use(session({ - secret: "meowmeow" + secret: "meowmeow" })); app.use(flash()); app.use(busboy()); @@ -88,392 +88,392 @@ app.use(bodyparser.urlencoded()); const KEY = process.env.KEY ? base32.decode(process.env.KEY.replace(/ /g, "")) : null; app.get("/@logout", (req, res) => { - if (KEY) { - req.session.login = false; - req.flash("success", "Signed out."); - res.redirect("/@login"); - return - } - req.flash("error", "You were never logged in..."); - res.redirect("back"); + if (KEY) { + req.session.login = false; + req.flash("success", "Signed out."); + res.redirect("/@login"); + return + } + req.flash("error", "You were never logged in..."); + res.redirect("back"); }); app.get("/@login", (req, res) => { - res.render("login", flashify(req, {})); + res.render("login", flashify(req, {})); }); app.post("/@login", (req, res) => { - let pass = notp.totp.verify(req.body.token.replace(" ", ""), KEY); - console.log(pass, req.body.token.replace(" ", "")); - if (pass) { - req.session.login = true; - res.redirect("/"); - return; - } - req.flash("error", "Bad token."); - res.redirect("/@login"); + let pass = notp.totp.verify(req.body.token.replace(" ", ""), KEY); + console.log(pass, req.body.token.replace(" ", "")); + if (pass) { + req.session.login = true; + res.redirect("/"); + return; + } + req.flash("error", "Bad token."); + res.redirect("/@login"); }); app.use((req, res, next) => { - if (!KEY) { - return next(); - } - if (req.session.login === true) { - return next(); - } - req.flash("error", "Please sign in."); - res.redirect("/@login"); + if (!KEY) { + return next(); + } + if (req.session.login === true) { + return next(); + } + req.flash("error", "Please sign in."); + res.redirect("/@login"); }); function relative(...paths) { - return paths.reduce((a, b) => path.join(a, b), process.cwd()); + return paths.reduce((a, b) => path.join(a, b), process.cwd()); } function flashify(req, obj) { - let error = req.flash("error"); - if (error && error.length > 0) { - if (!obj.errors) { - obj.errors = []; - } - obj.errors.push(error); - } - let success = req.flash("success"); - if (success && success.length > 0) { - if (!obj.successes) { - obj.successes = []; - } - obj.successes.push(success); - } - obj.isloginenabled = !!KEY; - return obj; + let error = req.flash("error"); + if (error && error.length > 0) { + if (!obj.errors) { + obj.errors = []; + } + obj.errors.push(error); + } + let success = req.flash("success"); + if (success && success.length > 0) { + if (!obj.successes) { + obj.successes = []; + } + obj.successes.push(success); + } + obj.isloginenabled = !!KEY; + return obj; } app.all("/*", (req, res, next) => { - res.filename = req.params[0]; + res.filename = req.params[0]; - let fileExists = new Promise((resolve, reject) => { - // check if file exists - fs.stat(relative(res.filename), (err, stats) => { - if (err) { - return reject(err); - } - return resolve(stats); - }); - }); + let fileExists = new Promise((resolve, reject) => { + // check if file exists + fs.stat(relative(res.filename), (err, stats) => { + if (err) { + return reject(err); + } + return resolve(stats); + }); + }); - fileExists.then((stats) => { - res.stats = stats; - next(); - }).catch((err) => { - res.stats = { error: err }; - next(); - }); + fileExists.then((stats) => { + res.stats = stats; + next(); + }).catch((err) => { + res.stats = { error: err }; + next(); + }); }); // currently unused app.put("/*", (req, res) => { - if (res.stats.error) { - req.busboy.on("file", (key, file, filename) => { - if (key == "file") { - let save = fs.createWriteStream(relative(res.filename)); - file.pipe(save); - save.on("close", () => { - res.flash("success", "File saved. "); - res.redirect("back"); - }); - save.on("error", (err) => { - res.flash("error", err.toString()); - res.redirect("back"); - }); - } - }); - req.busboy.on("field", (key, value) => { + if (res.stats.error) { + req.busboy.on("file", (key, file, filename) => { + if (key == "file") { + let save = fs.createWriteStream(relative(res.filename)); + file.pipe(save); + save.on("close", () => { + res.flash("success", "File saved. "); + res.redirect("back"); + }); + save.on("error", (err) => { + res.flash("error", err.toString()); + res.redirect("back"); + }); + } + }); + req.busboy.on("field", (key, value) => { - }); - req.pipe(req.busboy); - } - else { - req.flash("error", "File exists, cannot overwrite. "); - res.redirect("back"); - } + }); + req.pipe(req.busboy); + } + else { + req.flash("error", "File exists, cannot overwrite. "); + res.redirect("back"); + } }); app.post("/*@upload", (req, res) => { - res.filename = req.params[0]; + res.filename = req.params[0]; - let buff = null; - let saveas = null; - req.busboy.on("file", (key, stream, filename) => { - if (key == "file") { - let buffs = []; - stream.on("data", (d) => { - buffs.push(d); - }); - stream.on("end", () => { - buff = Buffer.concat(buffs); - buffs = null; - }); - } - }); - req.busboy.on("field", (key, value) => { - if (key == "saveas") { - saveas = value; - } - }); - req.busboy.on("finish", () => { - if (!buff || !saveas) { - return res.status(400).end(); - } - let fileExists = new Promise((resolve, reject) => { - // check if file exists - fs.stat(relative(res.filename, saveas), (err, stats) => { - if (err) { - return reject(err); - } - return resolve(stats); - }); - }); + let buff = null; + let saveas = null; + req.busboy.on("file", (key, stream, filename) => { + if (key == "file") { + let buffs = []; + stream.on("data", (d) => { + buffs.push(d); + }); + stream.on("end", () => { + buff = Buffer.concat(buffs); + buffs = null; + }); + } + }); + req.busboy.on("field", (key, value) => { + if (key == "saveas") { + saveas = value; + } + }); + req.busboy.on("finish", () => { + if (!buff || !saveas) { + return res.status(400).end(); + } + let fileExists = new Promise((resolve, reject) => { + // check if file exists + fs.stat(relative(res.filename, saveas), (err, stats) => { + if (err) { + return reject(err); + } + return resolve(stats); + }); + }); - fileExists.then((stats) => { - req.flash("error", "File exists, cannot overwrite. "); - res.redirect("back"); - }).catch((err) => { + fileExists.then((stats) => { + req.flash("error", "File exists, cannot overwrite. "); + res.redirect("back"); + }).catch((err) => { const saveName = relative(res.filename, saveas); - console.log("saving file to " + saveName); - let save = fs.createWriteStream(saveName); - save.on("close", () => { + console.log("saving file to " + saveName); + let save = fs.createWriteStream(saveName); + save.on("close", () => { if (res.headersSent) { return; } - if (buff.length === 0) { - req.flash("success", "File saved. Warning: empty file."); - } - else { - buff = null; - req.flash("success", "File saved. "); - } - res.redirect("back"); - }); - save.on("error", (err) => { - req.flash("error", err.toString()); - res.redirect("back"); - }); - save.write(buff); - save.end(); - }); - }); - req.pipe(req.busboy); + if (buff.length === 0) { + req.flash("success", "File saved. Warning: empty file."); + } + else { + buff = null; + req.flash("success", "File saved. "); + } + res.redirect("back"); + }); + save.on("error", (err) => { + req.flash("error", err.toString()); + res.redirect("back"); + }); + save.write(buff); + save.end(); + }); + }); + req.pipe(req.busboy); }); app.post("/*@mkdir", (req, res) => { - res.filename = req.params[0]; + res.filename = req.params[0]; - let folder = req.body.folder; - if (!folder || folder.length < 1) { - return res.status(400).end(); - } + let folder = req.body.folder; + if (!folder || folder.length < 1) { + return res.status(400).end(); + } - let fileExists = new Promise((resolve, reject) => { - // Check if file exists - fs.stat(relative(res.filename, folder), (err, stats) => { - if (err) { - return reject(err); - } - return resolve(stats); - }); - }); + let fileExists = new Promise((resolve, reject) => { + // Check if file exists + fs.stat(relative(res.filename, folder), (err, stats) => { + if (err) { + return reject(err); + } + return resolve(stats); + }); + }); - fileExists.then((stats) => { - req.flash("error", "Folder exists, cannot overwrite. "); - res.redirect("back"); - }).catch((err) => { - fs.mkdir(relative(res.filename, folder), (err) => { - if (err) { - req.flash("error", err.toString()); - res.redirect("back"); - return; - } - req.flash("success", "Folder created. "); - res.redirect("back"); - }); - }); + fileExists.then((stats) => { + req.flash("error", "Folder exists, cannot overwrite. "); + res.redirect("back"); + }).catch((err) => { + fs.mkdir(relative(res.filename, folder), (err) => { + if (err) { + req.flash("error", err.toString()); + res.redirect("back"); + return; + } + req.flash("success", "Folder created. "); + res.redirect("back"); + }); + }); }); app.post("/*@delete", (req, res) => { - res.filename = req.params[0]; + res.filename = req.params[0]; - let files = JSON.parse(req.body.files); - if (!files || !files.map) { - req.flash("error", "No files selected."); - res.redirect("back"); - return; // res.status(400).end(); - } + let files = JSON.parse(req.body.files); + if (!files || !files.map) { + req.flash("error", "No files selected."); + res.redirect("back"); + return; // res.status(400).end(); + } - let promises = files.map(f => { - return new Promise((resolve, reject) => { - fs.stat(relative(res.filename, f), (err, stats) => { - if (err) { - return reject(err); - } - resolve({ - name: f, - isdirectory: stats.isDirectory(), - isfile: stats.isFile() - }); - }); - }); - }); - Promise.all(promises).then((files) => { - let promises = files.map(f => { - return new Promise((resolve, reject) => { - let op = null; - if (f.isdirectory) { - op = (dir, cb) => rimraf(dir, { + let promises = files.map(f => { + return new Promise((resolve, reject) => { + fs.stat(relative(res.filename, f), (err, stats) => { + if (err) { + return reject(err); + } + resolve({ + name: f, + isdirectory: stats.isDirectory(), + isfile: stats.isFile() + }); + }); + }); + }); + Promise.all(promises).then((files) => { + let promises = files.map(f => { + return new Promise((resolve, reject) => { + let op = null; + if (f.isdirectory) { + op = (dir, cb) => rimraf(dir, { glob: false }, cb); - } - else if (f.isfile) { - op = fs.unlink; - } - if (op) { - op(relative(res.filename, f.name), (err) => { - if (err) { - return reject(err); - } - resolve(); - }); - } - }); - }); - Promise.all(promises).then(() => { - req.flash("success", "Files deleted. "); - res.redirect("back"); - }).catch((err) => { - req.flash("error", "Unable to delete some files: " + err); - res.redirect("back"); - }); - }).catch((err) => { - req.flash("error", err.toString()); - res.redirect("back"); - }); + } + else if (f.isfile) { + op = fs.unlink; + } + if (op) { + op(relative(res.filename, f.name), (err) => { + if (err) { + return reject(err); + } + resolve(); + }); + } + }); + }); + Promise.all(promises).then(() => { + req.flash("success", "Files deleted. "); + res.redirect("back"); + }).catch((err) => { + req.flash("error", "Unable to delete some files: " + err); + res.redirect("back"); + }); + }).catch((err) => { + req.flash("error", err.toString()); + res.redirect("back"); + }); }); app.get("/*@download", (req, res) => { - res.filename = req.params[0]; + res.filename = req.params[0]; - let files = null; - try { - files = JSON.parse(req.query.files); - } catch (e) {} - if (!files || !files.map) { - req.flash("error", "No files selected."); - res.redirect("back"); - return; // res.status(400).end(); - } + let files = null; + try { + files = JSON.parse(req.query.files); + } catch (e) {} + if (!files || !files.map) { + req.flash("error", "No files selected."); + res.redirect("back"); + return; // res.status(400).end(); + } - let promises = files.map(f => { - return new Promise((resolve, reject) => { - fs.stat(relative(res.filename, f), (err, stats) => { - if (err) { - return reject(err); - } - resolve({ - name: f, - isdirectory: stats.isDirectory(), - isfile: stats.isFile() - }); - }); - }); - }); - Promise.all(promises).then((files) => { - let zip = archiver("zip", {}); - zip.on("error", function(err) { - res.status(500).send({ - error: err.message - }); - }); + let promises = files.map(f => { + return new Promise((resolve, reject) => { + fs.stat(relative(res.filename, f), (err, stats) => { + if (err) { + return reject(err); + } + resolve({ + name: f, + isdirectory: stats.isDirectory(), + isfile: stats.isFile() + }); + }); + }); + }); + Promise.all(promises).then((files) => { + let zip = archiver("zip", {}); + zip.on("error", function(err) { + res.status(500).send({ + error: err.message + }); + }); - files.filter(f => f.isfile).forEach((f) => { - zip.file(relative(res.filename, f.name), { name: f.name }); - }); - files.filter(f => f.isdirectory).forEach((f) => { - zip.directory(relative(res.filename, f.name), f.name); - }); + files.filter(f => f.isfile).forEach((f) => { + zip.file(relative(res.filename, f.name), { name: f.name }); + }); + files.filter(f => f.isdirectory).forEach((f) => { + zip.directory(relative(res.filename, f.name), f.name); + }); - res.attachment("Archive.zip"); - zip.pipe(res); + res.attachment("Archive.zip"); + zip.pipe(res); - zip.finalize(); - }).catch((err) => { - console.log(err); - req.flash("error", err.toString()); - res.redirect("back"); - }); + zip.finalize(); + }).catch((err) => { + console.log(err); + req.flash("error", err.toString()); + res.redirect("back"); + }); }); const shellable = process.env.SHELL != "false" && process.env.SHELL; const cmdable = process.env.CMD != "false" && process.env.CMD; if (shellable || cmdable) { - const shellArgs = process.env.SHELL.split(" "); - const exec = process.env.SHELL == "login" ? "/usr/bin/env" : shellArgs[0]; - const args = process.env.SHELL == "login" ? ["login"] : shellArgs.slice(1); + const shellArgs = process.env.SHELL.split(" "); + const exec = process.env.SHELL == "login" ? "/usr/bin/env" : shellArgs[0]; + const args = process.env.SHELL == "login" ? ["login"] : shellArgs.slice(1); - const child_process = require("child_process"); + const child_process = require("child_process"); - app.post("/*@cmd", (req, res) => { - res.filename = req.params[0]; + app.post("/*@cmd", (req, res) => { + res.filename = req.params[0]; - let cmd = req.body.cmd; - if (!cmd || cmd.length < 1) { - return res.status(400).end(); - } + let cmd = req.body.cmd; + if (!cmd || cmd.length < 1) { + return res.status(400).end(); + } - child_process.exec(cmd, { - cwd: relative(res.filename), - timeout: 60 * 1000, - }, (err, stdout, stderr) => { - if (err) { - req.flash("error", "Command failed due to non-zero exit code"); - } - res.render("cmd", flashify(req, { - path: res.filename, - cmd: cmd, - stdout: stdout, - stderr: stderr, - })); - }); - }); + child_process.exec(cmd, { + cwd: relative(res.filename), + timeout: 60 * 1000, + }, (err, stdout, stderr) => { + if (err) { + req.flash("error", "Command failed due to non-zero exit code"); + } + res.render("cmd", flashify(req, { + path: res.filename, + cmd: cmd, + stdout: stdout, + stderr: stderr, + })); + }); + }); - const pty = require("node-pty"); - const WebSocket = require("ws"); + const pty = require("node-pty"); + const WebSocket = require("ws"); - app.get("/*@shell", (req, res) => { - res.filename = req.params[0]; + app.get("/*@shell", (req, res) => { + res.filename = req.params[0]; - res.render("shell", flashify(req, { - path: res.filename, - })); - }); + res.render("shell", flashify(req, { + path: res.filename, + })); + }); - const ws = new WebSocket.Server({ server: http }); + const ws = new WebSocket.Server({ server: http }); ws.on("connection", (socket, request) => { console.log(request.url); const { path } = querystring.parse(request.url.split("?")[1]); - let cwd = relative(path); - let term = pty.spawn(exec, args, { - name: "xterm-256color", - cols: 80, - rows: 30, - cwd: cwd, - }); - console.log("pid " + term.pid + " shell " + process.env.SHELL + " started in " + cwd); + let cwd = relative(path); + let term = pty.spawn(exec, args, { + name: "xterm-256color", + cols: 80, + rows: 30, + cwd: cwd, + }); + console.log("pid " + term.pid + " shell " + process.env.SHELL + " started in " + cwd); - term.on("data", (data) => { - socket.send(data, { binary: true }); - }); - term.on("exit", (code) => { - console.log("pid " + term.pid + " ended") - socket.close(); - }); - socket.on("message", (data) => { + term.on("data", (data) => { + socket.send(data, { binary: true }); + }); + term.on("exit", (code) => { + console.log("pid " + term.pid + " ended") + socket.close(); + }); + socket.on("message", (data) => { // special messages should decode to Buffers if (Buffer.isBuffer(data)) { switch (data.readUInt16BE(0)) { @@ -482,12 +482,12 @@ if (shellable || cmdable) { return; } } - term.write(data); - }); - socket.on("close", () => { - term.end(); - }); - }); + term.write(data); + }); + socket.on("close", () => { + term.end(); + }); + }); } const EXT_IMAGES = [".jpg", ".jpeg", ".png", ".webp", ".svg", ".gif", ".tiff"]; @@ -501,82 +501,83 @@ function isimage(f) { } app.get("/*", (req, res) => { - if (res.stats.error) { - res.render("list", flashify(req, { - shellable: shellable, - cmdable: cmdable, - path: res.filename, - errors: [ - res.stats.error - ] - })); - } - else if (res.stats.isDirectory()) { - if (!req.url.endsWith("/")) { - return res.redirect(req.url + "/"); - } + if (res.stats.error) { + res.render("list", flashify(req, { + shellable: shellable, + cmdable: cmdable, + path: res.filename, + errors: [ + res.stats.error + ] + })); + } + else if (res.stats.isDirectory()) { + if (!req.url.endsWith("/")) { + return res.redirect(req.url + "/"); + } - let readDir = new Promise((resolve, reject) => { - fs.readdir(relative(res.filename), (err, filenames) => { - if (err) { - return reject(err); - } - return resolve(filenames); - }); - }); + let readDir = new Promise((resolve, reject) => { + fs.readdir(relative(res.filename), (err, filenames) => { + if (err) { + return reject(err); + } + return resolve(filenames); + }); + }); - readDir.then((filenames) => { - let promises = filenames.map(f => { - return new Promise((resolve, reject) => { - fs.stat(relative(res.filename, f), (err, stats) => { - if (err) { - return reject(err); - } - resolve({ - name: f, - isdirectory: stats.isDirectory(), - issmallimage: isimage(f) && stats.size < 1024 * 10, - size: stats.size - }); - }); - }); - }); + readDir.then((filenames) => { + const promises = filenames.map(f => new Promise((resolve, reject) => { + fs.stat(relative(res.filename, f), (err, stats) => { + if (err) { + return resolve({ + name: f, + error: err + }); + } + resolve({ + name: f, + isdirectory: stats.isDirectory(), + issmallimage: isimage(f) && stats.size < 1024 * 10, + size: stats.size + }); + }); + })); - Promise.all(promises).then((files) => { - res.render("list", flashify(req, { - shellable: shellable, - cmdable: cmdable, - path: res.filename, - files: files, - })); - }).catch((err) => { - res.render("list", flashify(req, { - shellable: shellable, - cmdable: cmdable, - path: res.filename, - errors: [ - err - ] - })); - }); - }).catch((err) => { - res.render("list", flashify(req, { - shellable: shellable, - cmdable: cmdable, - path: res.filename, - errors: [ - err - ] - })); - }); - } - else if (res.stats.isFile()) { - res.sendFile(relative(res.filename), { + Promise.all(promises).then((files) => { + res.render("list", flashify(req, { + shellable: shellable, + cmdable: cmdable, + path: res.filename, + files: files, + })); + }).catch((err) => { + res.render("list", flashify(req, { + shellable: shellable, + cmdable: cmdable, + path: res.filename, + errors: [ + err + ] + })); + }); + }).catch((err) => { + res.render("list", flashify(req, { + shellable: shellable, + cmdable: cmdable, + path: res.filename, + errors: [ + err + ] + })); + }); + } + else if (res.stats.isFile()) { + res.sendFile(relative(res.filename), { headers: { "Content-Security-Policy": "default-src 'self'; script-src 'none'; sandbox" }, dotfiles: "allow" }); - } + } }); diff --git a/views/list.handlebars b/views/list.handlebars index de05243..72cce2c 100644 --- a/views/list.handlebars +++ b/views/list.handlebars @@ -23,10 +23,14 @@ {{#if isdirectory}} {{name}}/ + {{else}} + {{#if error}} + {{name}}/ {{else}} {{name}} {{filesize size}} {{/if}} + {{/if}} {{#if issmallimage}}