8seg-server/eightserve.py
2023-12-23 14:05:28 +01:00

153 lines
5.8 KiB
Python

#!/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)