fix(unifi): open webinterface via https
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 5m46s

This commit is contained in:
Ramon 2026-08-06 08:39:26 +02:00
parent 9008e39fe4
commit 8f62df90d8
8 changed files with 133 additions and 13 deletions

View file

@ -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://<app-ip>: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

View file

@ -1 +1 @@
0.8.17-beta
0.8.18-beta

View file

@ -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": [
{

View file

@ -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/<name>/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",

View file

@ -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(), "")

View file

@ -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",

View file

@ -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"] == {}

View file

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