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 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 23:00:27 +02:00
co-authored by Claude Opus 5
parent 44ad7dc6b3
commit 92b49a59a5
3 changed files with 69 additions and 5 deletions
+45 -3
View File
@@ -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')))
+1 -1
View File
@@ -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":…}`}
/>