mailwolt/scripts/mailwolt-apply-domains

383 lines
12 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
UI_HOST=""; WEBMAIL_HOST=""; MAIL_HOST=""; SSL_AUTO=0
while [[ $# -gt 0 ]]; do
case "$1" in
--ui-host) UI_HOST="$2"; shift 2 ;;
--webmail-host) WEBMAIL_HOST="$2"; shift 2 ;;
--mail-host) MAIL_HOST="$2"; shift 2 ;;
--ssl-auto) SSL_AUTO="$2"; shift 2 ;;
*) shift ;;
esac
done
PHPV=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;')
PHP_FPM_SOCK="/run/php/php${PHPV}-fpm.sock"
[ -S "$PHP_FPM_SOCK" ] || PHP_FPM_SOCK="/run/php/php-fpm.sock"
APP_DIR="/var/www/mailwolt"
NGINX_SITE="/etc/nginx/sites-available/mailwolt.conf"
ACME_ROOT="/var/www/letsencrypt"
mkdir -p "${ACME_ROOT}/.well-known/acme-challenge"
# --- Phase 1: HTTP-only Vhosts mit ACME-Challenge ---
cat > "${NGINX_SITE}" <<CONF
server {
listen 80;
listen [::]:80;
server_name ${UI_HOST} ${WEBMAIL_HOST} ${MAIL_HOST};
root ${APP_DIR}/public;
index index.php index.html;
location /.well-known/acme-challenge/ {
root ${ACME_ROOT};
try_files \$uri =404;
}
location / {
try_files \$uri \$uri/ /index.php?\$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:${PHP_FPM_SOCK};
}
location ^~ /livewire/ {
try_files \$uri /index.php?\$query_string;
}
}
CONF
nginx -t && systemctl reload nginx
# --- Phase 2: Let's Encrypt Zertifikate holen ---
# Prüfen ob Server globales IPv6 hat (nötig wenn AAAA-Records existieren)
has_global_ipv6() {
ip -6 addr show scope global 2>/dev/null | grep -q 'inet6'
}
cert_needs_action() {
local domain="$1"
local cert="/etc/letsencrypt/live/${domain}/fullchain.pem"
[ ! -f "${cert}" ] && return 0
# 0 = gültig für >10 Tage → überspringen; 1 = läuft ab → erneuern
openssl x509 -checkend 864000 -noout -in "${cert}" 2>/dev/null && return 1
return 0
}
certbot_safe() {
local domain="$1"
local has_aaaa=""
# dig ist nicht immer installiert — Fallback auf getent
if command -v dig >/dev/null 2>&1; then
has_aaaa=$(dig +short AAAA "${domain}" 2>/dev/null | head -1) || has_aaaa=""
else
has_aaaa=$(getent ahostsv6 "${domain}" 2>/dev/null | awk '{print $1}' | head -1) || has_aaaa=""
fi
if [ -n "${has_aaaa}" ] && ! has_global_ipv6; then
echo "[!] ${domain}: AAAA-Record vorhanden aber kein IPv6 auf diesem Server — Let's Encrypt würde fehlschlagen. Self-signed wird verwendet."
return 1
fi
certbot certonly --webroot \
-w "${ACME_ROOT}" \
-d "${domain}" \
--non-interactive --agree-tos \
--email "webmaster@${domain}" \
--no-eff-email
}
if [ "${SSL_AUTO}" = "1" ]; then
for DOMAIN in "${UI_HOST}" "${WEBMAIL_HOST}" "${MAIL_HOST}"; do
[ -z "${DOMAIN}" ] && continue
if cert_needs_action "${DOMAIN}"; then
certbot_safe "${DOMAIN}" || true
fi
done
fi
# --- Phase 3: Finale Vhosts ---
# Backup der aktuellen nginx-Config (Wiederherstellung bei Fehler)
NGINX_BACKUP="${NGINX_SITE}.bak"
[ -f "${NGINX_SITE}" ] && cp "${NGINX_SITE}" "${NGINX_BACKUP}"
# Cert-Erkennung: fullchain.pem direkt, Renewal-Datei, oder umbenannter Cert-Ordner
cert_exists() {
local d="$1"
[ -z "$d" ] && return 1
[ -f "/etc/letsencrypt/live/${d}/fullchain.pem" ] && return 0
[ -f "/etc/letsencrypt/renewal/${d}.conf" ] && return 0
ls "/etc/letsencrypt/live/${d}"-*/fullchain.pem 2>/dev/null | grep -q . && return 0
return 1
}
UI_HAS_CERT=0; WM_HAS_CERT=0; MAIL_HAS_CERT=0
cert_exists "${UI_HOST}" && UI_HAS_CERT=1
cert_exists "${WEBMAIL_HOST}" && WM_HAS_CERT=1
cert_exists "${MAIL_HOST}" && MAIL_HAS_CERT=1
(
if [ "${UI_HAS_CERT}" = "1" ] || [ "${WM_HAS_CERT}" = "1" ]; then
# Mindestens ein Web-Cert vorhanden → HTTP-Redirect Block (inkl. MAIL_HOST für ACME-Renewal)
cat <<CONF
server {
listen 80;
listen [::]:80;
server_name ${UI_HOST} ${WEBMAIL_HOST} ${MAIL_HOST};
location /.well-known/acme-challenge/ {
root ${ACME_ROOT};
try_files \$uri =404;
}
location / { return 301 https://\$host\$request_uri; }
}
CONF
else
# Kein Cert → HTTP-only, App läuft auf Port 80 weiter
cat <<CONF
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
root ${APP_DIR}/public;
index index.php index.html;
location /.well-known/acme-challenge/ {
root ${ACME_ROOT};
try_files \$uri =404;
}
location / { try_files \$uri \$uri/ /index.php?\$query_string; }
location ~ \.php\$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:${PHP_FPM_SOCK};
}
location ^~ /livewire/ { try_files \$uri /index.php?\$query_string; }
location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)\$ { expires 30d; access_log off; }
}
CONF
fi
if [ -n "${UI_HOST}" ] && [ "${UI_HAS_CERT}" = "1" ]; then
cat <<CONF
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name ${UI_HOST};
ssl_certificate /etc/letsencrypt/live/${UI_HOST}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/${UI_HOST}/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
root ${APP_DIR}/public;
index index.php index.html;
location /ws/ {
proxy_pass http://127.0.0.1:8080/;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_read_timeout 60s;
}
location /apps/ {
proxy_pass http://127.0.0.1:8080/apps/;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host \$host;
}
location / { try_files \$uri \$uri/ /index.php?\$query_string; }
location ~ \.php\$ {
include snippets/fastcgi-php.conf;
fastcgi_param HTTPS on;
fastcgi_pass unix:${PHP_FPM_SOCK};
}
location ^~ /livewire/ { try_files \$uri /index.php?\$query_string; }
location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)\$ { expires 30d; access_log off; }
}
CONF
fi
if [ -n "${WEBMAIL_HOST}" ] && [ "${WM_HAS_CERT}" = "1" ]; then
cat <<CONF
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name ${WEBMAIL_HOST};
ssl_certificate /etc/letsencrypt/live/${WEBMAIL_HOST}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/${WEBMAIL_HOST}/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
root ${APP_DIR}/public;
index index.php index.html;
location / { try_files \$uri \$uri/ /index.php?\$query_string; }
location ~ \.php\$ {
include snippets/fastcgi-php.conf;
fastcgi_param HTTPS on;
fastcgi_pass unix:${PHP_FPM_SOCK};
}
location ^~ /livewire/ { try_files \$uri /index.php?\$query_string; }
location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)\$ { expires 30d; access_log off; }
}
CONF
fi
) > "${NGINX_SITE}"
# State-Dateien schreiben — korrekt pro Domain, nicht pauschal "done"
STATE_DIR="/var/lib/mailwolt/wizard"
if [ -d "${STATE_DIR}" ]; then
# UI: done nur wenn LE-Cert tatsächlich vorhanden, sonst error
if [ -f "${STATE_DIR}/ui" ]; then
if [ "${UI_HAS_CERT}" = "1" ]; then
printf "done" > "${STATE_DIR}/ui"
else
printf "error" > "${STATE_DIR}/ui"
fi
fi
# Webmail: done nur wenn LE-Cert tatsächlich vorhanden, sonst error
if [ -f "${STATE_DIR}/webmail" ]; then
if [ "${WM_HAS_CERT}" = "1" ]; then
printf "done" > "${STATE_DIR}/webmail"
else
printf "error" > "${STATE_DIR}/webmail"
fi
fi
# Mail-Domain: LE-Cert für Postfix/Dovecot
if [ -f "${STATE_DIR}/mail" ]; then
if [ "${MAIL_HAS_CERT}" = "1" ]; then
printf "done" > "${STATE_DIR}/mail"
else
printf "error" > "${STATE_DIR}/mail"
fi
fi
# done-Signal: immer schreiben damit der 2s-Poll stoppt
if [ "${UI_HAS_CERT}" = "1" ] || [ "${WM_HAS_CERT}" = "1" ]; then
# Erst done schreiben, dann 6s warten bevor nginx auf HTTPS wechselt —
# so hat der Browser Zeit den "Zum Login"-Button zu rendern bevor der Switch passiert
printf "1" > "${STATE_DIR}/done"
sleep 6
else
printf "0" > "${STATE_DIR}/done"
fi
fi
# --- Phase 4: Postfix + Dovecot mit Mail-Domain-Zertifikat konfigurieren ---
if [ -n "${MAIL_HOST}" ]; then
# Hostname immer setzen, unabhängig vom Zertifikat
if command -v postconf >/dev/null 2>&1; then
postconf -e "myhostname = ${MAIL_HOST}"
postconf -e "myorigin = ${MAIL_HOST}"
postconf -e "mydestination = ${MAIL_HOST}, localhost.localdomain, localhost"
fi
# /etc/aliases bereinigen: root → /dev/null verhindert Bounce-Schleife
if [ -f /etc/aliases ]; then
# Vorhandene root/clamav-Zeilen ersetzen oder am Ende ergänzen
if grep -q '^root:' /etc/aliases; then
sed -i 's|^root:.*|root: /dev/null|' /etc/aliases
else
echo 'root: /dev/null' >> /etc/aliases
fi
if grep -q '^clamav:' /etc/aliases; then
sed -i 's|^clamav:.*|clamav: /dev/null|' /etc/aliases
else
echo 'clamav: /dev/null' >> /etc/aliases
fi
command -v newaliases >/dev/null 2>&1 && newaliases
fi
fi
if [ -n "${MAIL_HOST}" ] && [ "${MAIL_HAS_CERT}" = "1" ]; then
MAIL_CERT="/etc/letsencrypt/live/${MAIL_HOST}/fullchain.pem"
MAIL_KEY="/etc/letsencrypt/live/${MAIL_HOST}/privkey.pem"
# Postfix TLS
if command -v postconf >/dev/null 2>&1; then
postconf -e "smtpd_tls_cert_file = ${MAIL_CERT}"
postconf -e "smtpd_tls_key_file = ${MAIL_KEY}"
postconf -e "smtp_tls_cert_file = ${MAIL_CERT}"
postconf -e "smtp_tls_key_file = ${MAIL_KEY}"
systemctl reload postfix 2>/dev/null || true
fi
# Dovecot
if [ -d /etc/dovecot/conf.d ]; then
cat > /etc/dovecot/conf.d/10-ssl.conf <<DCONF
ssl = yes
ssl_cert = <${MAIL_CERT}
ssl_key = <${MAIL_KEY}
ssl_min_protocol = TLSv1.2
DCONF
systemctl reload dovecot 2>/dev/null || true
fi
fi
# --- Phase 5: .env aktualisieren + Assets neu bauen ---
ENV_FILE="${APP_DIR}/.env"
if [ -n "${UI_HOST}" ] && [ -f "${ENV_FILE}" ]; then
APP_SCHEME="http"
[ "${UI_HAS_CERT}" = "1" ] && APP_SCHEME="https"
REVERB_PORT_VAL="443"
[ "${UI_HAS_CERT}" != "1" ] && REVERB_PORT_VAL="80"
_env_set() {
local key="$1" val="$2"
if grep -q "^${key}=" "${ENV_FILE}"; then
sed -i "s|^${key}=.*|${key}=${val}|" "${ENV_FILE}"
else
echo "${key}=${val}" >> "${ENV_FILE}"
fi
}
_env_set "APP_HOST" "${UI_HOST}"
_env_set "APP_URL" "${APP_SCHEME}://\${APP_HOST}"
# Reverb: Client-seitige Verbindung über nginx-Proxy (Domain + 443)
_env_set "REVERB_HOST" "\${APP_HOST}"
_env_set "REVERB_PORT" "${REVERB_PORT_VAL}"
_env_set "REVERB_SCHEME" "${APP_SCHEME}"
_env_set "REVERB_PATH" "/ws"
# Reverb: Server-seitiger Prozess (lokal auf 8080)
_env_set "REVERB_SERVER_HOST" "127.0.0.1"
_env_set "REVERB_SERVER_PORT" "8080"
_env_set "REVERB_SERVER_SCHEME" "http"
_env_set "REVERB_SERVER_PATH" ""
# Vite-Variablen (werden ins JS-Bundle eingebacken)
_env_set "VITE_REVERB_HOST" "\${REVERB_HOST}"
_env_set "VITE_REVERB_PORT" "\${REVERB_PORT}"
_env_set "VITE_REVERB_SCHEME" "\${REVERB_SCHEME}"
_env_set "VITE_REVERB_PATH" "\${REVERB_PATH}"
# Assets neu bauen damit wsHost im JS den richtigen Domain-Namen hat
if command -v npm >/dev/null 2>&1 && [ -f "${APP_DIR}/package.json" ]; then
cd "${APP_DIR}" && npm run build --silent 2>/dev/null || true
fi
# Laravel-Caches leeren (als App-User, nicht root)
APP_USER=$(stat -c '%U' "${APP_DIR}/artisan" 2>/dev/null || echo "www-data")
su -s /bin/sh "${APP_USER}" -c "php '${APP_DIR}/artisan' optimize:clear --quiet" 2>/dev/null || true
su -s /bin/sh "${APP_USER}" -c "php '${APP_DIR}/artisan' config:cache --quiet" 2>/dev/null || true
su -s /bin/sh "${APP_USER}" -c "php '${APP_DIR}/artisan' route:cache --quiet" 2>/dev/null || true
fi
if nginx -t 2>/dev/null; then
systemctl reload nginx
echo "mailwolt-apply-domains fertig"
else
echo "[!] nginx -t fehlgeschlagen — alte Konfiguration wird wiederhergestellt"
[ -f "${NGINX_BACKUP}" ] && cp "${NGINX_BACKUP}" "${NGINX_SITE}"
nginx -t && systemctl reload nginx
echo "[!] mailwolt-apply-domains mit Fehler beendet"
exit 1
fi