server WIP

This commit is contained in:
jaseg 2023-12-23 14:05:28 +01:00
parent 7d00923533
commit 7622cc99ca
6 changed files with 198 additions and 10 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
__pycache__
db.sqlite3
venv

6
eightserve.cfg Normal file
View file

@ -0,0 +1,6 @@
DISPLAY_COLUMNS = 32
DATABASE_URL = "sqlite:///db.sqlite3"
SUSLIST_TIMEOUT = "01:00"
SUSLIST_THRESHOLD = 5
MESSAGE_MAX_LINES = 10

View file

@ -1,15 +1,26 @@
#!/usr/bin/env python
from quart import Quart, url_for, redirect, session, make_response, render_template, request, send_file, abort, flash
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')
app.config.from_envvar('APP_CONFIG', silent=True)
if app.config['SECRET_KEY'] is None:
if (p := Path('/run/secrets/eightserve')).is_file():
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
@ -18,7 +29,12 @@ 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
@ -29,13 +45,109 @@ async def ensure_session_id():
@app.route('/', methods=['GET', 'POST'])
async def index():
return await render_template('index.html')
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 session.get('consent', False):
return await fun(*args, **kwargs)
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:
return redirect(url_for('/'))
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)

34
migrations/0.py Normal file
View file

@ -0,0 +1,34 @@
async def migrate(conn):
await conn.execute('''CREATE TABLE IF NOT EXISTS messages (
message TEXT,
suppress_display INTEGER,
moderated INTEGER,
remote_ip TEXT,
timestamp_received TEXT DEFAULT (datetime('now')),
timestamp_displayed TEXT DEFAULT NULL
)''')
await conn.execute('''CREATE TABLE IF NOT EXISTS shitlist (
remote_ip TEXT,
reason TEXT,
timestamp_added TEXT DEFAULT (datetime('now'))
)''')
await conn.execute('''CREATE TABLE IF NOT EXISTS suslist (
remote_ip TEXT,
reason TEXT,
timestamp_added TEXT DEFAULT (datetime('now'))
)''')
await conn.execute('''CREATE TABLE IF NOT EXISTS badwords (
word TEXT,
reason TEXT,
timestamp_added TEXT DEFAULT (datetime('now'))
)''')
await conn.execute('''INSERT INTO badwords (word, reason) VALUES ("bitcoin", "cryptocurrency")''')
async def valid_migration(conn):
return True

View file

@ -1,5 +1,8 @@
<html>
<!DOCTYPE html>
<html lang="en">
<head>
<title>8seg Web Interface</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<h1>8seg Web Interface</h1>
@ -7,10 +10,14 @@
offensive messages, spam or similar. 8seg is currently open to the congress network on the hope that y'all will
behave civilly. Please don't make me put a login wall in front of this thing, alright? ;) </p>
<p> If you have any questions, or you see 8seg being used by someone for bad, please email me at
<p>Please do not post automated messages at a frequency higher than one per 20 minutes. No spam, no
commercial advertising and absolutely nothing cryptocurrency-related.</p>
<p> If you have any questions, or you see 8seg being used for something stupid or bad, please email me at
<a href="mailto:37c3.m@jaseg.de">37c3.m@jaseg.de</a> </p>
<form action="POST">
<form method="POST">
<input type="hidden" name="csrf_token" value="{{csrf_token}}"/>
<button type="submit">I agree to not be an asshole</button>
</form>
</body>

View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>8seg Web Interface</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
{% with messages = get_flashed_messages() %}
{% if messages %}
<ul class=flashes>
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
There are approximately {{count}} messages in queue.
<form id="message-form" method="POST">
<input type="hidden" name="csrf_token" value="{{csrf_token}}"/>
<textarea name="message" cols="{{cols}}" rows="{{rows}}" placeholder="Message up to {{cols}} characters per line, displayed line by line">{{message}}</textarea>
<button type="submit">Post</button>
</form>
</body>
<style>