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
107 lines
5.1 KiB
Python
107 lines
5.1 KiB
Python
"""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
|