server WIP
This commit is contained in:
parent
7d00923533
commit
7622cc99ca
6 changed files with 198 additions and 10 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
__pycache__
|
||||
db.sqlite3
|
||||
venv
|
||||
6
eightserve.cfg
Normal file
6
eightserve.cfg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
|
||||
DISPLAY_COLUMNS = 32
|
||||
DATABASE_URL = "sqlite:///db.sqlite3"
|
||||
SUSLIST_TIMEOUT = "01:00"
|
||||
SUSLIST_THRESHOLD = 5
|
||||
MESSAGE_MAX_LINES = 10
|
||||
126
eightserve.py
126
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)
|
||||
|
||||
|
|
|
|||
34
migrations/0.py
Normal file
34
migrations/0.py
Normal 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
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue