Bläddra i källkod

Url shortener, file upload

isundil 2 år sedan
förälder
incheckning
ac05e4f5f9
6 ändrade filer med 154 tillägg och 18 borttagningar
  1. 75 15
      router/input.js
  2. 10 0
      src/databaseHelper.js
  3. 21 2
      src/routerUtils.js
  4. 27 0
      templates/files.js
  5. 1 1
      templates/pastit.js
  6. 20 0
      templates/short.js

+ 75 - 15
router/input.js

@@ -6,15 +6,27 @@ const PasteContent = require('../models/pasteContent.js').PasteContent;
 const mCrypto = require('../src/crypto.js');
 const Security = require('../src/security.js');
 
-async function renderRawPage(app, res, entity) {
+async function renderRawPage(app, req, res, entity) {
     if (entity.type === 'paste')
         return await app.routerUtils.staticServe(res, app.getData(entity.privId));
+    else if (entity.type === 'file') {
+        let data = JSON.parse(entity.data);
+        return await app.routerUtils.staticDownload(res, app.getData(entity.privId), data.name, data.type);
+    }
+    else if (entity.type === 'short')
+        return res.end(entity.data); // FIXME stats + ?rel
     app.routerUtils.onInternalError(res, "Unknown type: " +entity.type);
 }
 
-async function renderPublicPage(app, res, entity) {
+async function renderPublicPage(app, req, res, entity) {
     if (entity.type === 'paste')
         return await app.routerUtils.staticServe(res, app.getData(entity.privId));
+    else if (entity.type === 'file') {
+        let data = JSON.parse(entity.data);
+        return await app.routerUtils.staticDownload(res, app.getData(entity.privId), data.name, data.type);
+    }
+    else if (entity.type === 'short')
+        return res.end(entity.data); // FIXME stats + ?rel
     app.routerUtils.onInternalError(res, "Unknown type: " +entity.type);
 }
 
@@ -25,9 +37,28 @@ function renderPrivatePage(app, res, entity) {
 }
 
 module.exports = { register: app => {
+    // Root
     app.router.get("/", (req, res) => {
         app.routerUtils.redirect(res, '/pastit');
     });
+
+    // Access page
+    app.router.get("/x/:id", async (req, res) => {
+        let entity = await app.databaseHelper.findOne(PasteContent, { privId: req.params.id, publicId: req.params.id }, " or ");
+        if (entity && entity.privId === req.params.id)
+            return renderPrivatePage(app, res, entity);
+        if (entity && !entity.expired)
+            return renderPublicPage(app, req, res, entity);
+        app.routerUtils.onPageNotFound(res);
+    });
+    app.router.get("/x/raw/:id", async (req, res) => {
+        let entity = await app.databaseHelper.findOne(PasteContent, { privId: req.params.id, publicId: req.params.id }, " or ");
+        if (entity && !entity.expired)
+            return renderRawPage(app, req, res, entity);
+        app.routerUtils.onPageNotFound(res);
+    });
+
+    // pastebin tool
     app.router.get("/pastit", (req, res) => {
         let context = app.routerUtils.commonRenderInfos();
         context.page_title += " - Pastit";
@@ -37,7 +68,7 @@ module.exports = { register: app => {
         const content = req.body.content;
         const privId = mCrypto.string(content);
         const captchaOk = await Security.captchaCheck(req.body['g-recaptcha-response'], req.headers['x-forwarded-for'] || req.socket.remoteAddress);
-        let  entity = await app.databaseHelper.findOne(PasteContent, { privId: privId });
+        let entity = await app.databaseHelper.findOne(PasteContent, { privId: privId });
 
         if (!captchaOk)
             return app.routerUtils.jsonResponse(res, { err: "Invalid captcha input", id: null });
@@ -55,19 +86,48 @@ module.exports = { register: app => {
         }
         app.routerUtils.jsonResponse(res, { err: null, id: entity.publicId });
     });
-    app.router.get("/pastit/:id", async (req, res) => {
-        let entity = await app.databaseHelper.findOne(PasteContent, { privId: req.params.id, publicId: req.params.id }, " or ");
-        if (entity && entity.privId === req.params.id)
-            return renderPrivatePage(app, res, entity);
-        if (entity && !entity.expired)
-            return renderPublicPage(app, res, entity);
-        app.routerUtils.onPageNotFound(res);
+
+    // URL shortener tool
+    app.router.get("/short", (req, res) => {
+        let context = app.routerUtils.commonRenderInfos();
+        context.page_title += " - Shortener";
+        res.end(whiskers.render(require('../templates/short.js'), context));
     });
-    app.router.get("/pastit/raw/:id", async (req, res) => {
-        let entity = await app.databaseHelper.findOne(PasteContent, { privId: req.params.id, publicId: req.params.id }, " or ");
-        if (entity && !entity.expired)
-            return renderRawPage(app, res, entity);
-        app.routerUtils.onPageNotFound(res);
+    app.router.post("/short", async (req, res) => {
+        const link = req.body.content;
+        const privId = mCrypto.string(await app.databaseHelper.count(PasteContent) + link);
+        const captchaOk = await Security.captchaCheck(req.body['g-recaptcha-response'], req.headers['x-forwarded-for'] || req.socket.remoteAddress);
+
+        if (!captchaOk)
+            return app.routerUtils.jsonResponse(res, { err: "Invalid captcha input", id: null });
+        if (!link || !link.length)
+            return app.routerUtils.jsonResponse(res, { err: "Empty input", id: null });
+        entity = new PasteContent(privId, "short");
+        entity.data = link;
+        await app.databaseHelper.insertOne(entity);
+        app.routerUtils.jsonResponse(res, { err: null, id: entity.privId });
+    });
+
+    // Files tool
+    app.router.get("/files", (req, res) => {
+        let context = app.routerUtils.commonRenderInfos();
+        context.page_title += " - File host";
+        res.end(whiskers.render(require('../templates/files.js'), context));
+    });
+    app.router.post("/files", async (req, res) => {
+        const formData = (req.body["multipart-data"] || []).reduce((res, x) => { res[x.fieldName] = x; return res; }, {});
+        const privId = mCrypto.string(await app.databaseHelper.count(PasteContent) + (req.content?.fileName || ""));
+        const captchaOk = await Security.captchaCheck(formData["g-recaptcha-response"]?.fileData, req.headers['x-forwarded-for'] || req.socket.remoteAddress);
+
+        if (!captchaOk)
+            return app.routerUtils.jsonResponse(res, { err: "Invalid captcha input", id: null });
+        if (!formData.content?.fileData || !formData.content.fileData.length)
+            return app.routerUtils.jsonResponse(res, { err: "Empty input", id: null });
+        const entity = new PasteContent(privId, "file");
+        entity.data = JSON.stringify({ name: formData.content.fileName, type: formData.content.fileType });
+        fs.writeFileSync(app.getData(privId), formData.content.fileData, {encoding: formData.content.fileType.indexOf('text') >= 0 ? 'utf8' : 'binary'});
+        await app.databaseHelper.insertOne(entity);
+        app.routerUtils.jsonResponse(res, { err: null, id: entity.privId });
     });
 }};
 

+ 10 - 0
src/databaseHelper.js

@@ -64,6 +64,16 @@ DatabaseHelper.prototype.insertOne = async function(object) {
     await this.runSql(request, args);
 }
 
+DatabaseHelper.prototype.count = async function(objectPrototype, where) {
+    let whereArgs = this.buildWhere(where);
+    let query = "select count(*) as c from `" +objectPrototype.prototype.getTableName.call(null) +"` where " +whereArgs.columns.join(" and ");
+    let result = await this.runSql(query, whereArgs.args);
+    let resultArr = [];
+    for (let i of result)
+        return i.c;
+    return 0;
+}
+
 DatabaseHelper.prototype.upsertOne = async function(obj) {
     let columns = [];
     let columnsVal = [];

+ 21 - 2
src/routerUtils.js

@@ -40,7 +40,7 @@ RouterUtils.prototype.onPageNotFound = function(res) {
     return this.httpResponse(res, 404, "Page not found...");
 }
 
-RouterUtils.prototype.staticServe = async function(res, filePath) {
+RouterUtils.prototype.staticServe = async function(res, filePath, contentType) {
     return new Promise((ok, ko) => {
         const stream = fs.createReadStream(filePath);
         const fileSize = fs.statSync(filePath)?.size || undefined;
@@ -50,7 +50,26 @@ RouterUtils.prototype.staticServe = async function(res, filePath) {
             return;
         }
         res.writeHead(200, {
-            "Content-Type": mime.contentType(path.basename(filePath)),
+            "Content-Type": contentType || mime.contentType(path.basename(filePath)),
+            "Content-Length": fileSize
+        });
+        stream.pipe(res);
+        stream.once('end', () => ok());
+    });
+}
+
+RouterUtils.prototype.staticDownload = async function(res, filePath, fileName, contentType) {
+    return new Promise((ok, ko) => {
+        const stream = fs.createReadStream(filePath);
+        const fileSize = fs.statSync(filePath)?.size || undefined;
+        if (!stream || !fileSize) {
+            console.error("RouterUtils::staticGet", filePath);
+            this.httpResponse(res, 500, "Internal Server Error");
+            return;
+        }
+        res.writeHead(200, {
+            "Content-Type": contentType || mime.contentType(path.basename(filePath)),
+            "Content-Disposition": `attachment; filename=\"${fileName}\"`,
             "Content-Length": fileSize
         });
         stream.pipe(res);

+ 27 - 0
templates/files.js

@@ -0,0 +1,27 @@
+
+module.exports = require('./header.js') +`
+<form id="form" action="#" method="POST" enctype="multipart/form-data">
+<input type="file" id="fileInput" name="content" required />
+<div id="error" class="hidden"></div>
+<button class="g-recaptcha" data-sitekey="{reCaptcha_public}" data-callback="onSubmit" data-action="submit">Submit</button>
+</form>
+<script>
+function onSubmit(token) {
+    $.ajax({
+        type: 'POST',
+        url: '#',
+        data: new FormData(document.getElementById('form')),
+        processData: false,
+        contentType: false,
+        success: data => {
+            if (data.err) {
+                $("#error").text("Error: " +data.err).removeClass("hidden");
+            } else if (data.id) {
+                $("#error").text("").addClass("hidden");
+                document.location.href = "/x/" +data.id;
+            }
+        }
+    });
+}
+</script>
+` + require('./footer.js');

+ 1 - 1
templates/pastit.js

@@ -12,7 +12,7 @@ function onSubmit(token) {
             $("#error").text("Error: " +data.err).removeClass("hidden");
         } else if (data.id) {
             $("#error").text("").addClass("hidden");
-            document.location.href = "/pastit/" +data.id;
+            document.location.href = "/x/" +data.id;
         }
     });
 }

+ 20 - 0
templates/short.js

@@ -0,0 +1,20 @@
+
+module.exports = require('./header.js') +`
+<form id="form" action="#" method="POST">
+<input type="text" name="content" required />
+<div id="error" class="hidden"></div>
+<button class="g-recaptcha" data-sitekey="{reCaptcha_public}" data-callback="onSubmit" data-action="submit">Submit</button>
+</form>
+<script>
+function onSubmit(token) {
+    $.post(document.location.href, $("#form").serialize(), data => {
+        if (data.err) {
+            $("#error").text("Error: " +data.err).removeClass("hidden");
+        } else if (data.id) {
+            $("#error").text("").addClass("hidden");
+            document.location.href = "/x/" +data.id;
+        }
+    });
+}
+</script>
+` + require('./footer.js');