censar: leer la config del servidor web — cinco sitios sirven un root que NO EXISTE
La sonda DNS dice si un dominio resuelve; no si el servidor tiene algo que servirle. Eso lo dice el
FICHERO DE CONFIGURACIÓN, y leerlo no toca al origen: ni una petición, ni riesgo de fail2ban.
Para leerlo se agregó un LECTOR de Caddy al centro (`formatos/caddy.py` ya tenía el escritor), y el
censo lo usa en vez de tener su propio parser a medias — los de nginx y apache ya existían, y dos
parsers del mismo formato es cómo se separan sin que nadie lo note. Cuarto par de la familia web.
Sobre el Caddyfile real de gioser, cinco sitios apuntan a un `root` que no existe — y son justo los
que devolvían los 502 que en su día hicieron que el censo SE BANEARA A SÍ MISMO al sondearlos:
aura.gioser.net → /var/www/aura_frontend · sigma → /var/www/sigma/frontend
summa → /var/www/summa/frontend · kosmofono → … · dev.summa → …
Es evidencia MÁS FUERTE que el DNS: no hay nada que servir, devuelve 502 resuelva donde resuelva. Va
como recomendación `muere` con la ruta y el fichero donde está el bloque.
**Y sirve para lo contrario, que es donde el aviso hacía daño.** Un directorio que la config SÍ
referencia no es huérfano: el plan marcaba `/var/www/git-tawasuyu` como «nadie lo recuerda» estando
servido, y ese aviso aplicado tira `git.tawasuyu.net`.
Dos bugs propios, los dos encontrados contra el fichero real y no sobre un ejemplo mío:
· El `root` de ese sitio vive DENTRO de un `handle`, y yo saltaba los bloques anidados enteros por no
saber modelarlos. Que el pivote no sepa MODELAR algo no es razón para no VERLO: el bloque sigue
marcado SIN-TRADUCIR pero sus `root`/`reverse_proxy` se leen. Con eso aparecieron dos raíces
ausentes más, también anidadas.
· Detectar la cabecera de sitio con una lista negra de directivas dejaba pasar `log { output file … {`
y el snippet `(acceso) {`: DOS dominios inventados que el censo habría puesto a decidir. La regla
que aguanta es positiva — todos los tokens de la cabecera tienen que PARECER una dirección.
Sin regresión: `nginx → caddy` y `apache → caddy` siguen dando `Valid configuration` con el caddy del
corpus.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
This commit is contained in:
@@ -288,6 +288,75 @@ def declarados(t):
|
||||
return fuentes
|
||||
|
||||
|
||||
# Config del servidor web → qué sitio apunta a qué directorio. Se lee con los LECTORES del centro de
|
||||
# traducción (`formatos/`), no con un parser propio: ya existen para caddy, nginx y apache, y tener
|
||||
# dos parsers del mismo formato es cómo se separan sin que nadie lo note.
|
||||
_CONFS = [("/etc/caddy/Caddyfile", "caddy"),
|
||||
("/etc/nginx/nginx.conf", "nginx"),
|
||||
("/etc/nginx/sites-enabled/*", "nginx"),
|
||||
("/etc/apache2/sites-enabled/*", "apache"),
|
||||
("/etc/httpd/conf.d/*", "apache")]
|
||||
|
||||
|
||||
def sitios_web(t):
|
||||
"""Los sitios declarados en la config, con su raíz y si esa raíz EXISTE.
|
||||
|
||||
── POR QUÉ ESTO NO LO PODÍA DECIR EL DNS ───────────────────────────────────────────────────
|
||||
La sonda DNS dice si un dominio resuelve; no si el servidor tiene algo que servirle. Un bloque
|
||||
cuyo `root` apunta a un directorio que NO existe es un fósil que devuelve 502 — y eso se lee del
|
||||
fichero: **sin red, sin pedirle una página al servidor, sin arriesgar su fail2ban**. Medido en
|
||||
gioser: 3 sitios (`aura`, `sigma`, `kosmofono`) apuntan a raíces borradas, lo que explica los
|
||||
502 que en su día hicieron que el censo se baneara a sí mismo al sondearlos por HTTP.
|
||||
|
||||
Y sirve para lo contrario: un directorio que la config SÍ referencia no es un huérfano. Sin esto
|
||||
el censo marcó `/var/www/git-tawasuyu` como «nadie lo recuerda» cuando es un sitio vivo."""
|
||||
import formatos
|
||||
lectores, _ = formatos.cargar()
|
||||
res = []
|
||||
for patron, fmt in _CONFS:
|
||||
if fmt not in lectores:
|
||||
continue
|
||||
ficheros = [patron]
|
||||
if "*" in patron:
|
||||
out, ok = t.run(f"ls -1 {patron} 2>/dev/null")
|
||||
ficheros = out.split() if ok and out.strip() else []
|
||||
for f in ficheros:
|
||||
texto, ok = t.run(f"cat {f} 2>/dev/null")
|
||||
if not ok or not texto.strip() or texto.startswith("(sonda falló"):
|
||||
continue
|
||||
try:
|
||||
cfg = lectores[fmt][0](texto, f)
|
||||
except Exception as e:
|
||||
# Un parser que revienta NO puede tumbar el censo entero, pero tampoco callarse:
|
||||
# sin esto uno cree que el servidor no declara sitios, que es lo contrario del dato.
|
||||
res.append({"config": f, "formato": fmt, "nombres": "", "raiz": "",
|
||||
"raiz_existe": "error", "detalle": f"no se pudo leer: {e}"[:120]})
|
||||
continue
|
||||
for s in cfg.sitios:
|
||||
# El `root` del sitio Y los de sus rutas anidadas: un `handle` con su propio `root`
|
||||
# referencia disco igual, y no verlo es lo que hacía pasar por huérfano a un sitio vivo.
|
||||
raices = [(s.raiz or "").rstrip("/")] + [
|
||||
a.valor.rstrip("/") for r in s.rutas for a in r.acciones if a.tipo == "archivos"]
|
||||
for raiz in dict.fromkeys(x for x in raices if x) or [""]:
|
||||
res.append({"config": f, "formato": fmt,
|
||||
"nombres": " ".join(s.nombres), "raiz": raiz,
|
||||
"raiz_existe": "", "detalle": ""})
|
||||
# Un solo comando para todas las raíces: una sonda por directorio sobre SSH es lentísima.
|
||||
raices = sorted({r["raiz"] for r in res if r["raiz"]})
|
||||
if raices:
|
||||
guion = "; ".join(f'[ -d {shlex.quote(d)} ] && echo "SI {d}" || echo "NO {d}"' for d in raices)
|
||||
out, _ = t.run(guion)
|
||||
estado = {}
|
||||
for l in out.splitlines():
|
||||
p = l.split(None, 1)
|
||||
if len(p) == 2:
|
||||
estado[p[1]] = "si" if p[0] == "SI" else "no"
|
||||
for r in res:
|
||||
if r["raiz"]:
|
||||
r["raiz_existe"] = estado.get(r["raiz"], "?")
|
||||
return res
|
||||
|
||||
|
||||
def dominios(t):
|
||||
"""Los dominios que la máquina DICE servir, contrastados con DNS y con un GET real.
|
||||
|
||||
@@ -578,6 +647,11 @@ def emitir_toml(c):
|
||||
L.append(f"paquete = {toml_escape(s.get('paquete',''))}")
|
||||
L.append('decision = "" # muda | muere | (vacío = sin decidir)')
|
||||
L.append("")
|
||||
for x in c.get("sitios", []):
|
||||
L.append("[[sitio]]")
|
||||
for k, v in x.items():
|
||||
L.append(f"{k} = {toml_escape(v)}")
|
||||
L.append("")
|
||||
for x in c.get("contenido", []):
|
||||
L.append("[[contenido]]")
|
||||
for k, v in x.items():
|
||||
@@ -700,6 +774,7 @@ def main():
|
||||
pts = puertos(t)
|
||||
nombres = dominios(t)
|
||||
cont = contenido_web(t)
|
||||
sitios = sitios_web(t)
|
||||
sondear = (a.probe or a.probe_dns or bool(a.host)) and not a.no_probe
|
||||
http_ok = not a.probe_dns
|
||||
if nombres and not sondear:
|
||||
@@ -721,7 +796,7 @@ def main():
|
||||
censo = {
|
||||
"fecha": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
"identidad": ident, "servicios": svc, "huerfanos": huerfanos, "puertos": pts,
|
||||
"dominios": doms, "datos": dat, "contenido": cont, "montajes": montajes(t),
|
||||
"dominios": doms, "datos": dat, "contenido": cont, "sitios": sitios, "montajes": montajes(t),
|
||||
"fuera_de_git": fuera_de_git(t),
|
||||
}
|
||||
resumen(censo)
|
||||
|
||||
Reference in New Issue
Block a user