Files
takana/scripts/mudanza/formatos/apache.py
T
SergioandClaude Opus 5 3137d47f16 traducir: CENTRO de traducción con modelo pivote y plugins — N+M en vez de N×M
Traducir de a pares no escala: con nginx, apache, caddy, haproxy y lighttpd son 20 traductores, y
cada formato nuevo agrega 2N. Con un modelo intermedio son N lectores + M escritores: un formato
nuevo cuesta uno o dos plugins y estrena todos los pares de golpe.

**Probado, no argumentado**: el lector de apache se agregó SIN TOCAR el escritor de Caddy, y con eso
`apache → caddy` funciona sin que exista ningún traductor de ese par. Las dos salidas las valida el
caddy del propio corpus: `Valid configuration` en las dos.

Los plugins se DESCUBREN (`formatos/` + `@lector` / `@escritor`): una lista mantenida a mano se
desincroniza y el formato nuevo «no existe» sin que nada falle. `--list` dice qué pares habilita.

**El acta viaja DENTRO del modelo**, y es la decisión que hace que el pivote no mienta: si fuera un
efecto de cada traductor, lo que el LECTOR no entendió se perdería antes de llegar al escritor. El
escritor además puede agregarle — Caddy no tiene equivalente de `location ~`, así que en vez de
emitir un `handle` de prefijo que SE PARECE, lo manda al acta. Parecerse es peor que faltar.

Y un bug del lector de apache, encontrado probando: `ProxyPass` tiene dos formas y dentro de
`<Location>` lleva sólo la URL. Mirar sólo la de tres tokens dejaba sin traducir la forma más común y
el acta la reportaba como «no reconocida» — un falso negativo que manda a escribir a mano algo que el
lector sí sabe hacer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 18:27:32 +00:00

107 lines
5.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Plugin de formato: LECTOR de Apache httpd (SDD 29).
Existe para PROBAR que el pivote paga: con este fichero, `apache → caddy` funciona **sin haber
escrito un traductor apache→caddy**. El escritor de Caddy no se tocó. Ése es el argumento entero
contra traducir de a pares — N×M contra N+M.
"""
import os, re, sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from modelo import Config, Sitio, Ruta, Accion # noqa: E402
from formatos import lector # noqa: E402
IRRELEVANTES = {"ServerRoot", "Listen", "LoadModule", "TypesConfig", "ErrorLog", "CustomLog",
"LogLevel", "Timeout", "KeepAlive", "User", "Group", "PidFile", "ServerAdmin"}
@lector("apache")
def leer(texto):
cfg = Config(formato_origen="apache")
A = cfg.acta
sitio, ruta = None, None
for ln, linea in enumerate(texto.split("\n"), 1):
s = linea.split("#")[0].strip()
if not s:
continue
m = re.match(r'<(/?)(\w+)([^>]*)>', s)
if m:
cierra, tag, arg = m.group(1), m.group(2), m.group(3).strip()
if tag == "VirtualHost":
if cierra:
if sitio:
cfg.sitios.append(sitio); sitio = None
else:
sitio = Sitio(origen=ln)
puerto = arg.split(":")[-1].strip('"> ')
if puerto.isdigit():
sitio.puertos.append(puerto)
A.anota(ln, "traducido", s)
elif tag in ("Directory", "Location", "LocationMatch", "DirectoryMatch"):
if cierra:
if sitio and ruta:
sitio.rutas.append(ruta); ruta = None
else:
clase = "regex" if tag.endswith("Match") else "prefijo"
ruta = Ruta(patron=arg.strip('"'), clase=clase, origen=ln)
elif tag in ("IfModule", "Files", "FilesMatch", "Limit"):
A.anota(ln, "SIN-TRADUCIR", s,
f"`<{tag}>` no tiene traducción mecánica: su efecto depende de qué módulos "
f"están cargados y del orden")
continue
p = s.split()
cab = p[0] if p else ""
destino = ruta if ruta is not None else sitio
if sitio is None:
if cab in IRRELEVANTES:
A.anota(ln, "no-aplica", s, "ajuste global del proceso httpd")
else:
A.anota(ln, "SIN-TRADUCIR", s, "directiva global no reconocida")
continue
if cab in ("ServerName", "ServerAlias"):
sitio.nombres += [x.strip('"') for x in p[1:]]
A.anota(ln, "traducido", s)
elif cab == "DocumentRoot":
sitio.raiz = p[1].strip('"') if len(p) > 1 else ""
A.anota(ln, "traducido", s)
elif cab == "DirectoryIndex":
sitio.indices = p[1:]
A.anota(ln, "traducido", s)
elif cab == "SSLCertificateFile":
sitio.tls["cert"] = p[1].strip('"') if len(p) > 1 else ""
A.anota(ln, "traducido", s)
elif cab == "SSLCertificateKeyFile":
sitio.tls["key"] = p[1].strip('"') if len(p) > 1 else ""
A.anota(ln, "traducido", s)
elif cab in ("ProxyPass", "ProxyPassReverse") and len(p) > 1:
# ⚠ `ProxyPass` tiene DOS formas y la diferencia importa: dentro de un `<Location>` lleva
# sólo la URL (`ProxyPass http://…`), fuera lleva ruta y URL (`ProxyPass /api http://…`).
# Mirar sólo la de tres tokens dejaba sin traducir justo la forma que más se usa, y el
# acta lo reportaba como «directiva no reconocida» — un falso negativo que manda a
# escribir a mano algo que el lector sí sabe hacer.
url = p[2].strip('"') if len(p) > 2 else p[1].strip('"')
if cab == "ProxyPassReverse":
A.anota(ln, "no-aplica", s, "el destino reescribe las cabeceras del proxy por su cuenta")
else:
if len(p) > 2 and ruta is None:
destino.rutas.append(Ruta(patron=p[1].strip('"'), clase="prefijo", origen=ln))
destino.rutas[-1].acciones.append(Accion("proxy", url))
else:
destino.acciones.append(Accion("proxy", url))
A.anota(ln, "traducido", s)
elif cab == "Redirect" and len(p) > 1:
code = p[1] if p[1].isdigit() else "302"
dest = p[-1]
destino.acciones.append(Accion("redir", f"{code} {dest}"))
A.anota(ln, "traducido", s)
elif cab in ("RewriteEngine", "RewriteCond", "RewriteRule"):
A.anota(ln, "SIN-TRADUCIR", s,
"mod_rewrite no tiene traducción mecánica: es un lenguaje propio con estado "
"entre reglas")
elif cab in ("Options", "AllowOverride", "Require", "Order", "Allow", "Deny"):
A.anota(ln, "decision", s, "control de acceso o de listado: el destino lo expresa distinto")
else:
A.anota(ln, "SIN-TRADUCIR", s, "directiva no reconocida por este lector")
return cfg