コミットを比較
4 コミット
a31c1eafc4
...
a41f0d4f14
作成者 | SHA1 | 日付 |
---|---|---|
n9k | a41f0d4f14 | |
n9k | 46f9b0ec08 | |
n9k | 22c84bc230 | |
n9k | 90e1e2099a |
|
@ -12,7 +12,7 @@ from anonstream.quart import Quart
|
|||
compress = Compress()
|
||||
|
||||
def create_app(toml_config):
|
||||
app = Quart('anonstream')
|
||||
app = Quart('anonstream', static_folder=None)
|
||||
app.jinja_options['trim_blocks'] = True
|
||||
app.jinja_options['lstrip_blocks'] = True
|
||||
|
||||
|
|
|
@ -3,19 +3,21 @@
|
|||
|
||||
import math
|
||||
|
||||
from quart import current_app, request, render_template, abort, make_response, redirect, url_for, abort
|
||||
from quart import current_app, request, render_template, abort, make_response, redirect, url_for, abort, send_from_directory
|
||||
from werkzeug.exceptions import TooManyRequests
|
||||
|
||||
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 watching, create_eyes, renew_eyes, EyesException, RatelimitedEyes
|
||||
from anonstream.routes.wrappers import with_user_from, auth_required
|
||||
from anonstream.routes.wrappers import with_user_from, auth_required, clean_cache_headers
|
||||
from anonstream.utils.security import generate_csp
|
||||
|
||||
STATIC_DIRECTORY = current_app.root_path / 'static'
|
||||
|
||||
@current_app.route('/')
|
||||
@with_user_from(request)
|
||||
async def home(user):
|
||||
async def home(timestamp, user):
|
||||
return await render_template(
|
||||
'home.html',
|
||||
csp=generate_csp(),
|
||||
|
@ -24,7 +26,7 @@ async def home(user):
|
|||
|
||||
@current_app.route('/stream.mp4')
|
||||
@with_user_from(request)
|
||||
async def stream(user):
|
||||
async def stream(timestamp, user):
|
||||
if not is_online():
|
||||
return abort(404)
|
||||
|
||||
|
@ -57,10 +59,16 @@ async def login():
|
|||
|
||||
@current_app.route('/captcha.jpg')
|
||||
@with_user_from(request)
|
||||
async def captcha(user):
|
||||
async def captcha(timestamp, user):
|
||||
digest = request.args.get('digest', '')
|
||||
image = get_captcha_image(digest)
|
||||
if image is None:
|
||||
return abort(410)
|
||||
else:
|
||||
return image, {'Content-Type': 'image/jpeg'}
|
||||
|
||||
@current_app.route('/static/<filename>')
|
||||
@with_user_from(request)
|
||||
@clean_cache_headers
|
||||
async def static(timestamp, user, filename):
|
||||
return await send_from_directory(STATIC_DIRECTORY, filename)
|
||||
|
|
|
@ -19,7 +19,7 @@ USERS_BY_TOKEN = current_app.users_by_token
|
|||
|
||||
@current_app.route('/stream.html')
|
||||
@with_user_from(request)
|
||||
async def nojs_stream(user):
|
||||
async def nojs_stream(timestamp, user):
|
||||
return await render_template(
|
||||
'nojs_stream.html',
|
||||
csp=generate_csp(),
|
||||
|
@ -29,7 +29,7 @@ async def nojs_stream(user):
|
|||
|
||||
@current_app.route('/info.html')
|
||||
@with_user_from(request)
|
||||
async def nojs_info(user):
|
||||
async def nojs_info(timestamp, user):
|
||||
update_presence(user)
|
||||
uptime, viewership = get_stream_uptime_and_viewership()
|
||||
return await render_template(
|
||||
|
@ -45,7 +45,7 @@ async def nojs_info(user):
|
|||
|
||||
@current_app.route('/chat/messages.html')
|
||||
@with_user_from(request)
|
||||
async def nojs_chat_messages(user):
|
||||
async def nojs_chat_messages(timestamp, user):
|
||||
reading(user)
|
||||
return await render_template_with_etag(
|
||||
'nojs_chat_messages.html',
|
||||
|
@ -60,12 +60,12 @@ async def nojs_chat_messages(user):
|
|||
|
||||
@current_app.route('/chat/messages')
|
||||
@with_user_from(request)
|
||||
async def nojs_chat_messages_redirect(user):
|
||||
async def nojs_chat_messages_redirect(timestamp, user):
|
||||
return redirect(url_for('nojs_chat_messages', token=user['token'], _anchor='end'))
|
||||
|
||||
@current_app.route('/chat/users.html')
|
||||
@with_user_from(request)
|
||||
async def nojs_chat_users(user):
|
||||
async def nojs_chat_users(timestamp, user):
|
||||
users_by_presence = get_users_by_presence()
|
||||
return await render_template_with_etag(
|
||||
'nojs_chat_users.html',
|
||||
|
@ -80,7 +80,7 @@ async def nojs_chat_users(user):
|
|||
|
||||
@current_app.route('/chat/form.html')
|
||||
@with_user_from(request)
|
||||
async def nojs_chat_form(user):
|
||||
async def nojs_chat_form(timestamp, user):
|
||||
state_id = request.args.get('state', type=int)
|
||||
state = pop_state(user, state_id)
|
||||
prefer_chat_form = request.args.get('landing') != 'appearance'
|
||||
|
@ -100,7 +100,7 @@ async def nojs_chat_form(user):
|
|||
|
||||
@current_app.post('/chat/form')
|
||||
@with_user_from(request)
|
||||
async def nojs_chat_form_redirect(user):
|
||||
async def nojs_chat_form_redirect(timestamp, user):
|
||||
comment = (await request.form).get('comment', '')
|
||||
if comment:
|
||||
state_id = add_state(
|
||||
|
@ -113,7 +113,7 @@ async def nojs_chat_form_redirect(user):
|
|||
|
||||
@current_app.post('/chat/message')
|
||||
@with_user_from(request)
|
||||
async def nojs_submit_message(user):
|
||||
async def nojs_submit_message(timestamp, user):
|
||||
form = await request.form
|
||||
|
||||
comment = form.get('comment', '')
|
||||
|
@ -160,7 +160,7 @@ async def nojs_submit_message(user):
|
|||
|
||||
@current_app.post('/chat/appearance')
|
||||
@with_user_from(request)
|
||||
async def nojs_submit_appearance(user):
|
||||
async def nojs_submit_appearance(timestamp, user):
|
||||
form = await request.form
|
||||
|
||||
# Collect form data
|
||||
|
|
|
@ -2,9 +2,6 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import asyncio
|
||||
|
||||
from math import inf
|
||||
|
||||
from quart import current_app, websocket
|
||||
|
||||
from anonstream.user import see, reading
|
||||
|
@ -13,10 +10,10 @@ from anonstream.routes.wrappers import with_user_from
|
|||
|
||||
@current_app.websocket('/live')
|
||||
@with_user_from(websocket)
|
||||
async def live(user):
|
||||
async def live(timestamp, user):
|
||||
queue = asyncio.Queue()
|
||||
user['websockets'][queue] = -inf
|
||||
reading(user)
|
||||
user['websockets'][queue] = timestamp
|
||||
reading(user, timestamp=timestamp)
|
||||
|
||||
producer = websocket_outbound(queue, user)
|
||||
consumer = websocket_inbound(queue, user)
|
||||
|
|
|
@ -6,6 +6,7 @@ import hmac
|
|||
import re
|
||||
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 werkzeug.security import check_password_hash
|
||||
|
@ -31,6 +32,12 @@ TOKEN_ALPHABET = (
|
|||
)
|
||||
RE_TOKEN = re.compile(r'[%s]{1,256}' % re.escape(TOKEN_ALPHABET))
|
||||
|
||||
def try_unquote(string):
|
||||
if string is None:
|
||||
return None
|
||||
else:
|
||||
return unquote(string)
|
||||
|
||||
def check_auth(context):
|
||||
auth = context.authorization
|
||||
return (
|
||||
|
@ -77,7 +84,7 @@ def with_user_from(context):
|
|||
else:
|
||||
token = (
|
||||
context.args.get('token')
|
||||
or context.cookies.get('token')
|
||||
or try_unquote(context.cookies.get('token'))
|
||||
or generate_token()
|
||||
)
|
||||
if hmac.compare_digest(token, CONFIG['AUTH_TOKEN']):
|
||||
|
@ -104,10 +111,10 @@ def with_user_from(context):
|
|||
USERS_UPDATE_BUFFER.add(token)
|
||||
|
||||
# Set cookie
|
||||
response = await f(user, *args, **kwargs)
|
||||
if context.cookies.get('token') != token:
|
||||
response = await f(timestamp, user, *args, **kwargs)
|
||||
if try_unquote(context.cookies.get('token')) != token:
|
||||
response = await make_response(response)
|
||||
response.headers['Set-Cookie'] = f'token={token}; path=/'
|
||||
response.headers['Set-Cookie'] = f'token={quote(token)}; path=/'
|
||||
return response
|
||||
|
||||
return wrapper
|
||||
|
@ -127,3 +134,28 @@ async def render_template_with_etag(template, deferred_kwargs, **kwargs):
|
|||
**kwargs,
|
||||
)
|
||||
return rendered_template, {'ETag': etag}
|
||||
|
||||
def clean_cache_headers(f):
|
||||
@wraps(f)
|
||||
async def wrapper(*args, **kwargs):
|
||||
response = await f(*args, **kwargs)
|
||||
|
||||
# Remove Last-Modified
|
||||
try:
|
||||
response.headers.pop('Last-Modified')
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# Obfuscate ETag
|
||||
try:
|
||||
original_etag = response.headers['ETag']
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
parts = CONFIG['SECRET_KEY'] + b'etag\0' + original_etag.encode()
|
||||
tag = hashlib.sha256(parts).hexdigest()
|
||||
response.headers['ETag'] = f'"{tag}"'
|
||||
|
||||
return response
|
||||
|
||||
return wrapper
|
||||
|
|
読み込み中…
新しいイシューから参照