"""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 `` 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