コミットを比較

...

4 コミット

作成者 SHA1 メッセージ 日付
n9k a41f0d4f14 Escape disallowed cookie characters 2022-06-20 04:15:09 +00:00
n9k 46f9b0ec08 Reset websocket aliveness timer on first connecting
This should eliminate the possibilty of the websocket-closing background
task closing a newly opened websocket that hasn't yet ponged our ping
(if we have even sent a ping yet).
2022-06-20 04:15:09 +00:00
n9k 22c84bc230 Give timestamp to route handlers 2022-06-20 04:15:09 +00:00
n9k 90e1e2099a Manual static folder 2022-06-20 04:15:08 +00:00
5個のファイルの変更62行の追加25行の削除

ファイルの表示

@ -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