Files
einkaufsapp/tools/check-nginx.py
2026-08-08 20:31:58 +02:00

140 lines
5.0 KiB
Python
Raw Permalink 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.
#!/usr/bin/env python3
"""Statische Prüfung der nginx-Konfiguration im web-Container.
Findet drei Fehlerklassen, die uns bereits begegnet sind und die nginx
erst beim Start bemerkt - oder gar nicht:
1. Doppelte Direktiven im selben Kontext.
nginx bricht mit "directive is duplicate" ab. Passiert leicht, wenn
eine location proxy_common.conf einbindet und etwas wiederholt, das
dort schon steht.
2. Fehlende Sicherheitskopfzeilen.
nginx vererbt add_header NUR, wenn die untergeordnete Ebene gar kein
add_header setzt. Ein einziges Cache-Control in einer location lässt
alle Kopfzeilen des server-Blocks verschwinden - ohne Fehlermeldung.
3. Ein types-Block im server-Kontext.
Der ersetzt die geerbte MIME-Tabelle vollständig, statt sie zu
ergänzen. CSS und JavaScript kommen dann als
application/octet-stream an und der Browser lehnt ES-Module ab.
Aufruf:
python3 tools/check-nginx.py # prüft web/
python3 tools/check-nginx.py pfad/zu/web
"""
import re
import sys
from pathlib import Path
# add_header darf mehrfach auftreten - jedes weitere hängt an, statt zu
# ersetzen. Alle anderen hier geprüften Direktiven dürfen es nicht.
REPEATABLE = {"add_header", "proxy_set_header", "try_files", "include",
"limit_req", "gzip_types", "error_page", "set"}
def directive_names(text: str) -> list[str]:
out = []
for line in text.splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
out.append(line.split()[0])
return out
def check(web_dir: Path) -> int:
conf_path = web_dir / "nginx.conf"
if not conf_path.is_file():
print(f"nginx.conf nicht gefunden unter {conf_path}", file=sys.stderr)
return 2
conf = conf_path.read_text(encoding="utf-8")
problems: list[str] = []
includes = {}
for name in ("proxy_common.conf", "security_headers.conf"):
path = web_dir / name
if path.is_file():
includes[name] = set(directive_names(path.read_text(encoding="utf-8")))
else:
problems.append(f"{name} fehlt, wird aber eingebunden")
includes[name] = set()
# ---- 3. types-Block ----
if re.search(r"^\s*types\s*\{", conf, re.M):
problems.append(
"types-Block gefunden: ersetzt im server-Kontext die gesamte "
"MIME-Tabelle. Stattdessen default_type in der betroffenen "
"location setzen."
)
# ---- Klammernbilanz ----
if conf.count("{") != conf.count("}"):
problems.append(
f"Geschweifte Klammern unausgeglichen: "
f"{conf.count('{')} auf, {conf.count('}')} zu"
)
# ---- pro location ----
for match in re.finditer(r"(location[^{]*)\{(.*?\n )\}", conf, re.S):
header = match.group(1).strip()
body = match.group(2)
lines = [
line.strip()
for line in body.splitlines()
if line.strip() and not line.strip().startswith("#")
]
included = {name for name in includes if any(name in line for line in lines)}
own = [line.split()[0] for line in lines if not line.startswith("include")]
for name in sorted(set(own)):
if own.count(name) > 1 and name not in REPEATABLE:
problems.append(f"{header}: {name} steht {own.count(name)}× im Block")
for inc in included:
if name in includes[inc] and name not in REPEATABLE:
problems.append(
f"{header}: {name} steht auch in {inc} nginx bricht "
"mit \"directive is duplicate\" ab"
)
if "add_header" in own and "security_headers.conf" not in included:
problems.append(
f"{header}: setzt add_header, bindet aber security_headers.conf "
"nicht ein die Kopfzeilen des server-Blocks gehen hier verloren"
)
# ---- Ereigniskanal ----
sse = re.search(r"location[^{]*events[^{]*\{(.*?\n )\}", conf, re.S)
if sse:
body = sse.group(1)
if "gzip off" not in body:
problems.append(
"Ereigniskanal: gzip off fehlt Komprimierung sammelt die "
"Ereignisse, statt sie einzeln durchzureichen"
)
common = (web_dir / "proxy_common.conf")
if common.is_file() and "proxy_buffering off" not in common.read_text():
problems.append(
"proxy_common.conf: proxy_buffering off fehlt ohne das "
"puffert nginx die Server-Sent Events"
)
else:
problems.append("Kein location-Block für den Ereigniskanal gefunden")
if problems:
print(f"{len(problems)} Problem(e) in {conf_path}:\n")
for p in problems:
print(f" - {p}")
return 1
print(f"{conf_path}: keine Probleme gefunden")
return 0
if __name__ == "__main__":
target = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("web")
raise SystemExit(check(target))