Spaces:
Sleeping
Sleeping
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)}"
|