#!/usr/bin/env python import re import secrets from pathlib import Path import os import hashlib from uuid import uuid4 from quart import Quart, url_for, redirect, session, make_response, render_template, request, send_file, abort, flash, g from quart_db import QuartDB import datetime app = Quart(__name__) app.config.from_envvar('APP_CONFIG', silent=True) if app.config['SECRET_KEY'] is None: if (p := Path('/tmp/eightserve-secret')).is_file(): app.config['SECRET_KEY'] = p.read_bytes() else: app.config['SECRET_KEY'] = os.urandom(32) try: p.touch(0o600) p.write_bytes(app.config['SECRET_KEY']) except: pass app.config.update( SESSION_COOKIE_SECURE=True, SESSION_COOKIE_HTTPONLY=True, SESSION_COOKIE_SAMESITE='Lax', # SERVER_NAME='8seg.jaseg.de', ) DISPLAY_COLUMNS = app.config.get('DISPLAY_COLUMNS', 32) MESSAGE_MAX_LINES = app.config.get('MESSAGE_MAX_LINES', 10) db = QuartDB(app, url=app.config.get('DATABASE_URL', 'sqlite:///db.sqlite3'), ) @app.before_request async def ensure_session_id(): if 'session_id' not in session: session['session_id'] = str(uuid4()) @app.route('/', methods=['GET', 'POST']) async def index(): if session.get('consent'): return redirect(url_for('post')) elif (await request.form).get('csrf_token') == session.get('csrf_token', -1): session['consent'] = True return redirect(url_for('post')) else: tok = session['csrf_token'] = secrets.token_urlsafe() return await render_template('index.html', csrf_token=tok) @app.route('/post', methods=['GET', 'POST']) async def post(): if not session.get('consent'): return redirect(url_for('index')) result = await g.connection.fetch_one( 'SELECT COUNT(*) AS count FROM messages WHERE timestamp_displayed IS NULL AND suppress_display = 0') digest = hashlib.sha3_256(b'eightserve_queue_length_random') digest.update(request.remote_addr.encode()) digest.update(app.config['SECRET_KEY']) digest.update(str(result['count']).encode()) digest.update(datetime.datetime.now().strftime('%y%m%d%H%M%S').encode()) digest = digest.digest() count = round(max(5, (result['count'] * (1 + (digest[23]/255 - 0.5) * 0.1)) + (digest[42//2]%16 - 8))/5) * 5 if request.method == 'POST': data = await request.form message = data.get('message', '') sub = re.sub("[^a-zA-Z0-9\',._:<(\[\])>!?^]", "", message).strip() if data.get('csrf_token') != session.get('csrf_token', -1): await flash('Incorrect CSRF token.') elif not re.sub('[^a-zA-Z0-9\',._:<(\[\])>!?^]', '', message).strip(): await flash('Message does not contain any displayable characters.') elif any(len(line.strip()) > DISPLAY_COLUMNS*2 for line in message.splitlines()): await flash('Message has one or more lines that are too long.') elif len(message.splitlines()) > MESSAGE_MAX_LINES: await flash('Message has too many lines.') else: result = await g.connection.fetch_one( 'SELECT COUNT(*) AS count FROM shitlist WHERE remote_ip = :ip', {'ip': request.remote_addr}) on_shitlist = result['count'] > 0 result = await g.connection.fetch_one( 'SELECT COUNT(*) AS count FROM suslist WHERE remote_ip = :ip AND timestamp_added > datetime("now", :timeout)', {'ip': request.remote_addr, 'timeout': '-' + app.config.get('SUSLIST_TIMEOUT', '01:00')}) on_suslist = result['count'] > app.config.get('SUSLIST_THRESHOLD', 5) badwords = {row['word'] for row in await g.connection.fetch_all('SELECT word FROM badwords')} words = [re.sub(r'[^a-zA-Z]', '', word).lower().strip() for word in message.split()] has_badwords = any(word in badwords for word in words if word) if on_shitlist: suppress = True moderated = False elif on_suslist or has_badwords: moderated = True suppress = True await g.connection.execute('INSERT INTO suslist (remote_ip, reason) VALUES (:ip, :reason)', {'ip': request.remote_addr, 'reason': 'suslist' if on_suslist else 'badwords'}) else: moderated = False suppress = False flags = [] if on_suslist: flags.append('suslist') if on_shitlist: flags.append('shitlist') if moderated: flags.append('moderated') if suppress: flags.append('suppress') flags = f' ({", ".join(flags)})' if flags else '' print(f'Message submitted to queue(n={result["count"]}){flags}: {message!r}') await g.connection.execute( '''INSERT INTO messages (message, suppress_display, moderated, remote_ip) VALUES (:message, :suppress_display, :moderated, :remote_ip)''', dict( message=message, suppress_display=suppress, moderated=moderated, remote_ip=request.remote_addr)) await flash('Message submitted.') message = '' tok = session['csrf_token'] = secrets.token_urlsafe() return await render_template('post.html', cols=DISPLAY_COLUMNS, rows=MESSAGE_MAX_LINES, message=message, count=count, csrf_token=tok) else: tok = session['csrf_token'] = secrets.token_urlsafe() return await render_template('post.html', cols=DISPLAY_COLUMNS, rows=MESSAGE_MAX_LINES, message='', count=count, csrf_token=tok) if __name__ == '__main__': app.run(debug=True)