#!/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/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/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//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 — 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.9.00-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:-}" "$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 > '$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/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/null 2>&1; then oude_stty="$(stty -g /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:-}:${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