commit 9cf254ed8ee4b24687877af391592cb11761a6e3 Author: truetype Date: Sat Oct 17 20:06:23 2026 +0000 Store Max Lab: app di prova maxlab-probe diff --git a/maxlab-probe/docker-compose.yml b/maxlab-probe/docker-compose.yml new file mode 100644 index 0000000..005c833 --- /dev/null +++ b/maxlab-probe/docker-compose.yml @@ -0,0 +1,30 @@ +version: "3.7" + +# Max Lab Probe - app di prova della Fase 0 (da disinstallare dopo il test) +services: + app_proxy: + environment: + # __1, come richiesto da Umbrel + APP_HOST: maxlab-probe_web_1 + APP_PORT: 8080 + PROXY_AUTH_ADD: "false" + + web: + image: python:3.12-alpine + restart: on-failure + command: ["python", "-u", "/probe/probe.py"] + ports: + # Verifica: porta pubblicata direttamente, senza app_proxy + - "8899:8080" + volumes: + - ${APP_DATA_DIR}/probe.py:/probe/probe.py:ro + # Verifica: socket Docker e app-data in sola lettura + - /var/run/docker.sock:/var/run/docker.sock:ro + - ${APP_DATA_DIR}/..:/umbrel-app-data:ro + + sidecar: + # Verifica: secondo servizio che condivide la rete di "web" + image: caddy:2-alpine + restart: on-failure + network_mode: "service:web" + command: ["caddy", "respond", "--listen", "127.0.0.1:9000", "sidecar ok"] diff --git a/maxlab-probe/icon.svg b/maxlab-probe/icon.svg new file mode 100644 index 0000000..049ba23 --- /dev/null +++ b/maxlab-probe/icon.svg @@ -0,0 +1 @@ + diff --git a/maxlab-probe/probe.py b/maxlab-probe/probe.py new file mode 100644 index 0000000..ada22bc --- /dev/null +++ b/maxlab-probe/probe.py @@ -0,0 +1,170 @@ +"""Max Lab Probe - verifiche tecniche della Fase 0. + +Server HTTP minimale (solo libreria standard) che, a ogni richiesta, +esegue le verifiche e restituisce un report testuale: + 1. header ricevuti (nome host, X-Forwarded-*) + 2. accesso in sola lettura al socket Docker + 3. accesso in sola lettura a ~/umbrel/app-data e lettura dei manifest + 4. raggiungibilita' dei container delle altre app (saltando app_proxy) + 5. rete condivisa con il servizio "sidecar" (network_mode: service:web) +""" + +import http.client +import json +import os +import re +import socket +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + +DOCKER_SOCK = "/var/run/docker.sock" +APP_DATA = "/umbrel-app-data" +SIDECAR = ("127.0.0.1", 9000) +LISTEN_PORT = 8080 + + +class UnixHTTPConnection(http.client.HTTPConnection): + def __init__(self, path, timeout=3): + super().__init__("localhost", timeout=timeout) + self.unix_path = path + + def connect(self): + s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + s.settimeout(self.timeout) + s.connect(self.unix_path) + self.sock = s + + +def docker_get(path): + conn = UnixHTTPConnection(DOCKER_SOCK) + try: + conn.request("GET", path) + resp = conn.getresponse() + return json.loads(resp.read()) + finally: + conn.close() + + +def http_status(host, port, path="/"): + try: + conn = http.client.HTTPConnection(host, int(port), timeout=2) + conn.request("GET", path, headers={"Host": f"{host}:{port}"}) + resp = conn.getresponse() + resp.read(256) + location = resp.getheader("Location") or "" + conn.close() + return f"HTTP {resp.status}" + (f" -> {location}" if location else "") + except Exception as exc: # noqa: BLE001 - riportiamo qualunque errore + return f"ERRORE {type(exc).__name__}: {exc}" + + +def read_text(path): + try: + with open(path, encoding="utf-8", errors="replace") as fh: + return fh.read() + except OSError: + return "" + + +def find(pattern, text): + m = re.search(pattern, text, re.MULTILINE) + return m.group(1).strip().strip("\"'") if m else None + + +def discover_apps(): + apps = [] + for app_id in sorted(os.listdir(APP_DATA)): + base = os.path.join(APP_DATA, app_id) + if not os.path.isdir(base): + continue + manifest = read_text(os.path.join(base, "umbrel-app.yml")) + compose = read_text(os.path.join(base, "docker-compose.yml")) + apps.append( + { + "id": app_id, + "name": find(r"^name:\s*(.+)$", manifest) or "?", + "port": find(r"^port:\s*(\d+)", manifest), + "app_host": find(r"APP_HOST[:=]\s*([^\s#]+)", compose), + "app_port": find(r"APP_PORT[:=]\s*([^\s#]+)", compose), + } + ) + return apps + + +def build_report(handler): + out = [] + ok = lambda cond: "OK " if cond else "FAIL" # noqa: E731 + + out.append("MAX LAB PROBE - verifiche Fase 0") + out.append("=" * 60) + + # 1. Header ricevuti + out.append("\n[1] Richiesta ricevuta") + out.append(f" client: {handler.client_address[0]}") + for name in ("Host", "X-Forwarded-For", "X-Forwarded-Proto", "X-Forwarded-Host"): + out.append(f" {name}: {handler.headers.get(name, '-')}") + + # 2. Socket Docker + out.append("\n[2] Socket Docker (sola lettura)") + containers = [] + try: + containers = docker_get("/containers/json") + out.append(f" {ok(True)} {len(containers)} container in esecuzione") + me = next( + (c for c in containers if c["Id"].startswith(socket.gethostname())), None + ) + if me: + nets = ", ".join(me["NetworkSettings"]["Networks"].keys()) + out.append(f" questo container: {me['Names'][0]} - reti: {nets}") + except Exception as exc: # noqa: BLE001 + out.append(f" {ok(False)} {type(exc).__name__}: {exc}") + + # 3. app-data + out.append("\n[3] Cartella app-data (sola lettura)") + apps = [] + try: + apps = discover_apps() + out.append(f" {ok(bool(apps))} {len(apps)} app trovate") + except Exception as exc: # noqa: BLE001 + out.append(f" {ok(False)} {type(exc).__name__}: {exc}") + + # 4. Raggiungibilita' container delle app, saltando app_proxy + out.append("\n[4] Container delle app raggiunti direttamente") + running = {n.lstrip("/") for c in containers for n in c.get("Names", [])} + for app in apps: + if not app["app_host"] or not app["app_port"]: + out.append(f" ---- {app['id']}: nessun app_proxy nel compose") + continue + host, port = app["app_host"], app["app_port"] + state = "attivo" if host in running else "non trovato tra i container" + result = http_status(host, port) + out.append( + f" {app['id']} ({app['name']}) porta UI {app['port']} -> " + f"{host}:{port} [{state}] {result}" + ) + + # 5. Sidecar con rete condivisa + out.append("\n[5] Servizio sidecar sulla stessa rete (127.0.0.1:9000)") + res = http_status(*SIDECAR) + out.append(f" {ok(res.startswith('HTTP 200'))} {res}") + + out.append("\nFine report. Disinstalla l'app dopo il test: elenca le tue app.") + return "\n".join(out) + "\n" + + +class Handler(BaseHTTPRequestHandler): + def do_GET(self): # noqa: N802 - nome imposto da BaseHTTPRequestHandler + body = build_report(self).encode("utf-8") + self.send_response(200) + self.send_header("Content-Type", "text/plain; charset=utf-8") + self.send_header("Content-Length", str(len(body))) + self.send_header("Cache-Control", "no-store") + self.end_headers() + self.wfile.write(body) + + def log_message(self, fmt, *args): + print(f"{self.client_address[0]} {fmt % args}", flush=True) + + +if __name__ == "__main__": + print(f"Max Lab Probe in ascolto su :{LISTEN_PORT}", flush=True) + ThreadingHTTPServer(("0.0.0.0", LISTEN_PORT), Handler).serve_forever() diff --git a/maxlab-probe/umbrel-app.yml b/maxlab-probe/umbrel-app.yml new file mode 100644 index 0000000..1e30223 --- /dev/null +++ b/maxlab-probe/umbrel-app.yml @@ -0,0 +1,25 @@ +manifestVersion: 1 +id: maxlab-probe +name: Max Lab Probe +tagline: Verifiche tecniche per Umbrel Lab Gateway (Fase 0) +icon: https://gitea.solarpi.net/truetype/umbrel-maxlab-store/raw/branch/main/maxlab-probe/icon.svg +category: developer +version: "0.0.1" +port: 8898 +description: >- + App di prova temporanea. Mostra un report su: porta pubblicata + direttamente (8899), accesso in sola lettura al socket Docker e ad + app-data, raggiungibilita' dei container delle altre app senza + app_proxy, rete condivisa tra due servizi. Da disinstallare dopo il test. +developer: truetype +website: https://gitea.solarpi.net/truetype/umbrel-maxlab-store +submitter: truetype +submission: https://gitea.solarpi.net/truetype/umbrel-maxlab-store +repo: https://gitea.solarpi.net/truetype/umbrel-maxlab-store +support: https://gitea.solarpi.net/truetype/umbrel-maxlab-store/issues +gallery: [] +releaseNotes: Prima versione di prova. +dependencies: [] +path: "" +defaultUsername: "" +defaultPassword: "" diff --git a/umbrel-app-store.yml b/umbrel-app-store.yml new file mode 100644 index 0000000..67a7327 --- /dev/null +++ b/umbrel-app-store.yml @@ -0,0 +1,2 @@ +id: "maxlab" +name: "Max Lab"