From 92b49a59a5d0dc23377eda83037d4b84c672484f Mon Sep 17 00:00:00 2001 From: D4rkst3r Date: Tue, 11 Aug 2026 23:00:27 +0200 Subject: [PATCH] fix: /api/exists gab ohne Token Auskunft -- der Proxy hatte sie zwischengespeichert GEMESSEN, nicht vermutet: ein GET auf https://fivemanage.d4rkst3r.de/api/exists/vehicles/adder.webp antwortet OHNE Authorization-Kopf mit 200 und voller Auskunft -- Pfad, Groesse, SHA-256, Zeitpunkt. Nicht weil die Pruefung fehlt, sondern weil Nginx Proxy Manager die berechtigte Antwort weggelegt hat und sie danach an jeden ausliefert. Der Grund steht in dessen assets.conf und greift auf JEDE URL mit Bildendung: location ~* ^.*\.(css|js|jpe?g|gif|png|webp|...)$ { proxy_cache public-cache; proxy_cache_key $host$request_uri; <- ohne Authorization proxy_ignore_headers Set-Cookie Cache-Control Expires ...; proxy_cache_valid any 30m; } Unser eigenes Cache-Control hilft nicht, proxy_ignore_headers wirft es weg. Und es geht auch andersherum: landet zuerst ein 401 im Zwischenspeicher, bekommen ihn 30 Minuten lang alle -- auch die mit gueltigem Token. Der Block "location ^~ /api/ { proxy_cache off; }" schliesst das. Aber eine ZUGANGSPRUEFUNG, DIE AN EINER ZEILE IN EINER FREMDEN OBERFLAECHE HAENGT, IST KEINE: sie ueberlebt kein Neuanlegen des Proxy-Hosts und keinen Umzug. Deshalb eine zweite Form, die von sich aus dicht ist: GET /api/exists?pfad=vehicles/adder.webp Die Abfrage zaehlt fuer die location-Regel nicht mit, die URL endet also nie auf eine Bildendung. Gemessen: mit Token 200, ohne Token 401, und KEINE Cache-Control-Kopfzeile vom Proxy -- die Regel greift nicht. Die alte Form bleibt, weil das Fotostudio sie benutzt (server/upload.lua:185). Dokumentation und die API-Seite im Dashboard nennen ab jetzt nur noch die neue. Co-Authored-By: Claude Opus 5 --- docs/API.md | 24 ++++++++++++++++++- server/src/routes/upload.ts | 48 ++++++++++++++++++++++++++++++++++--- server/ui/src/pages/Api.tsx | 2 +- 3 files changed, 69 insertions(+), 5 deletions(-) diff --git a/docs/API.md b/docs/API.md index 38e84fe..fe8cb45 100644 --- a/docs/API.md +++ b/docs/API.md @@ -235,11 +235,33 @@ heraus wäre entweder Mixed Content oder ginge ins Leere. --- -## GET /api/exists/<pfad> +## GET /api/exists?pfad=<pfad> Für „nur fehlende" in einem Serienlauf: eine winzige Antwort gegen 130 KB Upload. +> **Diese Form und nicht mehr `/api/exists/`** — der Grund ist +> nachgemessen und unangenehm. Die alte Form endet auf `.webp`, und Nginx +> Proxy Managers `assets.conf` greift auf **jede** URL mit Bildendung: +> +> ```nginx +> location ~* ^.*\.(css|js|jpe?g|gif|png|webp|...)$ { +> proxy_cache public-cache; +> proxy_ignore_headers Set-Cookie Cache-Control Expires ...; +> proxy_cache_valid any 30m; +> } +> ``` +> +> Der Proxy legt die berechtigte Antwort weg und liefert sie danach **an jeden +> aus, auch ohne Token** — gemessen: 200 mit voller Auskunft, `Authorization` +> gar nicht gesetzt. Unser eigenes `Cache-Control` hilft nicht, +> `proxy_ignore_headers` wirft es weg. Und es geht auch andersherum: landet +> zuerst ein `401` im Zwischenspeicher, bekommen ihn 30 Minuten lang alle. +> +> Die Abfrageform endet nie auf eine Bildendung und ist deshalb **von sich aus** +> dicht — ohne von einer Zeile in einer fremden Oberfläche abzuhängen. Die alte +> Form bleibt, weil Skripte sie benutzen; neu gebaut wird mit dieser. + ```json { "exists": true, "path": "vehicles/adder.webp", "url": "…", "size": 132760, "sha256": "…", "updated_at": 1786466462488 } diff --git a/server/src/routes/upload.ts b/server/src/routes/upload.ts index 9c1c3e1..279c88f 100644 --- a/server/src/routes/upload.ts +++ b/server/src/routes/upload.ts @@ -86,6 +86,11 @@ function artVon(mime: string): string { uploadRoutes.use('/upload', requireToken) uploadRoutes.use('/media/*', requireToken) +// BEIDE Formen hinter der Wache. Die Abfrageform steht als eigener Eintrag da: +// '/exists/*' trifft sie NICHT, weil dort kein Schraegstrich mehr folgt -- und +// eine Auskunft ohne Token waere genau das Loch, das die Abfrageform schliessen +// soll. +uploadRoutes.use('/exists', requireToken) uploadRoutes.use('/exists/*', requireToken) /** Den Rumpf einsammeln, in welcher der drei Formen er auch kommt. */ @@ -285,11 +290,40 @@ uploadRoutes.delete('/media/:pfad{.+}', async (c) => { }) /** Nachsehen, ob es etwas schon gibt — damit ein Lauf "nur fehlende" - * beantworten kann, ohne 900 Bilder hochzuladen. */ -uploadRoutes.get('/exists/:pfad{.+}', (c) => { + * beantworten kann, ohne 900 Bilder hochzuladen. + * + * ZWEI FORMEN, UND DIE ZWEITE IST DIE WICHTIGE: + * + * GET /api/exists/vehicles/adder.webp die alte, mit Pfad + * GET /api/exists?pfad=vehicles/adder.webp die neue, mit Abfrage + * + * Der Grund ist NACHGEMESSEN und unangenehm. Nginx Proxy Manager bringt eine + * `assets.conf` mit, deren Regel auf JEDE URL greift, die auf .webp, .png, .js + * ... endet: + * + * location ~* ^.*\.(css|js|jpe?g|gif|png|webp|...)$ { + * proxy_cache public-cache; + * proxy_ignore_headers Set-Cookie Cache-Control Expires ...; + * proxy_cache_valid any 30m; + * } + * + * Die ERSTE Form endet auf ".webp" und faellt damit hinein. Der Proxy legt die + * Antwort weg -- und liefert sie danach an JEDEN aus, auch ohne Token. + * Gemessen: 200 mit vollstaendiger Auskunft, Authorization-Kopf gar nicht + * gesetzt. Unser eigenes `Cache-Control` hilft nicht, `proxy_ignore_headers` + * wirft es weg. + * + * Ein `location ^~ /api/`-Block im Proxy schliesst das -- aber eine + * Zugangspruefung, die an einer Zeile in einer fremden Oberflaeche haengt, ist + * keine. Die ZWEITE Form endet nie auf eine Bildendung (die Abfrage zaehlt + * fuer die Regel nicht mit) und ist deshalb von sich aus dicht. + * + * Die alte Form bleibt, weil Skripte sie benutzen -- aber die Dokumentation + * nennt nur noch die neue. */ +function existsAntwort(c: Context<{ Variables: Vars }>, roh: string) { let path: string try { - path = checkPath(c.req.param('pfad')) + path = checkPath(roh) } catch { return c.json({ exists: false }, 200) } @@ -302,4 +336,12 @@ uploadRoutes.get('/exists/:pfad{.+}', (c) => { ? { exists: true, path, url: publicUrlFor(path), ...row } : { exists: false, path }, ) +} + +uploadRoutes.get('/exists', (c) => { + const roh = c.req.query('pfad') ?? c.req.query('path') ?? '' + if (!roh) return c.json({ error: 'Abfrage "pfad" fehlt' }, 400) + return existsAntwort(c, roh) }) + +uploadRoutes.get('/exists/:pfad{.+}', (c) => existsAntwort(c, c.req.param('pfad'))) diff --git a/server/ui/src/pages/Api.tsx b/server/ui/src/pages/Api.tsx index 1161fef..52b0df4 100644 --- a/server/ui/src/pages/Api.tsx +++ b/server/ui/src/pages/Api.tsx @@ -131,7 +131,7 @@ curl -X POST ${B}/api/upload \\ marke="curl-exists" text={`# Gibt es das schon? — für "nur fehlende" in einem Serienlauf curl -H "Authorization: Bearer d4rk_…" \\ - ${B}/api/exists/vehicles/adder.webp + ${B}/api/exists?pfad=vehicles/adder.webp # → {"exists":true,"url":"${F}/vehicles/adder.webp","size":…}`} />