Logicaler project structure, see rest of commit message

Incoming requests are handled in anonstream/routes/. Route handlers
mainly depend on files in anonstream/, which in turn depend on files in
anonstream/helpers/ and anonstream/utils/. Utils are pure functions and
helpers are almost pure functions; they don't mutate state but they
do depend on the global app config.
このコミットが含まれているのは:
n9k 2022-02-17 00:20:51 +00:00
コミット 7dbcd43f30
12個のファイルの変更103行の追加84行の削除

ファイルの表示

@ -5,7 +5,7 @@ from collections import OrderedDict
from quart import Quart from quart import Quart
from werkzeug.security import generate_password_hash from werkzeug.security import generate_password_hash
from anonstream.utils.users import generate_token from anonstream.utils.user import generate_token
from anonstream.segments import DirectoryCache from anonstream.segments import DirectoryCache
async def create_app(): async def create_app():

ファイルの表示

@ -3,8 +3,8 @@ from datetime import datetime
from quart import escape from quart import escape
from anonstream.users import users_for_websocket from anonstream.user import users_for_websocket
from anonstream.utils.chat import generate_nonce_hash from anonstream.helpers.chat import generate_nonce_hash
class Rejected(Exception): class Rejected(Exception):
pass pass
@ -13,9 +13,9 @@ async def broadcast(websockets, payload):
for queue in websockets: for queue in websockets:
await queue.put(payload) await queue.put(payload)
async def add_chat_message(chat, users, websockets, secret, user, nonce, comment): async def add_chat_message(chat, users, websockets, user, nonce, comment):
# check message # check message
nonce_hash = generate_nonce_hash(secret, nonce) nonce_hash = generate_nonce_hash(nonce)
if nonce_hash in chat['nonce_hashes']: if nonce_hash in chat['nonce_hashes']:
raise Rejected('Discarded suspected duplicate message') raise Rejected('Discarded suspected duplicate message')
if len(comment) == 0: if len(comment) == 0:

9
anonstream/helpers/chat.py ノーマルファイル
ファイルの表示

@ -0,0 +1,9 @@
import hashlib
from quart import current_app
CONFIG = current_app.config
def generate_nonce_hash(nonce):
parts = CONFIG['SECRET_KEY'] + b'nonce-hash\0' + nonce.encode()
return hashlib.sha256(parts).digest()

54
anonstream/helpers/user.py ノーマルファイル
ファイルの表示

@ -0,0 +1,54 @@
import hashlib
import base64
from collections import OrderedDict
from math import inf
from quart import current_app
CONFIG = current_app.config
def generate_token_hash(token):
parts = CONFIG['SECRET_KEY'] + b'token-hash\0' + token.encode()
digest = hashlib.sha256(parts).digest()
return base64.b32encode(digest)[:26].lower().decode()
def generate_user(secret, token, broadcaster, timestamp):
return {
'token': token,
'token_hash': generate_token_hash(token),
'broadcaster': broadcaster,
'name': None,
'color': '#c7007f',
'tripcode': None,
'notices': OrderedDict(),
'seen': {
'first': timestamp,
'last': timestamp,
},
'watching_last': -inf,
}
def get_default_name(user):
return (
CONFIG['DEFAULT_HOST_NAME']
if user['broadcaster'] else
CONFIG['DEFAULT_ANON_NAME']
)
def is_watching(timestamp, user):
return user['watching_last'] >= timestamp - CONFIG['THRESHOLD_IDLE']
def is_idle(timestamp, user):
return is_present(timestamp, user) and not is_watching(timestamp, user)
def is_present(timestamp, user):
return user['seen']['last'] >= timestamp - CONFIG['THRESHOLD_ABSENT']
def is_absent(timestamp, user):
return not is_present(timestamp, user)
def is_visible(timestamp, messages, user):
has_visible_messages = any(
message['token'] == user['token'] for message in messages
)
return has_visible_messages or is_present(timestamp, user)

ファイルの表示

@ -1,9 +1,10 @@
from quart import current_app, request, render_template, redirect, url_for from quart import current_app, request, render_template, redirect, url_for
from anonstream.stream import get_stream_title from anonstream.stream import get_stream_title
from anonstream.users import get_default_name, add_notice, pop_notice from anonstream.user import add_notice, pop_notice
from anonstream.chat import add_chat_message, Rejected from anonstream.chat import add_chat_message, Rejected
from anonstream.routes.wrappers import with_user_from from anonstream.routes.wrappers import with_user_from
from anonstream.helpers.user import get_default_name
from anonstream.utils.chat import generate_nonce from anonstream.utils.chat import generate_nonce
@current_app.route('/info.html') @current_app.route('/info.html')
@ -52,7 +53,6 @@ async def nojs_submit_message(user):
chat=current_app.chat, chat=current_app.chat,
users=current_app.users, users=current_app.users,
websockets=current_app.websockets, websockets=current_app.websockets,
secret=current_app.config['SECRET_KEY'],
user=user, user=user,
nonce=nonce, nonce=nonce,
comment=comment, comment=comment,

ファイルの表示

@ -15,8 +15,6 @@ async def live(user):
queue=queue, queue=queue,
messages=current_app.chat['messages'].values(), messages=current_app.chat['messages'].values(),
users=current_app.users, users=current_app.users,
default_host_name=current_app.config['DEFAULT_HOST_NAME'],
default_anon_name=current_app.config['DEFAULT_ANON_NAME'],
) )
consumer = websocket_inbound( consumer = websocket_inbound(
queue=queue, queue=queue,
@ -24,7 +22,6 @@ async def live(user):
users=current_app.users, users=current_app.users,
connected_websockets=current_app.websockets, connected_websockets=current_app.websockets,
user=user, user=user,
secret=current_app.config['SECRET_KEY'],
) )
try: try:
await asyncio.gather(producer, consumer) await asyncio.gather(producer, consumer)

ファイルの表示

@ -4,9 +4,10 @@ from functools import wraps
from quart import current_app, request, abort, make_response from quart import current_app, request, abort, make_response
from werkzeug.security import check_password_hash from werkzeug.security import check_password_hash
from anonstream.users import sunset, user_for_websocket from anonstream.user import sunset, user_for_websocket
from anonstream.websocket import broadcast from anonstream.websocket import broadcast
from anonstream.utils.users import generate_token, generate_user from anonstream.helpers.user import generate_user
from anonstream.utils.user import generate_token
def check_auth(context): def check_auth(context):
auth = context.authorization auth = context.authorization

ファイルの表示

@ -4,20 +4,16 @@ from math import inf
from quart import current_app from quart import current_app
from anonstream.wrappers import with_timestamp, with_first_argument from anonstream.wrappers import with_timestamp, with_first_argument
from anonstream.utils.users import user_for_websocket from anonstream.helpers.user import is_visible
from anonstream.utils.user import user_for_websocket
from anonstream.utils import listmap from anonstream.utils import listmap
def get_default_name(user): CONFIG = current_app.config
return (
current_app.config['DEFAULT_HOST_NAME']
if user['broadcaster'] else
current_app.config['DEFAULT_ANON_NAME']
)
def add_notice(user, notice): def add_notice(user, notice):
notice_id = time.time_ns() // 1_000_000 notice_id = time.time_ns() // 1_000_000
user['notices'][notice_id] = notice user['notices'][notice_id] = notice
if len(user['notices']) > current_app.config['LIMIT_NOTICES']: if len(user['notices']) > CONFIG['MAX_NOTICES']:
user['notices'].popitem(last=False) user['notices'].popitem(last=False)
return notice_id return notice_id
@ -42,31 +38,13 @@ def users_for_websocket(timestamp, messages, users):
for user in visible_users for user in visible_users
} }
def is_watching(timestamp, user):
return user['watching_last'] >= timestamp - current_app.config['THRESHOLD_IDLE']
def is_idle(timestamp, user):
return is_present(timestamp, user) and not is_watching(timestamp, user)
def is_present(timestamp, user):
return user['seen']['last'] >= timestamp - current_app.config['THRESHOLD_ABSENT']
def is_absent(timestamp, user):
return not is_present(timestamp, user)
def is_visible(timestamp, messages, user):
has_visible_messages = any(
message['token'] == user['token'] for message in messages
)
return has_visible_messages or is_present(timestamp, user)
last_checkup = -inf last_checkup = -inf
def sunset(messages, users): def sunset(messages, users):
global last_checkup global last_checkup
timestamp = int(time.time()) timestamp = int(time.time())
if timestamp - last_checkup < current_app.config['USER_CHECKUP_PERIOD']: if timestamp - last_checkup < CONFIG['USER_CHECKUP_PERIOD']:
return [] return []
to_delete = [] to_delete = []

ファイルの表示

@ -8,10 +8,6 @@ class NonceReuse(Exception):
def generate_nonce(): def generate_nonce():
return secrets.token_urlsafe(16) return secrets.token_urlsafe(16)
def generate_nonce_hash(secret, nonce):
parts = secret + b'nonce-hash\0' + nonce.encode()
return hashlib.sha256(parts).digest()
def message_for_websocket(users, message): def message_for_websocket(users, message):
message_keys = ('id', 'date', 'time_minutes', 'time_seconds', 'markup') message_keys = ('id', 'date', 'time_minutes', 'time_seconds', 'markup')
user_keys = ('token_hash',) user_keys = ('token_hash',)

14
anonstream/utils/user.py ノーマルファイル
ファイルの表示

@ -0,0 +1,14 @@
import base64
import hashlib
import secrets
from collections import OrderedDict
from math import inf
def generate_token():
return secrets.token_hex(16)
def user_for_websocket(user, include_token_hash=True):
keys = ['broadcaster', 'name', 'color', 'tripcode']
if include_token_hash:
keys.append('token_hash')
return {key: user[key] for key in keys}

ファイルの表示

@ -1,35 +0,0 @@
import base64
import hashlib
import secrets
from collections import OrderedDict
from math import inf
def generate_token():
return secrets.token_hex(16)
def generate_token_hash(secret, token):
parts = secret + b'token-hash\0' + token.encode()
digest = hashlib.sha256(parts).digest()
return base64.b32encode(digest)[:26].lower().decode()
def generate_user(secret, token, broadcaster, timestamp):
return {
'token': token,
'token_hash': generate_token_hash(secret, token),
'broadcaster': broadcaster,
'name': None,
'color': '#c7007f',
'tripcode': None,
'notices': OrderedDict(),
'seen': {
'first': timestamp,
'last': timestamp,
},
'watching_last': -inf,
}
def user_for_websocket(user, include_token_hash=True):
keys = ['broadcaster', 'name', 'color', 'tripcode']
if include_token_hash:
keys.append('token_hash')
return {key: user[key] for key in keys}

ファイルの表示

@ -1,16 +1,19 @@
import asyncio import asyncio
from quart import websocket from quart import current_app, websocket
from anonstream.stream import get_stream_title, get_stream_uptime from anonstream.stream import get_stream_title, get_stream_uptime
from anonstream.chat import broadcast, add_chat_message, Rejected from anonstream.chat import broadcast, add_chat_message, Rejected
from anonstream.users import is_present, users_for_websocket, see from anonstream.user import users_for_websocket, see
from anonstream.wrappers import with_first_argument from anonstream.wrappers import with_first_argument
from anonstream.helpers.user import is_present
from anonstream.utils import listmap from anonstream.utils import listmap
from anonstream.utils.chat import generate_nonce, message_for_websocket from anonstream.utils.chat import generate_nonce, message_for_websocket
from anonstream.utils.websocket import parse_websocket_data, Malformed from anonstream.utils.websocket import parse_websocket_data, Malformed
async def websocket_outbound(queue, messages, users, default_host_name, default_anon_name): CONFIG = current_app.config
async def websocket_outbound(queue, messages, users):
payload = { payload = {
'type': 'init', 'type': 'init',
'nonce': generate_nonce(), 'nonce': generate_nonce(),
@ -21,14 +24,17 @@ async def websocket_outbound(queue, messages, users, default_host_name, default_
messages, messages,
), ),
'users': users_for_websocket(messages, users), 'users': users_for_websocket(messages, users),
'default': {True: default_host_name, False: default_anon_name}, 'default': {
True: CONFIG['DEFAULT_HOST_NAME'],
False: CONFIG['DEFAULT_ANON_NAME'],
},
} }
await websocket.send_json(payload) await websocket.send_json(payload)
while True: while True:
payload = await queue.get() payload = await queue.get()
await websocket.send_json(payload) await websocket.send_json(payload)
async def websocket_inbound(queue, chat, users, connected_websockets, user, secret): async def websocket_inbound(queue, chat, users, connected_websockets, user):
while True: while True:
receipt = await websocket.receive_json() receipt = await websocket.receive_json()
see(user) see(user)
@ -46,7 +52,6 @@ async def websocket_inbound(queue, chat, users, connected_websockets, user, secr
chat, chat,
users, users,
connected_websockets, connected_websockets,
secret,
user, user,
nonce, nonce,
comment, comment,