From 50689975994541beb25f2b95cec41ec6ed890181 Mon Sep 17 00:00:00 2001 From: Ramon Date: Fri, 7 Aug 2026 15:13:53 +0200 Subject: [PATCH] fix(updates): neem het webadres van je releases ook aan Het adres uit de adresbalk van Forgejo of GitHub geeft HTML terug, en de enige melding was een JSONDecodeError. Dat adres wordt nu omgerekend naar /api/v1/repos/...; komt er toch HTML, dan zegt de melding wat er mis is. Een https://-schema voor het imageveld wordt weggehaald, want een image-adres heeft er nooit een. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01H9Vcwu7owJZ3nj5dCf4VFT --- CHANGELOG.md | 17 ++++- server-up/core/selfupdate.py | 9 +++ server-up/core/updater.py | 39 ++++++++++- server-up/translations/en.json | 2 +- server-up/translations/nl.json | 2 +- tests/test_update_adres.py | 119 +++++++++++++++++++++++++++++++++ 6 files changed, 181 insertions(+), 7 deletions(-) create mode 100644 tests/test_update_adres.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 47765fa..3efa51b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,18 @@ -# v0.8.30-beta — In ontwikkeling +# v0.8.30-beta — De updater neemt genoegen met het adres uit je adresbalk -Nog niets uitgebracht sinds v0.8.29. Wat hier onder komt te staan gaat mee in -de volgende uitgave; bij het uitbrengen wordt deze kop de releaseversie. +Wie in Forgejo naar zijn releases keek en dat adres in de instellingen plakte, +kreeg een webpagina in plaats van JSON — en als enige melding +`JSONDecodeError: Expecting value: line 1 column 1 (char 0)`. Daar leidt +niemand uit af dat er `/api/v1/repos/` in het adres had moeten staan. + +- **Het adres wordt omgerekend.** `https://git.example.nl/bes-r/server-up` en + `.../releases` worden allebei het juiste API-adres; GitHub krijgt zijn eigen + `api.github.com`. Een adres dat al klopt of dat we niet herkennen blijft staan. +- **Een begrijpelijke melding** als er tóch HTML terugkomt, met wat er dan wél + in het veld hoort. +- **`https://` voor het imageveld wordt weggehaald.** Een image-adres heeft + nooit een schema, dus `docker pull https://…` faalde op iets wat er in het + veld goed uitzag. # v0.8.29 — UniFi start weer op een Raspberry Pi 4 diff --git a/server-up/core/selfupdate.py b/server-up/core/selfupdate.py index 501849a..27a1ba7 100644 --- a/server-up/core/selfupdate.py +++ b/server-up/core/selfupdate.py @@ -82,6 +82,15 @@ def image_repo(info: dict | None = None) -> str: """ expliciet = (cfg.load().get("UPDATE_IMAGE") or "").strip() if expliciet: + # Een image-adres heeft nooit een schema. Wie het uit zijn adresbalk + # plakt neemt `https://` mee, en dan faalt `docker pull` op iets wat er + # in het veld goed uitziet. Weghalen is hier eenduidig: er bestaat geen + # geldig image dat zo begint. + for schema in ("https://", "http://"): + if expliciet.lower().startswith(schema): + expliciet = expliciet[len(schema):] + break + expliciet = expliciet.strip("/") return expliciet.rsplit(":", 1)[0] if _has_tag(expliciet) else expliciet img = (info or inspect_self()).get("image", "") if not img: diff --git a/server-up/core/updater.py b/server-up/core/updater.py index 09b378e..2886b3b 100644 --- a/server-up/core/updater.py +++ b/server-up/core/updater.py @@ -78,11 +78,46 @@ def image_tag(release_tag: str) -> str: return tag[1:] if tag.startswith("v") and _parse(tag) else tag +def api_url(url: str) -> str: + """Maak van een geplakt adres het API-adres. + + Wie in Forgejo of GitHub naar zijn releases kijkt en dat adres uit de + adresbalk plakt, geeft het adres van een wébpagina op. Dat levert HTML op, + en de enige melding die je daarna kreeg was `JSONDecodeError: Expecting + value: line 1 column 1` — waaruit niemand opmaakt dat er `/api/v1/` had + moeten staan. Dus rekenen we het om in plaats van erover te vallen. + + https://git.voorbeeld.nl/bes-r/server-up/releases + → https://git.voorbeeld.nl/api/v1/repos/bes-r/server-up/releases + https://github.com/bes-r/server-up/releases + → https://api.github.com/repos/bes-r/server-up/releases + """ + schoon = (url or "").strip().rstrip("/") + if not schoon or "/api/" in schoon: + return schoon + m = re.match(r"^(https?)://([^/]+)/([^/]+)/([^/]+?)(?:/releases.*|\.git)?$", + schoon) + if not m: + return schoon + schema, host, eigenaar, repo = m.groups() + if host.lower() in ("github.com", "www.github.com"): + return f"https://api.github.com/repos/{eigenaar}/{repo}/releases" + return f"{schema}://{host}/api/v1/repos/{eigenaar}/{repo}/releases" + + def _releases(url: str) -> list[dict]: req = urllib.request.Request( url, headers={"Accept": "application/json", "User-Agent": "server-up"}) with urllib.request.urlopen(req, timeout=8) as r: - data = json.loads(r.read().decode("utf-8")) + rauw = r.read().decode("utf-8", "replace") + try: + data = json.loads(rauw) + except ValueError: + # Bijna altijd een webpagina in plaats van de API. Zeg dát, in plaats + # van de gebruiker met een JSON-fout te laten zitten. + raise ValueError( + f"{url} geeft geen JSON terug maar een webpagina. Vul het " + "API-adres in, met /api/v1/repos/ erin.") from None return data if isinstance(data, list) else [data] @@ -98,7 +133,7 @@ def check(current: str, force: bool = False) -> dict: return dict(_CACHE) c = cfg.load() - url = (c.get("UPDATE_API_URL") or "").strip() + url = api_url(c.get("UPDATE_API_URL") or "") ch = channel() res = {"enabled": bool(url), "current": current, "channel": ch, "current_is_prerelease": is_prerelease(current), diff --git a/server-up/translations/en.json b/server-up/translations/en.json index ac72adc..8bd1fe6 100644 --- a/server-up/translations/en.json +++ b/server-up/translations/en.json @@ -138,7 +138,7 @@ "release_notes": "Release notes", "view_release": "View release", "update_api_url": "Releases API URL", - "update_api_hint": "Forgejo/GitHub Releases API returning the latest release. Empty = disabled.", + "update_api_hint": "Address of your releases. You may paste the address from your browser; Server Up converts it to the API address with /api/v1/repos/ itself. Empty = disabled.", "include_prerelease": "Include pre-releases (beta)", "login": "Sign in", "login_failed": "Sign-in failed", diff --git a/server-up/translations/nl.json b/server-up/translations/nl.json index f013c24..589bcc5 100644 --- a/server-up/translations/nl.json +++ b/server-up/translations/nl.json @@ -138,7 +138,7 @@ "release_notes": "Release notes", "view_release": "Bekijk release", "update_api_url": "Releases-API URL", - "update_api_hint": "Forgejo/GitHub Releases-API die de laatste release teruggeeft. Leeg = uitgeschakeld.", + "update_api_hint": "Adres van je releases. Je mag het adres uit de adresbalk plakken; Server Up rekent het zelf om naar het API-adres met /api/v1/repos/. Leeg = uitgeschakeld.", "include_prerelease": "Pre-releases (beta) meenemen", "login": "Inloggen", "login_failed": "Inloggen mislukt", diff --git a/tests/test_update_adres.py b/tests/test_update_adres.py new file mode 100644 index 0000000..66bcc52 --- /dev/null +++ b/tests/test_update_adres.py @@ -0,0 +1,119 @@ +"""Het adres van de releases-API en van het image, zoals mensen ze invullen. + +Wie in Forgejo naar zijn releases kijkt en dat adres uit de adresbalk plakt, +geeft het adres van een webpagina op. De enige melding die daarop volgde was +`JSONDecodeError: Expecting value: line 1 column 1 (char 0)`, en daar leidt +niemand uit af dat er `/api/v1/repos/` in had moeten staan. +""" +import sys +from pathlib import Path + +import pytest + +WORTEL = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(WORTEL / "server-up")) + + +# ── Het releases-adres ─────────────────────────────────────────────────────── + +@pytest.mark.parametrize("ingevuld,verwacht", [ + # Wat je uit de adresbalk van Forgejo plakt. + ("https://git.voorbeeld.nl/bes-r/server-up/releases", + "https://git.voorbeeld.nl/api/v1/repos/bes-r/server-up/releases"), + # Zonder /releases erachter. + ("https://git.voorbeeld.nl/bes-r/server-up", + "https://git.voorbeeld.nl/api/v1/repos/bes-r/server-up/releases"), + # Met poort, en http. + ("http://10.0.20.22:3000/bes-r/server-up/releases", + "http://10.0.20.22:3000/api/v1/repos/bes-r/server-up/releases"), + # De kloon-URL. + ("https://git.voorbeeld.nl/bes-r/server-up.git", + "https://git.voorbeeld.nl/api/v1/repos/bes-r/server-up/releases"), + # GitHub heeft zijn API op een eigen host. + ("https://github.com/bes-r/server-up/releases", + "https://api.github.com/repos/bes-r/server-up/releases"), +]) +def test_webadres_wordt_een_apiadres(env, ingevuld, verwacht): + from core import updater + assert updater.api_url(ingevuld) == verwacht + + +@pytest.mark.parametrize("al_goed", [ + "https://git.voorbeeld.nl/api/v1/repos/bes-r/server-up/releases", + "https://api.github.com/repos/bes-r/server-up/releases", +]) +def test_een_apiadres_blijft_ongemoeid(env, al_goed): + from core import updater + assert updater.api_url(al_goed) == al_goed + + +def test_leeg_blijft_leeg(env): + from core import updater + assert updater.api_url("") == "" + assert updater.api_url(" ") == "" + + +def test_onherkenbaar_adres_blijft_staan(env): + """Niet slimmer doen dan we zijn: wat we niet herkennen laten we met rust, + zodat een eigen proxy-adres blijft werken.""" + from core import updater + raar = "https://voorbeeld.nl/een/diep/pad/met/releases.json" + assert updater.api_url(raar) == raar + + +def test_html_geeft_een_begrijpelijke_fout(env, monkeypatch): + """De melding moet zeggen wát er mis is, niet dat JSON niet parseert.""" + from core import updater + + class _Antwoord: + def read(self): + return b"\n" + def __enter__(self): + return self + def __exit__(self, *a): + return False + + monkeypatch.setattr(updater.urllib.request, "urlopen", + lambda *a, **k: _Antwoord()) + with pytest.raises(ValueError) as fout: + updater._releases("https://git.voorbeeld.nl/bes-r/server-up/releases") + melding = str(fout.value) + assert "webpagina" in melding + assert "/api/v1/repos/" in melding + assert "JSONDecode" not in melding + + +def test_check_gebruikt_het_omgerekende_adres(env, monkeypatch): + from core import updater + env["core"].patch({"UPDATE_API_URL": + "https://git.voorbeeld.nl/bes-r/server-up/releases"}) + gebruikt = [] + monkeypatch.setattr(updater, "_releases", + lambda u: gebruikt.append(u) or [ + {"tag_name": "v9.9.99", "prerelease": False}]) + res = updater.check("0.8.28", force=True) + assert gebruikt == [ + "https://git.voorbeeld.nl/api/v1/repos/bes-r/server-up/releases"] + assert res["update_available"] is True + + +# ── Het imageadres ─────────────────────────────────────────────────────────── + +def test_schema_voor_het_image_wordt_weggehaald(env): + """`docker pull https://…` bestaat niet, maar in het veld ziet het er goed + uit. Er is geen geldig image dat met een schema begint, dus weghalen kan.""" + from core import selfupdate + env["core"].patch({"UPDATE_IMAGE": "https://git.voorbeeld.nl/bes-r/server-up"}) + assert selfupdate.image_repo({}) == "git.voorbeeld.nl/bes-r/server-up" + + env["core"].patch({"UPDATE_IMAGE": "http://10.0.20.22:3000/bes-r/server-up/"}) + assert selfupdate.image_repo({}) == "10.0.20.22:3000/bes-r/server-up" + + +def test_image_zonder_schema_blijft_gelijk(env): + from core import selfupdate + env["core"].patch({"UPDATE_IMAGE": "git.voorbeeld.nl/bes-r/server-up"}) + assert selfupdate.image_repo({}) == "git.voorbeeld.nl/bes-r/server-up" + # Een tag hoort er niet in; die komt van de release. + env["core"].patch({"UPDATE_IMAGE": "git.voorbeeld.nl/bes-r/server-up:0.8.28"}) + assert selfupdate.image_repo({}) == "git.voorbeeld.nl/bes-r/server-up"