Files
takana/scripts/mudanza/formatos/apache.py
T
SergioandClaude Opus 5 fefa92ec5a traducir: familia «servicio» — systemd → tarjeta de arje, y el pivote pasa a ser uno POR FAMILIA
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
2026-09-11 19:15:21 +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", 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