feat: ein Name statt zwei, und PUBLIC_URL ist nur noch der Ursprung
fivecdn.d4rkst3r.de und fivemanage.d4rkst3r.de fallen weg -- wir betreiben kein Fivemanage, also tragen unsere Adressen auch nicht dessen Namen. Dashboard und API liegen an der Wurzel, die Dateien unter /f/. Der Ein-Namen-Betrieb war bis jetzt nicht ausdrueckbar. Der Kommentar in config.ts versprach "leer lassen, wenn alles unter einem Namen laufen soll", aber optional() faellt bei leerem Wert auf den Host aus PUBLIC_URL zurueck -- und dann gilt JEDE Anfrage als Anfrage an den Dateiwirt: kein Dashboard, keine API, /health gibt 404 und die Anmeldung 405 "hier gibt es nur Dateien". Nichts ist kaputt, und niemand kommt darauf. Deshalb ist der Ein-Namen-Betrieb jetzt der Standard und der zweite Name die Ansage. PUBLIC_URL ist nur noch der Ursprung; das /f haengt der Dienst selbst an. Wer es mitschriebe, bekaeme Adressen mit /f/f/, und das faellt erst auf, wenn das erste Bild fehlt. Praefix und Route haengen jetzt an derselben Stelle (config.filePrefix), damit die zurueckgegebene Adresse und die Route, die sie ausliefert, nicht auseinanderlaufen koennen. Beide Betriebsarten durchgemessen: mit einem Namen kommt .../f/vehicles/adder.png zurueck und liefert die Datei; mit gesetztem FILES_HOST kommt sie ohne Praefix, liegt unter dem Dateihost, ist am Dashboard-Namen zusaetzlich unter /f/ erreichbar, und unter dem Dateihost gibt es weiterhin keine API. Welche Betriebsart laeuft, sagt der Dienst in der zweiten Startzeile. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+41
-15
@@ -27,16 +27,27 @@ kein zweiter Dienst, kein Passwort dazwischen, und eine Sicherung ist ein
|
|||||||
`cp -a` über ein Volume. Wenn daraus je Millionen Zeilen werden, wird
|
`cp -a` über ein Volume. Wenn daraus je Millionen Zeilen werden, wird
|
||||||
`server/src/db.ts` ausgetauscht — die Aufrufer merken davon nichts.
|
`server/src/db.ts` ausgetauscht — die Aufrufer merken davon nichts.
|
||||||
|
|
||||||
**Zwei Hostnamen, ein Prozess.**
|
**Ein Name.** `fivecdn.d4rkst3r.de` und `fivemanage.d4rkst3r.de` fallen weg —
|
||||||
|
wir betreiben kein Fivemanage, also tragen unsere Adressen auch nicht dessen
|
||||||
|
Namen. Dashboard und API liegen an der Wurzel, die Dateien unter `/f/`:
|
||||||
|
|
||||||
| | |
|
```
|
||||||
|---|---|
|
https://media.d4rkst3r.de/ Dashboard und API
|
||||||
| `fivecdn.d4rkst3r.de` | nur Dateien. Kein Dashboard, keine API, nichts anzumelden |
|
https://media.d4rkst3r.de/f/vehicles/adder.webp
|
||||||
| `fivemanage.d4rkst3r.de` | Dashboard und API |
|
```
|
||||||
|
|
||||||
Das hält die öffentliche Adresse frei von Angriffsfläche und die URLs hübsch:
|
Ein DNS-Eintrag, ein Host im Proxy. Aufgegeben ist damit eine Eigenschaft, die
|
||||||
`https://fivecdn.d4rkst3r.de/vehicles/adder.webp`, ohne `/f/`-Präfix. Die
|
zwei Namen mitbrächten: die öffentliche Adresse trägt jetzt auch das
|
||||||
Vorlage im Handy bleibt damit schlicht `{model}.webp`.
|
Anmeldeformular.
|
||||||
|
|
||||||
|
Zwei Namen kann der Dienst weiterhin — `FILES_HOST` setzen, dann gibt es unter
|
||||||
|
diesem Namen ausschließlich Dateien und die URLs kommen ohne `/f/` aus. Beides
|
||||||
|
ist gemessen. Der Wechsel ist billig: die Adressen stehen **nirgends in der
|
||||||
|
Datenbank**, sie werden bei jeder Antwort aus `PUBLIC_URL` gebaut.
|
||||||
|
|
||||||
|
**Der Pfad bleibt in beiden Fällen der Schlüssel.** `vehicles/adder.webp`
|
||||||
|
bleibt `vehicles/adder.webp`, die Vorlage im Handy also `{model}.webp`. Nur der
|
||||||
|
Teil davor ändert sich, und der steht in einer Variablen.
|
||||||
|
|
||||||
**Der Pfad ist der Schlüssel.** `X-Path: vehicles/adder.webp` legt die Datei
|
**Der Pfad ist der Schlüssel.** `X-Path: vehicles/adder.webp` legt die Datei
|
||||||
genau dort ab. Vorhersagbare URLs sind für das Fotostudio die ganze
|
genau dort ab. Vorhersagbare URLs sind für das Fotostudio die ganze
|
||||||
@@ -100,13 +111,14 @@ cp server/.env.example server/.env
|
|||||||
npm --prefix server ci && npm --prefix server run dev
|
npm --prefix server ci && npm --prefix server run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Der Stolperstein beim lokalen Start:** ohne `FILES_HOST` nimmt der Dienst
|
> **Der Stolperstein, den es nicht mehr gibt.** Anfangs war der Standard für
|
||||||
> den Host aus `PUBLIC_URL` — lokal also `localhost`. Dann gilt *jede* Anfrage
|
> `FILES_HOST` der Host aus `PUBLIC_URL`. Bei zwei Namen ist das richtig, bei
|
||||||
> als Anfrage an den Dateiwirt, und dort gibt es absichtlich weder Dashboard
|
> einem tödlich: dann galt *jede* Anfrage als Anfrage an den Dateiwirt,
|
||||||
> noch API: `/health` antwortet 404, die Anmeldung 405 „hier gibt es nur
|
> `/health` gab 404 und die Anmeldung 405 „hier gibt es nur Dateien". Nichts
|
||||||
> Dateien". Nichts ist kaputt. In Produktion sind es zwei verschiedene Namen
|
> war kaputt, und niemand kommt darauf. Seit der Entscheidung für einen Namen
|
||||||
> und der Standard stimmt; lokal gehört ein Name hin, den niemand aufruft.
|
> ist der Ein-Namen-Betrieb der Standard und der zweite Name die Ansage —
|
||||||
> Steht samt Begründung in `server/.env.example`.
|
> lokal läuft es damit ohne Sonderbehandlung. Welche Betriebsart gilt, sagt
|
||||||
|
> der Dienst in der zweiten Startzeile.
|
||||||
|
|
||||||
**Was der Test gefunden hat** — alles im Grundgerüst, alles repariert:
|
**Was der Test gefunden hat** — alles im Grundgerüst, alles repariert:
|
||||||
|
|
||||||
@@ -150,6 +162,20 @@ HEAD, Bereichsanfragen und 416 · unter dem Dateihost gibt es weder API noch
|
|||||||
|
|
||||||
Nicht getestet: Docker, das Dashboard (gibt es noch nicht), und echte Last.
|
Nicht getestet: Docker, das Dashboard (gibt es noch nicht), und echte Last.
|
||||||
|
|
||||||
|
### ✅ Fertig — ein Name statt zwei
|
||||||
|
|
||||||
|
`PUBLIC_URL` ist jetzt nur noch der Ursprung; das `/f` hängt der Dienst selbst
|
||||||
|
an. Wer es mitschriebe, bekäme Adressen mit `/f/f/` — und das fiele erst auf,
|
||||||
|
wenn das erste Bild fehlt. Prefix und Route werden deshalb an genau **einer**
|
||||||
|
Stelle entschieden (`config.filePrefix`), damit die zurückgegebene Adresse und
|
||||||
|
die Route, die sie ausliefert, nicht auseinanderlaufen können.
|
||||||
|
|
||||||
|
Beide Betriebsarten sind durchgemessen: mit einem Namen kommt
|
||||||
|
`…/f/vehicles/adder.png` zurück und liefert die Datei; mit gesetztem
|
||||||
|
`FILES_HOST` kommt sie ohne Präfix, liegt unter dem Dateihost, ist am
|
||||||
|
Dashboard-Namen zusätzlich unter `/f/` erreichbar, und unter dem Dateihost gibt
|
||||||
|
es weiterhin keine API.
|
||||||
|
|
||||||
### ⬜ Als Nächstes
|
### ⬜ Als Nächstes
|
||||||
|
|
||||||
**2 · Dockerfile und Compose.** Ein Abbild, das Server und gebaute Oberfläche
|
**2 · Dockerfile und Compose.** Ein Abbild, das Server und gebaute Oberfläche
|
||||||
|
|||||||
+22
-16
@@ -3,26 +3,32 @@
|
|||||||
# Ohne PUBLIC_URL und ADMIN_PASSWORD startet der Dienst nicht — das ist
|
# Ohne PUBLIC_URL und ADMIN_PASSWORD startet der Dienst nicht — das ist
|
||||||
# Absicht, siehe config.ts.
|
# Absicht, siehe config.ts.
|
||||||
|
|
||||||
# Unter welcher Adresse die Dateien oeffentlich stehen. Daraus wird die URL
|
# Unter welcher Adresse der Dienst oeffentlich steht. NUR der Ursprung, ohne
|
||||||
# gebaut, die nach dem Upload zurueckkommt. Ohne abschliessenden Schraegstrich.
|
# Pfad und ohne abschliessenden Schraegstrich.
|
||||||
PUBLIC_URL=http://localhost:8080/f
|
#
|
||||||
|
# Das /f/ haengt der Dienst selbst an. Wer es hier mitschreibt, bekommt
|
||||||
|
# Adressen mit /f/f/ — und das faellt erst auf, wenn das erste Bild fehlt.
|
||||||
|
#
|
||||||
|
# In Produktion also: PUBLIC_URL=https://media.d4rkst3r.de
|
||||||
|
PUBLIC_URL=http://localhost:8080
|
||||||
|
|
||||||
# DER STOLPERSTEIN BEIM LOKALEN START.
|
# EIN NAME ODER ZWEI.
|
||||||
#
|
#
|
||||||
# Ohne diese Zeile nimmt der Dienst den Host aus PUBLIC_URL — lokal also
|
# Leer lassen (Standard) heisst EIN Name: Dashboard und API an der Wurzel,
|
||||||
# "localhost". Dann gilt jede Anfrage als Anfrage an den Dateiwirt, und dort
|
# Dateien unter /f/. Die zurueckgegebene Adresse lautet dann
|
||||||
# gibt es absichtlich weder Dashboard noch API: /health antwortet 404, die
|
# https://media.d4rkst3r.de/f/vehicles/adder.webp. Ein DNS-Eintrag, ein Host
|
||||||
# Anmeldung 405 "hier gibt es nur Dateien". Nichts ist kaputt, der Dienst tut
|
# im Proxy — dafuer traegt die oeffentliche Adresse auch das Anmeldeformular.
|
||||||
# genau das, was er soll — nur eben unter dem einen Namen, den man lokal hat.
|
|
||||||
#
|
#
|
||||||
# In Produktion sind es zwei verschiedene Namen und der Standard stimmt:
|
# Wer hier einen Hostnamen eintraegt, bekommt ZWEI Namen: unter diesem gibt es
|
||||||
# PUBLIC_URL=https://fivecdn.d4rkst3r.de
|
# dann ausschliesslich Dateien, kein Dashboard, keine API, nichts anzumelden.
|
||||||
# FILES_HOST=fivecdn.d4rkst3r.de (Dashboard: fivemanage.d4rkst3r.de)
|
# Die URLs kommen ohne /f/ aus, und der Name, den jeder Spieler kennt, hat
|
||||||
|
# keine Angriffsflaeche. Beide Namen muessen im Proxy auf denselben Port
|
||||||
|
# zeigen; PUBLIC_URL ist dann der Datei-Name:
|
||||||
|
# PUBLIC_URL=https://cdn.d4rkst3r.de
|
||||||
|
# FILES_HOST=cdn.d4rkst3r.de
|
||||||
#
|
#
|
||||||
# Lokal deshalb ein Name, den niemand aufruft — dann bleibt localhost fuer
|
# Welche Betriebsart laeuft, sagt der Dienst beim Start.
|
||||||
# Dashboard und API frei. Zum Testen des Dateiwirts:
|
FILES_HOST=
|
||||||
# curl -H "Host: files.localhost" http://localhost:8080/vehicles/adder.webp
|
|
||||||
FILES_HOST=files.localhost
|
|
||||||
|
|
||||||
# Wird beim ERSTEN Start angelegt. Ein spaeter geaendertes Passwort setzt das
|
# Wird beim ERSTEN Start angelegt. Ein spaeter geaendertes Passwort setzt das
|
||||||
# bestehende nicht zurueck.
|
# bestehende nicht zurueck.
|
||||||
|
|||||||
+29
-12
@@ -40,11 +40,30 @@ for (const dir of [dataDir, filesDir]) {
|
|||||||
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Unter welcher Adresse die Dateien oeffentlich stehen. Daraus wird die URL
|
/** Unter welcher Adresse der Dienst oeffentlich steht. Daraus wird die URL
|
||||||
* gebaut, die nach dem Upload zurueckkommt — und die im Handy in der Vorlage
|
* gebaut, die nach dem Upload zurueckkommt — und die im Handy in der Vorlage
|
||||||
* steht. Ohne abschliessenden Schraegstrich. */
|
* steht. Nur der Ursprung, ohne Pfad und ohne abschliessenden Schraegstrich:
|
||||||
|
* https://media.d4rkst3r.de
|
||||||
|
*
|
||||||
|
* Das /f/ im Ein-Namen-Betrieb haengt der Dienst selbst an. Wer es hier
|
||||||
|
* mitschriebe, bekaeme Adressen mit /f/f/ — und die faende niemand, bis das
|
||||||
|
* erste Bild fehlt. */
|
||||||
const publicUrl = required('PUBLIC_URL').replace(/\/+$/, '')
|
const publicUrl = required('PUBLIC_URL').replace(/\/+$/, '')
|
||||||
|
|
||||||
|
/** Ein Name oder zwei?
|
||||||
|
*
|
||||||
|
* Gesetzt → ZWEI Namen: unter diesem Host gibt es ausschliesslich Dateien,
|
||||||
|
* kein Dashboard, keine API. Die oeffentliche Adresse hat damit
|
||||||
|
* keine Angriffsflaeche, und die URLs kommen ohne Praefix aus.
|
||||||
|
* Leer → EIN Name: Dashboard und API an der Wurzel, Dateien unter /f/.
|
||||||
|
*
|
||||||
|
* Frueher war der Standard der Host aus PUBLIC_URL. Das ist bei zwei Namen
|
||||||
|
* richtig und bei einem toedlich: dann gilt JEDE Anfrage als Anfrage an den
|
||||||
|
* Dateiwirt, /health antwortet 404 und die Anmeldung 405 "hier gibt es nur
|
||||||
|
* Dateien". Nichts ist kaputt, und niemand kommt darauf. Deshalb ist der
|
||||||
|
* Ein-Namen-Betrieb jetzt der Standard und der zweite Name die Ansage. */
|
||||||
|
const filesHost = (process.env.FILES_HOST ?? '').trim().toLowerCase()
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
port: number('PORT', 8080),
|
port: number('PORT', 8080),
|
||||||
dataDir,
|
dataDir,
|
||||||
@@ -53,16 +72,14 @@ export const config = {
|
|||||||
|
|
||||||
publicUrl,
|
publicUrl,
|
||||||
|
|
||||||
/** Der Hostname, unter dem NUR Dateien ausgeliefert werden.
|
/** Der Hostname, unter dem NUR Dateien ausgeliefert werden — oder leer,
|
||||||
*
|
* wenn alles unter einem Namen laeuft. Siehe oben. */
|
||||||
* Trifft eine Anfrage unter diesem Namen ein, gibt es weder Dashboard
|
filesHost,
|
||||||
* noch API — nur GET auf Dateien. Das haelt die oeffentliche Adresse frei
|
|
||||||
* von jeder Angriffsflaeche und macht die URLs zugleich huebsch:
|
/** Was zwischen Adresse und Pfad steht. Bei zwei Namen nichts, bei einem
|
||||||
* https://fivecdn.d4rkst3r.de/vehicles/adder.webp statt eines /f/-Praefix.
|
* "/f" — beides an genau EINER Stelle entschieden, damit die gebaute URL
|
||||||
*
|
* und die Route, die sie ausliefert, nicht auseinanderlaufen koennen. */
|
||||||
* Leer lassen, wenn alles unter einem Namen laufen soll — dann liegen die
|
filePrefix: filesHost ? '' : '/f',
|
||||||
* Dateien unter /f/. */
|
|
||||||
filesHost: optional('FILES_HOST', new URL(publicUrl).host),
|
|
||||||
|
|
||||||
/** Wird beim Start angelegt, falls es noch keinen Benutzer gibt. */
|
/** Wird beim Start angelegt, falls es noch keinen Benutzer gibt. */
|
||||||
adminPassword: required('ADMIN_PASSWORD'),
|
adminPassword: required('ADMIN_PASSWORD'),
|
||||||
|
|||||||
+31
-14
@@ -1,19 +1,23 @@
|
|||||||
// Der Einstiegspunkt.
|
// Der Einstiegspunkt.
|
||||||
//
|
//
|
||||||
// ZWEI HOSTNAMEN, EIN PROZESS. Kommt eine Anfrage unter FILES_HOST herein,
|
// EIN NAME ODER ZWEI, ein Prozess in beiden Faellen. Was gilt, entscheidet
|
||||||
// gibt es nur Dateien — kein Dashboard, keine API, kein Anmeldeformular. Das
|
// FILES_HOST, und der Start sagt es in einer Zeile.
|
||||||
// hat zwei Gruende:
|
|
||||||
//
|
//
|
||||||
// 1. Die oeffentliche Adresse bleibt huebsch. Das Handy fragt
|
// EIN NAME (Standard). Dashboard und API an der Wurzel, Dateien unter /f/:
|
||||||
// https://fivecdn.d4rkst3r.de/vehicles/adder.webp ab und nicht
|
// https://media.d4rkst3r.de/f/vehicles/adder.webp
|
||||||
// irgendein /f/-Praefix — die Vorlage lautet weiterhin schlicht
|
// Ein DNS-Eintrag, ein Host im Proxy. Dafuer traegt die oeffentliche Adresse
|
||||||
// {model}.webp.
|
// auch das Anmeldeformular.
|
||||||
// 2. Der Name, der im Internet steht und den jeder Spieler kennt, hat
|
|
||||||
// ueberhaupt keine Angriffsflaeche. Es gibt dort nichts anzumelden.
|
|
||||||
//
|
//
|
||||||
// Unter dem anderen Namen laeuft das Dashboard samt API, und die Dateien sind
|
// ZWEI NAMEN. Unter FILES_HOST gibt es dann ausschliesslich Dateien — kein
|
||||||
// dort zusaetzlich unter /f/ erreichbar — praktisch fuer die Galerie, wenn
|
// Dashboard, keine API, nichts anzumelden:
|
||||||
// jemand den zweiten Hostnamen noch nicht eingerichtet hat.
|
// https://cdn.example.de/vehicles/adder.webp
|
||||||
|
// Das haelt den Namen, den jeder Spieler kennt, ohne Angriffsflaeche und
|
||||||
|
// spart das Praefix. Unter dem anderen Namen laeuft das Dashboard, und die
|
||||||
|
// Dateien sind dort zusaetzlich unter /f/ erreichbar.
|
||||||
|
//
|
||||||
|
// In beiden Faellen gilt: DER PFAD IST DER SCHLUESSEL. vehicles/adder.webp
|
||||||
|
// bleibt vehicles/adder.webp, damit die Vorlage im Handy {model}.webp lauten
|
||||||
|
// kann. Nur der Teil davor aendert sich — und der steht in PUBLIC_URL.
|
||||||
|
|
||||||
import { createReadStream } from 'node:fs'
|
import { createReadStream } from 'node:fs'
|
||||||
import { stat } from 'node:fs/promises'
|
import { stat } from 'node:fs/promises'
|
||||||
@@ -96,6 +100,11 @@ async function serveFile(c: any, rawPath: string) {
|
|||||||
// ------------------------------------------------------------ Der Dateiwirt
|
// ------------------------------------------------------------ Der Dateiwirt
|
||||||
|
|
||||||
app.use('*', async (c, next) => {
|
app.use('*', async (c, next) => {
|
||||||
|
// Kein zweiter Name gesetzt: dann gibt es hier nichts zu entscheiden.
|
||||||
|
// Ohne diese Zeile wuerde im Ein-Namen-Betrieb jede Anfrage als Anfrage an
|
||||||
|
// den Dateiwirt gelten und die eigene API verschwinden.
|
||||||
|
if (!config.filesHost) return next()
|
||||||
|
|
||||||
const host = (c.req.header('host') ?? '').split(':')[0]?.toLowerCase()
|
const host = (c.req.header('host') ?? '').split(':')[0]?.toLowerCase()
|
||||||
if (host !== config.filesHost.split(':')[0]?.toLowerCase()) return next()
|
if (host !== config.filesHost.split(':')[0]?.toLowerCase()) return next()
|
||||||
|
|
||||||
@@ -225,7 +234,15 @@ setInterval(pruneSessions, 3600_000).unref()
|
|||||||
|
|
||||||
serve({ fetch: app.fetch, port: config.port }, (info) => {
|
serve({ fetch: app.fetch, port: config.port }, (info) => {
|
||||||
console.log(`[d4rk_media] Port ${info.port}`)
|
console.log(`[d4rk_media] Port ${info.port}`)
|
||||||
console.log(`[d4rk_media] Dateien oeffentlich unter ${config.publicUrl}`)
|
console.log(
|
||||||
console.log(`[d4rk_media] Dateiwirt hoert auf Host "${config.filesHost}"`)
|
`[d4rk_media] Dateien oeffentlich unter ${config.publicUrl}${config.filePrefix}/`,
|
||||||
|
)
|
||||||
|
// Welche Betriebsart laeuft, steht beim Start da und nicht im Quelltext.
|
||||||
|
// Wer sich wundert, warum das Dashboard nicht aufgeht, liest hier zuerst.
|
||||||
|
console.log(
|
||||||
|
config.filesHost
|
||||||
|
? `[d4rk_media] Zwei Namen: Dateiwirt hoert auf "${config.filesHost}", Dashboard und API unter jedem anderen`
|
||||||
|
: '[d4rk_media] Ein Name: Dashboard und API an der Wurzel, Dateien unter /f/',
|
||||||
|
)
|
||||||
console.log(`[d4rk_media] Daten in ${config.dataDir}`)
|
console.log(`[d4rk_media] Daten in ${config.dataDir}`)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -127,5 +127,9 @@ export async function fileExists(path: string): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Die oeffentliche Adresse einer Datei. */
|
/** Die oeffentliche Adresse einer Datei.
|
||||||
export const publicUrlFor = (path: string) => `${config.publicUrl}/${path}`
|
*
|
||||||
|
* filePrefix ist bei zwei Namen leer und bei einem "/f" — so kommt hier
|
||||||
|
* dieselbe Adresse heraus, unter der die Datei auch tatsaechlich liegt. */
|
||||||
|
export const publicUrlFor = (path: string) =>
|
||||||
|
`${config.publicUrl}${config.filePrefix}/${path}`
|
||||||
|
|||||||
Reference in New Issue
Block a user