From cbd494e3bf8e4124977ecec28e083f4e04fcacca Mon Sep 17 00:00:00 2001 From: n9k Date: Thu, 11 Aug 2022 06:10:22 +0000 Subject: [PATCH 01/10] Set cookie when access captcha solved --- anonstream/routes/core.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/anonstream/routes/core.py b/anonstream/routes/core.py index d1ea26c..db9e0ab 100644 --- a/anonstream/routes/core.py +++ b/anonstream/routes/core.py @@ -3,6 +3,7 @@ import math import re +from urllib.parse import quote from quart import current_app, request, render_template, abort, make_response, redirect, url_for, send_from_directory from werkzeug.exceptions import Forbidden, NotFound, TooManyRequests @@ -128,12 +129,13 @@ async def access(timestamp, user_or_token): failure_id = None user = generate_and_add_user(timestamp, token, verified=True) if failure_id is not None: - url = url_for('home', token=token, failure=failure_id) - raise abort(redirect(url, 303)) + response = redirect(url_for('home', token=token, failure=failure_id), 303) + else: + response = redirect(url_for('home', token=user['token']), 303) + response.headers['Set-Cookie'] = f'token={quote(user["token"])}; path=/' case dict() as user: - pass - url = url_for('home', token=user['token']) - return redirect(url, 303) + response = redirect(url_for('home', token=user['token']), 303) + return response @current_app.route('/static/') @with_user_from(request) From 2a67bee82cd08639b9045d13467f947dac64c8b8 Mon Sep 17 00:00:00 2001 From: n9k Date: Thu, 11 Aug 2022 06:19:17 +0000 Subject: [PATCH 02/10] If client supports cookies, clear token URL parameter Only on the homepage. --- anonstream/routes/core.py | 7 ++++++- anonstream/routes/wrappers.py | 21 +++++++++++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/anonstream/routes/core.py b/anonstream/routes/core.py index db9e0ab..19e95ad 100644 --- a/anonstream/routes/core.py +++ b/anonstream/routes/core.py @@ -23,7 +23,12 @@ CAPTCHA_SIGNER = current_app.captcha_signer STATIC_DIRECTORY = current_app.root_path / 'static' @current_app.route('/') -@with_user_from(request, fallback_to_token=True, ignore_allowedness=True) +@with_user_from( + request, + fallback_to_token=True, + ignore_allowedness=True, + redundant_token_redirect=True, +) async def home(timestamp, user_or_token): match user_or_token: case str() | None as token: diff --git a/anonstream/routes/wrappers.py b/anonstream/routes/wrappers.py index fcc1a77..4dfe5bf 100644 --- a/anonstream/routes/wrappers.py +++ b/anonstream/routes/wrappers.py @@ -8,7 +8,7 @@ import string from functools import wraps from urllib.parse import quote, unquote -from quart import current_app, request, make_response, render_template, request, url_for, Markup +from quart import current_app, request, make_response, render_template, redirect, url_for, Markup from werkzeug.exceptions import BadRequest, Unauthorized, Forbidden from werkzeug.security import check_password_hash @@ -87,7 +87,12 @@ def generate_and_add_user( USERS_UPDATE_BUFFER.add(token) return user -def with_user_from(context, fallback_to_token=False, ignore_allowedness=False): +def with_user_from( + context, + fallback_to_token=False, + ignore_allowedness=False, + redundant_token_redirect=False, +): def with_user_from_context(f): @wraps(f) async def wrapper(*args, **kwargs): @@ -129,6 +134,18 @@ def with_user_from(context, fallback_to_token=False, ignore_allowedness=False): f"terminal when they started anonstream." )) + # If token from the client's cookie is same as the token in the URL + # query string, the client supports cookies. If we want, we can + # redirect the client to this same URL path but with the token + # parameter removed, since we'll pick up their token from their + # cookie anyway. + if ( + redundant_token_redirect + and token_from_context is not None + and token_from_args == token_from_cookie + ): + return redirect(context.path, 303) + # Create response user = USERS_BY_TOKEN.get(token) if CONFIG['ACCESS_CAPTCHA'] and not broadcaster: From d05c5fec319657353c0773d4ecbbdb4e8465fbf5 Mon Sep 17 00:00:00 2001 From: n9k Date: Fri, 12 Aug 2022 03:59:23 +0000 Subject: [PATCH 03/10] Minor CSS: adjust access captcha height It was too high on mobile screens. --- anonstream/templates/captcha.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/anonstream/templates/captcha.html b/anonstream/templates/captcha.html index 3f21ee3..1391dd0 100644 --- a/anonstream/templates/captcha.html +++ b/anonstream/templates/captcha.html @@ -15,7 +15,7 @@ font-family: sans-serif; font-size: 14pt; display: grid; - grid-template-rows: calc(50% - 4rem) 1fr; + grid-template-rows: calc(50% - 10vh + 2rem) 1fr; height: 100vh; margin: 0; padding: 1rem; From 78753f7e0c723e3bf2b2074011c2c4af2765e7ca Mon Sep 17 00:00:00 2001 From: n9k Date: Fri, 12 Aug 2022 03:58:24 +0000 Subject: [PATCH 04/10] Minor CSS: make access captcha input like comment box --- anonstream/templates/captcha.html | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/anonstream/templates/captcha.html b/anonstream/templates/captcha.html index 1391dd0..6dc711a 100644 --- a/anonstream/templates/captcha.html +++ b/anonstream/templates/captcha.html @@ -37,12 +37,13 @@ border-radius: 2px; color: #ddd; font-size: 14pt; - padding: 4px 5px; + padding: 5px 6px; width: 10ch; - } - input[name="answer"]:hover { - background-color: #37373a; transition: 0.25s; + } + input[name="answer"]:focus { + background-color: black; + border-color: #3584e4; } input[type="submit"] { font-size: 14pt; From 6e9ba1a5db26fb9a529c176a5dd3c1b90e93f4d1 Mon Sep 17 00:00:00 2001 From: n9k Date: Fri, 12 Aug 2022 05:19:18 +0000 Subject: [PATCH 05/10] Minor CSS: padding on access captcha button --- anonstream/templates/captcha.html | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/anonstream/templates/captcha.html b/anonstream/templates/captcha.html index 6dc711a..f64350c 100644 --- a/anonstream/templates/captcha.html +++ b/anonstream/templates/captcha.html @@ -44,14 +44,16 @@ input[name="answer"]:focus { background-color: black; border-color: #3584e4; - } - input[type="submit"] { + } + input[type="submit"] { font-size: 14pt; - } - p { + padding-left: 8px; + padding-right: 8px; + } + p { grid-column: 1 / span 2; text-align: center; - } + } From 071edaef3a4e57de38efc9b05966d948fe7b77e5 Mon Sep 17 00:00:00 2001 From: n9k Date: Fri, 12 Aug 2022 05:08:53 +0000 Subject: [PATCH 06/10] Control socket: minor help text etc. fixups --- anonstream/control/spec/methods/emote.py | 2 +- anonstream/control/spec/methods/title.py | 2 +- anonstream/control/spec/methods/user.py | 29 ++++++++---------------- 3 files changed, 12 insertions(+), 21 deletions(-) diff --git a/anonstream/control/spec/methods/emote.py b/anonstream/control/spec/methods/emote.py index 1389d74..e709d88 100644 --- a/anonstream/control/spec/methods/emote.py +++ b/anonstream/control/spec/methods/emote.py @@ -16,7 +16,7 @@ EMOTES = current_app.emotes async def cmd_emote_help(): normal = ['emote', 'help'] response = ( - 'Usage: emote [show | reload]\n' + 'Usage: emote {show | reload}\n' 'Commands:\n' ' emote show........show all current emotes\n' ' emote reload......try to reload the emote schema (existing messages are not modified)\n' diff --git a/anonstream/control/spec/methods/title.py b/anonstream/control/spec/methods/title.py index d099ee9..92a0e3a 100644 --- a/anonstream/control/spec/methods/title.py +++ b/anonstream/control/spec/methods/title.py @@ -12,7 +12,7 @@ from anonstream.stream import get_stream_title, set_stream_title async def cmd_title_help(): normal = ['title', 'help'] response = ( - 'Usage: title [show | set TITLE]\n' + 'Usage: title {show | set TITLE}\n' 'Commands:\n' ' title [show].......show the stream title\n' ' title set TITLE....set the stream title to TITLE\n' diff --git a/anonstream/control/spec/methods/user.py b/anonstream/control/spec/methods/user.py index 5cd91fd..7dad2a1 100644 --- a/anonstream/control/spec/methods/user.py +++ b/anonstream/control/spec/methods/user.py @@ -58,10 +58,8 @@ async def cmd_user_get(user, attr): except (TypeError, ValueError) as e: raise CommandFailed('value is not representable in json') from e normal = [ - 'user', - 'get', - 'token', - json_dumps_contiguous(user['token']), + 'user', 'get', + 'token', json_dumps_contiguous(user['token']), attr, ] response = value_json + '\n' @@ -74,10 +72,8 @@ async def cmd_user_set(user, attr, value): if attr in USER_WEBSOCKET_ATTRS: USERS_UPDATE_BUFFER.add(user['token']) normal = [ - 'user', - 'set', - 'token', - json_dumps_contiguous(user['token']), + 'user', 'set', + 'token', json_dumps_contiguous(user['token']), attr, json_dumps_contiguous(value), ] @@ -86,11 +82,9 @@ async def cmd_user_set(user, attr, value): async def cmd_user_eyes_show(user): normal = [ - 'user', - 'eyes', - 'token', - json_dumps_contiguous(user['token']), - 'show' + 'user', 'eyes', + 'token', json_dumps_contiguous(user['token']), + 'show', ] response = json.dumps(user['eyes']['current']) + '\n' return normal, response @@ -101,12 +95,9 @@ async def cmd_user_eyes_delete(user, eyes_id): except KeyError: pass normal = [ - 'user', - 'eyes', - 'token', - json_dumps_contiguous(user['token']), - 'delete', - str(eyes_id), + 'user', 'eyes', + 'token', json_dumps_contiguous(user['token']), + 'delete', str(eyes_id), ] response = '' return normal, response From 26a86fac7aad15295bd711196fea8a5da516218d Mon Sep 17 00:00:00 2001 From: n9k Date: Fri, 12 Aug 2022 05:09:37 +0000 Subject: [PATCH 07/10] Control socket: show app.config options --- anonstream/control/parse.py | 2 ++ anonstream/control/spec/methods/config.py | 43 +++++++++++++++++++++++ anonstream/control/spec/methods/help.py | 1 + 3 files changed, 46 insertions(+) create mode 100644 anonstream/control/spec/methods/config.py diff --git a/anonstream/control/parse.py b/anonstream/control/parse.py index 7acf77f..b09bed6 100644 --- a/anonstream/control/parse.py +++ b/anonstream/control/parse.py @@ -5,6 +5,7 @@ from anonstream.control.spec import ParseException, Parsed from anonstream.control.spec.common import Str from anonstream.control.spec.methods.allowedness import SPEC as SPEC_ALLOWEDNESS from anonstream.control.spec.methods.chat import SPEC as SPEC_CHAT +from anonstream.control.spec.methods.config import SPEC as SPEC_CONFIG from anonstream.control.spec.methods.emote import SPEC as SPEC_EMOTE from anonstream.control.spec.methods.help import SPEC as SPEC_HELP from anonstream.control.spec.methods.quit import SPEC as SPEC_QUIT @@ -19,6 +20,7 @@ SPEC = Str({ 'user': SPEC_USER, 'allowednesss': SPEC_ALLOWEDNESS, 'emote': SPEC_EMOTE, + 'config': SPEC_CONFIG, }) async def parse(request): diff --git a/anonstream/control/spec/methods/config.py b/anonstream/control/spec/methods/config.py new file mode 100644 index 0000000..800ee00 --- /dev/null +++ b/anonstream/control/spec/methods/config.py @@ -0,0 +1,43 @@ +# SPDX-FileCopyrightText: 2022 n9k +# SPDX-License-Identifier: AGPL-3.0-or-later + +import json + +from quart import current_app + +from anonstream.control.exceptions import CommandFailed +from anonstream.control.spec.common import Str, End, ArgsString + +CONFIG = current_app.config + +async def cmd_config_help(): + normal = ['config', 'help'] + response = ( + 'Usage: config show OPTION\n' + 'Commands:\n' + ' config show OPTION....show entry in app.config\n' + 'Definitions:\n' + ' OPTION................app.config key, re:[A-Z0-9_]+\n' + ) + return normal, response + +async def cmd_config_show(option): + if option in {'SECRET_KEY', 'SECRET_KEY_STRING'}: + raise CommandFailed('not going to show our secret key') + try: + value = CONFIG[option] + except KeyError: + raise CommandFailed(f'no config option with key {option!r}') + try: + value_json = json.dumps(value) + except (TypeError, ValueError): + raise CommandFailed(f'value is not json serializable') + normal = ['config', 'show'] + response = value_json + '\n' + return normal, response + +SPEC = Str({ + None: End(cmd_config_help), + 'help': End(cmd_config_help), + 'show': ArgsString(End(cmd_config_show)), +}) diff --git a/anonstream/control/spec/methods/help.py b/anonstream/control/spec/methods/help.py index bd251e4..697c638 100644 --- a/anonstream/control/spec/methods/help.py +++ b/anonstream/control/spec/methods/help.py @@ -28,6 +28,7 @@ async def cmd_help(): ' allowedness remove SET STRING..remove from the blacklist/whitelist\n' ' emote show.....................show all current emotes\n' ' emote reload...................try reloading the emote schema\n' + ' config show OPTION.............show app config option\n' ) return normal, response From 3fca390a300d74e781573f2bbdd4e0c3b444c00e Mon Sep 17 00:00:00 2001 From: n9k Date: Fri, 12 Aug 2022 05:18:23 +0000 Subject: [PATCH 08/10] Control socket: show all chat messages --- anonstream/control/spec/methods/chat.py | 16 +++++++++++++--- anonstream/control/spec/methods/help.py | 6 +++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/anonstream/control/spec/methods/chat.py b/anonstream/control/spec/methods/chat.py index 64ee2ea..1474461 100644 --- a/anonstream/control/spec/methods/chat.py +++ b/anonstream/control/spec/methods/chat.py @@ -2,6 +2,9 @@ # SPDX-License-Identifier: AGPL-3.0-or-later import itertools +import json + +from quart import current_app from anonstream.chat import delete_chat_messages from anonstream.control.exceptions import CommandFailed @@ -10,6 +13,8 @@ from anonstream.control.spec.common import Str, End, Args, ArgsJsonString, ArgsU from anonstream.control.spec.utils import get_item, json_dumps_contiguous from anonstream.chat import add_chat_message, Rejected +MESSAGES = current_app.messages + class ArgsSeqs(Args): def consume(self, words, index): seqs = [] @@ -33,13 +38,12 @@ class ArgsSeqs(Args): async def cmd_chat_help(): normal = ['chat', 'help'] response = ( - 'Usage: chat delete SEQS\n' + 'Usage: chat {show | delete SEQS | add USER NONCE COMMENT}\n' 'Commands:\n' - #' chat show [MESSAGES]...........show chat messages\n' + ' chat show......................show all chat messages\n' ' chat delete SEQS...............delete chat messages\n' ' chat add USER NONCE COMMENT....add chat message\n' 'Definitions:\n' - #' MESSAGES...................undefined\n' ' SEQS.......................=SEQ [SEQ...]\n' ' SEQ........................a chat message\'s seq, base-10 integer\n' ' USER.......................={token TOKEN | hash HASH}\n' @@ -50,6 +54,11 @@ async def cmd_chat_help(): ) return normal, response +async def cmd_chat_show(): + normal = ['chat', 'show'] + response = json.dumps(tuple(MESSAGES), separators=(',', ':')) + '\n' + return normal, response + async def cmd_chat_delete(*seqs): delete_chat_messages(seqs) normal = ['chat', 'delete', *map(str, seqs)] @@ -74,6 +83,7 @@ async def cmd_chat_add(user, nonce, comment): SPEC = Str({ None: End(cmd_chat_help), 'help': End(cmd_chat_help), + 'show': End(cmd_chat_show), 'delete': ArgsSeqs(End(cmd_chat_delete)), 'add': ArgsUser(ArgsJsonString(ArgsJsonString(End(cmd_chat_add)))), }) diff --git a/anonstream/control/spec/methods/help.py b/anonstream/control/spec/methods/help.py index 697c638..59e4529 100644 --- a/anonstream/control/spec/methods/help.py +++ b/anonstream/control/spec/methods/help.py @@ -19,9 +19,9 @@ async def cmd_help(): ' user eyes USER [show]..........show a list of active video responses\n' ' user eyes USER delete EYES_ID..end a video response\n' ' user add VERIFIED TOKEN........add new user\n' - #' chat show MESSAGES.............show a list of messages\n' - ' chat delete SEQS...............delete a set of messages\n' - ' chat add USER NONCE COMMENT....add chat message\n' + ' chat show......................show a list of all chat messages\n' + ' chat delete SEQS...............delete a set of chat messages\n' + ' chat add USER NONCE COMMENT....add a chat message\n' ' allowedness [show].............show the current allowedness\n' ' allowedness setdefault BOOLEAN.set the default allowedness\n' ' allowedness add SET STRING.....add to the blacklist/whitelist\n' From 8426a3490a60db112b271bf148b07b9c53444020 Mon Sep 17 00:00:00 2001 From: n9k Date: Fri, 12 Aug 2022 05:19:42 +0000 Subject: [PATCH 09/10] Event socket: add event for setting appearance --- anonstream/user.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/anonstream/user.py b/anonstream/user.py index 5ecc711..a160462 100644 --- a/anonstream/user.py +++ b/anonstream/user.py @@ -8,6 +8,7 @@ from math import inf from quart import current_app +from anonstream.events import notify_event_sockets from anonstream.wrappers import try_except_log, with_timestamp, get_timestamp from anonstream.helpers.user import get_default_name, get_presence, Presence from anonstream.helpers.captcha import check_captcha_digest, Answer @@ -98,6 +99,21 @@ def try_change_appearance(user, name, color, password, want_tripcode): # Add to the users update buffer USERS_UPDATE_BUFFER.add(user['token']) + # Notify event sockets that a user's appearance was set + # NOTE: Changing appearance is currently NOT ratelimited. + # Applications using the event socket API should buffer these + # events or do something else to a prevent a potential denial of + # service. + notify_event_sockets({ + 'type': 'appearance', + 'event': { + 'token': user['token'], + 'name': user['name'], + 'color': user['color'], + 'tripcode': user['tripcode'], + } + }) + return errors def change_name(user, name, dry_run=False): From 12338747de72b42ca6967b1da1191f8be5f6bee1 Mon Sep 17 00:00:00 2001 From: n9k Date: Fri, 12 Aug 2022 05:25:57 +0000 Subject: [PATCH 10/10] Websocket: send form field maxlengths --- anonstream/static/anonstream.js | 5 +++++ anonstream/websocket.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/anonstream/static/anonstream.js b/anonstream/static/anonstream.js index 295ec19..b82aa85 100644 --- a/anonstream/static/anonstream.js +++ b/anonstream/static/anonstream.js @@ -665,6 +665,11 @@ const on_websocket_message = async (event) => { info_button.dataset.visible = ""; } + // form input maxlengths + chat_form_comment.maxLength = receipt.maxlength.comment; + chat_appearance_form_name.maxLength = receipt.maxlength.name; + chat_appearance_form_password.maxLength = receipt.maxlength.password; + // chat form nonce chat_form_nonce.value = receipt.nonce; diff --git a/anonstream/websocket.py b/anonstream/websocket.py index 48d8467..8ec3675 100644 --- a/anonstream/websocket.py +++ b/anonstream/websocket.py @@ -36,6 +36,11 @@ async def websocket_outbound(queue, user): 'scrollback': CONFIG['MAX_CHAT_SCROLLBACK'], 'digest': get_random_captcha_digest_for(user), 'pingpong': CONFIG['TASK_BROADCAST_PING'], + 'maxlength': { + 'comment': CONFIG['CHAT_COMMENT_MAX_LENGTH'], + 'name': CONFIG['CHAT_NAME_MAX_LENGTH'], + 'password': CONFIG['CHAT_TRIPCODE_PASSWORD_MAX_LENGTH'], + }, }) while True: payload = await queue.get()