diff --git a/anonstream/__init__.py b/anonstream/__init__.py index cc6af77..a66403e 100644 --- a/anonstream/__init__.py +++ b/anonstream/__init__.py @@ -42,6 +42,8 @@ def create_app(config_file): 'CHAT_NAME_MAX_LENGTH': config['chat']['max_name_length'], 'CHAT_NAME_MIN_CONTRAST': config['chat']['min_name_contrast'], 'CHAT_BACKGROUND_COLOUR': color_to_colour(config['chat']['background_color']), + 'FLOOD_DURATION': config['flood']['duration'], + 'FLOOD_THRESHOLD': config['flood']['threshold'], 'CAPTCHA_LIFETIME': config['captcha']['lifetime'], 'CAPTCHA_FONTS': config['captcha']['fonts'], 'CAPTCHA_ALPHABET': config['captcha']['alphabet'], diff --git a/anonstream/captcha.py b/anonstream/captcha.py index a4b9f5e..34e8e9c 100644 --- a/anonstream/captcha.py +++ b/anonstream/captcha.py @@ -35,6 +35,12 @@ def get_random_captcha_digest(): return digest +def get_random_captcha_digest_for(user): + if user['verified']: + return None + else: + return get_random_captcha_digest() + def get_captcha_image(digest): try: captcha = CAPTCHAS[digest] diff --git a/anonstream/routes/nojs.py b/anonstream/routes/nojs.py index 6d21617..707a5c9 100644 --- a/anonstream/routes/nojs.py +++ b/anonstream/routes/nojs.py @@ -1,9 +1,9 @@ from quart import current_app, request, render_template, redirect, url_for, escape, Markup -from anonstream.captcha import get_random_captcha_digest +from anonstream.captcha import get_random_captcha_digest_for from anonstream.chat import add_chat_message, Rejected from anonstream.stream import get_stream_title -from anonstream.user import add_state, pop_state, try_change_appearance, verify, BadCaptcha +from anonstream.user import add_state, pop_state, try_change_appearance, verify, deverify, BadCaptcha from anonstream.routes.wrappers import with_user_from, render_template_with_etag from anonstream.helpers.chat import get_scrollback from anonstream.helpers.user import get_default_name @@ -45,14 +45,13 @@ async def nojs_form(user): state_id = request.args.get('state', type=int) state = pop_state(user, state_id) prefer_chat_form = request.args.get('landing') != 'appearance' - digest = None if user['verified'] else get_random_captcha_digest() return await render_template( 'nojs_form.html', user=user, state=state, prefer_chat_form=prefer_chat_form, nonce=generate_nonce(), - digest=digest, + digest=get_random_captcha_digest_for(user), default_name=get_default_name(user), ) @@ -86,7 +85,7 @@ async def nojs_submit_message(user): else: nonce = form.get('nonce', '') try: - # if the comment is empty but the captcha was just solved, + # If the comment is empty but the captcha was just solved, # be lenient: don't raise an exception and don't create a notice add_chat_message( user, @@ -98,6 +97,7 @@ async def nojs_submit_message(user): notice, *_ = e.args state_id = add_state(user, notice=notice) else: + deverify(user) state_id = None return redirect(url_for( diff --git a/anonstream/user.py b/anonstream/user.py index 9c3197f..fa07cfb 100644 --- a/anonstream/user.py +++ b/anonstream/user.py @@ -131,3 +131,19 @@ def verify(user, digest, answer): verification_happened = True return verification_happened + +@with_timestamp +def deverify(timestamp, user): + if not user['verified']: + return + + n_user_messages = 0 + for message in reversed(MESSAGES): + message_sent_ago = timestamp - message['timestamp'] + if message_sent_ago >= CONFIG['FLOOD_DURATION']: + break + elif message['token'] == user['token']: + n_user_messages += 1 + + if n_user_messages >= CONFIG['FLOOD_THRESHOLD']: + user['verified'] = False diff --git a/anonstream/websocket.py b/anonstream/websocket.py index 495e3f2..686b4dc 100644 --- a/anonstream/websocket.py +++ b/anonstream/websocket.py @@ -3,9 +3,9 @@ import asyncio from quart import current_app, websocket from anonstream.stream import get_stream_title, get_stream_uptime -from anonstream.captcha import get_random_captcha_digest +from anonstream.captcha import get_random_captcha_digest_for from anonstream.chat import get_all_messages_for_websocket, add_chat_message, Rejected -from anonstream.user import get_all_users_for_websocket, see, verify, BadCaptcha +from anonstream.user import get_all_users_for_websocket, see, verify, deverify, BadCaptcha from anonstream.utils.chat import generate_nonce from anonstream.utils.websocket import parse_websocket_data, Malformed @@ -24,7 +24,7 @@ async def websocket_outbound(queue, user): False: CONFIG['DEFAULT_ANON_NAME'], }, 'scrollback': CONFIG['MAX_CHAT_SCROLLBACK'], - 'digest': None if user['verified'] else get_random_captcha_digest(), + 'digest': get_random_captcha_digest_for(user), } await websocket.send_json(payload) while True: @@ -51,7 +51,7 @@ async def websocket_inbound(queue, user): payload = { 'type': 'captcha', 'notice': notice, - 'digest': get_random_captcha_digest(), + 'digest': get_random_captcha_digest_for(user), } else: try: @@ -63,9 +63,11 @@ async def websocket_inbound(queue, user): 'notice': notice, } else: + deverify(user) payload = { 'type': 'ack', 'nonce': nonce, 'next': generate_nonce(), + 'digest': get_random_captcha_digest_for(user), } queue.put_nowait(payload) diff --git a/config.toml b/config.toml index 02b8b7d..b5cc138 100644 --- a/config.toml +++ b/config.toml @@ -35,6 +35,10 @@ max_name_length = 24 min_name_contrast = 3.0 background_color = "#232327" +[flood] +duration = 20 +threshold = 4 + [thresholds] user_notwatching = 8 user_tentative = 20