From 7622cc99caef48cb75ea2e096ad252e99b80f3ec Mon Sep 17 00:00:00 2001
From: jaseg
Date: Sat, 23 Dec 2023 14:05:28 +0100
Subject: [PATCH] server WIP
---
.gitignore | 3 ++
eightserve.cfg | 6 +++
eightserve.py | 126 ++++++++++++++++++++++++++++++++++++++++---
migrations/0.py | 34 ++++++++++++
templates/index.html | 13 +++--
templates/post.html | 26 +++++++++
6 files changed, 198 insertions(+), 10 deletions(-)
create mode 100644 .gitignore
create mode 100644 eightserve.cfg
create mode 100644 migrations/0.py
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1ba40d3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+__pycache__
+db.sqlite3
+venv
diff --git a/eightserve.cfg b/eightserve.cfg
new file mode 100644
index 0000000..f993fbf
--- /dev/null
+++ b/eightserve.cfg
@@ -0,0 +1,6 @@
+
+DISPLAY_COLUMNS = 32
+DATABASE_URL = "sqlite:///db.sqlite3"
+SUSLIST_TIMEOUT = "01:00"
+SUSLIST_THRESHOLD = 5
+MESSAGE_MAX_LINES = 10
diff --git a/eightserve.py b/eightserve.py
index 22f13b4..c7f24c8 100644
--- a/eightserve.py
+++ b/eightserve.py
@@ -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)
+
diff --git a/migrations/0.py b/migrations/0.py
new file mode 100644
index 0000000..ca90295
--- /dev/null
+++ b/migrations/0.py
@@ -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
diff --git a/templates/index.html b/templates/index.html
index 8efabbb..b818ff0 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -1,5 +1,8 @@
-
+
+
+ 8seg Web Interface
+
8seg Web Interface
@@ -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? ;)
- If you have any questions, or you see 8seg being used by someone for bad, please email me at
+
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.
+
+ If you have any questions, or you see 8seg being used for something stupid or bad, please email me at
37c3.m@jaseg.de
-
diff --git a/templates/post.html b/templates/post.html
index e69de29..5658424 100644
--- a/templates/post.html
+++ b/templates/post.html
@@ -0,0 +1,26 @@
+
+
+
+ 8seg Web Interface
+
+
+
+ {% with messages = get_flashed_messages() %}
+ {% if messages %}
+
+ {% for message in messages %}
+ - {{ message }}
+ {% endfor %}
+
+ {% endif %}
+ {% endwith %}
+
+ There are approximately {{count}} messages in queue.
+
+
+
+