Flood detection

このコミットが含まれているのは:
n9k 2022-02-20 22:07:32 +00:00
コミット 8c9b0d9da0
6個のファイルの変更39行の追加9行の削除

ファイルの表示

@ -42,6 +42,8 @@ def create_app(config_file):
'CHAT_NAME_MAX_LENGTH': config['chat']['max_name_length'], 'CHAT_NAME_MAX_LENGTH': config['chat']['max_name_length'],
'CHAT_NAME_MIN_CONTRAST': config['chat']['min_name_contrast'], 'CHAT_NAME_MIN_CONTRAST': config['chat']['min_name_contrast'],
'CHAT_BACKGROUND_COLOUR': color_to_colour(config['chat']['background_color']), '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_LIFETIME': config['captcha']['lifetime'],
'CAPTCHA_FONTS': config['captcha']['fonts'], 'CAPTCHA_FONTS': config['captcha']['fonts'],
'CAPTCHA_ALPHABET': config['captcha']['alphabet'], 'CAPTCHA_ALPHABET': config['captcha']['alphabet'],

ファイルの表示

@ -35,6 +35,12 @@ def get_random_captcha_digest():
return 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): def get_captcha_image(digest):
try: try:
captcha = CAPTCHAS[digest] captcha = CAPTCHAS[digest]

ファイルの表示

@ -1,9 +1,9 @@
from quart import current_app, request, render_template, redirect, url_for, escape, Markup 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.chat import add_chat_message, Rejected
from anonstream.stream import get_stream_title 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.routes.wrappers import with_user_from, render_template_with_etag
from anonstream.helpers.chat import get_scrollback from anonstream.helpers.chat import get_scrollback
from anonstream.helpers.user import get_default_name 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_id = request.args.get('state', type=int)
state = pop_state(user, state_id) state = pop_state(user, state_id)
prefer_chat_form = request.args.get('landing') != 'appearance' prefer_chat_form = request.args.get('landing') != 'appearance'
digest = None if user['verified'] else get_random_captcha_digest()
return await render_template( return await render_template(
'nojs_form.html', 'nojs_form.html',
user=user, user=user,
state=state, state=state,
prefer_chat_form=prefer_chat_form, prefer_chat_form=prefer_chat_form,
nonce=generate_nonce(), nonce=generate_nonce(),
digest=digest, digest=get_random_captcha_digest_for(user),
default_name=get_default_name(user), default_name=get_default_name(user),
) )
@ -86,7 +85,7 @@ async def nojs_submit_message(user):
else: else:
nonce = form.get('nonce', '') nonce = form.get('nonce', '')
try: 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 # be lenient: don't raise an exception and don't create a notice
add_chat_message( add_chat_message(
user, user,
@ -98,6 +97,7 @@ async def nojs_submit_message(user):
notice, *_ = e.args notice, *_ = e.args
state_id = add_state(user, notice=notice) state_id = add_state(user, notice=notice)
else: else:
deverify(user)
state_id = None state_id = None
return redirect(url_for( return redirect(url_for(

ファイルの表示

@ -131,3 +131,19 @@ def verify(user, digest, answer):
verification_happened = True verification_happened = True
return verification_happened 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

ファイルの表示

@ -3,9 +3,9 @@ import asyncio
from quart import current_app, websocket from quart import current_app, websocket
from anonstream.stream import get_stream_title, get_stream_uptime 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.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.chat import generate_nonce
from anonstream.utils.websocket import parse_websocket_data, Malformed from anonstream.utils.websocket import parse_websocket_data, Malformed
@ -24,7 +24,7 @@ async def websocket_outbound(queue, user):
False: CONFIG['DEFAULT_ANON_NAME'], False: CONFIG['DEFAULT_ANON_NAME'],
}, },
'scrollback': CONFIG['MAX_CHAT_SCROLLBACK'], '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) await websocket.send_json(payload)
while True: while True:
@ -51,7 +51,7 @@ async def websocket_inbound(queue, user):
payload = { payload = {
'type': 'captcha', 'type': 'captcha',
'notice': notice, 'notice': notice,
'digest': get_random_captcha_digest(), 'digest': get_random_captcha_digest_for(user),
} }
else: else:
try: try:
@ -63,9 +63,11 @@ async def websocket_inbound(queue, user):
'notice': notice, 'notice': notice,
} }
else: else:
deverify(user)
payload = { payload = {
'type': 'ack', 'type': 'ack',
'nonce': nonce, 'nonce': nonce,
'next': generate_nonce(), 'next': generate_nonce(),
'digest': get_random_captcha_digest_for(user),
} }
queue.put_nowait(payload) queue.put_nowait(payload)

ファイルの表示

@ -35,6 +35,10 @@ max_name_length = 24
min_name_contrast = 3.0 min_name_contrast = 3.0
background_color = "#232327" background_color = "#232327"
[flood]
duration = 20
threshold = 4
[thresholds] [thresholds]
user_notwatching = 8 user_notwatching = 8
user_tentative = 20 user_tentative = 20