feat: die Archive lassen sich aus dem Panel holen
Einstellungen -> "Sicherungen holen": die fertigen Archive mit Datum, Groesse
und einem Knopf. Der erste Schritt, der eine Kopie VON DIESEM RECHNER HERUNTER
bringt -- bisher haette man sich dafuer per Fernwartung auf den Server klicken
muessen, und genau daran hat es gelegen. Nicht Unwille, sondern
Umstaendlichkeit.
Der Ordner haengt NUR LESEND in den Container. Ein Dienst, der seine eigenen
Sicherungen loeschen kann, ist genau das Werkzeug, das man einem uebernommenen
Dienst nicht in die Hand geben will. Nachgemessen im Container:
touch /sicherungen/probe -> Read-only file system
Und es gibt NUR EINE FORM VON NAMEN -- nicht "alles ausser .. und /", sondern
ein Muster, das genau auf die eigenen Archive passt. Wer einen Pfad
hineinschmuggeln will, muesste ihn erst so benennen. Das ist der Unterschied
zwischen dicht gemacht und dicht gebaut:
../../etc/passwd -> 404
..%2f..%2fdata%2fmedia.db -> 400
media.db -> 400
<archiv>.tar.gz.evil -> 400
ohne Anmeldung -> 401
Der ganze Weg einmal wirklich gegangen, nicht nur die Kopfzeilen: 336 MB in
15,9 s durch den Proxy, sha256 auf der Platte gleich sha256 heruntergeladen,
tar tzf zeigt 4639 Eintraege. Gestroemt und nicht in den Speicher gelesen --
sonst haetten Dienst UND Proxy die 336 MB gleichzeitig im Arbeitsspeicher.
Jeder Griff steht im Verwaltungs-Verlauf. Geloggt wird der GRIFF und nicht der
Erfolg: ein abgebrochener Download taucht ebenfalls auf, und das ist gewollt --
wer spaeter fragt, wer sich den Bestand geholt hat, will die Anfrage sehen.
Die fuenf neuen Pruefungen sind in tools/rundgang.py mit drin; der steht jetzt
bei 29 in Ordnung, 0 nicht.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+45
@@ -1235,6 +1235,51 @@ Dazu steht auf der Serverseite schon alles bereit: `sshd` läuft (Automatic,
|
|||||||
fehlt ein Konto, das nur lesen darf — und ein Skript auf dem Heimrechner, das
|
fehlt ein Konto, das nur lesen darf — und ein Skript auf dem Heimrechner, das
|
||||||
**holt**. Kandidaten und Begründung in `docs/ideen.md`.
|
**holt**. Kandidaten und Begründung in `docs/ideen.md`.
|
||||||
|
|
||||||
|
### ✅ Fertig — die Archive lassen sich aus dem Panel holen
|
||||||
|
|
||||||
|
Einstellungen → **Sicherungen holen**: die fertigen Archive mit Datum, Größe
|
||||||
|
und einem Knopf. Der erste Schritt, der eine Kopie **von diesem Rechner
|
||||||
|
herunter** bringt — bisher hätte man sich dafür per Fernwartung auf den Server
|
||||||
|
klicken müssen, und genau daran hat es gelegen. Nicht Unwille, sondern
|
||||||
|
Umständlichkeit.
|
||||||
|
|
||||||
|
**Der Ordner hängt nur lesend.** Ein Dienst, der seine eigenen Sicherungen
|
||||||
|
löschen kann, ist genau das Werkzeug, das man einem übernommenen Dienst nicht
|
||||||
|
in die Hand geben will. Herausgeben reicht. Nachgemessen im Container:
|
||||||
|
|
||||||
|
```
|
||||||
|
touch /sicherungen/probe -> Read-only file system
|
||||||
|
```
|
||||||
|
|
||||||
|
**Und es gibt nur eine Form von Namen** — nicht „alles außer `..` und `/`",
|
||||||
|
sondern ein Muster, das genau auf die eigenen Archive passt. Wer einen Pfad
|
||||||
|
hineinschmuggeln will, müsste ihn erst so benennen. Das ist der Unterschied
|
||||||
|
zwischen dicht gemacht und dicht gebaut:
|
||||||
|
|
||||||
|
```
|
||||||
|
../../etc/passwd -> 404
|
||||||
|
..%2f..%2fdata%2fmedia.db -> 400
|
||||||
|
media.db -> 400
|
||||||
|
d4rk_media-…-0854.tar.gz.evil -> 400
|
||||||
|
ohne Anmeldung -> 401
|
||||||
|
```
|
||||||
|
|
||||||
|
Der ganze Weg einmal wirklich gegangen, nicht nur die Kopfzeilen:
|
||||||
|
|
||||||
|
```
|
||||||
|
336 MB in 15,9 s durch den Proxy (~21 MB/s)
|
||||||
|
sha256 auf der Platte == sha256 heruntergeladen
|
||||||
|
tar tzf -> 4639 Eintraege
|
||||||
|
```
|
||||||
|
|
||||||
|
Jeder Griff steht im **Verwaltungs-Verlauf** (`sicherung | geholt | <Datei> |
|
||||||
|
<Bytes> | <wer>`). Geloggt wird der Griff und nicht der Erfolg: ein
|
||||||
|
abgebrochener Download taucht dort ebenfalls auf, und das ist so gewollt — wer
|
||||||
|
später fragt, wer sich den Bestand geholt hat, will die Anfrage sehen.
|
||||||
|
|
||||||
|
Die fünf neuen Prüfungen sind in `tools/rundgang.py` mit drin; der steht jetzt
|
||||||
|
bei **29 in Ordnung, 0 nicht**.
|
||||||
|
|
||||||
### ⬜ Als Nächstes
|
### ⬜ Als Nächstes
|
||||||
|
|
||||||
**Der Vergleichsmaßstab ist weg.** Am 11.08.2026 sind die Lite-Container
|
**Der Vergleichsmaßstab ist weg.** Am 11.08.2026 sind die Lite-Container
|
||||||
|
|||||||
@@ -81,6 +81,17 @@ services:
|
|||||||
# genau ein Kopiervorgang.
|
# genau ein Kopiervorgang.
|
||||||
- media-data:/data
|
- media-data:/data
|
||||||
|
|
||||||
|
# Die fertigen Archive, damit man sie sich aus dem Panel holen kann,
|
||||||
|
# ohne sich per Fernwartung auf den Server zu klicken. Genau das war
|
||||||
|
# der Grund, aus dem es lange keine Kopie ausserhalb dieser Maschine
|
||||||
|
# gab: nicht Unwillen, sondern Umstaendlichkeit.
|
||||||
|
#
|
||||||
|
# DAS :ro AM ENDE IST DER WICHTIGE TEIL. Ein Dienst, der seine
|
||||||
|
# eigenen Sicherungen loeschen kann, ist genau das Werkzeug, das man
|
||||||
|
# einem uebernommenen Dienst nicht in die Hand geben will.
|
||||||
|
# Herausgeben reicht.
|
||||||
|
- ${BACKUP_DIR:-C:/backup/d4rk_media}:/sicherungen:ro
|
||||||
|
|
||||||
ports:
|
ports:
|
||||||
# Links der Host-Port, rechts der im Container. Nur die LINKE Zahl
|
# Links der Host-Port, rechts der im Container. Nur die LINKE Zahl
|
||||||
# muss auf diesem Server einmalig sein.
|
# muss auf diesem Server einmalig sein.
|
||||||
|
|||||||
@@ -158,6 +158,14 @@ Sicherungsziel kaum.
|
|||||||
| **Discord-Kanal** (nur der Bot) | dasselbe | 9 MB je Datei — für d4rk_media mit 336 MB unbrauchbar |
|
| **Discord-Kanal** (nur der Bot) | dasselbe | 9 MB je Datei — für d4rk_media mit 336 MB unbrauchbar |
|
||||||
| 5-TB-Platte **am Server** | nur Plattenausfall | dritte Platte im selben Gehäuse — `D:` kann das schon |
|
| 5-TB-Platte **am Server** | nur Plattenausfall | dritte Platte im selben Gehäuse — `D:` kann das schon |
|
||||||
|
|
||||||
|
### Der erste Schritt ist gemacht: von Hand holen
|
||||||
|
|
||||||
|
Einstellungen → **Sicherungen holen** listet die Archive und gibt sie heraus
|
||||||
|
(nur lesend eingehängt, Namensmuster statt Filter, jeder Griff im
|
||||||
|
Verwaltungs-Verlauf). Das ersetzt keine geplante Abholung — aber es macht die
|
||||||
|
Auswärtskopie zu etwas, das man im Vorbeigehen erledigt, statt zu einem
|
||||||
|
Vorhaben. Was noch fehlt, ist, dass es **von allein** passiert.
|
||||||
|
|
||||||
### Warum HOLEN und nicht SCHICKEN
|
### Warum HOLEN und nicht SCHICKEN
|
||||||
|
|
||||||
Ein Ziel, in das der Server selbst schreiben darf, ist auch eines, das ein
|
Ein Ziel, in das der Server selbst schreiben darf, ist auch eines, das ein
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import {
|
|||||||
} from '../auth.js'
|
} from '../auth.js'
|
||||||
import { mkdir, readdir, readFile, rm, rmdir, stat } from 'node:fs/promises'
|
import { mkdir, readdir, readFile, rm, rmdir, stat } from 'node:fs/promises'
|
||||||
import { join } from 'node:path'
|
import { join } from 'node:path'
|
||||||
|
import { createReadStream } from 'node:fs'
|
||||||
import {
|
import {
|
||||||
DISCORD_KEYS,
|
DISCORD_KEYS,
|
||||||
MARKE_KEY,
|
MARKE_KEY,
|
||||||
@@ -1938,6 +1939,79 @@ const GEHEIM = new Set<string>([DISCORD_KEYS.clientSecret, SICHERUNG_KEYS.passwo
|
|||||||
* behaupten, die es dort nie gab. */
|
* behaupten, die es dort nie gab. */
|
||||||
const NICHT_MITNEHMEN = [/^sicherung\.letzte$/, /^discord\.zuletzt/]
|
const NICHT_MITNEHMEN = [/^sicherung\.letzte$/, /^discord\.zuletzt/]
|
||||||
|
|
||||||
|
// ------------------------------------------------------- Sicherungen holen
|
||||||
|
//
|
||||||
|
// WOZU. Die Archive liegen auf dem WIRT, nicht im Container -- und wer sie
|
||||||
|
// woanders haben will, musste sich bisher per Fernwartung auf den Server
|
||||||
|
// klicken und sie von Hand ziehen. Das ist der Grund, aus dem es keine Kopie
|
||||||
|
// ausserhalb dieser Maschine gibt: nicht Unwillen, sondern Umstaendlichkeit.
|
||||||
|
//
|
||||||
|
// DER ORDNER HAENGT NUR LESEND. Ein Dienst, der seine eigenen Sicherungen
|
||||||
|
// loeschen kann, ist genau das Werkzeug, das man einem uebernommenen Dienst
|
||||||
|
// nicht in die Hand geben will. Herausgeben reicht.
|
||||||
|
//
|
||||||
|
// UND ES GIBT NUR EINE FORM VON NAMEN. Nicht "alles ausser .. und /", sondern
|
||||||
|
// ein Muster, das genau auf die eigenen Archive passt -- wer einen Pfad
|
||||||
|
// hineinschmuggeln will, muesste ihn erst so benennen. Das ist der Unterschied
|
||||||
|
// zwischen "dicht gemacht" und "dicht gebaut".
|
||||||
|
const SICHERUNGEN_DIR = process.env.SICHERUNGEN_DIR || '/sicherungen'
|
||||||
|
const ARCHIV_MUSTER = /^d4rk_media-\d{4}-\d{2}-\d{2}-\d{4}\.tar\.gz$/
|
||||||
|
|
||||||
|
dashRoutes.get('/sicherungen', async (c) => {
|
||||||
|
let namen: string[]
|
||||||
|
try {
|
||||||
|
namen = await readdir(SICHERUNGEN_DIR)
|
||||||
|
} catch {
|
||||||
|
// Nicht eingehaengt ist kein Fehler, sondern ein Zustand -- die
|
||||||
|
// Oberflaeche sagt dann, was zu tun ist, statt eine rote Meldung zu
|
||||||
|
// zeigen, die nach Defekt aussieht.
|
||||||
|
return c.json({ eingehaengt: false, items: [], ordner: SICHERUNGEN_DIR })
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = []
|
||||||
|
for (const name of namen.filter((n) => ARCHIV_MUSTER.test(n))) {
|
||||||
|
try {
|
||||||
|
const s = await stat(join(SICHERUNGEN_DIR, name))
|
||||||
|
items.push({ name, size: s.size, at: s.mtimeMs })
|
||||||
|
} catch {
|
||||||
|
/* zwischendurch weggeraeumt -- dann gehoert es nicht in die Liste */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items.sort((a, b) => b.at - a.at)
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
eingehaengt: true,
|
||||||
|
ordner: SICHERUNGEN_DIR,
|
||||||
|
items,
|
||||||
|
bytes: items.reduce((n, i) => n + i.size, 0),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
dashRoutes.get('/sicherungen/:name', async (c) => {
|
||||||
|
const name = c.req.param('name')
|
||||||
|
if (!ARCHIV_MUSTER.test(name)) {
|
||||||
|
return c.json({ error: 'Kein Sicherungsarchiv.' }, 400)
|
||||||
|
}
|
||||||
|
|
||||||
|
const pfad = join(SICHERUNGEN_DIR, name)
|
||||||
|
let groesse: number
|
||||||
|
try {
|
||||||
|
groesse = (await stat(pfad)).size
|
||||||
|
} catch {
|
||||||
|
return c.json({ error: 'Gibt es nicht (mehr).' }, 404)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gestroemt und nicht in den Speicher gelesen: das Archiv ist 336 MB, und
|
||||||
|
// die haetten sonst der Dienst UND der Proxy gleichzeitig im Arbeitsspeicher.
|
||||||
|
logVerwaltung(c.get('user').username, 'sicherung', 'geholt', name, `${groesse} Bytes`)
|
||||||
|
return c.body(createReadStream(pfad) as any, 200, {
|
||||||
|
'Content-Type': 'application/gzip',
|
||||||
|
'Content-Length': String(groesse),
|
||||||
|
'Content-Disposition': `attachment; filename="${name}"`,
|
||||||
|
'Cache-Control': 'no-store',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
dashRoutes.get('/einstellungen/export', (c) => {
|
dashRoutes.get('/einstellungen/export', (c) => {
|
||||||
const mitGeheim = c.req.query('geheim') === '1'
|
const mitGeheim = c.req.query('geheim') === '1'
|
||||||
const alle = db.prepare('SELECT key, value FROM settings ORDER BY key').all() as {
|
const alle = db.prepare('SELECT key, value FROM settings ORDER BY key').all() as {
|
||||||
|
|||||||
@@ -230,6 +230,13 @@ export type Uebersicht = {
|
|||||||
vorlagen: Vorlage[]
|
vorlagen: Vorlage[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Sicherung = {
|
||||||
|
eingehaengt: boolean
|
||||||
|
ordner: string
|
||||||
|
bytes: number
|
||||||
|
items: { name: string; size: number; at: number }[]
|
||||||
|
}
|
||||||
|
|
||||||
export type Vorlage = { name: string; muster: string }
|
export type Vorlage = { name: string; muster: string }
|
||||||
|
|
||||||
/** Ein Konto. `weg` sagt, worueber jemand hereinkommt — die wichtigere Auskunft
|
/** Ein Konto. `weg` sagt, worueber jemand hereinkommt — die wichtigere Auskunft
|
||||||
@@ -480,6 +487,7 @@ export const api = {
|
|||||||
),
|
),
|
||||||
|
|
||||||
einstellungen: () => get<Einstellungen>('/api/dash/settings'),
|
einstellungen: () => get<Einstellungen>('/api/dash/settings'),
|
||||||
|
sicherungen: () => get<Sicherung>('/api/dash/sicherungen'),
|
||||||
sicherungSpeichern: (sicherung: Record<string, string | number>) =>
|
sicherungSpeichern: (sicherung: Record<string, string | number>) =>
|
||||||
post<{ ok: true; bereit: boolean; fehlt: string[] }>('/api/dash/settings', { sicherung }),
|
post<{ ok: true; bereit: boolean; fehlt: string[] }>('/api/dash/settings', { sicherung }),
|
||||||
meldungenSpeichern: (meldungen: { webhook?: string; anlaesse?: string[] }) =>
|
meldungenSpeichern: (meldungen: { webhook?: string; anlaesse?: string[] }) =>
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { Download, HardDriveDownload, Loader2 } from 'lucide-react'
|
||||||
|
import { api, type Sicherung } from '../api'
|
||||||
|
import { useStore } from '../store'
|
||||||
|
import { bytes, zeit } from '../format'
|
||||||
|
import { Karte } from './Bausteine'
|
||||||
|
|
||||||
|
/** Die fertigen Archive herunterladen.
|
||||||
|
*
|
||||||
|
* WOZU: die Sicherung lief längst, aber sie lag ausschließlich auf **dieser**
|
||||||
|
* Maschine — auf zwei Platten, und die dritte Kopie in der Nextcloud läuft
|
||||||
|
* auch hier. Wer sie woanders haben wollte, musste sich per Fernwartung auf
|
||||||
|
* den Server klicken und sie von Hand ziehen. Das ist der Grund, aus dem es
|
||||||
|
* keine Kopie außerhalb gab: nicht Unwillen, sondern Umständlichkeit.
|
||||||
|
*
|
||||||
|
* Ein Knopf hier ersetzt keine geplante Abholung — aber er macht die
|
||||||
|
* Auswärtskopie zu etwas, das man im Vorbeigehen erledigt, statt zu einem
|
||||||
|
* Vorhaben. */
|
||||||
|
export function SicherungenKarte() {
|
||||||
|
const note = useStore((s) => s.note)
|
||||||
|
const [daten, setDaten] = useState<Sicherung | null>(null)
|
||||||
|
const [laedt, setLaedt] = useState(true)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
api.sicherungen()
|
||||||
|
.then(setDaten)
|
||||||
|
.catch((err) => note('fehler', err instanceof Error ? err.message : 'Unbekannt'))
|
||||||
|
.finally(() => setLaedt(false))
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (laedt) {
|
||||||
|
return (
|
||||||
|
<Karte titel="Sicherungen holen">
|
||||||
|
<div className="flex justify-center py-6">
|
||||||
|
<Loader2 className="animate-spin text-schrift-leise" />
|
||||||
|
</div>
|
||||||
|
</Karte>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Karte
|
||||||
|
titel="Sicherungen holen"
|
||||||
|
rechts={
|
||||||
|
daten?.items.length ? (
|
||||||
|
<span className="text-xs text-schrift-leise">
|
||||||
|
{daten.items.length} Stück · {bytes(daten.bytes)}
|
||||||
|
</span>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{!daten?.eingehaengt ? (
|
||||||
|
<p className="text-sm text-schrift-leise">
|
||||||
|
Der Sicherungsordner ist nicht eingehängt. In der{' '}
|
||||||
|
<code className="font-mono text-xs">docker-compose.yml</code> braucht der
|
||||||
|
Dienst{' '}
|
||||||
|
<code className="font-mono text-xs">
|
||||||
|
{daten?.ordner ?? '/sicherungen'}
|
||||||
|
</code>{' '}
|
||||||
|
— nur lesend.
|
||||||
|
</p>
|
||||||
|
) : daten.items.length === 0 ? (
|
||||||
|
<p className="text-sm text-schrift-leise">
|
||||||
|
Noch keine Archive in <code className="font-mono text-xs">{daten.ordner}</code>.
|
||||||
|
Die Sicherung läuft täglich um 04:30.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p className="mb-3 flex items-start gap-2 text-xs text-schrift-leise">
|
||||||
|
<HardDriveDownload size={14} className="mt-0.5 shrink-0" />
|
||||||
|
<span>
|
||||||
|
Alle Kopien liegen auf <strong className="text-schrift">dieser</strong>{' '}
|
||||||
|
Maschine — auch die in der Nextcloud, die hier mitläuft. Ein Archiv,
|
||||||
|
das du herunterlädst, ist das erste, das den Server verlässt.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul className="flex flex-col divide-y divide-rand">
|
||||||
|
{daten.items.map((s) => (
|
||||||
|
<li key={s.name} className="flex items-center gap-3 py-2">
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="truncate font-mono text-xs text-schrift">
|
||||||
|
{s.name}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-schrift-leise">
|
||||||
|
{zeit(s.at)} · {bytes(s.size)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/* Ein <a download> und kein fetch: 336 MB gehören nicht
|
||||||
|
durch den Speicher des Browsers, und der Name kommt
|
||||||
|
aus dem Content-Disposition. */}
|
||||||
|
<a
|
||||||
|
href={`/api/dash/sicherungen/${s.name}`}
|
||||||
|
onClick={() => note('ok', `${s.name} wird geladen — ${bytes(s.size)}.`)}
|
||||||
|
className="flex shrink-0 items-center gap-1.5 rounded-md border
|
||||||
|
border-rand px-2.5 py-1.5 text-xs text-schrift-leise
|
||||||
|
hover:bg-flaeche-hoch hover:text-schrift"
|
||||||
|
>
|
||||||
|
<Download size={14} />
|
||||||
|
Holen
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Karte>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import { bytes, zeit } from '../format'
|
|||||||
import { Feld, Karte, Knopf } from '../components/Bausteine'
|
import { Feld, Karte, Knopf } from '../components/Bausteine'
|
||||||
import { DiscordKarte } from '../components/DiscordKarte'
|
import { DiscordKarte } from '../components/DiscordKarte'
|
||||||
import { VorlagenKarte } from '../components/VorlagenKarte'
|
import { VorlagenKarte } from '../components/VorlagenKarte'
|
||||||
|
import { SicherungenKarte } from '../components/SicherungenKarte'
|
||||||
import { FreigabenKarte } from '../components/FreigabenKarte'
|
import { FreigabenKarte } from '../components/FreigabenKarte'
|
||||||
import { BenutzerKarte } from '../components/BenutzerKarte'
|
import { BenutzerKarte } from '../components/BenutzerKarte'
|
||||||
import { MitnehmenKarte } from '../components/MitnehmenKarte'
|
import { MitnehmenKarte } from '../components/MitnehmenKarte'
|
||||||
@@ -46,6 +47,7 @@ export function Einstellungen() {
|
|||||||
|
|
||||||
<MitnehmenKarte />
|
<MitnehmenKarte />
|
||||||
<SicherungKarte />
|
<SicherungKarte />
|
||||||
|
<SicherungenKarte />
|
||||||
<MeldungenKarte />
|
<MeldungenKarte />
|
||||||
<DiscordKarte />
|
<DiscordKarte />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+19
-1
@@ -89,7 +89,8 @@ if r.stdout != '200':
|
|||||||
print('\n-- Was ohne Anmeldung dicht sein muss ' + '-' * 32)
|
print('\n-- Was ohne Anmeldung dicht sein muss ' + '-' * 32)
|
||||||
for weg in ('/api/dash/media', '/api/dash/tokens', '/api/dash/verlauf/export',
|
for weg in ('/api/dash/media', '/api/dash/tokens', '/api/dash/verlauf/export',
|
||||||
'/api/dash/papierkorb', '/api/dash/benutzer', '/api/dash/settings',
|
'/api/dash/papierkorb', '/api/dash/benutzer', '/api/dash/settings',
|
||||||
'/api/dash/einstellungen/export', '/api/dash/verwaltung'):
|
'/api/dash/einstellungen/export', '/api/dash/verwaltung',
|
||||||
|
'/api/dash/sicherungen'):
|
||||||
code, *_ = curl(f'{BASIS}{weg}', keks=False)
|
code, *_ = curl(f'{BASIS}{weg}', keks=False)
|
||||||
pruefe(weg, code == '401', f'HTTP {code}')
|
pruefe(weg, code == '401', f'HTTP {code}')
|
||||||
|
|
||||||
@@ -155,6 +156,23 @@ for anhang, name in (('', 'dateien'), ('?was=verwaltung', 'verwaltung')):
|
|||||||
pruefe(f'CSV {name}', name in r.stdout and nackte == 0,
|
pruefe(f'CSV {name}', name in r.stdout and nackte == 0,
|
||||||
f'{roh.count(CRLF)} Zeilen, {nackte} nackte LF')
|
f'{roh.count(CRLF)} Zeilen, {nackte} nackte LF')
|
||||||
|
|
||||||
|
# -- Die Archive zum Mitnehmen -------------------------------------------------
|
||||||
|
print(chr(10) + '-- Die Archive zum Mitnehmen ' + '-' * 40)
|
||||||
|
code, _, _, rumpf = curl(f'{BASIS}/api/dash/sicherungen')
|
||||||
|
sich = json.loads(rumpf)
|
||||||
|
pruefe('Sicherungsordner eingehaengt', sich.get('eingehaengt') is True,
|
||||||
|
f"{len(sich.get('items', []))} Archive in {sich.get('ordner')}")
|
||||||
|
|
||||||
|
# Der Weg nach draussen ist der, den man pruefen MUSS: dort liegt der ganze
|
||||||
|
# Bestand als eine Datei, und ein Muster, das zu viel durchlaesst, gaebe jede
|
||||||
|
# Datei des Servers heraus. Geprueft wird deshalb, was NICHT gehen darf.
|
||||||
|
for boese, was in (('../../etc/passwd', 'Pfad hoch'),
|
||||||
|
('..%2f..%2fdata%2fmedia.db', 'kodiert hoch'),
|
||||||
|
('media.db', 'andere Datei'),
|
||||||
|
('d4rk_media-2026-01-01-0000.tar.gz.evil', 'Anhang drangehaengt')):
|
||||||
|
code, *_ = curl(f'{BASIS}/api/dash/sicherungen/{boese}')
|
||||||
|
pruefe(f'abgewiesen: {was}', code in ('400', '404'), f'HTTP {code}')
|
||||||
|
|
||||||
# -- Papierkorb und Sicherung --------------------------------------------------
|
# -- Papierkorb und Sicherung --------------------------------------------------
|
||||||
print('\n-- Papierkorb und Sicherung ' + '-' * 42)
|
print('\n-- Papierkorb und Sicherung ' + '-' * 42)
|
||||||
code, _, _, rumpf = curl(f'{BASIS}/api/dash/papierkorb')
|
code, _, _, rumpf = curl(f'{BASIS}/api/dash/papierkorb')
|
||||||
|
|||||||
Reference in New Issue
Block a user