feedBack/lib/routers/version.py
Byron Gamatos 32ebc7671e
Some checks are pending
ship-ci / ci (push) Waiting to run
refactor(server): extract the version route into routers/version.py (R3) (#856)
GET /api/version + its exclusive _safe_http_url URL validator. Bodies verbatim
except @app -> @router and the VERSION-file lookup: Path(__file__).parent (the
app root when this lived at the top level) -> Path(__file__).resolve().parents[2]
(routers -> lib -> app root). VERSION ships at the app root in every packaging
path (Dockerfile COPY VERSION /app/, desktop bundle).

server.py: 7,275 -> 7,214.

Verified: pyflakes clean; route table IDENTICAL (143); pytest 2401 passed (33 in
test_version_endpoint, incl the URL-validation + env-override cases); eslint 0.
Boot smoke: /api/version returns the real version + validated source/license URLs.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 00:56:36 +02:00

82 lines
3.3 KiB
Python

"""App version + source/license URLs (/api/version).
Extracted verbatim from ``server.py`` (R3) except the decorator (``@app`` ->
``@router``) and the VERSION-file lookup: ``Path(__file__).parent`` (app root
when this lived at the top level) -> ``Path(__file__).resolve().parents[2]``
(routers -> lib -> app root). VERSION ships at the app root in every packaging
path (Dockerfile COPY, desktop bundle).
"""
import os
from pathlib import Path
from fastapi import APIRouter
router = APIRouter()
def _safe_http_url(raw):
"""Return `raw` stripped + trailing-slash-stripped if it parses as an
http(s) URL with a non-empty host; else None.
Used to validate operator-supplied `APP_SOURCE_URL` / `APP_LICENSE_URL`
env vars before they reach `<a href>` in the UI. A bare prefix check
like `startswith(("http://","https://"))` accepts malformed inputs
such as `"https://"` (no host) or `"https:///foo"` (empty host) that
still produce broken hrefs — and, when used as a base for the default
`license_url`, garbage like `"https:///blob/main/LICENSE"`.
"""
from urllib.parse import urlsplit
if not raw:
return None
s = raw.strip().rstrip("/")
if not s:
return None
try:
parsed = urlsplit(s)
except ValueError:
return None
if parsed.scheme.lower() not in ("http", "https"):
return None
# `netloc` includes any `user:pass@` and `:port` — strings like
# "http://:80/path" have non-empty netloc (":80") but no real
# hostname. Validate `hostname` so only URLs with an actual host
# are accepted.
if not parsed.hostname:
return None
return s
@router.get("/api/version")
def get_version():
env_version = os.environ.get("APP_VERSION", "").strip()
if env_version:
version = env_version
else:
version_file = Path(__file__).resolve().parents[2] / "VERSION" # R3: app root from lib/routers/
version = "unknown"
if version_file.exists():
try:
version = version_file.read_text().strip()
except (OSError, UnicodeDecodeError):
pass
default_source_url = "https://github.com/got-feedback/feedBack"
# APP_SOURCE_URL / APP_LICENSE_URL flow straight into <a href> in the UI,
# so validate with urllib.parse rather than a bare prefix check — a prefix
# check accepts malformed values like "https://" (no host) which produce
# broken hrefs (and a constructed license_url like "https:///blob/main/LICENSE").
# _safe_http_url requires scheme in {http,https} AND a non-empty hostname
# (not just netloc — that would still accept port-only authorities like
# "http://:80/path"); fall back to the safe default otherwise.
source_url = _safe_http_url(os.environ.get("APP_SOURCE_URL")) or default_source_url
# APP_LICENSE_URL: explicit override for the LICENSE link. The default
# constructed value (source_url + "/blob/main/LICENSE") is GitHub-
# specific and assumes the repo's default branch is `main`; non-GitHub
# hosts (GitLab, Gitea, self-hosted) need an explicit value.
license_url = _safe_http_url(os.environ.get("APP_LICENSE_URL")) or (source_url + "/blob/main/LICENSE")
return {
"version": version,
"source_url": source_url,
"license_url": license_url,
}