var querystring = require("querystring"); var fs = require("fs"); var pathlib = require("path"); var formidable = require("formidable"); var crypto = require("crypto"); var basepath = "/admin/api/"; var slideshow; exports.init = function(_slideshow) { slideshow = _slideshow; } var tokens = {}; function pad(str, n) { if (str.length >= n) return str; var missing = str.length; for (var i = 0; i < n - missing; ++i) str = "0" + str; return str; } // Used in every method handler to make sure the correct arguments are provided function hasargs(query, respond, expected) { var missing = []; for (var e of expected) { if (query[e] === undefined) missing.push(e); } if (missing.length > 0) { respond("Missing arguments: "+missing.join(", ")); return false; } else { return true; } } // Method handlers are defined here var methods = { // List all slides in the slides directory list_slides: function(query, conf, req, respond) { fs.readdir(conf.slides, (err, files) => { if (err) return respond(err); respond(null, files); }); }, // Get metadata about a slide slide_meta: function(query, conf, req, respond) { if (!hasargs(query, respond, [ "slide" ])) return; var path = pathlib.join(conf.slides, query.slide); fs.stat(path, (err, stat) => { if (err || !stat.isDirectory()) return respond(path+" is not a slide."); fs.readFile(pathlib.join(path, "meta.json"), (err, res) => { if (err && err.code === "ENOENT") return respond(null, {}); else if (err) return respond(err); try { respond(null, JSON.parse(res)); } catch (err) { respond(err); } }); }); }, // Get a list of files of a slide slide_file_list: function(query, conf, req, respond) { if (!hasargs(query, respond, [ "slide" ])) return; var dir = pathlib.join(conf.slides, query.slide); fs.readdir(dir, (err, files) => { if (err) return respond(err); respond(null, { files: files }); }); }, // Get a slide's HTML slide_content: function(query, conf, req, respond) { if (!hasargs(query, respond, [ "slide" ])) return; var path = pathlib.join(conf.slides, query.slide, "index.md"); fs.readFile(path, "utf-8", (err, text) => { if (err && err.code === "ENOENT") return respond(null, { text: "" }); else if (err) return respond(err); respond(null, { text: text }); }); }, // Update a slide's HTML slide_content_update: function(query, conf, req, respond) { if (!hasargs(query, respond, [ "slide", "text" ])) return; var path = pathlib.join(conf.slides, query.slide, "index.md"); fs.writeFile(path, query.text, err => { respond(err); }); }, // Rename a file slide_file_rename: function(query, conf, req, respond) { if (!hasargs(query, respond, [ "slide", "from", "to" ])) return; var op = pathlib.join(conf.slides, query.slide, query.from); var np = pathlib.join(conf.slides, query.slide, query.to); fs.rename(op, np, err => respond(err)); }, // Delete a file slide_file_delete: function(query, conf, req, respond) { if (!hasargs(query, respond, [ "slide", "file" ])) return; var path = pathlib.join(conf.slides, query.slide, query.file); fs.unlink(path, err => respond(err)); }, // Upload a file to a slide slide_file_upload: function(query, conf, req, respond) { if (!hasargs(query, respond, [ "slide" ])) return; var form = new formidable.IncomingForm(); form.uploadDir = pathlib.join(conf.slides, query.slide); form.keepExtensions = true; form.on("fileBegin", (name, file) => { file.path = pathlib.join(form.uploadDir, file.name); }); form.parse(req, (err, fields, files) => { if (err) return respond(err); }); form.on("error", err => { respond(err); }); form.on("end", () => { respond(); }); }, // Create a slide // Lots of synchronous fs stuff, we don't want races slide_create: function(query, conf, req, respond) { var dirs; try { dirs = fs.readdirSync(conf.slides); } catch (err) { return respond(err); } dirs = dirs.sort(); var biggest = dirs[dirs.length - 1]; var newId = pad((parseInt(biggest) + 1).toString(), biggest.length); var path = pathlib.join(conf.slides, newId); try { fs.mkdirSync(path); fs.writeFileSync(pathlib.join(path, "index.md"), ""); } catch (err) { return respond(err); } slideshow.updateSlides(); respond(null, newId); }, // Delete a slide // Also synchronous fs stuff slide_delete: function(query, conf, req, respond) { if (!hasargs(query, respond, [ "slide" ])) return; var path = pathlib.join(conf.slides, query.slide); var files; try { files = fs.readdirSync(path); } catch (err) { return respond(err); } for (var f of files) { try { fs.unlinkSync(pathlib.join(path, f)); } catch (err) { return respond(err); } } try { fs.rmdirSync(path); } catch (err) { return respond(err); } respond(); } } exports.canServe = function(parts) { // Temporary, while working on stuff var name = parts.pathname.replace(basepath, ""); return methods[name] !== undefined || name === "login"; } var sessTokens = []; function loginHandler(conf, req, respond) { var pass = req.headers["session-pass"]; if (!conf.password) return respond(null, false); if (!pass) return respond(null, false); if (pass !== conf.password) return respond(null, false); var token = crypto.randomBytes(16).toString("hex"); var id = sessTokens.length; sessTokens[id] = token; // Time out after 30 minutes setTimeout(() => { sessTokens[id] = undefined; }, 30 * 60 * 1000); respond(null, token); } function validateToken(req) { var cookie = req.headers.cookie; if (!cookie) return false; var token; for (var c of cookie.split(/;\s*/)) { var parts = c.split("="); if (parts[0] === "token") { token = parts[1]; break; } } if (!token) return false; for (var i = 0; i < sessTokens.length; ++i) { if (sessTokens[i] && sessTokens[i] === token) return true; } return false; } exports.serve = function(parts, conf, req, res) { var name = parts.pathname.replace(basepath, ""); // Better than manually doing res.end(JSON.stringify(obj)) everywhere function respond(err, obj) { var result = { obj: obj, err: err ? err.toString() : null }; if (err) res.writeHead(400); else res.writeHead(200); res.end(JSON.stringify(result)); } // Special login handler if (name === "login") return loginHandler(conf, req, respond); // Verify token if (!validateToken(req)) return respond("EINVALTOKEN"); var fn = methods[name]; if (!fn) { res.writeHead(404); res.end(); return; } var query = querystring.parse(parts.query); for (var i in query) { query[i] = decodeURIComponent(query[i]); } // Finally, call method handler fn(query, conf, req, respond); }