mirror of
https://github.com/nubenetes/awesome-kubernetes.git
synced 2026-08-18 03:46:27 +00:00
refactor: implementar validador de enlaces ultra-conservador y whitelist de dominios seguros
This commit is contained in:
+42
-22
@@ -7,52 +7,71 @@ class MarkdownSanitizer:
|
||||
def __init__(self):
|
||||
# Captura [texto](url)
|
||||
self.link_pattern = re.compile(r'\[([^\]]+)\]\((https?://[^\)]+)\)')
|
||||
# Dominios de alta confianza que NUNCA se borran
|
||||
|
||||
# 1. WHITELIST: Dominios de alta confianza que NUNCA se borran.
|
||||
# No validamos estos para evitar bloqueos de bots o errores temporales.
|
||||
self.safe_domains = [
|
||||
'github.com', 'medium.com', 'dev.to', 'twitter.com', 'x.com',
|
||||
'hashnode.com', 'linkedin.com', 'dzone.com', 'youtube.com',
|
||||
'aws.amazon.com', 'kubernetes.io', 'cncf.io', 'microsoft.com',
|
||||
'google.com', 'oracle.com', 'redhat.com', 'ibm.com', 'stackoverflow.com'
|
||||
'hashnode.com', 'hashnode.dev', 'linkedin.com', 'dzone.com',
|
||||
'youtube.com', 'aws.amazon.com', 'kubernetes.io', 'cncf.io',
|
||||
'microsoft.com', 'google.com', 'oracle.com', 'redhat.com',
|
||||
'ibm.com', 'stackoverflow.com', 'substack.com', 'vimeo.com',
|
||||
'infoq.com', 'reddit.com', 'docker.com', 'terraform.io'
|
||||
]
|
||||
|
||||
async def _check_url_robust(self, session: aiohttp.ClientSession, url: str, retries: int = 5) -> Tuple[bool, Optional[str]]:
|
||||
async def _check_url_robust(self, session: aiohttp.ClientSession, url: str, retries: int = 4) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Validador conservador de URLs.
|
||||
Retorna (is_alive, final_url).
|
||||
Implementa User-Agent realista y backoff exponencial.
|
||||
"""
|
||||
if any(domain in url.lower() for domain in self.safe_domains):
|
||||
url_lower = url.lower()
|
||||
|
||||
# Regla de oro: Si está en la whitelist, es válido.
|
||||
if any(domain in url_lower for domain in self.safe_domains):
|
||||
return True, None
|
||||
|
||||
# Headers realistas para evitar ser detectados como bot básico
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
'Referer': 'https://www.google.com/'
|
||||
}
|
||||
|
||||
for attempt in range(retries):
|
||||
try:
|
||||
# Usamos HEAD primero por eficiencia, si falla probamos GET
|
||||
# Usamos GET porque muchos sitios bloquean HEAD
|
||||
async with session.get(url, timeout=15, allow_redirects=True, headers=headers) as response:
|
||||
if response.status < 400:
|
||||
final_url = str(response.url).rstrip('/')
|
||||
original_url = url.split('#')[0].rstrip('/')
|
||||
# Detectar redirecciones permanentes para limpiar el repo
|
||||
if final_url != original_url and response.status in [301, 308]:
|
||||
print(f"[REDIRECCIÓN] {url} -> {final_url}")
|
||||
return True, str(response.url)
|
||||
return True, None
|
||||
|
||||
if response.status == 429 or response.status >= 500:
|
||||
# Rate limiting o error de servidor: esperar y reintentar
|
||||
await asyncio.sleep((2 ** attempt) + 1)
|
||||
continue
|
||||
# SOLO borramos si es un 404 confirmado
|
||||
if response.status == 404:
|
||||
if attempt < retries - 1:
|
||||
await asyncio.sleep(2 ** attempt)
|
||||
continue
|
||||
print(f"[BORRADO 404] {url}")
|
||||
return False, None
|
||||
|
||||
# Ante 403, 401, 429 o 5xx, MANTENEMOS el enlace por seguridad.
|
||||
# Son errores que suelen ser temporales o bloqueos de seguridad al bot.
|
||||
print(f"[AVISO] Saltando validación de {url} por estado {response.status}")
|
||||
return True, None
|
||||
|
||||
# Si es un error 404/403 definitivo después de varios reintentos
|
||||
return False, None
|
||||
except Exception as e:
|
||||
if attempt < retries - 1:
|
||||
await asyncio.sleep((2 ** attempt) + 1)
|
||||
# Backoff exponencial: 1s, 2s, 4s...
|
||||
await asyncio.sleep(2 ** attempt)
|
||||
continue
|
||||
# Si hay timeout persistente, lo mantenemos vivo.
|
||||
return True, None
|
||||
|
||||
# Por defecto, si hay duda (timeout persistente), lo mantenemos vivo
|
||||
return True, None
|
||||
|
||||
async def sanitize_document(self, markdown_content: str) -> Tuple[str, dict]:
|
||||
@@ -61,8 +80,8 @@ class MarkdownSanitizer:
|
||||
stats = {"fixed": 0, "removed": 0, "duplicates": 0}
|
||||
seen_in_file = set()
|
||||
|
||||
# Reducimos concurrencia de 30 a 10 para ser más "amigables" con los servidores
|
||||
connector = aiohttp.TCPConnector(limit=10)
|
||||
# Limitamos concurrencia para no saturar
|
||||
connector = aiohttp.TCPConnector(limit=5)
|
||||
async with aiohttp.ClientSession(connector=connector) as session:
|
||||
for line in lines:
|
||||
match = self.link_pattern.search(line)
|
||||
@@ -73,6 +92,7 @@ class MarkdownSanitizer:
|
||||
text, url = match.groups()
|
||||
clean_url = url.split('#')[0].rstrip('/')
|
||||
|
||||
# Limpieza de duplicados en el mismo archivo
|
||||
if clean_url in seen_in_file:
|
||||
stats["duplicates"] += 1
|
||||
continue
|
||||
@@ -86,25 +106,25 @@ class MarkdownSanitizer:
|
||||
stats["fixed"] += 1
|
||||
new_lines.append(line)
|
||||
else:
|
||||
print(f"[-] Eliminando enlace caído confirmado: {url}")
|
||||
stats["removed"] += 1
|
||||
|
||||
return "\n".join(new_lines), stats
|
||||
|
||||
def inject_curated_link(self, markdown_text: str, category: str, title: str, url: str, description: str) -> str:
|
||||
# Evitar duplicados antes de inyectar
|
||||
# No inyectar si ya existe
|
||||
if url.split('#')[0].rstrip('/') in markdown_text:
|
||||
return markdown_text
|
||||
|
||||
new_entry = f" - [{title}]({url}) - {description}"
|
||||
lines = markdown_text.splitlines()
|
||||
|
||||
# Buscar el mejor sitio (debajo del encabezado de la categoría o al final)
|
||||
# Buscar encabezado de categoría
|
||||
for i, line in enumerate(lines):
|
||||
if category.lower() in line.lower() and (line.startswith("#") or line.startswith("-")):
|
||||
lines.insert(i + 1, new_entry)
|
||||
return "\n".join(lines)
|
||||
|
||||
# Si no existe la sección, crearla al final
|
||||
lines.append(f"\n## {category}\n")
|
||||
lines.append(new_entry)
|
||||
return "\n".join(lines)
|
||||
|
||||
Reference in New Issue
Block a user