diff --git a/anonstream/routes/core.py b/anonstream/routes/core.py index 73637c5..69f0b15 100644 --- a/anonstream/routes/core.py +++ b/anonstream/routes/core.py @@ -3,14 +3,14 @@ import math -from quart import current_app, request, render_template, abort, make_response, redirect, url_for, abort, send_from_directory -from werkzeug.exceptions import TooManyRequests +from quart import current_app, request, render_template, abort, make_response, redirect, url_for, send_from_directory +from werkzeug.exceptions import NotFound, TooManyRequests from anonstream.access import add_failure, pop_failure from anonstream.captcha import get_captcha_image, get_random_captcha_digest from anonstream.segments import segments, StopSendingSegments from anonstream.stream import is_online, get_stream_uptime -from anonstream.user import watching, create_eyes, renew_eyes, EyesException, RatelimitedEyes +from anonstream.user import watching, create_eyes, renew_eyes, EyesException, RatelimitedEyes, TooManyEyes from anonstream.routes.wrappers import with_user_from, auth_required, clean_cache_headers, generate_and_add_user from anonstream.helpers.captcha import check_captcha_digest, Answer from anonstream.utils.security import generate_csp @@ -43,28 +43,40 @@ async def home(timestamp, user_or_token): @with_user_from(request) async def stream(timestamp, user): if not is_online(): - return abort(404) - - try: - eyes_id = create_eyes(user, dict(request.headers)) - except RatelimitedEyes as e: - retry_after, *_ = e.args - return TooManyRequests(), {'Retry-After': math.ceil(retry_after)} - except EyesException: - return abort(429) - - def segment_read_hook(uri): + raise NotFound('The stream is offline.') + else: try: - 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"]}') - watching(user) - - generator = segments(segment_read_hook, token=user['token']) - response = await make_response(generator) - response.headers['Content-Type'] = 'video/mp4' - response.timeout = None + eyes_id = create_eyes(user, dict(request.headers)) + except RatelimitedEyes as e: + retry_after, *_ = e.args + error = TooManyRequests( + f'You have requested the stream recently. ' + f'Try again in {retry_after:.1f} seconds.' + ) + response = await current_app.handle_http_exception(error) + response = await make_response(response) + response.headers['Retry-After'] = math.ceil(retry_after) + raise abort(response) + except TooManyEyes as e: + n_eyes, *_ = e.args + raise TooManyRequests( + f'You have made {n_eyes} concurrent requests for the stream. ' + f'End one of those before making a new request.' + ) + else: + def segment_read_hook(uri): + try: + 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"]}') + watching(user) + generator = segments(segment_read_hook, token=user['token']) + response = await make_response(generator) + response.headers['Content-Type'] = 'video/mp4' + response.timeout = None return response @current_app.route('/login') diff --git a/anonstream/routes/wrappers.py b/anonstream/routes/wrappers.py index a18bd46..339489b 100644 --- a/anonstream/routes/wrappers.py +++ b/anonstream/routes/wrappers.py @@ -8,7 +8,8 @@ import string from functools import wraps from urllib.parse import quote, unquote -from quart import current_app, request, abort, make_response, render_template, request +from quart import current_app, request, make_response, render_template, request, url_for, Markup +from werkzeug.exceptions import BadRequest, Unauthorized, Forbidden from werkzeug.security import check_password_hash from anonstream.broadcast import broadcast @@ -57,18 +58,18 @@ def auth_required(f): 'their terminal.' ) if request.authorization is None: - body = ( - f'\n' - f'

{hint}

\n' - ) + description = hint else: - body = ( - f'\n' - f'

Wrong username or password. Refresh the page to try again.

\n' - f'

{hint}

\n' + description = Markup( + f'Wrong username or password. Refresh the page to try again. ' + f'
' + f'{hint}' ) - return body, 401, {'WWW-Authenticate': 'Basic'} - + error = Unauthorized(description) + response = await current_app.handle_http_exception(error) + response = await make_response(response) + response.headers['WWW-Authenticate'] = 'Basic' + return response return wrapper def generate_and_add_user(timestamp, token=None, broadcaster=False): @@ -103,7 +104,11 @@ def with_user_from(context, fallback_to_token=False): # Reject invalid tokens if isinstance(token, str) and not RE_TOKEN.fullmatch(token): - raise abort(400) + raise BadRequest(Markup( + f'Your token contains disallowed characters or is too ' + f'long. Tokens must match this regular expression:
' + f'{RE_TOKEN.pattern}' + )) # Only logged in broadcaster may have the broadcaster's token if ( @@ -111,7 +116,13 @@ def with_user_from(context, fallback_to_token=False): and isinstance(token, str) and hmac.compare_digest(token, CONFIG['AUTH_TOKEN']) ): - raise abort(401) + raise Unauthorized(Markup( + f"You are using the broadcaster's token but you are " + f"not logged in. The broadcaster should " + f"click here " + f"and log in with the credentials printed in their " + f"terminal when they started anonstream." + )) # Create response user = USERS_BY_TOKEN.get(token) @@ -123,7 +134,12 @@ def with_user_from(context, fallback_to_token=False): #assert not broadcaster response = await f(timestamp, token, *args, **kwargs) else: - raise abort(403) + raise Forbidden(Markup( + f"You have not solved the access captcha. " + f"" + f"Click here." + f"" + )) else: if user is None: user = generate_and_add_user(timestamp, token, broadcaster) diff --git a/anonstream/templates/error.html b/anonstream/templates/error.html index 42c71d3..38aa7ce 100644 --- a/anonstream/templates/error.html +++ b/anonstream/templates/error.html @@ -13,15 +13,30 @@ text-align: center; text-shadow: 2px 0px 1px orangered; } + main { + margin: auto; + max-width: 52rem; + } h1 { font-size: 32pt; } a { color: #42a5d7; } + code { + background-color: #333; + padding: 2px; + border-radius: 2px; + overflow-wrap: anywhere; + } -

{{ error.code }} {{ error.name }}

+
+

{{ error.code }} {{ error.name }}

+ {% if error.description != error.__class__.description %} +

{{ error.description }}

+ {% endif %} +
diff --git a/anonstream/user.py b/anonstream/user.py index d9f4eea..ccdb360 100644 --- a/anonstream/user.py +++ b/anonstream/user.py @@ -259,7 +259,7 @@ def create_eyes(timestamp, user, headers): # 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 + raise TooManyEyes(len(user['eyes']['current'])) # Treat eyes as a queue, expire old eyes upon creating new eyes # if the limit would have been exceeded otherwise elif user['eyes']['current']: