From 8f62df90d8b3ea29dfc7946fef916aba70411664 Mon Sep 17 00:00:00 2001 From: Ramon Date: Thu, 6 Aug 2026 08:39:26 +0200 Subject: [PATCH] fix(unifi): open webinterface via https --- CHANGELOG.md | 16 +++++++++ VERSION | 2 +- apps/unifi-network/template.json | 6 +++- server-up/app.py | 56 ++++++++++++++++++++++++++------ server-up/core/boilerplates.py | 17 ++++++++++ tests/test_apps.py | 2 +- tests/test_boilerplates.py | 25 ++++++++++++++ tests/test_invulmenu.py | 22 +++++++++++++ 8 files changed, 133 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be31e4d..fb2d9c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +# v0.8.18-beta — UniFi opent via HTTPS + +De UniFi-container was wel correct op zijn HTTPS-poort gepubliceerd, maar +Server Up maakte er altijd een `http://`-link van. Daardoor leek de app +onbereikbaar terwijl de browser met het verkeerde protocol verbinding maakte. + +- **UniFi declareert HTTPS expliciet.** Het template vermeldt voortaan zowel + het protocol als de interne webpoort 8443. +- **De juiste hostpoort wordt gekozen.** Server Up koppelt de interne webpoort + aan de ingestelde hostpoort, ook wanneer een databasepoort eerder in het + Compose-bestand staat. +- **Bestaande installaties werken mee.** Ontbrekende installatiemetadata wordt + uit het actuele bronsjabloon aangevuld; opnieuw installeren is niet nodig. +- **Eigen IP-adressen blijven correct.** Een UniFi-stack met een eigen IP opent + als `https://:8443`. + # v0.8.17-beta — Elf nieuwe apps voor beheer, automatisering en monitoring De app-store bevatte nog weinig lichte infrastructuurapps die zowel op een diff --git a/VERSION b/VERSION index e359986..5d2d6a4 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.8.17-beta +0.8.18-beta diff --git a/apps/unifi-network/template.json b/apps/unifi-network/template.json index 7728d2e..16c4090 100644 --- a/apps/unifi-network/template.json +++ b/apps/unifi-network/template.json @@ -24,7 +24,11 @@ "amd64", "arm64" ], - "architecture_note": "Op een Raspberry Pi is een 64-bits besturingssysteem vereist. Voor een Pi 4 is de database bewust vastgezet op MongoDB 4.4.29." + "architecture_note": "Op een Raspberry Pi is een 64-bits besturingssysteem vereist. Voor een Pi 4 is de database bewust vastgezet op MongoDB 4.4.29.", + "web": { + "scheme": "https", + "port": 8443 + } }, "variables": [ { diff --git a/server-up/app.py b/server-up/app.py index b335ced..0bafae2 100644 --- a/server-up/app.py +++ b/server-up/app.py @@ -910,18 +910,15 @@ def api_stack_credentials(name): items.append({"kind": soort, "title": f.get("title") or f["name"], "value": waarde, "secret": bool(f.get("secret"))}) - # Het adres waarop de app draait: eigen IP, anders de gepubliceerde poort. - adres = "" + # Het adres waarop de app draait. Sommige apps (waaronder UniFi) luisteren + # uitsluitend via HTTPS; het oude vaste http://-adres maakte hun link stuk. try: - poorten = compose_transform.published_ports(docker.read_compose(d)) + compose_text = docker.read_compose(d) except Exception: - poorten = [] - if meta.get("ip"): - adres = f"http://{meta['ip']}" - elif poorten: - eerste = str(poorten[0]).strip('"').split(":") - if len(eerste) >= 2 and eerste[-2].isdigit(): - adres = f"http://{request.host.split(':')[0]}:{eerste[-2]}" + compose_text = "" + bron_meta = boilerplates.metadata(bron) if bron else {} + adres = _stack_webadres(meta, bron_meta, compose_text, + request.host.split(":")[0]) if items: audit.log("stacks", "credentials_view", "ok", ref=name, @@ -933,6 +930,43 @@ def api_stack_credentials(name): self_signup=not items) +def _stack_webadres(meta: dict, bron_meta: dict, compose_text: str, + host: str) -> str: + """Bouw het browseradres uit template-metadata en Compose-poorten. + + De bronmetadata blijft bewust een fallback: daarmee krijgen ook stacks die + vóór deze wijziging zijn geïnstalleerd meteen het juiste HTTPS-adres. + """ + web = meta.get("web") if isinstance(meta.get("web"), dict) else {} + if not web and isinstance(bron_meta.get("web"), dict): + web = bron_meta["web"] + schema = str(web.get("scheme") or "http").lower() + if schema not in ("http", "https"): + schema = "http" + try: + doelpoort = int(web.get("port") or 0) + except (TypeError, ValueError): + doelpoort = 0 + + eigen_ip = str(meta.get("ip") or "").strip() + if eigen_ip: + poort = doelpoort + host = eigen_ip + else: + poorten = [p for p in compose_transform.poorten_per_service(compose_text) + if p.get("proto") == "tcp"] + gekozen = next((p for p in poorten + if doelpoort and p.get("container") == doelpoort), None) + gekozen = gekozen or (poorten[0] if poorten else None) + poort = (gekozen or {}).get("host") or (gekozen or {}).get("container") or 0 + + if not host or (not eigen_ip and not poort): + return "" + standaard = (schema == "http" and poort == 80) or ( + schema == "https" and poort == 443) + return f"{schema}://{host}{'' if not poort or standaard else f':{poort}'}" + + @app.route("/api/stacks//compose") def api_stack_compose(name): lib, d = _stack_dir(name) @@ -1177,6 +1211,7 @@ def api_stack_reconfigure(name): "network": net_name, "ip": net_ip, "ips": net_ips, "app_networks": app_netten, "service_ports": poorten_meta, + "web": boilerplates.metadata(bron).get("web") or {}, "connected": bool(app_netten)}) docker.compose_down(d, log_fn=lf, name=name) @@ -2098,6 +2133,7 @@ def api_store_install(): "service_ports": poorten_meta, "categories": meta.get("categories") or [], "tags": meta.get("tags") or [], + "web": meta.get("web") or {}, # Zonder de gebruikte waarden valt het installatieformulier # later niet opnieuw te openen om iets te wijzigen. "format": "boilerplate" if is_bp else "compose", diff --git a/server-up/core/boilerplates.py b/server-up/core/boilerplates.py index 1232ee0..e5c8be6 100644 --- a/server-up/core/boilerplates.py +++ b/server-up/core/boilerplates.py @@ -74,6 +74,7 @@ def metadata(d: Path) -> dict: "depends_on": _afhankelijkheden(md.get("depends_on")), "architectures": _architecturen(md.get("architectures")), "architecture_note": str(md.get("architecture_note") or "").strip(), + "web": _webconfig(md.get("web")), } @@ -85,6 +86,22 @@ _ARCH_ALIASES = { } +def _webconfig(waarde) -> dict: + """Normaliseer het webadres dat een app-template zelf declareert.""" + if not isinstance(waarde, dict): + return {} + schema = str(waarde.get("scheme") or "").strip().lower() + if schema not in ("http", "https"): + return {} + try: + poort = int(waarde.get("port") or 0) + except (TypeError, ValueError): + return {} + if not 1 <= poort <= 65535: + return {} + return {"scheme": schema, "port": poort} + + def _architectuur(waarde: str) -> str: return _ARCH_ALIASES.get(str(waarde or "").strip().lower(), "") diff --git a/tests/test_apps.py b/tests/test_apps.py index e46abf6..c05c1b5 100644 --- a/tests/test_apps.py +++ b/tests/test_apps.py @@ -397,7 +397,7 @@ def test_unifi_heeft_pi_geschikte_en_correct_geinitialiseerde_mongodb(): assert "clusterMonitor" in script assert 'role: "dbOwner"' in script assert bp.metadata(app)["architectures"] == ["amd64", "arm64"] - + assert bp.metadata(app)["web"] == {"scheme": "https", "port": 8443} AMD64_ALLEEN = { "cal-com", "comfyui", "dawarich", "focalboard", "formbricks", diff --git a/tests/test_boilerplates.py b/tests/test_boilerplates.py index b98218b..f79dbc9 100644 --- a/tests/test_boilerplates.py +++ b/tests/test_boilerplates.py @@ -145,3 +145,28 @@ def test_render_behoudt_uitvoerbaar_recht_van_tekstscript(tmp_path): dest = tmp_path / "out" bp.render_to_dir(src, dest, {"service_name": "test"}) assert (dest / "init.sh").stat().st_mode & stat.S_IXUSR + + +def test_webmetadata_wordt_genormaliseerd(tmp_path): + src = _maak_boilerplate(tmp_path, "services: {}\n") + pad = src / "template.json" + data = json.loads(pad.read_text(encoding="utf-8")) + data["metadata"]["web"] = {"scheme": "HTTPS", "port": "8443"} + pad.write_text(json.dumps(data), encoding="utf-8") + + assert bp.metadata(src)["web"] == {"scheme": "https", "port": 8443} + + +@pytest.mark.parametrize("web", [ + {"scheme": "ftp", "port": 21}, + {"scheme": "https", "port": 70000}, + {"scheme": "https", "port": "niet-een-poort"}, +]) +def test_ongeldige_webmetadata_wordt_genegeerd(tmp_path, web): + src = _maak_boilerplate(tmp_path, "services: {}\n") + pad = src / "template.json" + data = json.loads(pad.read_text(encoding="utf-8")) + data["metadata"]["web"] = web + pad.write_text(json.dumps(data), encoding="utf-8") + + assert bp.metadata(src)["web"] == {} diff --git a/tests/test_invulmenu.py b/tests/test_invulmenu.py index d5d54f4..205c490 100644 --- a/tests/test_invulmenu.py +++ b/tests/test_invulmenu.py @@ -339,3 +339,25 @@ def test_voorvertoning_gebruikt_de_ingevulde_waarden(client, env): # Het geheim hoort al in de .env-voorvertoning te staan, niet in compose. assert "MijnWachtwoord1" not in d["compose_preview"] assert "MijnWachtwoord1" in d["env_preview"] + + +def test_webadres_gebruikt_https_en_de_juiste_containerpoort(client): + import app as app_module + compose = ("services:\n database:\n image: mongo\n" + " ports:\n - \"27017:27017\"\n" + " unifi:\n image: unifi\n" + " ports:\n - \"9443:8443\"\n - \"8080:8080\"\n") + bron = {"web": {"scheme": "https", "port": 8443}} + + assert app_module._stack_webadres({}, bron, compose, "server.local") == \ + "https://server.local:9443" + assert app_module._stack_webadres({"ip": "192.168.1.50"}, bron, compose, + "server.local") == \ + "https://192.168.1.50:8443" + + +def test_webadres_zonder_metadata_blijft_http(client): + import app as app_module + compose = "services:\n app:\n image: nginx\n ports:\n - \"8123:80\"\n" + assert app_module._stack_webadres({}, {}, compose, "server.local") == \ + "http://server.local:8123"