From 37e65300245be45d4000797b3ada53c68022fc26 Mon Sep 17 00:00:00 2001 From: Filip Wandzio Date: Wed, 1 Oct 2025 22:21:25 +0200 Subject: Implement use flags for subcommants Optimize synapse pipeline --- wh.sh | 500 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 500 insertions(+) create mode 100755 wh.sh (limited to 'wh.sh') diff --git a/wh.sh b/wh.sh new file mode 100755 index 0000000..db1c12d --- /dev/null +++ b/wh.sh @@ -0,0 +1,500 @@ +#!/bin/sh +set -eu + +BASE_DIR="/opt/matrix" + +DOCKER_COMPOSE=$(command -v docker-compose || command -v docker compose) || { + echo "[ERROR] docker-compose or docker compose not found" >&2 + exit 1 +} + +usage() { + cat <<'EOF' +whiterabbit: auto-configure Matrix homeservers (Dendrite or Synapse) +with a global Coturn instance and Nginx + Let's Encrypt proxy. + +Main actions: + turn Install or configure the global Coturn server + dendrite Add a new Matrix server (Dendrite) + synapse Add a new Matrix server (Synapse) + list Show all configured Matrix servers + remove Remove a Matrix server by domain +EOF + exit 1 +} + +require_nonempty() { + VAR_NAME="$1" + VAR_VALUE="$2" + [ -n "$VAR_VALUE" ] || { + echo "[ERROR] $VAR_NAME cannot be empty" >&2 + exit 1 + } +} + +list_servers() { + echo "=== Installed servers ===" + for dir in "$BASE_DIR"/*; do + [ -d "$dir" ] || continue + name=$(basename "$dir") + + status=$(docker ps --filter "name=^${name}$" --format "{{.Status}}") + ports=$(docker ps --filter "name=^${name}$" --format "{{.Ports}}" | sed 's/, /\n /g') + + [ -n "$status" ] && { + echo "- $name [RUNNING] on:" + echo " $ports" + } || echo "- $name [STOPPED]" + done +} + +install_turn() { + echo "=== Installing global Coturn ===" + printf "TURN domain (eg. turn.example.com): " + read -r TURN_DOMAIN + require_nonempty TURN_DOMAIN "$TURN_DOMAIN" + + printf "TURN IP (eg. 127.0.0.1): " + read -r TURN_SERVER_IP + require_nonempty TURN_SERVER_IP "$TURN_SERVER_IP" + + printf "Listening device (eg. eth0): " + read -r TURN_LISTENING_DEVICE + require_nonempty TURN_LISTENING_DEVICE "$TURN_LISTENING_DEVICE" + + printf "TURN shared secret: " + read -r TURN_SECRET + require_nonempty TURN_SECRET "$TURN_SECRET" + + mkdir -p /etc/turn + cat < /etc/turn/turnserver.conf +listening-device=$TURN_LISTENING_DEVICE +listening-port=3478 +tls-listening-port=5349 +listening-ip=$TURN_SERVER_IP +min-port=49152 +max-port=65535 +use-auth-secret +static-auth-secret=$TURN_SECRET +realm=$TURN_DOMAIN +syslog +no-rfc5780 +no-stun-backward-compatibility +response-origin-only-with-rfc5780 +EOF + + echo "$TURN_DOMAIN" > /etc/turn/domain + echo "$TURN_SECRET" > /etc/turn/secret + + case $(command -v systemctl >/dev/null && echo y || echo n) in + y) systemctl enable --now coturn || true ;; + n) service coturn start || true ;; + esac + + echo "[OK] Global Coturn configured ($TURN_DOMAIN)" +} + +common_prompts() { + printf "Matrix domain (eg. matrix.example.com): " + read -r MATRIX_DOMAIN + require_nonempty MATRIX_DOMAIN "$MATRIX_DOMAIN" + + printf "Let's Encrypt certificate email: " + read -r EMAIL + require_nonempty EMAIL "$EMAIL" + DB_PASS=$(tr -dc 'A-Za-z0-9' "/etc/nginx/sites-available/$DOMAIN" +server { + listen 80; + server_name $DOMAIN; + + location /.well-known/matrix/server { + default_type application/json; + return 200 '{ "m.server": "$DOMAIN:443" }'; + } + + location /.well-known/matrix/client { + default_type application/json; + return 200 '{ + "m.homeserver": { "base_url": "https://$DOMAIN" }, + "m.identity_server": { "base_url": "https://vector.im" } + }'; + } + + location / { + proxy_pass http://127.0.0.1:$SERVICE_PORT; + proxy_set_header Host \$host; + proxy_set_header X-Forwarded-For \$remote_addr; + } +} +EOF + + ln -sf "/etc/nginx/sites-available/$DOMAIN" "/etc/nginx/sites-enabled/$DOMAIN" + nginx -t || { echo "[ERROR] Invalid nginx config"; exit 1; } + + + case $(command -v systemctl >/dev/null && echo y || echo n) in + y) systemctl reload nginx ;; + n) nginx -s reload ;; + esac + + case $(command -v certbot >/dev/null && echo y || echo n) in + y) certbot --nginx --non-interactive --agree-tos -m "$EMAIL" -d "$DOMAIN" || { + echo "[WARN] Certbot failed for $DOMAIN" + } ;; + n) echo "[WARN] Certbot not installed, skipping TLS setup" ;; + esac +} + + +install_dendrite() { + echo "=== Installing Matrix Dendrite ===" + common_prompts + + INSTALL_DIR="$BASE_DIR/$MATRIX_DOMAIN" + mkdir -p "$INSTALL_DIR/config" + cd "$INSTALL_DIR" || exit 1 + + cat < .env +POSTGRES_HOSTNAME=postgres +POSTGRES_VERSION=15-alpine +POSTGRES_USER=dendrite +POSTGRES_PASSWORD=$DB_PASS +POSTGRES_DB=dendrite + +MONOLITH_HOSTNAME=monolith +MONOLITH_IMAGE=matrixdotorg/dendrite-monolith:latest +MONOLITH_PORT_HTTP=$PORT_HTTP +MONOLITH_PORT_HTTPS=$PORT_HTTPS +EOF + +cat < compose.yml +services: + postgres: + hostname: \${POSTGRES_HOSTNAME:-postgres} + container_name: ${MATRIX_DOMAIN}-db + image: postgres:\${POSTGRES_VERSION:-15-alpine} + restart: always + volumes: + - ./dendrite_postgres_data:/var/lib/postgresql/data + environment: + POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:-$DB_PASS} + POSTGRES_USER: \${POSTGRES_USER:-dendrite} + POSTGRES_DB: \${POSTGRES_DB:-dendrite} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U \${POSTGRES_USER:-dendrite}"] + interval: 5s + timeout: 5s + retries: 5 + networks: + - internal + + monolith: + hostname: \${MONOLITH_HOSTNAME:-monolith} + container_name: ${MATRIX_DOMAIN} + image: \${MONOLITH_IMAGE:-matrixdotorg/dendrite-monolith:latest} + ports: + - \${MONOLITH_PORT_HTTP:-8008}:8008 + - \${MONOLITH_PORT_HTTPS:-8448}:8448 + volumes: + - ./config:/etc/dendrite + - ./dendrite_media:/var/dendrite/media + - ./dendrite_jetstream:/var/dendrite/jetstream + - ./dendrite_search_index:/var/dendrite/searchindex + - ./:/mnt + depends_on: + postgres: + condition: service_healthy + networks: + - internal + restart: unless-stopped + +networks: + internal: + attachable: true +volumes: + dendrite_postgres_data: + dendrite_media: + dendrite_jetstream: + dendrite_search_index: +EOF + +cat < config/dendrite.yaml +version: 2 +global: + server_name: $MATRIX_DOMAIN + private_key: /mnt/matrix_key.pem + tls_cert: /mnt/server.crt + tls_key: /mnt/server.key + old_private_keys: [] + key_validity_period: 168h0m0s + database: + connection_string: postgresql://dendrite:$DB_PASS@postgres/dendrite?sslmode=disable + max_open_conns: 90 + max_idle_conns: 5 + conn_max_lifetime: -1 + well_known_server_name: "$MATRIX_DOMAIN:443" + well_known_client_name: "https://$MATRIX_DOMAIN" + cache: + max_size_estimated: 1gb + max_age: 1h + trusted_third_party_id_servers: + - matrix.org + - vector.im + disable_federation: false + report_stats: + enabled: false +client_api: + registration_disabled: true + registration_shared_secret: "$REG_SECRET" + turn: + turn_user_lifetime: "5m" + turn_uris: + - turn:$TURN_DOMAIN?transport=udp + - turn:$TURN_DOMAIN?transport=tcp + turn_shared_secret: "$TURN_SECRET" +federation_api: + send_max_retries: 16 + disable_tls_validation: false +media_api: + base_path: ./media_store + max_file_size_bytes: 10485760 +sync_api: + search: + enabled: false + index_path: "./searchindex" +user_api: + auto_join_rooms: + - "#main:$MATRIX_DOMAIN" +logging: + - type: std + level: info + - type: file + level: info + params: + path: ./logs +jetstream: + addresses: [] + disable_tls_validation: false + storage_path: ./ + topic_prefix: Dendrite +metrics: + enabled: false + basic_auth: + username: metrics + password: metrics +dns_cache: + enabled: false + cache_size: 256 + cache_lifetime: "5m" +EOF + + echo "[INFO] Generating Dendrite keys..." + + docker run --rm --entrypoint="" -v $(pwd):/mnt matrixdotorg/dendrite-monolith:latest /usr/bin/generate-keys -private-key /mnt/matrix_key.pem -tls-cert /mnt/server.crt -tls-key /mnt/server.key + echo "[OK] Keys generated in $PWD" + + $DOCKER_COMPOSE up -d + configure_nginx "$MATRIX_DOMAIN" "$PORT_HTTP" + echo "[OK] Dendrite server ($MATRIX_DOMAIN) is running." +} + +install_synapse() { + echo "=== Installing Matrix Synapse ===" + common_prompts + + INSTALL_DIR="$BASE_DIR/$MATRIX_DOMAIN" + mkdir -p "$INSTALL_DIR" + cd "$INSTALL_DIR" || exit 1 + mkdir -p data db + chown -R 991:991 data/ + chown -R 999:999 db/ + DB_USER="synapse" + DB_NAME="synapse" + DB_PASS=$(openssl rand -hex 16) + REG_SECRET=$(openssl rand -hex 32) + + : "${PORT_HTTP:=8008}" + : "${PORT_HTTPS:=8448}" + + cat < .env +# Matrix Synapse environment +MATRIX_DOMAIN=$MATRIX_DOMAIN +PORT_HTTP=$PORT_HTTP +PORT_HTTPS=$PORT_HTTPS + +DB_USER=$DB_USER +DB_NAME=$DB_NAME +DB_PASS=$DB_PASS + +REG_SECRET=$REG_SECRET +TURN_DOMAIN=$TURN_DOMAIN +TURN_SECRET=$TURN_SECRET +EOF + + +cat < compose.yml +services: + db: + image: postgres:15-alpine + restart: always + environment: + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASS} + POSTGRES_DB: ${DB_NAME} + LANG: C + LC_ALL: C + command: postgres -c lc_collate=C -c lc_ctype=C + volumes: + - ./db:/var/lib/postgresql/data + networks: + - synapse-net + + synapse: + image: matrixdotorg/synapse:latest + restart: always + depends_on: + - db + environment: + SYNAPSE_SERVER_NAME: ${MATRIX_DOMAIN} + SYNAPSE_REPORT_STATS: "yes" + ports: + - "${PORT_HTTP}:8008" + - "${PORT_HTTPS}:8448" + volumes: + - ./data:/data + networks: + - synapse-net + +networks: + synapse-net: + driver: bridge +EOF + +$DOCKER_COMPOSE --env-file .env up -d +docker run -it --rm -v ./data:/data -e SYNAPSE_SERVER_NAME=$MATRIX_DOMAIN -e SYNAPSE_REPORT_STATS=no matrixdotorg/synapse:latest generate + +echo "[INFO] Generating homeserver.yaml for Postgres + TURN..." +cat < data/homeserver.yaml +server_name: $MATRIX_DOMAIN +pid_file: /data/homeserver.pid + +listeners: + - port: $PORT_HTTP + tls: false + type: http + x_forwarded: true + resources: + - names: [client, federation] + compress: false + +database: + name: psycopg2 + args: + user: $DB_USER + password: $DB_PASS + host: db + database: $DB_NAME + cp_min: 5 + cp_max: 10 + +enable_registration: false +registration_shared_secret: "$REG_SECRET" + +turn_uris: + - turn:$TURN_DOMAIN?transport=udp + - turn:$TURN_DOMAIN?transport=tcp +turn_shared_secret: "$TURN_SECRET" +turn_user_lifetime: "5m" + +log_config: "/data/$MATRIX_DOMAIN.log.config" +media_store_path: /data/media_store +report_stats: false + +macaroon_secret_key: $REG_SECRET +form_secret: $REG_SECRET +signing_key_path: "/data/$MATRIX_DOMAIN.signing.key" + +trusted_key_servers: + - server_name: "matrix.org" +suppress_key_server_warning: true +EOF + + echo "[INFO] Restarting Synapse with full config..." + $DOCKER_COMPOSE --env-file .env restart synapse + $DOCKER_COMPOSE --env-file .env restart synapse + + configure_nginx "$MATRIX_DOMAIN" "$PORT_HTTP" + echo "[OK] Synapse server ($MATRIX_DOMAIN) is running with Postgres." +} + + +remove_server() { + [ "$#" -eq 0 ] && echo "Usage: remove_server [domain2 ...]" && return 1 + + for DOMAIN in "$@"; do + SERVER_DIR="$BASE_DIR/$DOMAIN" + + case "$DOMAIN" in + ""|"/"|".") + echo "[ERROR] Refusing to delete dangerous path ($DOMAIN)" + continue + ;; + esac + + [ -d "$SERVER_DIR" ] && \ + echo "[INFO] Stopping and removing server: $DOMAIN" && \ + (cd "$SERVER_DIR" && $DOCKER_COMPOSE down -v) || \ + echo "[WARN] Failed to stop container for $DOMAIN" + + rm -rf "$SERVER_DIR" + rm -f "/etc/nginx/sites-available/$DOMAIN" "/etc/nginx/sites-enabled/$DOMAIN" + done + + nginx -t && (systemctl reload nginx 2>/dev/null || nginx -s reload) + echo "[OK] Done removing servers: $*" +} + +CMD="${1:-}" + +case "$CMD" in + -t) install_turn ;; + -d) install_dendrite ;; + -s) install_synapse ;; + -l) list_servers ;; + -r) + [ -z "$2" ] && { echo "Usage: $0 -r [domain2 ...]"; exit 1; } + shift + remove_server "$@" + ;; + turn) install_turn ;; + dendrite) install_dendrite ;; + synapse) install_synapse ;; + list) list_servers ;; + remove) + [ -z "$2" ] && { echo "Usage: $0 remove [domain2 ...]"; exit 1; } + shift + remove_server "$@" + ;; + *) usage ;; +esac + -- cgit v1.2.3