From 542d6c9ae58633479286b84186ea042c3ff53860 Mon Sep 17 00:00:00 2001 From: n9k Date: Sat, 28 May 2022 05:37:41 +0000 Subject: [PATCH] Detect chat flooding by counting lines Reject comments by line count. Ratelimit users by number of lines sent in chat. --- anonstream/__init__.py | 6 ++++-- anonstream/chat.py | 34 +++++++++++++++++++++++++++++++--- anonstream/helpers/user.py | 3 ++- anonstream/user.py | 4 ++-- anonstream/utils/chat.py | 8 ++++++++ config.toml | 6 +++++- 6 files changed, 52 insertions(+), 9 deletions(-) diff --git a/anonstream/__init__.py b/anonstream/__init__.py index 67d5aa3..563ad18 100644 --- a/anonstream/__init__.py +++ b/anonstream/__init__.py @@ -66,8 +66,10 @@ def create_app(config_file): 'CHAT_NAME_MIN_CONTRAST': config['chat']['min_name_contrast'], 'CHAT_BACKGROUND_COLOUR': color_to_colour(config['chat']['background_color']), 'CHAT_LEGACY_TRIPCODE_ALGORITHM': config['chat']['legacy_tripcode_algorithm'], - 'FLOOD_DURATION': config['flood']['duration'], - 'FLOOD_THRESHOLD': config['flood']['threshold'], + 'FLOOD_MESSAGE_DURATION': config['flood']['messages']['duration'], + 'FLOOD_MESSAGE_THRESHOLD': config['flood']['messages']['threshold'], + 'FLOOD_LINE_DURATION': config['flood']['lines']['duration'], + 'FLOOD_LINE_THRESHOLD': config['flood']['lines']['threshold'], 'CAPTCHA_LIFETIME': config['captcha']['lifetime'], 'CAPTCHA_FONTS': config['captcha']['fonts'], 'CAPTCHA_ALPHABET': config['captcha']['alphabet'], diff --git a/anonstream/chat.py b/anonstream/chat.py index 9c789e8..ea875f6 100644 --- a/anonstream/chat.py +++ b/anonstream/chat.py @@ -8,7 +8,7 @@ from quart import current_app, escape from anonstream.broadcast import broadcast, broadcast_users_update from anonstream.helpers.chat import generate_nonce_hash, get_scrollback -from anonstream.utils.chat import get_message_for_websocket +from anonstream.utils.chat import get_message_for_websocket, get_approx_linespan CONFIG = current_app.config MESSAGES_BY_ID = current_app.messages_by_id @@ -33,6 +33,26 @@ def add_chat_message(user, nonce, comment, ignore_empty=False): if ignore_empty and len(comment) == 0: return False + timestamp_ms = time.time_ns() // 1_000_000 + timestamp = timestamp_ms // 1000 + + # Check user + while user['linespan']: + linespan_timestamp, _ = user['linespan'][0] + if timestamp - linespan_timestamp >= CONFIG['FLOOD_LINE_DURATION']: + user['linespan'].popleft() + else: + break + total_recent_linespan = sum(map( + lambda linespan_tuple: linespan_tuple[1], + user['linespan'], + )) + if total_recent_linespan > CONFIG['FLOOD_LINE_THRESHOLD']: + raise Rejected( + f'Chat overuse in the last ' + f'{CONFIG["FLOOD_LINE_DURATION"]:.0f} seconds' + ) + # Check message message_id = generate_nonce_hash(nonce) if message_id in MESSAGES_BY_ID: @@ -41,10 +61,18 @@ def add_chat_message(user, nonce, comment, ignore_empty=False): raise Rejected('Message was empty') if len(comment) > 512: raise Rejected('Message exceeded 512 chars') + if comment.count('\n') + 1 > 12: + raise Rejected('Message exceeded 12 lines') + + linespan = get_approx_linespan(comment) + if linespan > 12: + raise Rejected('Message would span too many lines') + + # Record linespan + linespan_tuple = (timestamp, linespan) + user['linespan'].append(linespan_tuple) # Create and add message - timestamp_ms = time.time_ns() // 1_000_000 - timestamp = timestamp_ms // 1000 try: last_message = next(reversed(MESSAGES)) except StopIteration: diff --git a/anonstream/helpers/user.py b/anonstream/helpers/user.py index dbe48d4..82442c5 100644 --- a/anonstream/helpers/user.py +++ b/anonstream/helpers/user.py @@ -3,7 +3,7 @@ import hashlib import base64 -from collections import OrderedDict +from collections import deque, OrderedDict from math import inf from quart import current_app @@ -45,6 +45,7 @@ def generate_user(timestamp, token, broadcaster, presence): 'watching': -inf, }, 'presence': presence, + 'linespan': deque(), } def get_default_name(user): diff --git a/anonstream/user.py b/anonstream/user.py index 51df031..84a7a2d 100644 --- a/anonstream/user.py +++ b/anonstream/user.py @@ -152,12 +152,12 @@ def deverify(timestamp, user): n_user_messages = 0 for message in reversed(MESSAGES): message_sent_ago = timestamp - message['timestamp'] - if message_sent_ago >= CONFIG['FLOOD_DURATION']: + if message_sent_ago >= CONFIG['FLOOD_MESSAGE_DURATION']: break elif message['token'] == user['token']: n_user_messages += 1 - if n_user_messages >= CONFIG['FLOOD_THRESHOLD']: + if n_user_messages >= CONFIG['FLOOD_MESSAGE_THRESHOLD']: user['verified'] = False def _update_presence(timestamp, user): diff --git a/anonstream/utils/chat.py b/anonstream/utils/chat.py index b35b945..0b4d5f7 100644 --- a/anonstream/utils/chat.py +++ b/anonstream/utils/chat.py @@ -3,6 +3,7 @@ import base64 import hashlib +import math import secrets class NonceReuse(Exception): @@ -18,3 +19,10 @@ def get_message_for_websocket(user, message): **{key: message[key] for key in message_keys}, **{key: user[key] for key in user_keys}, } + +def get_approx_linespan(text): + def height(line): + return math.ceil(len(line) / 48) + linespan = sum(map(height, text.splitlines())) + linespan = linespan if linespan > 0 else 1 + return linespan diff --git a/config.toml b/config.toml index b5cf4b9..672a57e 100644 --- a/config.toml +++ b/config.toml @@ -49,10 +49,14 @@ min_name_contrast = 3.0 background_color = "#232327" legacy_tripcode_algorithm = false -[flood] +[flood.messages] duration = 20.0 threshold = 4 +[flood.lines] +duration = 20.0 +threshold = 20 + [thresholds] user_notwatching = 8.0 user_tentative = 20.0