All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 17m1s
Ik sprong met tientallen (0.7.90 → 0.8.00 → … → 0.9.00 → 0.10.00) en liep daarmee de reeks uit: dit schema rolt over bij .90, dus na 0.9.90 hoort 1.0.00 te komen. "0.10.00" bestond niet, en schond ook het formaat v0.0.00 uit de projectafspraken. De vijftien uitgaven van deze sessie zijn hernummerd naar 0.7.81 t/m 0.7.95, aaneengesloten. In één doorloop met een tabel, want 0.8.81 wordt 0.7.90 en 0.7.90 wordt 0.7.81 — achtereenvolgende vervangingen zouden elkaar overschrijven. Meegenomen: verwijzingen in de documentatie, .env.example, install.sh en de tests. Verkorte vormen als "vanaf v0.10" zijn vervangen door het volledige nummer, want die waren niet automatisch te herleiden. De commit-onderwerpen in de geschiedenis dragen nog de oude nummers; VERSION en CHANGELOG zijn leidend. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q9eqpADJSRs49SoGGr4NAy
1201 lines
50 KiB
Bash
Executable file
1201 lines
50 KiB
Bash
Executable file
#!/bin/sh
|
||
# Server Up — installeren, bijwerken of verwijderen op een Linux-server.
|
||
#
|
||
# curl -fsSL https://git.ramonbesselink.nl/bes-r/server-up/raw/branch/main/install.sh | sh
|
||
#
|
||
# Liever eerst lezen wat je uitvoert (aan te raden):
|
||
#
|
||
# curl -fsSL https://git.ramonbesselink.nl/bes-r/server-up/raw/branch/main/install.sh -o install.sh
|
||
# less install.sh
|
||
# sh install.sh
|
||
#
|
||
# POSIX sh, dus het werkt onder dash, ash en bash.
|
||
|
||
set -eu
|
||
|
||
SCRIPT_VERSIE="1.2"
|
||
GIT_HOST="git.ramonbesselink.nl"
|
||
GIT_PAD="bes-r/server-up"
|
||
BRANCH="main"
|
||
DOEL="/opt/server-up"
|
||
POORT="5000"
|
||
BIND="127.0.0.1"
|
||
TOKEN=""
|
||
JA=0
|
||
DROOG=0
|
||
ACTIE="install"
|
||
BEHEERDER=""
|
||
BEHEERDER_WW=""
|
||
BEHEERDER_WW_BESTAND=""
|
||
# Expliciet meegegeven waarden overschrijven wél een bestaande .env; de rest
|
||
# blijft staan, zodat een --update je instellingen niet terugzet.
|
||
BIND_EXPLICIET=0
|
||
POORT_EXPLICIET=0
|
||
# Onder welk account Server Up draait. Leeg = nog niet gekozen; 0 = root.
|
||
GEBRUIKER=""
|
||
GEBRUIKER_EXPLICIET=0
|
||
SU_UID=""
|
||
SU_GID=""
|
||
SU_NAAM=""
|
||
BASE_DIR="${BASE_DIR:-}"
|
||
BASE_DIR_EXPLICIET=0
|
||
# Heeft de server na het starten daadwerkelijk geantwoord? Bepaalt het slot en
|
||
# de exitcode; bij --dry-run is er niets om op te wachten.
|
||
GEREED=1
|
||
|
||
# ── Uitvoer ──────────────────────────────────────────────────────────────────
|
||
# Kleuren alleen als we naar een terminal schrijven; in een pipe of logbestand
|
||
# leveren escape-codes onleesbare rommel op.
|
||
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
|
||
C_OK=$(printf '\033[32m'); C_WAARSCHUW=$(printf '\033[33m')
|
||
C_FOUT=$(printf '\033[31m'); C_KOP=$(printf '\033[1m'); C_UIT=$(printf '\033[0m')
|
||
else
|
||
C_OK=''; C_WAARSCHUW=''; C_FOUT=''; C_KOP=''; C_UIT=''
|
||
fi
|
||
|
||
log() { printf '%s\n' " $*"; }
|
||
stap() { printf '\n%s%s%s\n' "$C_KOP" "$*" "$C_UIT"; }
|
||
goed() { printf ' %s✔%s %s\n' "$C_OK" "$C_UIT" "$*"; }
|
||
waarschuw() { printf ' %s!%s %s\n' "$C_WAARSCHUW" "$C_UIT" "$*"; }
|
||
fout() { printf '\n %s✖ %s%s\n\n' "$C_FOUT" "$*" "$C_UIT" >&2; exit 1; }
|
||
# Bij --dry-run alleen waarschuwen: je wilt de hele voorvertoning zien, ook op
|
||
# een machine waar Docker (nog) niet staat.
|
||
fout_tenzij_droog() {
|
||
if [ "$DROOG" -eq 1 ]; then waarschuw "$*"; else fout "$*"; fi
|
||
}
|
||
|
||
heeft() { command -v "$1" >/dev/null 2>&1; }
|
||
|
||
# Voert uit, of toont alleen wat er zou gebeuren bij --dry-run.
|
||
doe() {
|
||
if [ "$DROOG" -eq 1 ]; then
|
||
printf ' [droog] %s\n' "$*"
|
||
else
|
||
"$@"
|
||
fi
|
||
}
|
||
|
||
# Is er een terminal om iets aan te vragen? Bewust /dev/tty en niet stdin: bij
|
||
# `curl … | sh` — de manier die de README noemt — is stdin de pipe, terwijl de
|
||
# terminal gewoon bereikbaar is. Op stdin testen betekende dat élke vraag
|
||
# ongesteld bleef en zichzelf met "ja" beantwoordde: Docker werd van
|
||
# get.docker.com gehaald zonder dat het gevraagd was, en --uninstall brak je
|
||
# installatie af zonder bevestiging.
|
||
#
|
||
# Bestaan is niet genoeg: in een container zonder terminal bestaat /dev/tty wel
|
||
# maar levert openen "No such device or address".
|
||
# De openpoging staat in een subshell, niet tussen accolades: ':' is een
|
||
# special builtin, en een mislukte redirect daarop beëindigt volgens POSIX de
|
||
# hele shell. In een subshell sneuvelt alleen die subshell.
|
||
terminal_beschikbaar() {
|
||
[ -e /dev/tty ] || return 1
|
||
( : </dev/tty ) 2>/dev/null || return 1
|
||
return 0
|
||
}
|
||
|
||
vraag() {
|
||
# vraag "Vraag?" -> 0 bij ja. Met --yes of zonder terminal: altijd ja.
|
||
[ "$JA" -eq 1 ] && return 0
|
||
terminal_beschikbaar || return 0
|
||
printf ' %s [J/n] ' "$1"
|
||
read -r antwoord </dev/tty || return 0
|
||
case "$antwoord" in [nN]*) return 1 ;; *) return 0 ;; esac
|
||
}
|
||
|
||
gebruik() {
|
||
cat <<UITLEG
|
||
Server Up installatiescript ${SCRIPT_VERSIE}
|
||
|
||
Gebruik: install.sh [opties]
|
||
|
||
--dir PAD Installatiemap (standaard: ${DOEL})
|
||
--port POORT Poort voor de webinterface (standaard: ${POORT})
|
||
--bind ADRES Waarop de poort luistert (standaard: ${BIND})
|
||
Gebruik 0.0.0.0 om er van buiten de server bij te kunnen,
|
||
maar zet er dan wel een reverse proxy met TLS voor.
|
||
--branch NAAM Branch om te installeren (standaard: ${BRANCH})
|
||
--token TOKEN Toegangstoken, als je git-repo niet openbaar is
|
||
--base-dir PAD Hoofdmap voor stacks, appdata en backups
|
||
(standaard: /opt/serverup). Later wijzigen kan via
|
||
Instellingen → Paden, die de data meeverhuist.
|
||
--user NAAM Account waaronder Server Up draait ('serverup' wordt zo
|
||
nodig aangemaakt; 'root' is de oude situatie). Zonder deze
|
||
optie wordt ernaar gevraagd.
|
||
--admin NAAM Maak meteen een beheerdersaccount met deze naam
|
||
--admin-password-file PAD
|
||
Lees het wachtwoord uit een bestand. Kan ook via de
|
||
omgevingsvariabele SU_ADMIN_PASSWORD. Geef je geen van
|
||
beide, dan wordt ernaar gevraagd; kan dat niet (geen
|
||
terminal), dan wordt er één aangemaakt en getoond.
|
||
--create-admin Alleen het beheerdersaccount aanmaken, bij een installatie
|
||
die al draait. Werkt zolang er nog geen account bestaat.
|
||
--update Bijwerken naar de nieuwste versie
|
||
--doctor Een bestaande installatie doorlichten
|
||
--uninstall Stoppen en verwijderen (je gegevens blijven staan)
|
||
--yes Niets vragen, overal ja op antwoorden
|
||
--dry-run Alleen tonen wat er zou gebeuren
|
||
--help Deze uitleg
|
||
|
||
Het wachtwoord staat bewust niet als optie op de opdrachtregel: die is voor
|
||
iedereen op de server te zien met 'ps' en blijft in je shell-geschiedenis staan.
|
||
|
||
Voorbeelden:
|
||
sh install.sh
|
||
sh install.sh --bind 0.0.0.0 --port 8080
|
||
sh install.sh --admin ramon
|
||
SU_ADMIN_PASSWORD=... sh install.sh --admin ramon --yes
|
||
sh install.sh --update
|
||
UITLEG
|
||
}
|
||
|
||
# ── Argumenten ───────────────────────────────────────────────────────────────
|
||
while [ $# -gt 0 ]; do
|
||
case "$1" in
|
||
--dir) DOEL="${2:?--dir vereist een pad}"; shift 2 ;;
|
||
--port) POORT="${2:?--port vereist een nummer}"; POORT_EXPLICIET=1; shift 2 ;;
|
||
--bind) BIND="${2:?--bind vereist een adres}"; BIND_EXPLICIET=1; shift 2 ;;
|
||
--branch) BRANCH="${2:?--branch vereist een naam}"; shift 2 ;;
|
||
--token) TOKEN="${2:?--token vereist een waarde}"; shift 2 ;;
|
||
--user) GEBRUIKER="${2:?--user vereist een accountnaam}"
|
||
GEBRUIKER_EXPLICIET=1; shift 2 ;;
|
||
--base-dir) BASE_DIR="${2:?--base-dir vereist een pad}"
|
||
BASE_DIR_EXPLICIET=1; shift 2 ;;
|
||
--admin) BEHEERDER="${2:?--admin vereist een gebruikersnaam}"; shift 2 ;;
|
||
--admin-password-file)
|
||
BEHEERDER_WW_BESTAND="${2:?--admin-password-file vereist een pad}"; shift 2 ;;
|
||
--admin-password)
|
||
fout "Gebruik --admin-password-file of SU_ADMIN_PASSWORD; een wachtwoord op de opdrachtregel is voor iedereen zichtbaar met 'ps'." ;;
|
||
--update) ACTIE="update"; shift ;;
|
||
--doctor) ACTIE="doctor"; shift ;;
|
||
--uninstall) ACTIE="uninstall"; shift ;;
|
||
--create-admin) ACTIE="create-admin"; shift ;;
|
||
--yes|-y) JA=1; shift ;;
|
||
--dry-run) DROOG=1; shift ;;
|
||
--help|-h) gebruik; exit 0 ;;
|
||
*) fout "Onbekende optie: $1 (gebruik --help)" ;;
|
||
esac
|
||
done
|
||
|
||
case "$POORT" in ''|*[!0-9]*) fout "Ongeldige poort: $POORT" ;; esac
|
||
[ "$POORT" -ge 1 ] && [ "$POORT" -le 65535 ] || fout "Poort buiten bereik: $POORT"
|
||
|
||
# Docker publiceert op een adres, niet op een hostnaam. Alleen cijfers, punten
|
||
# en dubbele punten (IPv6) toestaan, zodat een typefout hier opvalt en niet pas
|
||
# als 'docker compose up' er een onbegrijpelijke fout over geeft.
|
||
geldig_bind() {
|
||
case "$1" in
|
||
''|*[!0-9.:a-fA-F]*) return 1 ;;
|
||
*) return 0 ;;
|
||
esac
|
||
}
|
||
geldig_bind "$BIND" || fout "Ongeldig bind-adres: $BIND (verwacht een IP-adres, bijvoorbeeld 127.0.0.1 of 0.0.0.0)"
|
||
|
||
# BASE_DIR wordt in de container op zichzelf gemount, dus een relatief pad zou
|
||
# binnen de container ergens anders uitkomen dan op de host. En een handvol
|
||
# systeemmappen wil je hier niet per ongeluk vullen met appdata.
|
||
if [ -n "$BASE_DIR" ]; then
|
||
case "$BASE_DIR" in
|
||
/*) ;;
|
||
*) fout "--base-dir vereist een volledig pad, beginnend met / (kreeg: $BASE_DIR)" ;;
|
||
esac
|
||
BASE_DIR="${BASE_DIR%/}"
|
||
case "$BASE_DIR" in
|
||
''|/|/etc|/bin|/sbin|/usr|/var|/tmp|/root|/boot|/dev|/proc|/sys)
|
||
fout "--base-dir mag niet $BASE_DIR zijn; kies een eigen map, bijvoorbeeld /srv/serverup." ;;
|
||
esac
|
||
fi
|
||
|
||
# Waar bereiken wíj de server na het starten? Compose publiceert op $BIND, dus
|
||
# met een specifiek adres luistert er niets op 127.0.0.1. Daar toch naartoe
|
||
# vragen leverde "reageerde niet binnen anderhalve minuut" en een overgeslagen
|
||
# beheerdersaccount op, terwijl alles gewoon draaide — precies de installatie
|
||
# die daarna zonder account open staat.
|
||
probe_adres() {
|
||
case "$BIND" in
|
||
''|0.0.0.0) printf '127.0.0.1' ;;
|
||
::|'[::]') printf '[::1]' ;;
|
||
*:*) printf '[%s]' "$BIND" ;; # IPv6 hoort tussen blokhaken
|
||
*) printf '%s' "$BIND" ;;
|
||
esac
|
||
}
|
||
|
||
# ── Rechten ──────────────────────────────────────────────────────────────────
|
||
# Alleen sudo gebruiken waar het echt nodig is: kan de gebruiker zelf al in de
|
||
# doelmap schrijven en docker aanroepen, dan blijft alles onder zijn eigen account.
|
||
SUDO=""
|
||
bepaal_rechten() {
|
||
if [ "$(id -u)" -eq 0 ]; then
|
||
return 0
|
||
fi
|
||
bovenliggend="$(dirname "$DOEL")"
|
||
if [ -w "$bovenliggend" ] || { [ -d "$DOEL" ] && [ -w "$DOEL" ]; }; then
|
||
if docker info >/dev/null 2>&1; then
|
||
goed "Draait onder je eigen account; sudo is niet nodig"
|
||
return 0
|
||
fi
|
||
fi
|
||
if heeft sudo; then
|
||
SUDO="sudo"
|
||
log "sudo wordt gebruikt voor ${DOEL} en docker"
|
||
else
|
||
fout "Geen root en geen sudo. Draai als root, of kies met --dir een map waar je zelf in mag schrijven."
|
||
fi
|
||
}
|
||
|
||
# ── Vereisten ────────────────────────────────────────────────────────────────
|
||
controleer_systeem() {
|
||
stap "Systeem controleren"
|
||
bepaal_rechten
|
||
[ "$(uname -s)" = "Linux" ] || waarschuw "Dit script is voor Linux; op $(uname -s) is het onbeproefd."
|
||
for hulp in curl tar; do
|
||
heeft "$hulp" || fout_tenzij_droog "'$hulp' ontbreekt. Installeer het en probeer opnieuw."
|
||
done
|
||
goed "$(uname -s) $(uname -m)"
|
||
}
|
||
|
||
controleer_docker() {
|
||
# Bij een proefdraai niets aanroepen wat via sudo loopt: dan staat er
|
||
# ineens een wachtwoordvraag op het scherm terwijl we net beloofd hebben
|
||
# dat er niets gebeurt — en zonder terminal blijft hij daarop hangen.
|
||
if [ "$DROOG" -eq 1 ]; then
|
||
if heeft docker; then
|
||
goed "Docker aanwezig (de daemon wordt bij een proefdraai niet aangesproken)"
|
||
else
|
||
waarschuw "Docker is niet geïnstalleerd; het script zou aanbieden dat te doen."
|
||
fi
|
||
return 0
|
||
fi
|
||
|
||
if heeft docker; then
|
||
if $SUDO docker info >/dev/null 2>&1; then
|
||
goed "Docker draait ($($SUDO docker version --format '{{.Server.Version}}' 2>/dev/null || echo '?'))"
|
||
else
|
||
fout_tenzij_droog "Docker is geïnstalleerd maar de daemon reageert niet. Start hem met: systemctl start docker"
|
||
fi
|
||
else
|
||
waarschuw "Docker is niet geïnstalleerd."
|
||
if vraag "Nu installeren via het officiële script van docker.com?"; then
|
||
stap "Docker installeren"
|
||
doe sh -c "curl -fsSL https://get.docker.com | $SUDO sh"
|
||
doe $SUDO systemctl enable --now docker || true
|
||
heeft docker || [ "$DROOG" -eq 1 ] || fout "Installeren van Docker is niet gelukt."
|
||
goed "Docker geïnstalleerd"
|
||
else
|
||
fout_tenzij_droog "Docker is vereist. Zie https://docs.docker.com/engine/install/"
|
||
fi
|
||
fi
|
||
|
||
if $SUDO docker compose version >/dev/null 2>&1; then
|
||
goed "Compose-plugin aanwezig"
|
||
else
|
||
fout_tenzij_droog "De docker-compose-plugin ontbreekt. Installeer 'docker-compose-plugin' via je pakketbeheerder."
|
||
fi
|
||
}
|
||
|
||
# ── Broncode ophalen ─────────────────────────────────────────────────────────
|
||
# De token gaat nooit in de URL en nooit in een argument. In de URL schrijft git
|
||
# hem verbatim in .git/config, waar hij blijft staan; als argument staat hij in
|
||
# /proc/<pid>/cmdline en is hij voor elke gebruiker op de server te lezen met
|
||
# 'ps'. Dat is precies waarom dit script --admin-password weigert.
|
||
#
|
||
# In plaats daarvan: een bestand met modus 0600 dat na afloop weer weg is. Git
|
||
# leest het via een credential-helper, curl via --config; in beide gevallen
|
||
# staat alleen het pád op de opdrachtregel.
|
||
bron_url() { printf 'https://%s/%s.git' "$GIT_HOST" "$GIT_PAD"; }
|
||
|
||
TOKEN_BESTAND=""
|
||
CURL_BESTAND=""
|
||
|
||
opruimen() {
|
||
[ -n "$TOKEN_BESTAND" ] && rm -f "$TOKEN_BESTAND"
|
||
[ -n "$CURL_BESTAND" ] && rm -f "$CURL_BESTAND"
|
||
TOKEN_BESTAND=""; CURL_BESTAND=""
|
||
}
|
||
trap opruimen EXIT HUP INT TERM
|
||
|
||
zet_token_klaar() {
|
||
[ -n "$TOKEN" ] || return 0
|
||
[ -n "$TOKEN_BESTAND" ] && return 0
|
||
TOKEN_BESTAND="$(mktemp "${TMPDIR:-/tmp}/su-token.XXXXXX")"
|
||
CURL_BESTAND="$(mktemp "${TMPDIR:-/tmp}/su-curl.XXXXXX")"
|
||
chmod 600 "$TOKEN_BESTAND" "$CURL_BESTAND"
|
||
# Schema en host uit bron_url halen in plaats van https hard in te vullen:
|
||
# git zoekt de opgeslagen inloggegevens op exact schema + host, dus een
|
||
# verschil daarin betekent stilzwijgend geen inloggegevens.
|
||
_url="$(bron_url)"
|
||
_schema="${_url%%://*}"
|
||
_rest="${_url#*://}"
|
||
_host="${_rest%%/*}"
|
||
# Formaat van `git credential store`: één URL per regel met de gegevens erin.
|
||
printf '%s://%s:x-oauth-basic@%s\n' "$_schema" "$TOKEN" "$_host" > "$TOKEN_BESTAND"
|
||
printf 'header = "Authorization: token %s"\n' "$TOKEN" > "$CURL_BESTAND"
|
||
}
|
||
|
||
git_doe() {
|
||
# git_doe <argumenten> — met de token uit het bestand als die er is.
|
||
if [ -n "$TOKEN_BESTAND" ]; then
|
||
doe $SUDO git -c "credential.helper=store --file=$TOKEN_BESTAND" "$@"
|
||
else
|
||
doe $SUDO git "$@"
|
||
fi
|
||
}
|
||
|
||
haal_broncode() {
|
||
stap "Broncode ophalen"
|
||
zet_token_klaar
|
||
if [ -d "$DOEL/.git" ]; then
|
||
log "Bestaande installatie bijwerken in $DOEL"
|
||
git_doe -C "$DOEL" fetch --depth 1 origin "$BRANCH"
|
||
# FETCH_HEAD en niet origin/$BRANCH: een tag krijgt geen origin/-ref,
|
||
# dus `--branch v0.7.92-beta` installeerde prima maar kon daarna nooit
|
||
# meer bijgewerkt worden ("fatal: ambiguous argument"). FETCH_HEAD wijst
|
||
# na een fetch altijd naar wat er net opgehaald is, tak of tag.
|
||
git_doe -C "$DOEL" reset --hard FETCH_HEAD
|
||
goed "Bijgewerkt naar $BRANCH"
|
||
opruimen
|
||
return 0
|
||
fi
|
||
|
||
doe $SUDO mkdir -p "$DOEL"
|
||
if heeft git; then
|
||
# Zonder .git kunnen we later niet netjes bijwerken, dus git heeft de
|
||
# voorkeur boven een los archief.
|
||
if git_doe clone --depth 1 --branch "$BRANCH" "$(bron_url)" "$DOEL"; then
|
||
goed "Gekloond naar $DOEL"
|
||
opruimen
|
||
return 0
|
||
fi
|
||
waarschuw "Klonen mislukte; ik probeer het archief."
|
||
fi
|
||
|
||
archief="https://${GIT_HOST}/${GIT_PAD}/archive/${BRANCH}.tar.gz"
|
||
log "Downloaden: $archief"
|
||
if [ "$DROOG" -eq 0 ]; then
|
||
tijdelijk="$(mktemp -d)"
|
||
curl -fsSL ${CURL_BESTAND:+--config "$CURL_BESTAND"} "$archief" \
|
||
| tar xz -C "$tijdelijk" --strip-components=1 \
|
||
|| fout "Ophalen mislukt. Is de repo openbaar? Zo niet, gebruik --token."
|
||
$SUDO cp -r "$tijdelijk/." "$DOEL/"
|
||
rm -rf "$tijdelijk"
|
||
fi
|
||
opruimen
|
||
goed "Uitgepakt naar $DOEL"
|
||
}
|
||
|
||
# ── Instellingen ─────────────────────────────────────────────────────────────
|
||
server_ip() { hostname -I 2>/dev/null | awk '{print $1}'; }
|
||
|
||
kies_bind() {
|
||
# Alleen vragen als er nog iets te kiezen valt: met --bind, met --yes of
|
||
# zonder terminal is de keuze al gemaakt.
|
||
[ "$BIND_EXPLICIET" -eq 1 ] && return 0
|
||
[ "$JA" -eq 1 ] && return 0
|
||
[ "$DROOG" -eq 1 ] && return 0
|
||
terminal_beschikbaar || return 0
|
||
|
||
# Installeer je opnieuw over een bestaande map, dan is je huidige keuze het
|
||
# uitgangspunt — anders zet enter je server ongemerkt terug op loopback.
|
||
huidig=""
|
||
if [ -f "$DOEL/.env" ]; then
|
||
lees_env_terug "$DOEL/.env"
|
||
huidig="$BIND"
|
||
fi
|
||
|
||
ip="$(server_ip)"
|
||
stap "Bereikbaarheid"
|
||
printf ' Waarop moet de webinterface luisteren?\n\n'
|
||
printf ' 1) Alleen deze server (127.0.0.1) — veiligst\n'
|
||
printf ' Erbij via een SSH-tunnel of een reverse proxy op deze machine.\n'
|
||
printf ' 2) Het hele netwerk (0.0.0.0)\n'
|
||
printf ' Direct bereikbaar op http://%s:%s — zet er wel TLS voor.\n' "${ip:-<server-ip>}" "$POORT"
|
||
printf ' 3) Een specifiek adres\n\n'
|
||
if [ -n "$huidig" ]; then
|
||
printf ' Nu ingesteld: %s\n' "$huidig"
|
||
printf ' Keuze [enter = zo laten]: '
|
||
else
|
||
printf ' Keuze [1]: '
|
||
fi
|
||
read -r keuze </dev/tty || keuze=""
|
||
case "$keuze" in
|
||
1) BIND="127.0.0.1" ;;
|
||
2) BIND="0.0.0.0" ;;
|
||
3) printf ' Adres: '
|
||
read -r adres </dev/tty || adres=""
|
||
geldig_bind "$adres" || fout "Ongeldig bind-adres: $adres"
|
||
BIND="$adres" ;;
|
||
'') BIND="${huidig:-127.0.0.1}" ;;
|
||
*) fout "Onbekende keuze: $keuze" ;;
|
||
esac
|
||
BIND_EXPLICIET=1
|
||
goed "Luistert op $BIND"
|
||
}
|
||
|
||
zet_env_regel() {
|
||
# zet_env_regel BESTAND SLEUTEL WAARDE — vervangt de regel of voegt hem toe.
|
||
# Waarden zijn hier altijd al gevalideerd (cijfers, punten, dubbele punten).
|
||
$SUDO sh -c "sed -i '/^$2=/d' '$1' && printf '%s=%s\n' '$2' '$3' >> '$1'"
|
||
}
|
||
|
||
# ── Onder welk account draait Server Up? ─────────────────────────────────────
|
||
# Server Up start als root — dat moet, want het docker-volume is van root — en
|
||
# zakt daarna af naar dit account. Zie docs/beveiliging.md voor wat dat wel en
|
||
# niet oplevert.
|
||
|
||
uid_van() { getent passwd "$1" 2>/dev/null | cut -d: -f3; }
|
||
gid_van() { getent passwd "$1" 2>/dev/null | cut -d: -f4; }
|
||
|
||
ingelogde_gebruiker() {
|
||
# Wie de installatie aanstuurt. Onder sudo is dat niet 'root' maar degene
|
||
# die het aanriep; zonder sudo valt logname terug op de terminal.
|
||
naam="${SUDO_USER:-}"
|
||
[ -n "$naam" ] || naam="$(logname 2>/dev/null || echo '')"
|
||
[ "$naam" = "root" ] && naam=""
|
||
printf '%s' "$naam"
|
||
}
|
||
|
||
bruikbare_gebruikers() {
|
||
# Gewone accounts: uid 1000–60000 en een echte shell. Systeemaccounts
|
||
# (daemon, www-data, nobody) horen hier niet tussen — die hebben eigen
|
||
# rechten en zouden verwarrend zijn om uit te kiezen.
|
||
awk -F: '$3 >= 1000 && $3 < 60000 &&
|
||
$7 !~ /(nologin|false|sync)$/ { print $1 " (uid " $3 ")" }' \
|
||
/etc/passwd 2>/dev/null
|
||
}
|
||
|
||
maak_serverup_account() {
|
||
# Systeemaccount: geen shell, geen wachtwoord, geen thuismap. Het bestaat
|
||
# alleen om eigenaar te zijn van bestanden.
|
||
if [ -n "$(uid_van serverup)" ]; then
|
||
log "Account 'serverup' bestaat al"
|
||
return 0
|
||
fi
|
||
if [ "$DROOG" -eq 1 ]; then
|
||
printf ' [droog] account serverup aanmaken (systeemaccount, geen shell)\n'
|
||
return 0
|
||
fi
|
||
if heeft useradd; then
|
||
$SUDO useradd --system --no-create-home --shell /usr/sbin/nologin serverup \
|
||
|| fout "Kon het account 'serverup' niet aanmaken."
|
||
elif heeft adduser; then
|
||
$SUDO adduser --system --no-create-home --shell /usr/sbin/nologin serverup \
|
||
|| fout "Kon het account 'serverup' niet aanmaken."
|
||
else
|
||
fout "Geen useradd of adduser gevonden. Maak zelf een account en geef het op met --user NAAM."
|
||
fi
|
||
goed "Account 'serverup' aangemaakt"
|
||
}
|
||
|
||
zet_gebruiker() {
|
||
# zet_gebruiker NAAM — vertaal een naam naar SU_UID/SU_GID.
|
||
SU_NAAM="$1"
|
||
if [ "$1" = "root" ]; then
|
||
SU_UID=0; SU_GID=0
|
||
waarschuw "Server Up draait als root; dat is de oude situatie."
|
||
return 0
|
||
fi
|
||
_uid="$(uid_van "$1")"
|
||
if [ -z "$_uid" ]; then
|
||
if [ "$1" = "serverup" ]; then
|
||
maak_serverup_account
|
||
_uid="$(uid_van serverup)"
|
||
fi
|
||
if [ -z "$_uid" ] && [ "$DROOG" -eq 1 ] && [ "$1" = "serverup" ]; then
|
||
# Alleen 'serverup' zouden we zélf hebben aangemaakt; dat het nu
|
||
# nog ontbreekt is dus geen fout. Een andere naam die niet bestaat
|
||
# is een typefout, en die hoor je juist bij een proefdraai te horen.
|
||
SU_UID=""; SU_GID=""
|
||
log "Zou draaien als 'serverup' (uid wordt bij het aanmaken toegekend)"
|
||
return 0
|
||
fi
|
||
[ -n "$_uid" ] || fout "Account '$1' bestaat niet. Maak het aan of kies een ander."
|
||
fi
|
||
SU_UID="$_uid"
|
||
SU_GID="$(gid_van "$1")"
|
||
SU_GID="${SU_GID:-$SU_UID}"
|
||
goed "Server Up draait als $1 ($SU_UID:$SU_GID)"
|
||
# Bewust géén lidmaatschap van de docker-groep: wie daarin zit kan een
|
||
# container starten die / mount, en is dus root op deze machine. De
|
||
# container leest de gid van de socket zelf uit en heeft dit niet nodig.
|
||
log "Niet toegevoegd aan de docker-groep — dat zou root-toegang geven en is niet nodig."
|
||
}
|
||
|
||
kies_gebruiker() {
|
||
[ "$GEBRUIKER_EXPLICIET" -eq 1 ] && { zet_gebruiker "$GEBRUIKER"; return 0; }
|
||
# Wat er al staat wint: bij --update wordt kies_bind niet aangeroepen, dus
|
||
# zonder dit zou een bestaande installatie elke keer opnieuw vragen.
|
||
lees_env_terug "$DOEL/.env"
|
||
[ -n "$SU_UID" ] && return 0
|
||
if [ "$JA" -eq 1 ] || ! terminal_beschikbaar; then
|
||
zet_gebruiker serverup
|
||
return 0
|
||
fi
|
||
|
||
_ingelogd="$(ingelogde_gebruiker)"
|
||
stap "Account"
|
||
printf ' Onder welk account moet Server Up draaien?\n'
|
||
printf ' Het draait nu nog als root; dat hoeft niet voor alles.\n\n'
|
||
printf " 1) Een nieuw account 'serverup' aanmaken — aanbevolen\n"
|
||
printf ' Systeemaccount zonder shell en zonder wachtwoord.\n'
|
||
if [ -n "$_ingelogd" ]; then
|
||
printf ' 2) %s — het account waarmee je nu werkt\n' "$_ingelogd"
|
||
else
|
||
printf ' 2) (geen ingelogd account gevonden)\n'
|
||
fi
|
||
printf ' 3) Een bestaand account kiezen\n'
|
||
printf ' 4) Als root draaien — zoals voorheen\n\n'
|
||
printf ' Keuze [1]: '
|
||
read -r keuze </dev/tty || keuze=""
|
||
case "$keuze" in
|
||
''|1) zet_gebruiker serverup ;;
|
||
2) [ -n "$_ingelogd" ] || fout "Er is geen ingelogd account gevonden; kies 1, 3 of 4."
|
||
zet_gebruiker "$_ingelogd" ;;
|
||
3) printf '\n Bruikbare accounts:\n'
|
||
bruikbare_gebruikers | sed 's/^/ /'
|
||
printf '\n Naam: '
|
||
read -r naam </dev/tty || naam=""
|
||
naam="$(printf '%s' "$naam" | tr -d '[:space:]')"
|
||
[ -n "$naam" ] || fout "Geen naam opgegeven."
|
||
zet_gebruiker "$naam" ;;
|
||
4) zet_gebruiker root ;;
|
||
*) fout "Onbekende keuze: $keuze" ;;
|
||
esac
|
||
}
|
||
|
||
zet_mappen_klaar() {
|
||
# De hoofdmap en de twee mappen die Server Up zélf beheert alvast op de
|
||
# juiste eigenaar zetten, zodat de eerste start niet op rechten struikelt.
|
||
#
|
||
# appdata blijft er bewust buiten: die mappen zijn van de apps (postgres
|
||
# draait als 999, een linuxserver-image als jouw PUID). Er een chown
|
||
# overheen halen breekt precies die containers.
|
||
[ "${SU_UID:-0}" = "0" ] && [ "$DROOG" -eq 0 ] && return 0
|
||
[ "${SU_NAAM:-root}" = "root" ] && return 0
|
||
_basis="${BASE_DIR:-/opt/serverup}"
|
||
if [ "$DROOG" -eq 1 ]; then
|
||
printf ' [droog] %s, %s/stacks en %s/backups op %s zetten (appdata blijft ongemoeid)\n' \
|
||
"$_basis" "$_basis" "$_basis" "$SU_NAAM"
|
||
return 0
|
||
fi
|
||
$SUDO mkdir -p "$_basis/stacks" "$_basis/backups" "$_basis/appdata"
|
||
$SUDO chown "$SU_UID:$SU_GID" "$_basis"
|
||
$SUDO chown -R "$SU_UID:$SU_GID" "$_basis/stacks" "$_basis/backups"
|
||
goed "Mappen klaargezet onder $_basis"
|
||
}
|
||
|
||
lees_env_terug() {
|
||
# Wat er in .env staat is de waarheid, niet wat dit script als standaard
|
||
# heeft. Zonder dit klopt bij --update de wachtlus en het slotadres niet
|
||
# meer zodra je ooit een andere poort hebt gekozen.
|
||
[ -f "$1" ] || return 0
|
||
# Lezen kan meestal zonder sudo; alleen als het bestand van root is en
|
||
# afgeschermd, is de omweg nodig.
|
||
if [ -r "$1" ]; then
|
||
_lees="cat"
|
||
elif [ -n "$SUDO" ]; then
|
||
_lees="$SUDO cat"
|
||
elif heeft sudo; then
|
||
_lees="sudo cat"
|
||
else
|
||
_lees="cat"
|
||
fi
|
||
_inhoud="$($_lees "$1" 2>/dev/null || true)"
|
||
_poort="$(printf '%s\n' "$_inhoud" | sed -n 's/^PORT=//p' | tail -1)"
|
||
_bind="$(printf '%s\n' "$_inhoud" | sed -n 's/^BIND=//p' | tail -1)"
|
||
# SU_UID/SU_GID en BASE_DIR overnemen, zodat --update niet opnieuw vraagt
|
||
# onder welk account het moet draaien en niet de verkeerde mappen chownt.
|
||
if [ -z "$SU_UID" ]; then
|
||
_uid="$(printf '%s\n' "$_inhoud" | sed -n 's/^SU_UID=//p' | tail -1)"
|
||
case "$_uid" in ''|*[!0-9]*) ;; *) SU_UID="$_uid"
|
||
_gid="$(printf '%s\n' "$_inhoud" | sed -n 's/^SU_GID=//p' | tail -1)"
|
||
case "$_gid" in ''|*[!0-9]*) SU_GID="$_uid" ;; *) SU_GID="$_gid" ;; esac ;;
|
||
esac
|
||
fi
|
||
if [ -z "$BASE_DIR" ] && [ "$BASE_DIR_EXPLICIET" -eq 0 ]; then
|
||
_basis="$(printf '%s\n' "$_inhoud" | sed -n 's/^BASE_DIR=//p' | tail -1)"
|
||
case "$_basis" in /*) BASE_DIR="$_basis" ;; esac
|
||
fi
|
||
if [ "$POORT_EXPLICIET" -eq 0 ]; then
|
||
case "$_poort" in ''|*[!0-9]*) ;; *) POORT="$_poort" ;; esac
|
||
fi
|
||
if [ "$BIND_EXPLICIET" -eq 0 ] && [ -n "$_bind" ] && geldig_bind "$_bind"; then
|
||
BIND="$_bind"
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
schrijf_env() {
|
||
stap "Instellingen"
|
||
env_bestand="$DOEL/.env"
|
||
|
||
if [ -f "$env_bestand" ]; then
|
||
# Een bestaande .env bevat jouw instellingen; die blijven staan. Alleen
|
||
# wat je nu expliciet meegeeft wordt doorgevoerd — dat is precies het
|
||
# verschil dat eerder ontbrak, waardoor --bind niets deed zodra er al
|
||
# een .env stond.
|
||
gewijzigd=0
|
||
if [ "$BIND_EXPLICIET" -eq 1 ]; then
|
||
if [ "$DROOG" -eq 1 ]; then
|
||
printf ' [droog] BIND=%s in bestaande .env zetten\n' "$BIND"
|
||
else
|
||
zet_env_regel "$env_bestand" BIND "$BIND"
|
||
fi
|
||
gewijzigd=1
|
||
fi
|
||
if [ "$POORT_EXPLICIET" -eq 1 ]; then
|
||
if [ "$DROOG" -eq 1 ]; then
|
||
printf ' [droog] PORT=%s in bestaande .env zetten\n' "$POORT"
|
||
else
|
||
zet_env_regel "$env_bestand" PORT "$POORT"
|
||
fi
|
||
gewijzigd=1
|
||
fi
|
||
# Het account is een keuze die je net gemaakt hebt; die hoort er altijd
|
||
# in te komen, ook bij een --update van een installatie die nog als root
|
||
# draaide.
|
||
if [ -n "$SU_UID" ]; then
|
||
if [ "$DROOG" -eq 1 ]; then
|
||
printf ' [droog] SU_UID=%s SU_GID=%s in bestaande .env zetten\n' \
|
||
"$SU_UID" "$SU_GID"
|
||
else
|
||
zet_env_regel "$env_bestand" SU_UID "$SU_UID"
|
||
zet_env_regel "$env_bestand" SU_GID "$SU_GID"
|
||
fi
|
||
gewijzigd=1
|
||
fi
|
||
# Alleen als je er expliciet om vroeg: anders zou een --update de
|
||
# hoofdmap terugzetten terwijl je data ergens anders staat.
|
||
if [ "$BASE_DIR_EXPLICIET" -eq 1 ] && [ -n "$BASE_DIR" ]; then
|
||
if [ "$DROOG" -eq 1 ]; then
|
||
printf ' [droog] BASE_DIR=%s in bestaande .env zetten\n' "$BASE_DIR"
|
||
else
|
||
zet_env_regel "$env_bestand" BASE_DIR "$BASE_DIR"
|
||
fi
|
||
waarschuw "BASE_DIR gewijzigd; bestaande data verhuist hier niet mee."
|
||
log "Verplaatsen doe je vanuit Instellingen → Paden."
|
||
gewijzigd=1
|
||
fi
|
||
if [ "$gewijzigd" -eq 1 ]; then
|
||
goed "Bestaande .env bijgewerkt (BIND=$BIND, PORT=$POORT)"
|
||
else
|
||
lees_env_terug "$env_bestand"
|
||
goed "Bestaande .env blijft ongemoeid (BIND=$BIND, PORT=$POORT)"
|
||
fi
|
||
return 0
|
||
fi
|
||
|
||
if [ "$DROOG" -eq 1 ]; then
|
||
printf ' [droog] .env aanmaken met BIND=%s PORT=%s BASE_DIR=%s, draaiend als %s\n' \
|
||
"$BIND" "$POORT" "${BASE_DIR:-/opt/serverup}" "${SU_NAAM:-root}"
|
||
return 0
|
||
fi
|
||
|
||
if [ -f "$DOEL/.env.example" ]; then
|
||
$SUDO cp "$DOEL/.env.example" "$env_bestand"
|
||
else
|
||
printf '' | $SUDO tee "$env_bestand" >/dev/null
|
||
fi
|
||
$SUDO sh -c "sed -i '/^BIND=/d;/^PORT=/d' '$env_bestand'"
|
||
$SUDO sh -c "printf 'BIND=%s\nPORT=%s\n' '$BIND' '$POORT' >> '$env_bestand'"
|
||
if [ -n "$SU_UID" ]; then
|
||
zet_env_regel "$env_bestand" SU_UID "$SU_UID"
|
||
zet_env_regel "$env_bestand" SU_GID "$SU_GID"
|
||
fi
|
||
[ -n "$BASE_DIR" ] && zet_env_regel "$env_bestand" BASE_DIR "$BASE_DIR"
|
||
goed "Aangemaakt: $env_bestand (BIND=$BIND, PORT=$POORT)"
|
||
}
|
||
|
||
# ── Starten ──────────────────────────────────────────────────────────────────
|
||
start() {
|
||
stap "Bouwen en starten"
|
||
if [ "$DROOG" -eq 0 ] && [ ! -f "$DOEL/docker-compose.yml" ]; then
|
||
fout "Geen docker-compose.yml in $DOEL — het ophalen van de broncode is misgegaan."
|
||
fi
|
||
log "Dit duurt de eerste keer enkele minuten."
|
||
doe $SUDO docker compose -f "$DOEL/docker-compose.yml" --project-directory "$DOEL" \
|
||
up -d --build --remove-orphans
|
||
goed "Container gestart"
|
||
}
|
||
|
||
wacht_op_gereed() {
|
||
[ "$DROOG" -eq 1 ] && return 0
|
||
# Aantal pogingen is instelbaar: op een trage machine duurt de eerste start
|
||
# langer dan anderhalve minuut, en de tests willen niet zo lang wachten.
|
||
pogingen="${SU_WACHT_POGINGEN:-45}"
|
||
basis="http://$(probe_adres):${POORT}"
|
||
stap "Wachten tot Server Up reageert"
|
||
printf ' '
|
||
i=0
|
||
# Zonder tijdslimiet blijft curl hangen op een adres waar niets naartoe
|
||
# routeert, en betekent het aantal pogingen niets meer.
|
||
tijden="--connect-timeout 2 --max-time 5"
|
||
while [ "$i" -lt "$pogingen" ]; do
|
||
if curl -fsS $tijden "${basis}/healthz" >/dev/null 2>&1; then
|
||
# Spatie na de dubbele punt toestaan: of json compact is,
|
||
# hangt af van de Flask-versie.
|
||
versie="$(curl -fsS $tijden "${basis}/healthz" 2>/dev/null \
|
||
| sed -n 's/.*"version" *: *"\([^"]*\)".*/\1/p')"
|
||
printf '\n'
|
||
goed "Server Up${versie:+ }${versie:-} draait"
|
||
return 0
|
||
fi
|
||
# Een puntje per poging, zodat je ziet dat er nog iets gebeurt.
|
||
printf '.'
|
||
i=$((i + 1))
|
||
sleep 2
|
||
done
|
||
printf '\n'
|
||
waarschuw "Server Up reageerde niet op ${basis}."
|
||
log "Bekijk de logs met: $SUDO docker compose -f $DOEL/docker-compose.yml logs --tail=50"
|
||
return 1
|
||
}
|
||
|
||
# ── Beheerdersaccount ────────────────────────────────────────────────────────
|
||
# Tussen "container draait" en "eerste account" ligt een venster waarin iedereen
|
||
# die de pagina bereikt zichzelf beheerder kan maken. Dat venster hoort zo kort
|
||
# mogelijk te zijn, dus kan het script het account meteen aanmaken.
|
||
WW_GEGENEREERD=0
|
||
BEHEERDER_WW_TOON=""
|
||
|
||
json_tekst() {
|
||
# Escapet een waarde voor gebruik tussen JSON-aanhalingstekens.
|
||
printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'
|
||
}
|
||
|
||
genereer_wachtwoord() {
|
||
LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom 2>/dev/null | head -c 24
|
||
}
|
||
|
||
vraag_wachtwoord() {
|
||
# Twee keer intypen, met de echo uit. Mislukt dat, dan valt hij terug op
|
||
# een gegenereerd wachtwoord in plaats van er een half account van te maken.
|
||
oude_stty=""
|
||
if stty -g </dev/tty >/dev/null 2>&1; then oude_stty="$(stty -g </dev/tty)"; fi
|
||
poging=0
|
||
while [ "$poging" -lt 3 ]; do
|
||
poging=$((poging + 1))
|
||
if [ -n "$oude_stty" ]; then stty -echo </dev/tty; fi
|
||
printf ' Wachtwoord voor %s (minstens 10 tekens): ' "$BEHEERDER"
|
||
read -r ww1 </dev/tty || ww1=""
|
||
printf '\n Nogmaals: '
|
||
read -r ww2 </dev/tty || ww2=""
|
||
if [ -n "$oude_stty" ]; then stty "$oude_stty" </dev/tty; fi
|
||
printf '\n'
|
||
if [ "$ww1" != "$ww2" ]; then
|
||
waarschuw "De twee invoeren verschillen."
|
||
elif [ "${#ww1}" -lt 10 ]; then
|
||
waarschuw "Minstens 10 tekens, dat is ook wat de server eist."
|
||
else
|
||
BEHEERDER_WW="$ww1"
|
||
return 0
|
||
fi
|
||
done
|
||
waarschuw "Geen bruikbaar wachtwoord ingevoerd; er wordt er een aangemaakt."
|
||
return 1
|
||
}
|
||
|
||
bepaal_wachtwoord() {
|
||
# Volgorde: bestand, omgevingsvariabele, vragen, anders zelf aanmaken.
|
||
[ -n "$BEHEERDER_WW" ] && return 0
|
||
if [ -n "$BEHEERDER_WW_BESTAND" ]; then
|
||
[ -r "$BEHEERDER_WW_BESTAND" ] || fout "Kan $BEHEERDER_WW_BESTAND niet lezen."
|
||
BEHEERDER_WW="$(head -n 1 "$BEHEERDER_WW_BESTAND")"
|
||
[ -n "$BEHEERDER_WW" ] || fout "$BEHEERDER_WW_BESTAND is leeg."
|
||
return 0
|
||
fi
|
||
if [ -n "${SU_ADMIN_PASSWORD:-}" ]; then
|
||
BEHEERDER_WW="$SU_ADMIN_PASSWORD"
|
||
return 0
|
||
fi
|
||
if [ "$JA" -eq 0 ] && terminal_beschikbaar; then
|
||
vraag_wachtwoord && return 0
|
||
fi
|
||
BEHEERDER_WW="$(genereer_wachtwoord)"
|
||
[ -n "$BEHEERDER_WW" ] || fout "Kon geen wachtwoord aanmaken (/dev/urandom niet leesbaar). Gebruik --admin-password-file."
|
||
WW_GEGENEREERD=1
|
||
return 0
|
||
}
|
||
|
||
vraag_beheerder() {
|
||
# Niets meegegeven, maar wel een terminal? Dan hier aanbieden, want dit is
|
||
# het moment waarop het uitmaakt.
|
||
[ -n "$BEHEERDER" ] && return 0
|
||
[ "$JA" -eq 1 ] && return 0
|
||
[ "$DROOG" -eq 1 ] && return 0
|
||
terminal_beschikbaar || return 0
|
||
|
||
stap "Beheerdersaccount"
|
||
if ! vraag "Nu meteen een beheerdersaccount aanmaken?"; then
|
||
return 0
|
||
fi
|
||
printf ' Gebruikersnaam: '
|
||
read -r naam </dev/tty || naam=""
|
||
naam="$(printf '%s' "$naam" | tr -d '[:space:]')"
|
||
if [ -z "$naam" ]; then
|
||
waarschuw "Geen naam opgegeven; overgeslagen."
|
||
return 0
|
||
fi
|
||
BEHEERDER="$naam"
|
||
return 0
|
||
}
|
||
|
||
beheerder_vooraf() {
|
||
# Alles wat om invoer vraagt gebeurt vóór het bouwen, zodat je daarna kunt
|
||
# weglopen terwijl het image gebouwd wordt.
|
||
[ "$DROOG" -eq 1 ] && return 0
|
||
vraag_beheerder
|
||
[ -n "$BEHEERDER" ] || return 0
|
||
bepaal_wachtwoord
|
||
return 0
|
||
}
|
||
|
||
maak_beheerder() {
|
||
[ -n "$BEHEERDER" ] || return 0
|
||
|
||
if [ "$DROOG" -eq 1 ]; then
|
||
stap "Beheerdersaccount"
|
||
printf ' [droog] account %s aanmaken via POST /api/auth/setup\n' "$BEHEERDER"
|
||
return 0
|
||
fi
|
||
|
||
bepaal_wachtwoord
|
||
|
||
# Het wachtwoord gaat via stdin naar curl, niet via de opdrachtregel:
|
||
# argumenten zijn voor elke gebruiker op de server zichtbaar in 'ps'.
|
||
antwoord="$(mktemp 2>/dev/null || echo /tmp/su-setup.$$)"
|
||
code="$(printf '{"username":"%s","password":"%s"}' \
|
||
"$(json_tekst "$BEHEERDER")" "$(json_tekst "$BEHEERDER_WW")" \
|
||
| curl -sS -o "$antwoord" -w '%{http_code}' \
|
||
--connect-timeout 5 --max-time 20 \
|
||
-X POST -H 'Content-Type: application/json' \
|
||
--data-binary @- \
|
||
"http://$(probe_adres):${POORT}/api/auth/setup" 2>/dev/null || true)"
|
||
# Bij een mislukte verbinding schrijft curl zelf al "000"; kan curl helemaal
|
||
# niet draaien, dan blijft het leeg. Beide gevallen zijn hetzelfde verhaal.
|
||
case "$code" in ''|*[!0-9]*) code="000" ;; esac
|
||
|
||
case "$code" in
|
||
200) goed "Beheerder '$BEHEERDER' aangemaakt"
|
||
# Alleen een zelf aangemaakt wachtwoord moet nog getoond worden;
|
||
# een ingetypt of meegegeven wachtwoord kent de gebruiker al.
|
||
if [ "$WW_GEGENEREERD" -eq 1 ]; then
|
||
BEHEERDER_WW_TOON="$BEHEERDER_WW"
|
||
fi ;;
|
||
409) waarschuw "Er bestond al een account; er is niets gewijzigd."
|
||
BEHEERDER=""; WW_GEGENEREERD=0 ;;
|
||
000) waarschuw "Server Up was niet bereikbaar; account niet aangemaakt."
|
||
BEHEERDER=""; WW_GEGENEREERD=0 ;;
|
||
*) melding="$(sed -n 's/.*"msg" *: *"\([^"]*\)".*/\1/p' "$antwoord" 2>/dev/null)"
|
||
waarschuw "Aanmaken mislukt (HTTP $code)${melding:+: }${melding:-}"
|
||
BEHEERDER=""; WW_GEGENEREERD=0 ;;
|
||
esac
|
||
rm -f "$antwoord"
|
||
BEHEERDER_WW=""
|
||
}
|
||
|
||
afsluiting() {
|
||
if [ "$BIND" = "0.0.0.0" ]; then
|
||
ip="$(hostname -I 2>/dev/null | awk '{print $1}')"
|
||
adres="http://${ip:-<server-ip>}:${POORT}"
|
||
else
|
||
adres="http://localhost:${POORT}"
|
||
fi
|
||
|
||
printf '\n%s────────────────────────────────────────────────────────%s\n' "$C_KOP" "$C_UIT"
|
||
if [ "$GEREED" -eq 1 ]; then
|
||
printf '%s Server Up draait op %s%s\n' "$C_OK" "$adres" "$C_UIT"
|
||
else
|
||
printf '%s Server Up antwoordde niet op %s%s\n' "$C_WAARSCHUW" "$adres" "$C_UIT"
|
||
fi
|
||
printf '%s────────────────────────────────────────────────────────%s\n\n' "$C_KOP" "$C_UIT"
|
||
|
||
if [ "$GEREED" -eq 0 ]; then
|
||
printf ' De container is aangemaakt, maar reageerde nog niet. Kijk wat\n'
|
||
printf ' er aan de hand is met:\n'
|
||
printf ' %s docker compose -f %s/docker-compose.yml logs --tail=50\n\n' "${SUDO:-}" "$DOEL"
|
||
fi
|
||
|
||
if [ -n "$BEHEERDER" ]; then
|
||
printf ' %s✔%s Inloggen als: %s%s%s\n' "$C_OK" "$C_UIT" "$C_KOP" "$BEHEERDER" "$C_UIT"
|
||
if [ "$WW_GEGENEREERD" -eq 1 ]; then
|
||
printf ' Wachtwoord: %s%s%s\n' "$C_KOP" "$BEHEERDER_WW_TOON" "$C_UIT"
|
||
printf ' %sSchrijf dit nu over — het staat nergens anders.%s\n' "$C_WAARSCHUW" "$C_UIT"
|
||
fi
|
||
printf '\n'
|
||
else
|
||
printf ' %sMaak nu meteen een beheerdersaccount aan.%s\n' "$C_WAARSCHUW" "$C_UIT"
|
||
printf ' Zolang dat niet gebeurd is, kan iedereen die deze pagina\n'
|
||
printf ' bereikt het account claimen.\n\n'
|
||
fi
|
||
|
||
if [ "$BIND" = "127.0.0.1" ]; then
|
||
printf ' Alleen bereikbaar vanaf deze server zelf. Van buitenaf erbij?\n'
|
||
printf ' 1. Zet een reverse proxy met TLS ervoor\n'
|
||
printf ' 2. sed -i "s/^BIND=.*/BIND=0.0.0.0/" %s/.env\n' "$DOEL"
|
||
printf ' 3. %s docker compose -f %s/docker-compose.yml up -d\n\n' "${SUDO:-}" "$DOEL"
|
||
else
|
||
printf ' %sDe interface staat open op je netwerk.%s Server Up beheert de\n' "$C_WAARSCHUW" "$C_UIT"
|
||
printf ' Docker-daemon, dus wie erbij kan, kan alles op deze server.\n'
|
||
printf ' Zet er een reverse proxy met TLS voor.\n\n'
|
||
fi
|
||
|
||
printf ' Draait als: %s\n' "${SU_NAAM:-root}"
|
||
printf ' Bijwerken: sh %s/install.sh --update\n' "$DOEL"
|
||
printf ' Logs: %s docker compose -f %s/docker-compose.yml logs -f\n' "${SUDO:-}" "$DOEL"
|
||
printf ' Documentatie: %s/README.md\n\n' "$DOEL"
|
||
}
|
||
|
||
# ── Doorlichten ──────────────────────────────────────────────────────────────
|
||
# De fouten die dit script had, waren allemaal van dezelfde soort: iets klopte
|
||
# niet en niemand merkte het, omdat er nergens werd nagekeken. Dit is het
|
||
# tegengif — het controleert de aannames in plaats van ze te doen.
|
||
DOK_FOUTEN=0
|
||
DOK_WAARSCHUWINGEN=0
|
||
|
||
dok_ok() { goed "$1"; }
|
||
dok_waar() { waarschuw "$1"; DOK_WAARSCHUWINGEN=$((DOK_WAARSCHUWINGEN + 1)); }
|
||
dok_fout() { printf ' %s✖%s %s\n' "$C_FOUT" "$C_UIT" "$1"; DOK_FOUTEN=$((DOK_FOUTEN + 1)); }
|
||
|
||
_dok_env() {
|
||
# _dok_env SLEUTEL — de waarde uit .env, of leeg.
|
||
[ -f "$DOEL/.env" ] || return 0
|
||
sed -n "s/^$1=//p" "$DOEL/.env" 2>/dev/null | tail -1
|
||
}
|
||
|
||
_dok_inspect() {
|
||
# _dok_inspect FORMAAT — docker inspect op de container, of leeg.
|
||
$SUDO docker inspect server-up --format "$1" 2>/dev/null || true
|
||
}
|
||
|
||
doorlichten() {
|
||
stap "Installatie"
|
||
if [ -d "$DOEL" ] && [ -f "$DOEL/docker-compose.yml" ]; then
|
||
dok_ok "Gevonden in $DOEL"
|
||
else
|
||
dok_fout "Geen installatie in $DOEL (geen docker-compose.yml)"
|
||
printf '\n'
|
||
return 1
|
||
fi
|
||
if [ -f "$DOEL/.env" ]; then
|
||
_rechten="$(stat -c '%a' "$DOEL/.env" 2>/dev/null || echo '?')"
|
||
case "$_rechten" in
|
||
600|400) dok_ok ".env staat op $_rechten" ;;
|
||
'?') dok_waar "Rechten van .env niet te bepalen" ;;
|
||
*) dok_waar ".env staat op $_rechten; er staan tokens en wachtwoorden in. Zet hem op 600." ;;
|
||
esac
|
||
else
|
||
dok_waar "Geen .env in $DOEL; alles draait op de standaardwaarden"
|
||
fi
|
||
[ -f "$DOEL/VERSION" ] && log "Versie in de map: $(cat "$DOEL/VERSION" 2>/dev/null)"
|
||
|
||
stap "Docker"
|
||
if $SUDO docker info >/dev/null 2>&1; then
|
||
dok_ok "Daemon reageert ($($SUDO docker version --format '{{.Server.Version}}' 2>/dev/null || echo '?'))"
|
||
else
|
||
dok_fout "De Docker-daemon reageert niet"
|
||
printf '\n'
|
||
return 1
|
||
fi
|
||
|
||
stap "Container"
|
||
_staat="$(_dok_inspect '{{.State.Status}}')"
|
||
if [ -z "$_staat" ]; then
|
||
dok_fout "Er is geen container 'server-up'. Start hem met: $SUDO docker compose -f $DOEL/docker-compose.yml up -d"
|
||
elif [ "$_staat" = "running" ]; then
|
||
_gezond="$(_dok_inspect '{{if .State.Health}}{{.State.Health.Status}}{{end}}')"
|
||
dok_ok "Draait${_gezond:+ (}${_gezond:-}${_gezond:+)}"
|
||
[ "${_gezond:-healthy}" = "healthy" ] || dok_waar "De healthcheck staat op '$_gezond'"
|
||
else
|
||
dok_fout "De container staat op '$_staat'"
|
||
fi
|
||
_img="$(_dok_inspect '{{.Config.Image}}')"
|
||
[ -n "$_img" ] && log "Image: $_img"
|
||
|
||
stap "Bereikbaarheid"
|
||
_poort="$(_dok_env PORT)"; _poort="${_poort:-$POORT}"
|
||
_bind="$(_dok_env BIND)"
|
||
[ -n "$_bind" ] && geldig_bind "$_bind" && BIND="$_bind"
|
||
POORT="$_poort"
|
||
_adres="http://$(probe_adres):${POORT}"
|
||
if curl -fsS --connect-timeout 3 --max-time 8 "${_adres}/healthz" >/dev/null 2>&1; then
|
||
dok_ok "Antwoordt op $_adres"
|
||
else
|
||
dok_fout "Geen antwoord op $_adres"
|
||
log "Dit is het adres waarop compose publiceert (BIND=$BIND)."
|
||
fi
|
||
|
||
stap "Account"
|
||
_uid_env="$(_dok_env SU_UID)"
|
||
_uid_echt="$($SUDO docker exec server-up id -u 2>/dev/null || echo '')"
|
||
if [ -z "$_uid_echt" ]; then
|
||
dok_waar "Kon niet in de container kijken onder welk account hij draait"
|
||
elif [ "$_uid_echt" = "0" ]; then
|
||
if [ -n "$_uid_env" ] && [ "$_uid_env" != "0" ]; then
|
||
dok_fout "SU_UID=$_uid_env staat in .env maar hij draait als root. Hercreëer de container: $SUDO docker compose -f $DOEL/docker-compose.yml up -d"
|
||
else
|
||
dok_waar "Draait als root. Zie docs/beveiliging.md; sh $DOEL/install.sh --update laat je kiezen."
|
||
fi
|
||
else
|
||
dok_ok "Draait als uid $_uid_echt"
|
||
[ -n "$_uid_env" ] && [ "$_uid_env" != "$_uid_echt" ] \
|
||
&& dok_waar "In .env staat SU_UID=$_uid_env; hercreëer de container om dat door te voeren."
|
||
fi
|
||
|
||
stap "Mappen"
|
||
_basis="$(_dok_env BASE_DIR)"; _basis="${_basis:-/opt/serverup}"
|
||
log "BASE_DIR: $_basis"
|
||
if $SUDO docker exec server-up test -d "$_basis" 2>/dev/null; then
|
||
# Bestaan is niet genoeg: zonder mount schrijft hij in zijn eigen
|
||
# overlay en zie je op de host niets terug.
|
||
if $SUDO docker exec server-up sh -c "grep -q ' $_basis ' /proc/self/mountinfo" 2>/dev/null; then
|
||
dok_ok "$_basis is in de container gekoppeld"
|
||
else
|
||
dok_fout "$_basis bestaat in de container maar is niet gekoppeld; wat hij daar schrijft komt niet op de host terecht"
|
||
fi
|
||
else
|
||
dok_waar "Kon $_basis niet in de container nakijken"
|
||
fi
|
||
for _sub in stacks appdata backups; do
|
||
if [ -d "$_basis/$_sub" ]; then
|
||
log " $_sub: $(find "$_basis/$_sub" -maxdepth 1 -mindepth 1 2>/dev/null | wc -l) items, eigenaar $(stat -c '%U:%G' "$_basis/$_sub" 2>/dev/null || echo '?')"
|
||
else
|
||
dok_waar " $_sub ontbreekt nog ($_basis/$_sub)"
|
||
fi
|
||
done
|
||
if heeft df; then
|
||
_vrij="$(df -h "$_basis" 2>/dev/null | awk 'NR>1 {print $4; exit}')"
|
||
[ -n "$_vrij" ] && log "Vrije ruimte: $_vrij"
|
||
fi
|
||
|
||
stap "Bijwerken vanuit de interface"
|
||
_werkmap="$(_dok_inspect '{{index .Config.Labels "com.docker.compose.project.working_dir"}}')"
|
||
if [ -z "$_werkmap" ]; then
|
||
dok_waar "Geen compose-labels; bijwerken vanuit de interface kan niet"
|
||
else
|
||
log "Werkmap volgens de container: $_werkmap"
|
||
case "$_img" in
|
||
*/*) dok_ok "Registry-image, dus de knop in de interface werkt" ;;
|
||
*) log "Zelfgebouwd image; bijwerken loopt via $DOEL/install.sh --update" ;;
|
||
esac
|
||
fi
|
||
|
||
printf '\n%s────────────────────────────────────────────────────────%s\n' "$C_KOP" "$C_UIT"
|
||
if [ "$DOK_FOUTEN" -gt 0 ]; then
|
||
printf '%s %s fout(en), %s waarschuwing(en)%s\n' "$C_FOUT" "$DOK_FOUTEN" "$DOK_WAARSCHUWINGEN" "$C_UIT"
|
||
elif [ "$DOK_WAARSCHUWINGEN" -gt 0 ]; then
|
||
printf '%s Geen fouten, %s waarschuwing(en)%s\n' "$C_WAARSCHUW" "$DOK_WAARSCHUWINGEN" "$C_UIT"
|
||
else
|
||
printf '%s Alles in orde%s\n' "$C_OK" "$C_UIT"
|
||
fi
|
||
printf '%s────────────────────────────────────────────────────────%s\n\n' "$C_KOP" "$C_UIT"
|
||
[ "$DOK_FOUTEN" -eq 0 ]
|
||
}
|
||
|
||
verwijderen() {
|
||
stap "Server Up verwijderen"
|
||
[ -d "$DOEL" ] || fout "Niets gevonden in $DOEL"
|
||
if ! vraag "Container stoppen en verwijderen? Je gegevens blijven staan."; then
|
||
log "Afgebroken."
|
||
exit 0
|
||
fi
|
||
doe $SUDO docker compose -f "$DOEL/docker-compose.yml" --project-directory "$DOEL" \
|
||
down --remove-orphans
|
||
goed "Container gestopt en verwijderd"
|
||
printf '\n'
|
||
log "Je gegevens staan nog in het docker-volume 'su-data' en in je stackmappen."
|
||
log "Alles definitief weg? Dan:"
|
||
log " $SUDO docker volume rm su-data"
|
||
log " $SUDO rm -rf $DOEL"
|
||
printf '\n'
|
||
}
|
||
|
||
# ── Hoofdprogramma ───────────────────────────────────────────────────────────
|
||
# De testsuite laadt dit bestand om losse functies te kunnen aanroepen — vooral
|
||
# de vragen die bepalen waarop de server straks luistert. Zonder deze uitgang
|
||
# zou het script bij het inladen meteen een hele installatie beginnen.
|
||
if [ -n "${SU_INSTALL_ALLEEN_FUNCTIES:-}" ]; then
|
||
return 0 2>/dev/null || exit 0
|
||
fi
|
||
|
||
printf '\n%sServer Up — installatiescript %s%s\n' "$C_KOP" "$SCRIPT_VERSIE" "$C_UIT"
|
||
[ "$DROOG" -eq 1 ] && waarschuw "Proefdraai: er wordt niets gewijzigd."
|
||
|
||
case "$ACTIE" in
|
||
uninstall)
|
||
bepaal_rechten
|
||
verwijderen
|
||
;;
|
||
doctor)
|
||
# Alleen kijken, nooit wijzigen. Daarom geen bepaal_rechten: die kan
|
||
# afbreken omdat je niet kunt schrijven, terwijl lezen prima gaat.
|
||
if [ "$(id -u)" -ne 0 ] && ! docker info >/dev/null 2>&1 && heeft sudo; then
|
||
SUDO="sudo"
|
||
fi
|
||
doorlichten
|
||
;;
|
||
update)
|
||
[ -d "$DOEL" ] || fout "Geen installatie gevonden in $DOEL. Installeer eerst zonder --update."
|
||
controleer_systeem
|
||
controleer_docker
|
||
haal_broncode
|
||
# Bestaande installaties draaien nog als root. Eén keer vragen of dat zo
|
||
# moet blijven; wie al gekozen heeft, wordt niet opnieuw lastiggevallen
|
||
# (kies_gebruiker ziet SU_UID uit .env staan).
|
||
kies_gebruiker
|
||
# Ook bij bijwerken: expliciete --bind/--port doorvoeren, en anders
|
||
# overnemen wat er al staat.
|
||
schrijf_env
|
||
zet_mappen_klaar
|
||
start
|
||
# Niet wegkijken als hij niet opkomt: "Bijgewerkt." bij een kapotte
|
||
# update zag er precies zo uit als een geslaagde, en een script dat dit
|
||
# aanroept zag exitcode 0.
|
||
if wacht_op_gereed; then
|
||
printf '\n'
|
||
goed "Bijgewerkt."
|
||
else
|
||
printf '\n'
|
||
log "Terug naar de vorige versie kan met:"
|
||
log " cd $DOEL && git log --oneline -5 # kies een eerdere versie"
|
||
log " $SUDO docker compose -f $DOEL/docker-compose.yml up -d --build"
|
||
fout "Bijwerken mislukt: de nieuwe versie kwam niet omhoog."
|
||
fi
|
||
;;
|
||
create-admin)
|
||
# Losse actie voor een installatie die al draait: handig als het
|
||
# aanmaken tijdens de installatie is misgegaan, of overgeslagen.
|
||
# Docker en schrijfrechten zijn hier niet nodig — alleen .env lezen
|
||
# en één verzoek naar de draaiende server — dus geen bepaal_rechten.
|
||
lees_env_terug "$DOEL/.env"
|
||
if [ -z "$BEHEERDER" ]; then
|
||
if terminal_beschikbaar && [ "$JA" -eq 0 ]; then
|
||
printf '\n Gebruikersnaam: '
|
||
read -r naam </dev/tty || naam=""
|
||
BEHEERDER="$(printf '%s' "$naam" | tr -d '[:space:]')"
|
||
fi
|
||
[ -n "$BEHEERDER" ] || fout "Geef de gebruikersnaam op met --admin NAAM."
|
||
fi
|
||
stap "Beheerdersaccount"
|
||
maak_beheerder
|
||
if [ -n "$BEHEERDER" ] && [ "$WW_GEGENEREERD" -eq 1 ]; then
|
||
printf '\n Wachtwoord: %s%s%s\n' "$C_KOP" "$BEHEERDER_WW_TOON" "$C_UIT"
|
||
printf ' %sSchrijf dit nu over — het staat nergens anders.%s\n\n' "$C_WAARSCHUW" "$C_UIT"
|
||
fi
|
||
;;
|
||
install)
|
||
controleer_systeem
|
||
controleer_docker
|
||
kies_bind
|
||
kies_gebruiker
|
||
beheerder_vooraf
|
||
haal_broncode
|
||
schrijf_env
|
||
zet_mappen_klaar
|
||
start
|
||
# Hier wél doorgaan: de aanwijzingen aan het eind zijn juist dán nuttig.
|
||
# Maar het slot mag niet doen alsof alles goed ging.
|
||
if wacht_op_gereed; then GEREED=1; else GEREED=0; fi
|
||
maak_beheerder
|
||
afsluiting
|
||
[ "$GEREED" -eq 1 ] || exit 1
|
||
;;
|
||
esac
|