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