Un unit de systemd no es un sitio web. Meterlo en `Sitio`/`Ruta` sería justo el «parecerse» que el acta existe para evitar, así que cada familia tiene su modelo y el centro empareja SÓLO dentro de la familia. Un par cruzado se rechaza con un error, no con un intento: `systemd → caddy` sale por `sys.exit`, porque traducir entre familias daría un fichero que PARECE correcto. Dos familias hoy: `web` (nginx, apache → caddy) y `servicio` (systemd → arje). 3 pares con 5 plugins. **Dónde una elisión silenciosa no pierde una opción sino que CAMBIA el sistema.** El payload `Native` de arje acepta `exec`, `argv` y `envp` y nada más (comprobado en la semilla real del producto): NO hay campo de usuario. Un `User=git` traducido en silencio correría el servicio COMO ROOT — escalación de privilegios en un fichero generado que nadie vuelve a leer. Sale SIN-TRADUCIR con su línea. Igual `EnvironmentFile=` (el fichero no está en la máquina donde se traduce ⇒ envp incompleto, falla tarde), `Type=forking` (arje supervisa al proceso que lanza ⇒ BUCLE DE REINICIO) y todo el endurecimiento, que callado entrega un servicio MENOS confinado que el original. Lo que sí tiene destino exacto: `Type=oneshot` → `"supervision": "OneShot"`, que ya existe en la semilla del producto — buscarlo antes de declararlo intraducible evitó una elisión inventada. **Lector y escritor no opinan del mismo campo.** `User=` lo CAPTURA el lector y lo JUZGA el escritor; cuando los dos anotaban, el acta decía dos cosas distintas de la misma línea, y un acta que se contradice se deja de leer. El lector registra en qué línea vio cada campo (`Servicio.lineas`) para que el escritor señale la línea real en vez de un 0. Controles: la tarjeta generada tiene el MISMO juego de claves que la tarjeta real de `sshd` del producto (+`_mudanza` de procedencia); dos corridas dan el fichero byte a byte idéntico; y la familia `web` sigue validando con el caddy del corpus (`Valid configuration` en los dos pares). De paso, `ulid_determinista` sale a `ids.py`: lo necesitaban `declarar.py` y el escritor de arje, y copiarlo habría dejado dos generadores de id que se pueden separar sin que nada falle. 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", familia="web")
|
||
def leer(texto, nombre=""):
|
||
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
|