fix: sechs Befunde aus dem ersten Start des Servers

Das Grundgeruest war typgeprueft, aber nie gelaufen. Beim ersten Start
haben sich sechs Dinge gezeigt, alle nachgemessen statt vermutet:

1. Die Token-Pruefung hing an '*' und galt damit auch fuer /api/dash/*,
   das daneben liegt. Das Dashboard bekam "Token fehlt oder ist
   unbekannt" auf die Anmeldung, obwohl es nie einen Token haben kann.
   Sie haengt jetzt an den drei eigenen Pfaden.

2. DELETE /api/media/... und GET /api/exists/... sahen am Ziel vorbei.
   c.req.path traegt den Einhaengepunkt mit, das replace(/^\/media\//)
   schnitt ihn nicht weg -- aus vehicles/adder.png wurde
   api/media/vehicles/adder.png. Loeschen fand nie etwas, exists meldete
   immer false. Jetzt :pfad{.+}; Hono liefert den Parameter fertig
   dekodiert (an einer Probe gemessen), ein zweites decodeURIComponent
   waere eine Dekodierung zu viel.

3. Verzeichnisdurchstieg in der SPA-Rueckfallroute: GET /..%5Cpackage.json
   hat unter Windows die Datei ausgeliefert. Hono reicht %5C durch,
   path.join behandelt den Backslash dort als Trenner, und eine
   Eindaemmung gab es nicht. Unter Linux traegt genau dieser Angriff
   nicht -- Glueck, keine Abwehr. Jetzt dieselbe resolve-Pruefung wie in
   storage.ts.

4. Die Auskunft "Oberflaeche ist nicht gebaut" war unerreichbar.
   createReadStream meldet eine fehlende Datei asynchron, das try/catch
   darum fing nichts. Ergebnis war ein leerer 200 samt ENOENT im Log.

5. CSS und JS kamen als application/octet-stream -- die MIME-Tabelle
   kennt nur Medientypen, das Dashboard haette weder Stylesheet noch
   Modul geladen. Die Oberflaeche bekommt eine eigene Tabelle: in der
   geteilten fehlt html mit Absicht, sonst koennte jeder mit einem
   Upload-Token eine Seite unter fivecdn.d4rkst3r.de veroeffentlichen.

6. Kaputtes JSON endete als nackter "Internal Server Error".
   Ausgerechnet das -- ein 500 ohne ein Wort dazu ist der Fehler, wegen
   dem wir hier neu bauen. Jetzt 400 mit Text.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 15:39:33 +02:00
co-authored by Claude Opus 5
parent e36db8c015
commit cfd0f0747a
3 changed files with 138 additions and 42 deletions
+23 -8
View File
@@ -20,7 +20,7 @@
// dem NUI, ist also schon Base64 — der Umweg ist damit keiner.
import { Hono } from 'hono'
import type { Context } from 'hono'
import type { Context, Next } from 'hono'
import { config } from '../config.js'
import { db, now, type Media, type Token } from '../db.js'
import { tokenAllows, tokenFromHeader } from '../auth.js'
@@ -40,14 +40,23 @@ type Vars = { token: Token }
export const uploadRoutes = new Hono<{ Variables: Vars }>()
uploadRoutes.use('*', async (c, next) => {
// Die Token-Pruefung haengt an den drei eigenen Pfaden und NICHT an '*'.
//
// Mit '*' galt sie fuer alles unterhalb des Einhaengepunkts — auch fuer
// /api/dash/*, das daneben liegt. Das Dashboard bekam dann "Token fehlt oder
// ist unbekannt" auf die Anmeldung, obwohl es nie einen Token haben kann.
const requireToken = async (c: Context<{ Variables: Vars }>, next: Next) => {
const token = tokenFromHeader(c.req.header('authorization'))
if (!token) {
return c.json({ error: 'Token fehlt oder ist unbekannt' }, 401)
}
c.set('token', token)
await next()
})
}
uploadRoutes.use('/upload', requireToken)
uploadRoutes.use('/media/*', requireToken)
uploadRoutes.use('/exists/*', requireToken)
/** Den Rumpf einsammeln, in welcher der drei Formen er auch kommt. */
async function readBody(
@@ -179,8 +188,14 @@ uploadRoutes.post('/upload', async (c) => {
})
})
/** Loeschen ueber den Pfad — Skripte kennen den Pfad, nicht unsere ID. */
uploadRoutes.delete('/media/*', async (c) => {
/** Loeschen ueber den Pfad — Skripte kennen den Pfad, nicht unsere ID.
*
* `:pfad{.+}` und nicht `*`: ein Sternchen wird von Hono nicht als Parameter
* erfasst, und `c.req.path` traegt den Einhaengepunkt mit — aus
* DELETE /api/media/vehicles/adder.png wurde damit der Pfad
* "api/media/vehicles/adder.png". Der Parameter kommt bereits dekodiert,
* ein zweites decodeURIComponent waere eine Dekodierung zu viel. */
uploadRoutes.delete('/media/:pfad{.+}', async (c) => {
const token = c.get('token')
if (!token.can_delete) {
return c.json({ error: 'dieser Token darf nicht loeschen' }, 403)
@@ -188,7 +203,7 @@ uploadRoutes.delete('/media/*', async (c) => {
let path: string
try {
path = checkPath(decodeURIComponent(c.req.path.replace(/^\/media\//, '')))
path = checkPath(c.req.param('pfad'))
} catch (err) {
if (err instanceof PathError) return c.json({ error: err.message }, 400)
throw err
@@ -206,10 +221,10 @@ uploadRoutes.delete('/media/*', async (c) => {
/** Nachsehen, ob es etwas schon gibt — damit ein Lauf "nur fehlende"
* beantworten kann, ohne 900 Bilder hochzuladen. */
uploadRoutes.get('/exists/*', (c) => {
uploadRoutes.get('/exists/:pfad{.+}', (c) => {
let path: string
try {
path = checkPath(decodeURIComponent(c.req.path.replace(/^\/exists\//, '')))
path = checkPath(c.req.param('pfad'))
} catch {
return c.json({ exists: false }, 200)
}