diff --git a/.env.example b/.env.example index 0407444..270b5d3 100644 --- a/.env.example +++ b/.env.example @@ -5,4 +5,7 @@ MAXLEN=5120 MAXFILES=128 SHOW_SUBMITTED=true -ADMIN_EMAIL=john.doe@example.com \ No newline at end of file +ADMIN_EMAIL=john.doe@example.com + +SESSION_SECRET= +SESSION_MEMCACHE_HOST=memcache:11211 diff --git a/.gitignore b/.gitignore index f0f9da2..c62fdc5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ node_modules .env package-lock.json +yarn.lock # code !*.js diff --git a/docker-compose.yml b/docker-compose.yml index 8e34039..7160529 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,5 +10,17 @@ services: volumes: - './usercontent:/opt/code/usercontent' # uncomment this for debug mode - #- './:/opt/code' - restart: always \ No newline at end of file + - './:/opt/code' + restart: always + networks: + - bin + memcache: + image: memcached + restart: always + networks: + - bin + +networks: + bin: + driver: bridge + diff --git a/gen_key.sh b/gen_key.sh new file mode 100755 index 0000000..d66c144 --- /dev/null +++ b/gen_key.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +errcho() { + echo $* >&2 +} + +if [[ $1 != '-y' ]]; then + errcho -e "\033[31mERROR: \033[0mThis will overwrite your current key." + errcho -e "\033[31mERROR: \033[0mRun it again with -y as first argument to confirm." + exit 1 +fi + +KEY=$(cat /dev/urandom | tr -dc '[:alpha:]' | fold -w 32 | head -n 1) + +sed -i "s/^SESSION_SECRET=[a-zA-Z0-9]*$/SESSION_SECRET=$KEY/g" .env + +echo Your key is $KEY \ No newline at end of file diff --git a/middleware/index.js b/middleware/index.js index c0b37d1..73b8b65 100644 --- a/middleware/index.js +++ b/middleware/index.js @@ -5,4 +5,22 @@ const bodyparse = require('body-parser'); router.use(bodyparse.json()); router.use(bodyparse.urlencoded({extended: true})); +const session = require('express-session'); +const memcache = require("connect-memcached")(session); +const crypto = require('crypto'); + +router.use( + session({ + secret: process.env.SESSION_SECRET, + secure: true, + resave: false, + saveUninitialized: true, + store: new memcache({ + hosts: [process.env.SESSION_MEMCACHE_HOST], + secret: process.env.SESSION_SECRET + + crypto.createHash('sha256', process.env.SESSION_SECRET).digest().toString('hex') + }) + }) +); + module.exports = router; \ No newline at end of file diff --git a/package.json b/package.json index e5c4041..33b467d 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,11 @@ "license": "MIT", "dependencies": { "body-parser": "^1.20.2", + "connect-memcached": "^2.0.0", "dotenv": "^16.0.3", "express": "^4.18.2", "express-async-handler": "^1.2.0", + "express-session": "^1.17.3", "fancy-log": "^2.0.0", "glob": "^9.2.1", "pug": "^3.0.2" diff --git a/public/static/main.css b/public/static/main.css index 28d53ca..039fade 100644 --- a/public/static/main.css +++ b/public/static/main.css @@ -46,4 +46,19 @@ input[type=submit] { } input[type=submit]:hover { box-shadow: 0 2px 4px #60806040; +} + +div.captcha_box { + margin:0 auto; + width:100px; + text-align:center; + background-color: white; + padding: 20px; + border: 1px solid gray; +} +p.c_s { + margin:0; + padding: 0; + font-size:11pt; + font-family: sans-serif } \ No newline at end of file diff --git a/routes/main.js b/routes/main.js index e0dd3c7..92767a5 100644 --- a/routes/main.js +++ b/routes/main.js @@ -3,11 +3,25 @@ const router = express.Router(); const handler = require('express-async-handler'); const content = require('../helpers/content'); +const crypto = require('crypto'); + async function index(req, res) { + + if (!req.session.captcha) { + req.session.captcha = crypto.randomBytes(8).toString('base64').substring(0, 6); + } + + req.session.captcha_input = crypto.randomBytes(8).toString('base64').substring(0,10); + if (!req.session.csrf) + req.session.csrf = crypto.randomBytes(10).toString('base64'); + res.render('main', { maxlen: process.env.MAXLEN, - submitted: content.submitted() + submitted: content.submitted(), + req, + crypto }); + } router.get('/', handler(index)); diff --git a/routes/upload.js b/routes/upload.js index b920e84..62bb863 100644 --- a/routes/upload.js +++ b/routes/upload.js @@ -2,11 +2,30 @@ const express = require('express'); const router = express.Router(); const handler = require('express-async-handler'); const content = require('../helpers/content'); +const crypto = require('crypto'); const { MAXFILES } = process.env; async function upload(req, res) { + if (req.body['_csrf'] != req.session.csrf) { + res.status(405).send('CSRF error'); + return; + } + + if (!req.body[req.session.captcha_input]) { + res.status(405).send('Captcha error; please go back and refresh the page a few times.'); + return; + } + + if (req.body[req.session.captcha_input] != req.session.captcha) { + res.status(405).send('Bad captcha'); + return; + } + + req.session.captcha = crypto.randomBytes(8).toString('base64').substring(0,6); + + if (content.submitted() >= MAXFILES) { res.status(405).send('Not allowed'); return; diff --git a/views/captcha.pug b/views/captcha.pug new file mode 100644 index 0000000..4ff860d --- /dev/null +++ b/views/captcha.pug @@ -0,0 +1,46 @@ +mixin captcha(text) + div(class='captcha_box') + - + const pyRange = (start, stop, step) => + Array.from( + { length: (stop - start) / step + 1 }, + (value, index) => start + index * step + ); + var captcha = text + var shuffled_indexes = pyRange(0, captcha.length - 1, 1); + shuffled_indexes.sort(() => crypto.randomInt(0,10) > 5 ? 1 : -1); + var current_index = -1; + const rint = (m, mx) => crypto.randomInt(m, mx); + + const blowfish = () => { + const rules = [ + 'font-weight:normal', + 'font-family:inherit', + 'font-weight:bold', + 'font-weight:normal', + 'display:block', + 'display:inline-block', + 'display:flex', + 'display:none', + 'display:none' + ]; + const n = rint(8, 16); + let out = ''; + + for (let i = 0; i < n; i++) { + out += rules[rint(0,rules.length - 1)] + ';'.repeat(rint(1,6)) + } + return out; + } + + + + each index in shuffled_indexes + - + current_index++ + var left_margin = 0; + + + p(class='c_s' style=`transform:translate(${((index - current_index) * (rint(80,82) / 10))}px, ${rint(-3,3)}px);${blowfish()};font-weight:${rint(0,2) == 1 ? 'bold' : 'normal'};color:#220000;display:inline-block`)= captcha[index] + each i in pyRange(0, rint(4,10), 1) + p(class='c_s' style=`transform:translate(${((index - current_index) * (rint(80,82) / 10))}px, ${rint(-3,3)}px);${blowfish()};font-weight:bold;color:#220000;display:none`)= a diff --git a/views/main.pug b/views/main.pug index b1b4cdc..0d80d88 100644 --- a/views/main.pug +++ b/views/main.pug @@ -1,13 +1,25 @@ extends template/main.pug +block root + include captcha.pug + block content - var exceeded = submitted >= process.env.MAXFILES form(action='/upload' method='POST') + input(type='hidden' name='_csrf' value=req.session.csrf) p(align='center') textarea(name='text' class='data' placeholder='Put your text in here!' + (maxlen ? ` (Max length is ${maxlen} bytes)` : '')) br if (!exceeded) + br + | Captcha: + br + input(type='text' name=req.session.captcha_input) + if (!exceeded) + +captcha(req.session.captcha) + p(align='center' style='padding-bottom:10px') input(type='submit' value='Upload!') + if (exceeded) p(style='color:darkred;font-weight:bold;font-size:9pt' align='center') | Max uploads limit exceeded. No more uploads would be accepted.