From 51265fb27730933dcbbab4425dddfd02080b3b5a Mon Sep 17 00:00:00 2001 From: n9k Date: Tue, 14 Jun 2022 02:44:08 +0000 Subject: [PATCH] Eyes: delete old eyes Also implements stack/queue behaviour where if the eyes limit would be exceeded, either the new eyes cause the oldest eyes to be deleted OR the new eyes aren't created at all. The default is the first option. --- anonstream/config.py | 6 ++++++ anonstream/quart.py | 2 +- anonstream/routes/core.py | 14 +++++++++----- anonstream/tasks.py | 16 ++++++++++++++++ anonstream/user.py | 30 ++++++++++++++++++++++++++---- config.toml | 7 +++++++ 6 files changed, 65 insertions(+), 10 deletions(-) diff --git a/anonstream/config.py b/anonstream/config.py index 204e306..dbd91a5 100644 --- a/anonstream/config.py +++ b/anonstream/config.py @@ -82,6 +82,7 @@ def toml_to_flask_section_memory(config): def toml_to_flask_section_tasks(config): cfg = config['tasks'] return { + 'TASK_ROTATE_EYES': cfg['rotate_eyes'], 'TASK_ROTATE_USERS': cfg['rotate_users'], 'TASK_ROTATE_CAPTCHAS': cfg['rotate_captchas'], 'TASK_ROTATE_WEBSOCKETS': cfg['rotate_websockets'], @@ -112,11 +113,16 @@ def toml_to_flask_section_chat(config): def toml_to_flask_section_flood(config): cfg = config['flood'] + assert cfg['video']['max_eyes'] >= 0 return { 'FLOOD_MESSAGE_DURATION': cfg['messages']['duration'], 'FLOOD_MESSAGE_THRESHOLD': cfg['messages']['threshold'], 'FLOOD_LINE_DURATION': cfg['lines']['duration'], 'FLOOD_LINE_THRESHOLD': cfg['lines']['threshold'], + 'FLOOD_VIDEO_MAX_EYES': cfg['video']['max_eyes'], + #'FLOOD_VIDEO_COOLDOWN': cfg['video']['cooldown'], + 'FLOOD_VIDEO_EYES_EXPIRE_AFTER': cfg['video']['expire_after'], + 'FLOOD_VIDEO_OVERWRITE': cfg['video']['overwrite'], } def toml_to_flask_section_captcha(config): diff --git a/anonstream/quart.py b/anonstream/quart.py index 5761e86..dfd5e16 100644 --- a/anonstream/quart.py +++ b/anonstream/quart.py @@ -6,7 +6,7 @@ from quart.asgi import ASGIHTTPConnection as ASGIHTTPConnection_ from quart.utils import encode_headers -RESPONSE_ITERATOR_TIMEOUT = 10 +RESPONSE_ITERATOR_TIMEOUT = 10.0 class ASGIHTTPConnection(ASGIHTTPConnection_): diff --git a/anonstream/routes/core.py b/anonstream/routes/core.py index 42149cc..c4642a8 100644 --- a/anonstream/routes/core.py +++ b/anonstream/routes/core.py @@ -6,7 +6,7 @@ from quart import current_app, request, render_template, abort, make_response, r from anonstream.captcha import get_captcha_image from anonstream.segments import segments, StopSendingSegments from anonstream.stream import is_online, get_stream_uptime -from anonstream.user import watched, create_eyes, renew_eyes, ExpiredEyes +from anonstream.user import watched, create_eyes, renew_eyes, EyesException from anonstream.routes.wrappers import with_user_from, auth_required from anonstream.utils.security import generate_csp @@ -25,12 +25,16 @@ async def stream(user): if not is_online(): return abort(404) - eyes_id = create_eyes(user, dict(request.headers)) + try: + eyes_id = create_eyes(user, dict(request.headers)) + except EyesException: + return abort(429) + def segment_read_hook(uri): try: - renew_eyes(user, eyes_id) - except ExpiredEyes as e: - raise StopSendingSegments(f'eyes {eyes_id} expired: {e}') from e + renew_eyes(user, eyes_id, just_read_new_segment=True) + except EyesException as e: + raise StopSendingSegments(f'eyes {eyes_id} not allowed: {e!r}') from e print(f'{uri}: {eyes_id}~{user["token"]}') watched(user) diff --git a/anonstream/tasks.py b/anonstream/tasks.py index 0047620..a31368a 100644 --- a/anonstream/tasks.py +++ b/anonstream/tasks.py @@ -43,6 +43,21 @@ def with_period(period): return periodically +@with_period(CONFIG['TASK_ROTATE_EYES']) +@with_timestamp +async def t_delete_eyes(timestamp, iteration): + if iteration == 0: + return + else: + for user in USERS: + to_delete = [] + for eyes_id, eyes in user['eyes']['current'].items(): + renewed_ago = timestamp - eyes['renewed'] + if renewed_ago >= CONFIG['FLOOD_VIDEO_EYES_EXPIRE_AFTER']: + to_delete.append(eyes_id) + for eyes_id in to_delete: + user['eyes']['current'].pop(eyes_id) + @with_period(CONFIG['TASK_ROTATE_USERS']) @with_timestamp async def t_sunset_users(timestamp, iteration): @@ -166,6 +181,7 @@ async def t_broadcast_stream_info_update(iteration): if payload: broadcast(USERS, payload={'type': 'info', **payload}) +current_app.add_background_task(t_delete_eyes) current_app.add_background_task(t_sunset_users) current_app.add_background_task(t_expire_captchas) current_app.add_background_task(t_close_websockets) diff --git a/anonstream/user.py b/anonstream/user.py index 6a6cc6a..5d04d85 100644 --- a/anonstream/user.py +++ b/anonstream/user.py @@ -25,7 +25,16 @@ class BadAppearance(ValueError): class BadCaptcha(ValueError): pass -class ExpiredEyes(Exception): +class EyesException(Exception): + pass + +class TooManyEyes(EyesException): + pass + +class DeletedEyes(EyesException): + pass + +class ExpiredEyes(EyesException): pass def add_state(user, **state): @@ -225,6 +234,16 @@ def get_users_by_presence(timestamp): @with_timestamp def create_eyes(timestamp, user, headers): + if len(user['eyes']['current']) >= CONFIG['FLOOD_VIDEO_MAX_EYES']: + # treat eyes as a stack, do not create new eyes if it would + # cause the limit to be exceeded + if not CONFIG['FLOOD_VIDEO_OVERWRITE']: + raise TooManyEyes + # treat eyes as a queue, expire old eyes upon creating new eyes + # if the limit would have been exceeded + elif user['eyes']['current']: + oldest_eyes_id = min(user['eyes']['current']) + user['eyes']['current'].pop(oldest_eyes_id) eyes_id = user['eyes']['total'] user['eyes']['total'] += 1 user['eyes']['current'][eyes_id] = { @@ -238,12 +257,15 @@ def create_eyes(timestamp, user, headers): return eyes_id @with_timestamp -def renew_eyes(timestamp, user, eyes_id): +def renew_eyes(timestamp, user, eyes_id, just_read_new_segment=False): try: eyes = user['eyes']['current'][eyes_id] except KeyError: - raise ExpiredEyes(None) - if timestamp - eyes['renewed'] >= 20.0: # TODO remove magic number + raise DeletedEyes(eyes_id) + renewed_ago = timestamp - eyes['renewed'] + if renewed_ago >= CONFIG['FLOOD_VIDEO_EYES_EXPIRE_AFTER']: user['eyes']['current'].pop(eyes_id) raise ExpiredEyes(eyes) eyes['renewed'] = timestamp + if just_read_new_segment: + eyes['n_segments'] += 1 diff --git a/config.toml b/config.toml index 717c64e..5fe9df0 100644 --- a/config.toml +++ b/config.toml @@ -34,6 +34,7 @@ chat_messages = 8192 chat_scrollback = 256 [tasks] +rotate_eyes = 3.0 rotate_users = 60.0 rotate_captchas = 60.0 rotate_websockets = 2.0 @@ -60,6 +61,12 @@ threshold = 4 duration = 20.0 threshold = 20 +[flood.video] +max_eyes = 3 +#cooldown = 12.0 +expire_after = 5.0 +overwrite = true + [thresholds] user_notwatching = 8.0 user_tentative = 20.0