Ctrl K
ComunidadGuías…

src/webui/services/room_password.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Room password entry: verify a share-link player's password, issue a room token.

The room password and room tokens are owned by ``room_access``; this service
only adds the transport-side policy (token lifetime, running the slow hash
check off the event loop).
"""

from __future__ import annotations

import asyncio
import logging
import math
import os
from collections.abc import Awaitable, Callable, Mapping
from typing import Any

from src.engine.modules import room_access

logger = logging.getLogger("trpg")

ROOM_TOKEN_TTL_ENV = "TRPG_ROOM_TOKEN_TTL_DAYS"
MAX_ROOM_TOKEN_TTL_DAYS = 365
ROOM_PASSWORD_CHANGED = "房间密码已更改,请重新输入"
ROOM_PASSWORD_WRONG = "房间密码错误"
# Set on the response of a wrong-password attempt: the only outcome the
# abuse guard counts against the room password rate limit.
_REJECTED_MARK = "diceframe_room_password_rejected"


def mark_room_password_rejected(response: Any) -> None:
    response[_REJECTED_MARK] = True


def is_room_password_rejected(response: Any) -> bool:
    try:
        return bool(response.get(_REJECTED_MARK))
    except AttributeError:
        return False


def room_token_ttl_seconds(environ: Mapping[str, str] | None = None) -> int:
    """Room token lifetime: ``TRPG_ROOM_TOKEN_TTL_DAYS`` (default 30, max 365)."""
    raw = str((os.environ if environ is None else environ).get(ROOM_TOKEN_TTL_ENV, "") or "").strip()
    if not raw:
        return room_access.DEFAULT_ROOM_TOKEN_TTL_SECONDS
    try:
        days = float(raw)
    except ValueError:
        days = math.nan
    if not math.isfinite(days) or days <= 0:
        logger.warning("%s=%r 无效,使用默认房间令牌有效期", ROOM_TOKEN_TTL_ENV, raw)
        return room_access.DEFAULT_ROOM_TOKEN_TTL_SECONDS
    return max(60, int(min(days, MAX_ROOM_TOKEN_TTL_DAYS) * 86400))


async def verify_and_issue_room_token(
    instance: Any,
    password: str,
    save: Callable[[Any], Awaitable[None]],
    *,
    environ: Mapping[str, str] | None = None,
) -> tuple[dict[str, Any], int]:
    """Return ``(response body, HTTP status)`` for a room password attempt."""
    stored = room_access.room_password_hash(instance)
    if not stored:
        return {"ok": False, "error": "该游戏未设置房间密码"}, 400
    # PBKDF2 is deliberately slow; keep it off the event loop. The thread
    # only sees the hash read here, never the live instance.
    matched = await asyncio.to_thread(room_access.verify_room_password_hash, password, stored)
    if not matched:
        return {"ok": False, "error": ROOM_PASSWORD_WRONG}, 403
    # The GM may have replaced or removed the password while the hash ran: a
    # token must never be issued under a password that is no longer current.
    if room_access.room_password_hash(instance) != stored:
        return {"ok": False, "error": ROOM_PASSWORD_CHANGED}, 409
    try:
        token, expires_at = room_access.issue_room_token(
            instance, ttl_seconds=room_token_ttl_seconds(environ),
        )
    except ValueError:
        return {"ok": False, "error": ROOM_PASSWORD_CHANGED}, 409
    await save(instance)
    return {"ok": True, "room_token": token, "expires_at": expires_at}, 200