You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

520 lines
18 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#!/usr/bin/env bash
#
# Monitor-Check – Installationsskript fuer Ubuntu 24.04 (funktioniert auch auf 22.04)
#
# sudo bash install.sh # gefuehrte Installation
# sudo bash install.sh --domain m.example.de --email ich@example.de --yes
# sudo bash install.sh --update # nur Dateien aktualisieren + Neustart
# sudo bash install.sh --dry-run # nur zeigen, was passieren wuerde
#
# Das Skript erwartet die Ordner server/ und web/ neben sich.
set -euo pipefail
# ------------------------------------------------------------------ Konstanten
APP_NAME="monitor-check"
APP_USER="monitor"
APP_DIR="/opt/${APP_NAME}"
DATA_DIR="/var/lib/${APP_NAME}"
UPLOAD_DIR="${DATA_DIR}/uploads"
SERVICE_FILE="/etc/systemd/system/${APP_NAME}.service"
NGINX_FILE="/etc/nginx/sites-available/${APP_NAME}"
NODE_MAJOR=20
SRC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# ------------------------------------------------------------------ Parameter
DOMAIN=""
EMAIL=""
TOKEN=""
PORT="8080"
MAX_UPLOAD_MB="15"
ASSUME_YES=0
DRY_RUN=0
MODE="install"
USE_TLS=1
USE_FIREWALL=1
USE_NGINX=1
PUBLIC_URL_OVERRIDE=""
BEHIND_PROXY=0
# ------------------------------------------------------------------ Ausgabe
if [[ -t 1 ]]; then
B="\033[1m"; DIM="\033[2m"; GRN="\033[32m"; YLW="\033[33m"; RED="\033[31m"; RST="\033[0m"
else
B=""; DIM=""; GRN=""; YLW=""; RED=""; RST=""
fi
schritt() { printf "\n${B}==> %s${RST}\n" "$*"; }
info() { printf " %s\n" "$*"; }
ok() { printf " ${GRN}✓${RST} %s\n" "$*"; }
warn() { printf " ${YLW}!${RST} %s\n" "$*"; }
fehler() { printf "\n${RED}Fehler:${RST} %s\n\n" "$*" >&2; exit 1; }
run() {
if [[ $DRY_RUN -eq 1 ]]; then
printf " ${DIM}[dry-run] %s${RST}\n" "$*"
else
"$@"
fi
}
# schreibt stdin nach $1
# Im Testlauf: mit MC_DRY_OUT=/pfad werden die erzeugten Dateien dort abgelegt.
schreibe() {
local ziel="$1"
if [[ $DRY_RUN -eq 1 ]]; then
printf " ${DIM}[dry-run] schreibe %s${RST}\n" "$ziel"
if [[ -n "${MC_DRY_OUT:-}" ]]; then
mkdir -p "${MC_DRY_OUT}$(dirname "$ziel")"
cat > "${MC_DRY_OUT}${ziel}"
else
cat >/dev/null
fi
else
cat > "$ziel"
fi
}
hilfe() {
sed -n '3,10p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
cat <<'EOF'
Optionen:
--domain <name> Domain, unter der das Tool erreichbar ist
--email <adresse> E-Mail fuer Let's Encrypt
--token <token> API-Token (sonst wird eins erzeugt)
--port <nummer> interner Node-Port (Standard 8080)
--max-upload <mb> maximale Fotogroesse in MB (Standard 15)
--behind-proxy hinter vorgeschaltetem Reverse-Proxy (kein certbot),
zusammen mit --public-url verwenden
--public-url <url> oeffentliche Adresse, z. B. https://monitore.example.de
(landet als Bild-Link in der Tabelle)
--no-nginx kein nginx im Container, Node direkt auf 0.0.0.0:<port>
--no-tls kein Zertifikat anfordern
--no-firewall ufw nicht konfigurieren
--update nur Anwendungsdateien erneuern und Dienst neu starten
--yes keine Rueckfragen
--dry-run nichts veraendern, nur anzeigen
--help diese Hilfe
EOF
exit 0
}
while [[ $# -gt 0 ]]; do
case "$1" in
--domain) DOMAIN="${2:-}"; shift 2 ;;
--email) EMAIL="${2:-}"; shift 2 ;;
--token) TOKEN="${2:-}"; shift 2 ;;
--port) PORT="${2:-}"; shift 2 ;;
--max-upload) MAX_UPLOAD_MB="${2:-}"; shift 2 ;;
--public-url) PUBLIC_URL_OVERRIDE="${2:-}"; shift 2 ;;
--behind-proxy) BEHIND_PROXY=1; USE_TLS=0; shift ;;
--no-nginx) USE_NGINX=0; shift ;;
--no-tls) USE_TLS=0; shift ;;
--no-firewall) USE_FIREWALL=0; shift ;;
--update) MODE="update"; shift ;;
--yes|-y) ASSUME_YES=1; shift ;;
--dry-run) DRY_RUN=1; shift ;;
--help|-h) hilfe ;;
*) fehler "Unbekannte Option: $1 (--help zeigt die Uebersicht)" ;;
esac
done
# ------------------------------------------------------------------ Vorpruefung
[[ $EUID -eq 0 || $DRY_RUN -eq 1 ]] || fehler "Bitte als root ausfuehren: sudo bash install.sh"
[[ -d "${SRC_DIR}/server" && -d "${SRC_DIR}/web" ]] || \
fehler "Ordner server/ und web/ nicht gefunden neben ${BASH_SOURCE[0]}"
[[ -f "${SRC_DIR}/server/server.js" ]] || fehler "server/server.js fehlt."
[[ -f "${SRC_DIR}/web/index.html" ]] || fehler "web/index.html fehlt."
if [[ -r /etc/os-release ]]; then
. /etc/os-release
[[ "${ID:-}" == "ubuntu" || "${ID_LIKE:-}" == *debian* ]] || \
warn "Nicht getestetes System (${PRETTY_NAME:-unbekannt}) – weiter auf eigenes Risiko."
fi
printf "\n${B}Monitor-Check – Installation${RST}\n"
[[ $DRY_RUN -eq 1 ]] && printf "${YLW}Testlauf: es wird nichts veraendert.${RST}\n" || true
# ------------------------------------------------------------------ Update-Modus
if [[ "$MODE" == "update" ]]; then
[[ -d "$APP_DIR" ]] || fehler "${APP_DIR} existiert nicht – zuerst normal installieren."
schritt "Anwendungsdateien erneuern"
run rsync -a --delete --exclude node_modules --exclude .env \
"${SRC_DIR}/server/" "${APP_DIR}/server/" 2>/dev/null || \
run cp -r "${SRC_DIR}/server/." "${APP_DIR}/server/"
run cp -r "${SRC_DIR}/web/." "${APP_DIR}/web/"
run chown -R "${APP_USER}:${APP_USER}" "$APP_DIR"
run bash -c "cd '${APP_DIR}/server' && npm install --omit=dev --no-audit --no-fund"
run systemctl restart "$APP_NAME"
ok "Aktualisiert. Status: systemctl status ${APP_NAME}"
exit 0
fi
# ------------------------------------------------------------------ Abfragen
frage() { # frage <Variable> <Text> <Standard>
local __var="$1" __text="$2" __default="${3:-}" __eingabe=""
local __aktuell="${!__var}"
if [[ -n "$__aktuell" ]]; then return; fi
if [[ $ASSUME_YES -eq 1 || ! -t 0 ]]; then
printf -v "$__var" '%s' "$__default"; return
fi
if [[ -n "$__default" ]]; then
read -r -p " ${__text} [${__default}]: " __eingabe || true
else
read -r -p " ${__text}: " __eingabe || true
fi
printf -v "$__var" '%s' "${__eingabe:-$__default}"
}
schritt "Angaben"
# Token aus bestehender Installation weiterverwenden
if [[ -z "$TOKEN" && -f "${APP_DIR}/server/.env" ]]; then
TOKEN="$(grep -E '^API_TOKEN=' "${APP_DIR}/server/.env" | cut -d= -f2- || true)"
[[ -n "$TOKEN" ]] && ok "Vorhandenes API-Token wird weiterverwendet." || true
fi
if [[ -z "$TOKEN" ]]; then
TOKEN="$(openssl rand -hex 24 2>/dev/null || head -c 24 /dev/urandom | od -An -tx1 | tr -d ' \n')"
ok "Neues API-Token erzeugt."
fi
# Hinter einem vorgeschalteten Proxy uebernimmt dieser TLS und Zertifikat.
[[ $USE_NGINX -eq 0 ]] && USE_TLS=0 || true
[[ $BEHIND_PROXY -eq 1 ]] && USE_TLS=0 || true
if [[ -z "$PUBLIC_URL_OVERRIDE" ]]; then
frage DOMAIN "Domain (leer lassen fuer Zugriff nur ueber IP)" ""
fi
if [[ -n "$DOMAIN" && $USE_TLS -eq 1 ]]; then
frage EMAIL "E-Mail fuer Let's Encrypt (leer = kein Zertifikat)" ""
[[ -z "$EMAIL" ]] && USE_TLS=0 || true
else
USE_TLS=0
fi
if [[ -n "$PUBLIC_URL_OVERRIDE" ]]; then
PUBLIC_URL="${PUBLIC_URL_OVERRIDE%/}"
[[ "$PUBLIC_URL" =~ ^https?:// ]] || fehler "--public-url muss mit http:// oder https:// beginnen."
elif [[ -n "$DOMAIN" ]]; then
PUBLIC_URL="$([[ $USE_TLS -eq 1 ]] && echo "https://${DOMAIN}" || echo "http://${DOMAIN}")"
else
IP="$(hostname -I 2>/dev/null | awk '{print $1}')"
[[ -z "$IP" ]] && IP="127.0.0.1" || true
PUBLIC_URL="http://${IP}"
warn "Ohne Domain laeuft alles ueber http – die Handy-Kamera bleibt dann gesperrt."
fi
# Ohne nginx im Container muss Node von aussen erreichbar sein.
if [[ $USE_NGINX -eq 1 ]]; then BIND_HOST="127.0.0.1"; else BIND_HOST="0.0.0.0"; fi
# Hostname aus der oeffentlichen URL – fuer die Proxy-Vorlage
PUBLIC_HOST="${PUBLIC_URL#*://}"; PUBLIC_HOST="${PUBLIC_HOST%%/*}"
CONTAINER_IP="$(hostname -I 2>/dev/null | awk '{print $1}')"
[[ -z "$CONTAINER_IP" ]] && CONTAINER_IP="<container-ip>" || true
UPSTREAM="$([[ $USE_NGINX -eq 1 ]] && echo "http://${CONTAINER_IP}:80" || echo "http://${CONTAINER_IP}:${PORT}")"
cat <<EOF
Domain : ${DOMAIN:-<nur IP>}
Oeffentliche URL: ${PUBLIC_URL}
TLS : $([[ $USE_TLS -eq 1 ]] && echo "Let's Encrypt im Container" || echo "nicht im Container (Proxy oder http)")
nginx lokal : $([[ $USE_NGINX -eq 1 ]] && echo "ja, Port 80" || echo "nein, Node direkt")
Node hoert auf: ${BIND_HOST}:${PORT}
Verzeichnisse : ${APP_DIR} , ${DATA_DIR}
API-Token : ${TOKEN}
EOF
if [[ $ASSUME_YES -eq 0 && $DRY_RUN -eq 0 && -t 0 ]]; then
read -r -p " Fortfahren? [J/n] " a || true
[[ "${a:-J}" =~ ^([JjYy]|)$ ]] || { echo " Abgebrochen."; exit 0; }
fi
# ------------------------------------------------------------------ Pakete
schritt "Systempakete installieren"
export DEBIAN_FRONTEND=noninteractive
run apt-get update -qq
if [[ $USE_NGINX -eq 1 ]]; then
run apt-get install -y -qq curl ca-certificates gnupg nginx openssl rsync
else
run apt-get install -y -qq curl ca-certificates gnupg openssl rsync
fi
ok "Basispakete vorhanden."
# Node.js
NODE_OK=0
if command -v node >/dev/null 2>&1; then
NODE_VER="$(node -p 'process.versions.node.split(".")[0]' 2>/dev/null || echo 0)"
[[ "$NODE_VER" -ge 18 ]] && NODE_OK=1 && ok "Node.js $(node -v) bereits installiert." || true
fi
if [[ $NODE_OK -eq 0 ]]; then
info "Node.js ${NODE_MAJOR} LTS wird eingerichtet …"
run bash -c "curl -fsSL https://deb.nodesource.com/setup_${NODE_MAJOR}.x | bash -"
run apt-get install -y -qq nodejs
ok "Node.js installiert."
fi
# ------------------------------------------------------------------ Benutzer
schritt "Benutzer und Verzeichnisse"
if id "$APP_USER" >/dev/null 2>&1; then
ok "Benutzer ${APP_USER} existiert."
else
run adduser --system --group --home "$APP_DIR" --no-create-home "$APP_USER"
ok "Benutzer ${APP_USER} angelegt."
fi
run mkdir -p "${APP_DIR}/server" "${APP_DIR}/web" "$UPLOAD_DIR"
# ------------------------------------------------------------------ Dateien
schritt "Anwendung kopieren"
run rsync -a --exclude node_modules --exclude .env --exclude uploads \
"${SRC_DIR}/server/" "${APP_DIR}/server/"
run rsync -a "${SRC_DIR}/web/" "${APP_DIR}/web/"
ok "Dateien liegen in ${APP_DIR}."
schritt "Node-Abhaengigkeiten installieren"
info "Das dauert beim ersten Mal ein bis zwei Minuten (sharp)."
run bash -c "cd '${APP_DIR}/server' && npm install --omit=dev --no-audit --no-fund"
ok "Abhaengigkeiten installiert."
# ------------------------------------------------------------------ Konfiguration
schritt "Konfiguration schreiben"
if [[ -f "${APP_DIR}/server/.env" && $DRY_RUN -eq 0 ]]; then
cp "${APP_DIR}/server/.env" "${APP_DIR}/server/.env.bak.$(date +%s)"
info "Bisherige .env gesichert."
fi
schreibe "${APP_DIR}/server/.env" <<EOF
# erzeugt von install.sh am $(date -Is)
PORT=${PORT}
HOST=${BIND_HOST}
API_TOKEN=${TOKEN}
PUBLIC_URL=${PUBLIC_URL}
UPLOAD_DIR=${UPLOAD_DIR}
WEB_DIR=${APP_DIR}/web
MAX_UPLOAD_MB=${MAX_UPLOAD_MB}
MAX_IMAGE_PX=1600
JPEG_QUALITY=78
ALLOWED_ORIGINS=${PUBLIC_URL}
TRUST_PROXY=$([[ $USE_TLS -eq 0 && $USE_NGINX -eq 1 ]] && echo 2 || echo 1)
RATE_LIMIT=300
EOF
run chown -R "${APP_USER}:${APP_USER}" "$APP_DIR" "$DATA_DIR"
run chmod 600 "${APP_DIR}/server/.env"
ok ".env geschrieben."
# ------------------------------------------------------------------ systemd
schritt "Dienst einrichten"
schreibe "$SERVICE_FILE" <<EOF
[Unit]
Description=Monitor-Check Upload- und Web-Server
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=${APP_USER}
Group=${APP_USER}
WorkingDirectory=${APP_DIR}/server
EnvironmentFile=${APP_DIR}/server/.env
ExecStart=$(command -v node || echo /usr/bin/node) ${APP_DIR}/server/server.js
Restart=always
RestartSec=3
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=${DATA_DIR}
ProtectKernelTunables=true
ProtectControlGroups=true
RestrictSUIDSGID=true
LockPersonality=true
StandardOutput=journal
StandardError=journal
SyslogIdentifier=${APP_NAME}
[Install]
WantedBy=multi-user.target
EOF
run systemctl daemon-reload
run systemctl enable --now "$APP_NAME"
run systemctl restart "$APP_NAME"
ok "Dienst ${APP_NAME} laeuft."
# ------------------------------------------------------------------ nginx
if [[ $USE_NGINX -eq 1 ]]; then
schritt "nginx einrichten"
SERVER_NAME="${DOMAIN:-_}"
schreibe "$NGINX_FILE" <<EOF
# erzeugt von install.sh
server {
listen 80;
listen [::]:80;
server_name ${SERVER_NAME};
client_max_body_size $((MAX_UPLOAD_MB + 5))m;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
location / {
proxy_pass http://127.0.0.1:${PORT};
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_read_timeout 120s;
}
location /uploads/ {
alias ${UPLOAD_DIR}/;
expires 30d;
add_header Cache-Control "public, immutable";
add_header X-Content-Type-Options "nosniff" always;
access_log off;
location ~* \\.(php|pl|py|cgi|sh)\$ { deny all; }
}
}
EOF
run ln -sfn "$NGINX_FILE" "/etc/nginx/sites-enabled/${APP_NAME}"
[[ -e /etc/nginx/sites-enabled/default ]] && run rm -f /etc/nginx/sites-enabled/default || true
run nginx -t
run systemctl reload nginx
ok "nginx liefert ${SERVER_NAME} aus."
else
schritt "nginx"
info "uebersprungen – Node bedient ${BIND_HOST}:${PORT} direkt."
fi
# ------------------------------------------------------------------ TLS
if [[ $USE_TLS -eq 1 ]]; then
schritt "Zertifikat anfordern"
run apt-get install -y -qq certbot python3-certbot-nginx
if run certbot --nginx -d "$DOMAIN" --agree-tos -m "$EMAIL" --redirect --non-interactive; then
ok "HTTPS aktiv fuer ${DOMAIN}."
else
warn "certbot ist fehlgeschlagen – zeigt der DNS-Eintrag von ${DOMAIN} schon hierher?"
warn "Spaeter nachholen: certbot --nginx -d ${DOMAIN}"
fi
fi
# ------------------------------------------------------------------ Firewall
if [[ $USE_FIREWALL -eq 1 ]]; then
schritt "Firewall"
run apt-get install -y -qq ufw
run ufw allow 22/tcp
if [[ $USE_NGINX -eq 1 ]]; then
run ufw allow 80/tcp
run ufw allow 443/tcp
PORTS="22, 80, 443"
else
run ufw allow "${PORT}/tcp"
PORTS="22, ${PORT}"
fi
run bash -c "ufw --force enable"
ok "ufw aktiv (${PORTS})."
fi
# ------------------------------------------------------------------ Test
schritt "Funktionstest"
if [[ $DRY_RUN -eq 0 ]]; then
sleep 2
if curl -fsS "http://127.0.0.1:${PORT}/gesundheit" >/dev/null 2>&1; then
ok "Node-Dienst antwortet."
else
warn "Node-Dienst antwortet nicht – pruefen mit: journalctl -u ${APP_NAME} -n 40 --no-pager"
fi
if [[ $USE_NGINX -eq 1 ]]; then
if curl -fsS -o /dev/null "http://127.0.0.1/" 2>/dev/null; then
ok "nginx liefert die Oberflaeche aus."
else
warn "nginx antwortet lokal nicht wie erwartet."
fi
fi
fi
# ------------------------------------------------------------------ Proxy-Vorlage
PROXY_DATEI="${APP_DIR}/PROXY-SNIPPET.conf"
if [[ $USE_TLS -eq 0 ]]; then
schritt "Vorlage fuer den vorgeschalteten Proxy"
schreibe "$PROXY_DATEI" <<EOF
# Fuer den vorgeschalteten Reverse-Proxy (TLS liegt dort).
# Wichtig ist client_max_body_size – sonst brechen Foto-Uploads mit 413 ab.
server {
listen 443 ssl http2;
server_name ${PUBLIC_HOST};
# ssl_certificate ... ;
# ssl_certificate_key ... ;
client_max_body_size $((MAX_UPLOAD_MB + 5))m;
location / {
proxy_pass ${UPSTREAM};
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_read_timeout 120s;
proxy_request_buffering off;
}
}
EOF
ok "Vorlage liegt in ${PROXY_DATEI} (Upstream: ${UPSTREAM})."
fi
# ------------------------------------------------------------------ Abschluss
ZUSAMMENFASSUNG="${APP_DIR}/ZUGANGSDATEN.txt"
if [[ $DRY_RUN -eq 0 ]]; then
cat > "$ZUSAMMENFASSUNG" <<EOF
Monitor-Check – Zugangsdaten (erzeugt $(date -Is))
Oberflaeche : ${PUBLIC_URL}
Upload-Endpoint : ${PUBLIC_URL}/api/upload
API-Token : ${TOKEN}
Dieses Token muss identisch in apps-script/Code.gs stehen (CONFIG.API_TOKEN)
und im Setup-Tab der Oberflaeche eingetragen werden.
EOF
chmod 600 "$ZUSAMMENFASSUNG"
fi
cat <<EOF
${GRN}${B}Fertig.${RST}
Oberflaeche ${B}${PUBLIC_URL}${RST}
Upload-Endpoint ${PUBLIC_URL}/api/upload
API-Token ${B}${TOKEN}${RST}
$([[ $DRY_RUN -eq 0 ]] && echo "notiert in ${ZUSAMMENFASSUNG}")
$([[ $USE_TLS -eq 0 ]] && cat <<PROXY
${B}Vorgeschalteter Proxy:${RST} Ziel ist ${B}${UPSTREAM}${RST}
Dort unbedingt setzen: client_max_body_size $((MAX_UPLOAD_MB + 5))m;
Fertige Vorlage: ${PROXY_DATEI}
PROXY
)
${B}Noch zu tun:${RST}
1. In der Google Tabelle: Erweiterungen > Apps Script, Inhalt von
apps-script/Code.gs einfuegen und CONFIG.API_TOKEN auf das Token oben setzen.
2. Bereitstellen > Neue Bereitstellung > Web-App
Ausfuehren als: Ich | Zugriff: ${B}Jeder${RST}
Die .../exec-URL kopieren.
3. ${PUBLIC_URL} am Handy oeffnen, Tab "Setup":
Apps-Script-URL, Token und Upload-Endpoint eintragen, dann
"Verbindung testen".
${B}Nuetzlich:${RST}
systemctl status ${APP_NAME}
journalctl -u ${APP_NAME} -f
sudo bash install.sh --update # nach Aenderungen an web/ oder server/
EOF

Powered by TurnKey Linux.