File size: 2,716 Bytes
70e641d
 
 
 
 
 
 
 
 
a5d73ff
70e641d
 
 
5118fc3
a5d73ff
 
5118fc3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a5d73ff
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5118fc3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
"""Limitación de tasa (slowapi) por IP.

Cubre el agujero de la versión PHP (sin rate limiting en ningún sitio): el endpoint de
IA (costoso, quema la cuota HF), el login (fuerza bruta) y papers (abuso de PubMed).
Los límites concretos son configurables en config.py.
"""

from __future__ import annotations

from fastapi import Request
from slowapi import Limiter
from slowapi.util import get_remote_address

from ..config import obtener_config
from .session import COOKIE_SESION, leer_sesion


def ip_cliente(request: Request) -> str:
    """IP real del cliente, teniendo en cuenta los proxies de confianza declarados.

    `get_remote_address` devuelve `request.client.host`, que detrás de un proxy es el proxy: en
    HF Spaces eso convertía `limite_login` y `limite_papers` en límites GLOBALES compartidos por
    todo el mundo (bypass de la fuerza bruta y auto-DoS a la vez).

    `X-Forwarded-For` NO se lee a ciegas —la manda el cliente, y creérsela permite falsificar la
    IP en cada petición y saltarse cualquier límite—. Se usa sólo si el operador declaró cuántos
    proxies hay delante (`proxy_saltos_confiables`), y se toma el elemento -N: cada proxy añade
    la dirección de su par, así que ése es el último valor escrito por infraestructura de
    confianza. Lo de más a la izquierda es lo que dijo el cliente y se descarta.
    """
    saltos = obtener_config().proxy_saltos_confiables
    if saltos > 0:
        reenviada = request.headers.get("x-forwarded-for", "")
        partes = [p.strip() for p in reenviada.split(",") if p.strip()]
        if len(partes) >= saltos:
            return partes[-saltos]
        # Menos entradas de las declaradas: la petición no vino por la cadena esperada. Se cae
        # al peer directo en vez de coger un valor que el cliente controle.
    return get_remote_address(request)


limiter = Limiter(key_func=ip_cliente)


def clave_usuario(request: Request) -> str:
    """Clave de rate limiting por USUARIO, no por IP.

    En una herramienta gratuita el recurso escaso es la cuota de GPU compartida (ZeroGPU es por
    cuenta, no por Space): un solo usuario puede agotarla para todos los demás. El límite por IP
    no lo evita —una IP doméstica cambia, y varios veterinarios de una misma clínica comparten
    IP, penalizándose entre sí—. Con la sesión como clave, el coste se imputa a quien lo genera.

    Sin sesión se cae a la IP: /api/interpret exige sesión, así que ese caso sólo aparece si
    cambia la guarda de auth.
    """
    sesion = leer_sesion(request.cookies.get(COOKIE_SESION))
    if sesion and sesion.get("email"):
        return f"user:{sesion['email']}"
    return f"ip:{ip_cliente(request)}"