Ctrl K
ComunidadGuías…

backend/media_token.py

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

Archivo original ↗Preguntar a KiBot
"""Short-lived signed tokens for browser-native media streaming.

The ``<video>`` element cannot attach an ``X-Api-Key`` header to its own
internal range requests, so the stream URL previously carried the raw API key
as a ``?apikey=`` query parameter. Query strings land in web-server access logs
(Gunicorn/Werkzeug), so every stream request wrote the full API key to disk in
plaintext — anyone with log read access could lift it.

Instead the client first POSTs to ``/media/stream-token`` (authenticated with
the normal header) and receives a short-lived HMAC token bound to the specific
file path. The token — not the key — goes in the stream URL. A leaked log line
now only exposes a path-scoped token that expires within hours.
"""

import base64
import hashlib
import hmac
import time

from flask import current_app, request

# A token must outlive a full playback session (movies run ~2-3 h) but stay
# short enough that a leaked URL is not a durable credential.
_DEFAULT_TTL_SECONDS = 6 * 60 * 60

_STREAM_PATH = "/api/v1/media/stream"


def _secret() -> bytes:
    key = current_app.config.get("SECRET_KEY") or ""
    return key.encode() if isinstance(key, str) else bytes(key)


def generate_stream_token(path: str, ttl: int = _DEFAULT_TTL_SECONDS) -> tuple[str, int]:
    """Return (token, expiry_epoch) binding ``path`` for ``ttl`` seconds."""
    expiry = int(time.time()) + ttl
    msg = f"{expiry}:{path}".encode()
    sig = hmac.new(_secret(), msg, hashlib.sha256).digest()
    sig_b64 = base64.urlsafe_b64encode(sig).decode().rstrip("=")
    return f"{expiry}.{sig_b64}", expiry


def verify_stream_token(path: str, token: str) -> bool:
    """Return True iff ``token`` is a valid, unexpired signature over ``path``."""
    if not token or "." not in token:
        return False
    expiry_str, _, sig_part = token.partition(".")
    try:
        expiry = int(expiry_str)
    except ValueError:
        return False
    if expiry < int(time.time()):
        return False
    msg = f"{expiry}:{path}".encode()
    expected = hmac.new(_secret(), msg, hashlib.sha256).digest()
    expected_b64 = base64.urlsafe_b64encode(expected).decode().rstrip("=")
    return hmac.compare_digest(sig_part, expected_b64)


def stream_request_has_valid_token() -> bool:
    """Auth-middleware helper: True iff the current request is a /media/stream
    GET carrying a ``token`` that validates against its ``path`` query arg."""
    if request.path != _STREAM_PATH:
        return False
    token = request.args.get("token", "")
    path = request.args.get("path", "")
    if not token or not path:
        return False
    return verify_stream_token(path, token)