anonstream/anonstream/routes/core.py

150 行
6.2 KiB
Python
Raw 通常表示 履歴

2022-06-16 10:12:37 +09:00
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
2022-03-07 23:51:59 +09:00
# SPDX-License-Identifier: AGPL-3.0-or-later
import math
import re
2022-06-22 16:52:07 +09:00
from quart import current_app, request, render_template, abort, make_response, redirect, url_for, send_from_directory
from werkzeug.exceptions import Forbidden, NotFound, TooManyRequests
2022-06-22 14:00:43 +09:00
from anonstream.access import add_failure, pop_failure
from anonstream.captcha import get_captcha_image, get_random_captcha_digest
2022-07-28 19:48:33 +09:00
from anonstream.locale import get_lang_and_locale_from, get_lang_from, get_locale_from
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, TooManyEyes, ensure_allowedness, Blacklisted, SecretClub
from anonstream.routes.wrappers import with_user_from, auth_required, generate_and_add_user, clean_cache_headers, etag_conditional
2022-06-22 14:00:43 +09:00
from anonstream.helpers.captcha import check_captcha_digest, Answer
2022-03-07 16:11:49 +09:00
from anonstream.utils.security import generate_csp
from anonstream.utils.user import identifying_string
2022-06-25 13:28:13 +09:00
from anonstream.wrappers import with_timestamp
2022-06-22 14:00:43 +09:00
CAPTCHA_SIGNER = current_app.captcha_signer
2022-06-19 17:26:38 +09:00
STATIC_DIRECTORY = current_app.root_path / 'static'
2022-07-28 19:48:33 +09:00
LANG = current_app.lang
2022-06-19 17:26:38 +09:00
@current_app.route('/')
@with_user_from(request, fallback_to_token=True, ignore_allowedness=True)
2022-06-22 14:00:43 +09:00
async def home(timestamp, user_or_token):
2022-07-29 17:04:54 +09:00
lang, locale = get_lang_and_locale_from(request, burrow=('anonstream',))
2022-06-22 14:00:43 +09:00
match user_or_token:
case str() | None as token:
2022-06-22 14:00:43 +09:00
failure_id = request.args.get('failure', type=int)
2022-07-28 19:48:33 +09:00
failure = pop_failure(failure_id)
2022-06-22 14:00:43 +09:00
response = await render_template(
'captcha.html',
csp=generate_csp(),
token=token,
2022-07-29 17:04:54 +09:00
request_lang=get_lang_from(request, validate=False),
locale=locale['captcha'],
2022-06-22 14:00:43 +09:00
digest=get_random_captcha_digest(),
2022-07-29 17:04:54 +09:00
failure=locale['internal'].get(failure),
2022-06-22 14:00:43 +09:00
)
case dict() as user:
try:
ensure_allowedness(user, timestamp=timestamp)
except Blacklisted:
2022-07-29 17:04:54 +09:00
raise Forbidden(locale['error']['blacklisted'])
except SecretClub:
# TODO allow changing tripcode
2022-07-29 17:04:54 +09:00
raise Forbidden(locale['error']['not_whitelisted'])
else:
response = await render_template(
'home.html',
csp=generate_csp(),
user=user,
2022-07-29 17:04:54 +09:00
lang=lang,
default_lang=LANG,
locale=locale['home'],
version=current_app.version,
)
2022-06-22 14:00:43 +09:00
return response
@current_app.route('/stream.mp4')
@with_user_from(request)
2022-06-19 17:51:42 +09:00
async def stream(timestamp, user):
2022-07-28 19:48:33 +09:00
locale = get_locale_from(request)['anonstream']['error']
if not is_online():
2022-07-28 19:48:33 +09:00
raise NotFound(locale['offline'])
2022-06-22 16:52:07 +09:00
else:
try:
eyes_id = create_eyes(user, tuple(request.headers))
2022-06-22 16:52:07 +09:00
except RatelimitedEyes as e:
retry_after, *_ = e.args
2022-07-28 19:48:33 +09:00
error = TooManyRequests(locale['ratelimit'] % retry_after)
2022-06-22 16:52:07 +09:00
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
2022-07-28 19:48:33 +09:00
raise TooManyRequests(locale['limit'] % n_eyes)
2022-06-22 16:52:07 +09:00
else:
2022-06-25 13:28:13 +09:00
@with_timestamp(precise=True)
def segment_read_hook(timestamp, uri):
user['last']['seen'] = timestamp
2022-06-22 16:52:07 +09:00
try:
2022-06-25 13:28:13 +09:00
renew_eyes(timestamp, user, eyes_id, just_read_new_segment=True)
2022-06-22 16:52:07 +09:00
except EyesException as e:
raise StopSendingSegments(
f'eyes {eyes_id} not allowed: {e!r}'
) from e
2022-06-25 13:28:13 +09:00
else:
user['last']['watching'] = timestamp
print(f'{uri}: \033[37m{eyes_id}\033[0m~{identifying_string(user)}')
generator = segments(segment_read_hook, token=f'\033[35m{user["token"]}\033[0m')
2022-06-22 16:52:07 +09:00
response = await make_response(generator)
response.headers['Content-Type'] = 'video/mp4'
response.timeout = None
return response
@current_app.route('/login')
@auth_required
async def login():
2022-07-29 17:04:54 +09:00
return redirect(url_for('home', lang=get_lang_from(request)), 303)
@current_app.route('/captcha.jpg')
2022-06-22 14:00:43 +09:00
@with_user_from(request, fallback_to_token=True)
async def captcha(timestamp, user_or_token):
digest = request.args.get('digest', '')
image = get_captcha_image(digest)
if image is None:
return abort(410)
else:
return image, {'Content-Type': 'image/jpeg'}
2022-06-19 17:26:38 +09:00
2022-06-22 14:00:43 +09:00
@current_app.post('/access')
@with_user_from(request, fallback_to_token=True, ignore_allowedness=True)
2022-06-22 14:00:43 +09:00
async def access(timestamp, user_or_token):
match user_or_token:
case str() | None as token:
2022-06-22 14:00:43 +09:00
form = await request.form
digest = form.get('digest', '')
answer = form.get('answer', '')
match check_captcha_digest(CAPTCHA_SIGNER, digest, answer):
case Answer.MISSING:
2022-07-28 19:48:33 +09:00
failure_id = add_failure('captcha_required')
2022-06-22 14:00:43 +09:00
case Answer.BAD:
2022-07-28 19:48:33 +09:00
failure_id = add_failure('captcha_incorrect')
2022-06-22 14:00:43 +09:00
case Answer.EXPIRED:
2022-07-28 19:48:33 +09:00
failure_id = add_failure('captcha_expired')
2022-06-22 14:00:43 +09:00
case Answer.OK:
failure_id = None
user = generate_and_add_user(timestamp, token, verified=True)
case dict() as user:
2022-07-29 17:04:54 +09:00
token = user['token']
failure_id = None
lang = get_lang_from(request, validate=failure_id is None)
url = url_for('home', token=token, lang=lang, failure=failure_id)
2022-06-22 14:00:43 +09:00
return redirect(url, 303)
2022-07-20 15:04:55 +09:00
@current_app.route('/static/<path:filename>')
2022-06-19 17:26:38 +09:00
@with_user_from(request)
@etag_conditional
2022-06-19 17:26:38 +09:00
@clean_cache_headers
2022-06-19 17:51:42 +09:00
async def static(timestamp, user, filename):
response = await send_from_directory(STATIC_DIRECTORY, filename)
if filename in {'style.css', 'anonstream.js'}:
response.headers['Cache-Control'] = 'no-cache'
return response