#!/bin/sh # SecuBox Nextcloud Platform Controller # Copyright (C) 2025 CyberMind.fr # # Manages Nextcloud in Debian-based LXC container with MariaDB & Redis CONFIG="nextcloud" LXC_NAME="nextcloud" NEXTCLOUD_VERSION="31.0.5" # Paths LXC_PATH="/srv/lxc" LXC_ROOTFS="$LXC_PATH/$LXC_NAME/rootfs" LXC_CONFIG="$LXC_PATH/$LXC_NAME/config" DATA_PATH="/srv/nextcloud" BACKUP_PATH="/srv/nextcloud/backups" # Logging log_info() { echo "[INFO] $*"; logger -t nextcloud "$*"; } log_error() { echo "[ERROR] $*" >&2; logger -t nextcloud -p err "$*"; } log_debug() { [ "$DEBUG" = "1" ] && echo "[DEBUG] $*"; } # Helpers require_root() { [ "$(id -u)" -eq 0 ] || { log_error "This command requires root privileges" exit 1 } } has_lxc() { command -v lxc-start >/dev/null 2>&1; } ensure_dir() { [ -d "$1" ] || mkdir -p "$1"; } uci_get() { uci -q get ${CONFIG}.$1; } uci_set() { uci set ${CONFIG}.$1="$2" && uci commit ${CONFIG}; } # Generate random password gen_password() { head -c 16 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 16 } # Load configuration load_config() { http_port="$(uci_get main.http_port)" || http_port="8080" data_path="$(uci_get main.data_path)" || data_path="/srv/nextcloud" domain="$(uci_get main.domain)" || domain="cloud.local" admin_user="$(uci_get main.admin_user)" || admin_user="admin" admin_password="$(uci_get main.admin_password)" memory_limit="$(uci_get main.memory_limit)" || memory_limit="1G" upload_max="$(uci_get main.upload_max)" || upload_max="512M" trusted_proxies="$(uci_get main.trusted_proxies)" || trusted_proxies="127.0.0.1" # Database settings db_name="$(uci_get db.name)" || db_name="nextcloud" db_user="$(uci_get db.user)" || db_user="nextcloud" db_password="$(uci_get db.password)" # Redis settings redis_enabled="$(uci_get redis.enabled)" || redis_enabled="1" redis_memory="$(uci_get redis.memory)" || redis_memory="128M" # HAProxy/SSL settings ssl_enabled="$(uci_get ssl.enabled)" || ssl_enabled="0" ssl_domain="$(uci_get ssl.domain)" ssl_acme="$(uci_get ssl.acme)" || ssl_acme="1" # Backup settings backup_enabled="$(uci_get backup.enabled)" || backup_enabled="1" backup_keep="$(uci_get backup.keep)" || backup_keep="7" DATA_PATH="$data_path" BACKUP_PATH="$data_path/backups" ensure_dir "$data_path" ensure_dir "$data_path/data" ensure_dir "$data_path/config" ensure_dir "$BACKUP_PATH" } # Usage usage() { cat < [options] Commands: install Create Debian LXC and install Nextcloud stack uninstall Remove container (preserves data) update Update Nextcloud to latest version start Start Nextcloud service (via init) stop Stop Nextcloud service (via init) restart Restart Nextcloud service status Show service status (JSON format) logs [-f] Show container logs shell Open shell in container occ Run Nextcloud OCC command backup [name] Create backup of data and database restore Restore from backup list-backups List available backups ssl-enable Register with HAProxy for SSL (via WAF) ssl-disable Remove HAProxy registration setup-mail Configure outbound email (SMTP) setup-backup-cron Setup scheduled backup cron job connections Show CalDAV/CardDAV/WebDAV URLs service-run Start service (used by init) service-stop Stop service (used by init) Configuration: /etc/config/nextcloud Data directory: /srv/nextcloud EOF } # Check prerequisites lxc_check_prereqs() { if ! has_lxc; then log_error "LXC not installed. Install with: opkg install lxc lxc-common" return 1 fi return 0 } # Create LXC rootfs from Debian lxc_create_rootfs() { local rootfs="$LXC_ROOTFS" local arch=$(uname -m) log_info "Creating Debian 12 rootfs for Nextcloud..." ensure_dir "$rootfs" # Use Debian mini rootfs from LXC images case "$arch" in x86_64) deb_arch="amd64" ;; aarch64) deb_arch="arm64" ;; armv7l) deb_arch="armhf" ;; *) log_error "Unsupported architecture: $arch"; return 1 ;; esac # Download Debian rootfs from LXC image server local base_url="https://images.linuxcontainers.org/images/debian/bookworm/${deb_arch}/default" local index_url="${base_url}/index.json" local tmpdir="/tmp/debian-rootfs-$$" mkdir -p "$tmpdir" log_info "Fetching latest Debian 12 rootfs..." local latest_date # Parse HTML directory listing for latest date folder latest_date=$(wget -q -O- "${base_url}/" 2>/dev/null | grep -oE "20[0-9]{6}_[0-9]{2}:[0-9]{2}" | tail -1) if [ -z "$latest_date" ]; then # Fallback: use today's date with common time latest_date="$(date +%Y%m%d)_05:24" log_info "Using fallback date: $latest_date" fi # URL encode the colon local latest_encoded=$(echo "$latest_date" | sed 's/:/%3A/g') local rootfs_url="${base_url}/${latest_encoded}/rootfs.tar.xz" local tmpfile="$tmpdir/rootfs.tar.xz" log_info "Downloading from: $rootfs_url" wget -q -O "$tmpfile" "$rootfs_url" 2>&1 || { log_error "Failed to download Debian rootfs from $rootfs_url" rm -rf "$tmpdir" return 1 } # Verify download if [ ! -s "$tmpfile" ]; then log_error "Downloaded file is empty" rm -rf "$tmpdir" return 1 fi log_info "Extracting rootfs..." tar -xJf "$tmpfile" -C "$rootfs" || { log_error "Failed to extract rootfs" rm -rf "$tmpdir" return 1 } rm -rf "$tmpdir" # Setup resolv.conf cp /etc/resolv.conf "$rootfs/etc/resolv.conf" 2>/dev/null || \ echo "nameserver 1.1.1.1" > "$rootfs/etc/resolv.conf" # Create required directories mkdir -p "$rootfs/opt" mkdir -p "$rootfs/run" mkdir -p "$rootfs/var/www/nextcloud" log_info "Rootfs created successfully" return 0 } # Mount filesystems for chroot mount_chroot_fs() { local rootfs="$1" mount --bind /dev "$rootfs/dev" 2>/dev/null || true mount --bind /dev/pts "$rootfs/dev/pts" 2>/dev/null || true mount -t proc proc "$rootfs/proc" 2>/dev/null || true mount -t sysfs sysfs "$rootfs/sys" 2>/dev/null || true } # Unmount chroot filesystems umount_chroot_fs() { local rootfs="$1" umount "$rootfs/sys" 2>/dev/null || true umount "$rootfs/proc" 2>/dev/null || true umount "$rootfs/dev/pts" 2>/dev/null || true umount "$rootfs/dev" 2>/dev/null || true } # Install packages inside container install_container_packages() { local rootfs="$LXC_ROOTFS" log_info "Installing Nextcloud stack packages..." # Create install script cat > "$rootfs/tmp/install-deps.sh" << 'SCRIPT' #!/bin/bash set -e export DEBIAN_FRONTEND=noninteractive # Install gpg first for apt verification apt-get update || true apt-get install -y --no-install-recommends gnupg gpgv ca-certificates || true # Update and install packages apt-get update apt-get install -y --no-install-recommends \ nginx \ mariadb-server \ redis-server \ php-fpm \ php-gd \ php-mysql \ php-curl \ php-mbstring \ php-intl \ php-gmp \ php-bcmath \ php-xml \ php-zip \ php-imagick \ php-redis \ php-apcu \ php-ldap \ unzip \ curl \ wget \ cron # Clean up apt-get clean rm -rf /var/lib/apt/lists/* touch /tmp/.deps-installed SCRIPT chmod +x "$rootfs/tmp/install-deps.sh" # Mount filesystems for chroot mount_chroot_fs "$rootfs" # Run via chroot (container not started yet) log_info "Running package installation via chroot..." chroot "$rootfs" /tmp/install-deps.sh local result=$? # Cleanup mounts umount_chroot_fs "$rootfs" if [ $result -ne 0 ]; then log_error "Failed to install packages" return 1 fi rm -f "$rootfs/tmp/install-deps.sh" log_info "Container packages installed" return 0 } # Download and install Nextcloud install_nextcloud() { local rootfs="$LXC_ROOTFS" local nc_dir="$rootfs/var/www/nextcloud" log_info "Downloading Nextcloud ${NEXTCLOUD_VERSION}..." local nc_url="https://download.nextcloud.com/server/releases/nextcloud-${NEXTCLOUD_VERSION}.zip" local tmpfile="/tmp/nextcloud.zip" wget -q --show-progress -O "$tmpfile" "$nc_url" || { log_error "Failed to download Nextcloud" return 1 } log_info "Extracting Nextcloud..." unzip -q "$tmpfile" -d "$rootfs/var/www/" || { log_error "Failed to extract Nextcloud" rm -f "$tmpfile" return 1 } rm -f "$tmpfile" # Set ownership (will be fixed inside container) chown -R 33:33 "$nc_dir" 2>/dev/null || true log_info "Nextcloud installed" return 0 } # Create startup script create_startup_script() { local rootfs="$LXC_ROOTFS" cat > "$rootfs/opt/start-nextcloud.sh" << 'STARTUP' #!/bin/bash set -e export PATH="/usr/sbin:/usr/bin:/sbin:/bin" # Start MariaDB echo "Starting MariaDB..." service mariadb start sleep 2 # Start Redis echo "Starting Redis..." service redis-server start # Configure PHP-FPM pool PHP_VERSION=$(ls /etc/php/ | head -1) PHP_FPM_POOL="/etc/php/${PHP_VERSION}/fpm/pool.d/www.conf" # Increase PHP limits sed -i "s/upload_max_filesize = .*/upload_max_filesize = ${NEXTCLOUD_UPLOAD_MAX:-512M}/" /etc/php/${PHP_VERSION}/fpm/php.ini sed -i "s/post_max_size = .*/post_max_size = ${NEXTCLOUD_UPLOAD_MAX:-512M}/" /etc/php/${PHP_VERSION}/fpm/php.ini sed -i "s/memory_limit = .*/memory_limit = 512M/" /etc/php/${PHP_VERSION}/fpm/php.ini sed -i "s/max_execution_time = .*/max_execution_time = 3600/" /etc/php/${PHP_VERSION}/fpm/php.ini # Enable opcache echo "opcache.enable=1" >> /etc/php/${PHP_VERSION}/fpm/php.ini echo "opcache.memory_consumption=128" >> /etc/php/${PHP_VERSION}/fpm/php.ini echo "opcache.interned_strings_buffer=16" >> /etc/php/${PHP_VERSION}/fpm/php.ini echo "opcache.max_accelerated_files=10000" >> /etc/php/${PHP_VERSION}/fpm/php.ini # Start PHP-FPM echo "Starting PHP-FPM..." service php${PHP_VERSION}-fpm start # Configure Nginx cat > /etc/nginx/sites-available/nextcloud << NGINX server { listen ${NEXTCLOUD_HTTP_PORT:-8080}; server_name _; root /var/www/nextcloud; index index.php index.html; client_max_body_size 512M; fastcgi_buffers 64 4K; gzip on; gzip_vary on; gzip_comp_level 4; gzip_min_length 256; gzip_proxied expired no-cache no-store private no_last_modified no_etag auth; gzip_types application/atom+xml text/javascript application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/wasm application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy; add_header Referrer-Policy "no-referrer" always; add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Permitted-Cross-Domain-Policies "none" always; add_header X-Robots-Tag "noindex, nofollow" always; add_header X-XSS-Protection "1; mode=block" always; location = /robots.txt { allow all; log_not_found off; access_log off; } location ^~ /.well-known { location = /.well-known/carddav { return 301 /remote.php/dav/; } location = /.well-known/caldav { return 301 /remote.php/dav/; } location /.well-known/acme-challenge { try_files $uri $uri/ =404; } location /.well-known/pki-validation { try_files $uri $uri/ =404; } return 301 /index.php$request_uri; } location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:$|/)(?!.*\.(css|js|svg|png|gif|ico|woff2?)$) { return 404; } location ~ ^/(?:\.|autotest|occ|issue|indie|db_|console) { return 404; } # Serve app static files correctly location ~ ^/apps/[^/]+/(?:css|js|img|l10n)/.+$ { try_files \$uri /index.php\$request_uri; add_header Cache-Control "public, max-age=15778463, immutable"; access_log off; } location ~ \.php(?:$|/) { rewrite ^/(?!index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|ocs-provider\/.+|.+\/richdocumentscode(_arm64)?\/proxy) /index.php$request_uri; fastcgi_split_path_info ^(.+?\.php)(/.*)$; set $path_info $fastcgi_path_info; try_files $fastcgi_script_name =404; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $path_info; fastcgi_param modHeadersAvailable true; fastcgi_param front_controller_active true; fastcgi_pass unix:/run/php/php${PHP_VERSION}-fpm.sock; fastcgi_intercept_errors on; fastcgi_request_buffering off; fastcgi_max_temp_file_size 0; } location ~ \.(?:css|js|mjs|svg|gif|png|jpg|ico|wasm|tflite|map|ogg|flac)$ { try_files $uri /index.php$request_uri; add_header Cache-Control "public, max-age=15778463, immutable"; access_log off; } location ~ \.woff2?$ { try_files $uri /index.php$request_uri; expires 7d; access_log off; } location /remote { return 301 /remote.php$request_uri; } location / { rewrite ^ /index.php\$request_uri last; } } NGINX rm -f /etc/nginx/sites-enabled/default ln -sf /etc/nginx/sites-available/nextcloud /etc/nginx/sites-enabled/nextcloud # Configure cron for background jobs echo "*/5 * * * * www-data php -f /var/www/nextcloud/cron.php" > /etc/cron.d/nextcloud chmod 644 /etc/cron.d/nextcloud service cron start # Start Nginx (foreground for procd) echo "Starting Nginx..." exec nginx -g 'daemon off;' STARTUP chmod +x "$rootfs/opt/start-nextcloud.sh" } # Setup database setup_database() { load_config # Generate password if not set if [ -z "$db_password" ]; then db_password=$(gen_password) uci_set db.password "$db_password" fi log_info "Setting up MariaDB database..." # Create setup script cat > "$LXC_ROOTFS/tmp/setup-db.sh" << DBSCRIPT #!/bin/bash service mariadb start sleep 2 mysql -u root << EOF CREATE DATABASE IF NOT EXISTS ${db_name} CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE USER IF NOT EXISTS '${db_user}'@'localhost' IDENTIFIED BY '${db_password}'; GRANT ALL PRIVILEGES ON ${db_name}.* TO '${db_user}'@'localhost'; FLUSH PRIVILEGES; EOF DBSCRIPT chmod +x "$LXC_ROOTFS/tmp/setup-db.sh" # Run via chroot chroot "$LXC_ROOTFS" /tmp/setup-db.sh || { log_error "Failed to setup database" return 1 } rm -f "$LXC_ROOTFS/tmp/setup-db.sh" log_info "Database setup complete" } # Configure Redis setup_redis() { load_config local rootfs="$LXC_ROOTFS" log_info "Configuring Redis..." # Get memory in MB local redis_mb="${redis_memory%M}" redis_mb="${redis_mb%m}" # Configure Redis cat > "$rootfs/etc/redis/redis.conf" << EOF bind 127.0.0.1 port 6379 daemonize yes pidfile /run/redis/redis-server.pid loglevel notice logfile /var/log/redis/redis-server.log databases 16 maxmemory ${redis_mb}mb maxmemory-policy allkeys-lru EOF log_info "Redis configured" } # Create LXC config lxc_create_config() { load_config ensure_dir "$(dirname "$LXC_CONFIG")" # Convert memory limit to bytes local mem_bytes case "$memory_limit" in *G|*g) mem_bytes=$((${memory_limit%[Gg]} * 1024 * 1024 * 1024)) ;; *M|*m) mem_bytes=$((${memory_limit%[Mm]} * 1024 * 1024)) ;; *K|*k) mem_bytes=$((${memory_limit%[Kk]} * 1024)) ;; *) mem_bytes="$memory_limit" ;; esac cat > "$LXC_CONFIG" << EOF # Nextcloud Platform LXC Configuration lxc.uts.name = $LXC_NAME lxc.rootfs.path = dir:$LXC_ROOTFS lxc.arch = $(uname -m) # Auto-start on boot lxc.start.auto = 1 lxc.start.delay = 5 # Network: use host network lxc.net.0.type = none # Mount points lxc.mount.auto = proc:mixed sys:ro lxc.mount.entry = $data_path/data var/www/nextcloud/data none bind,create=dir 0 0 lxc.mount.entry = $data_path/config var/www/nextcloud/config none bind,create=dir 0 0 # Environment lxc.environment = NEXTCLOUD_DOMAIN=$domain lxc.environment = NEXTCLOUD_HTTP_PORT=$http_port lxc.environment = NEXTCLOUD_UPLOAD_MAX=$upload_max lxc.environment = NEXTCLOUD_TRUSTED_PROXIES=$trusted_proxies # Memory limit lxc.cgroup2.memory.max = $mem_bytes # Security lxc.cap.drop = sys_admin sys_module mac_admin mac_override sys_time sys_rawio # Init command lxc.init.cmd = /opt/start-nextcloud.sh EOF log_info "LXC config created" } # Container control lxc_running() { lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING" } lxc_exists() { [ -f "$LXC_CONFIG" ] && [ -d "$LXC_ROOTFS" ] } lxc_stop() { if lxc_running; then log_info "Stopping Nextcloud container..." lxc-stop -n "$LXC_NAME" -k 2>/dev/null || true sleep 2 fi } lxc_run() { load_config lxc_stop if ! lxc_exists; then log_error "Container not installed. Run: nextcloudctl install" return 1 fi # Regenerate config in case settings changed lxc_create_config log_info "Starting Nextcloud container..." exec lxc-start -n "$LXC_NAME" -F -f "$LXC_CONFIG" } # Execute command in container lxc_exec() { if ! lxc_running; then log_error "Container not running" return 1 fi lxc-attach -n "$LXC_NAME" -- "$@" } # Run OCC command lxc_occ() { if ! lxc_running; then log_error "Container not running" return 1 fi lxc-attach -n "$LXC_NAME" -- su -s /bin/bash www-data -c "php /var/www/nextcloud/occ $*" } # Commands cmd_install() { require_root load_config log_info "Installing Nextcloud Platform..." lxc_check_prereqs || exit 1 # Generate admin password if not set if [ -z "$admin_password" ]; then admin_password=$(gen_password) uci_set main.admin_password "$admin_password" log_info "Generated admin password: $admin_password" fi # Create container if ! lxc_exists; then lxc_create_rootfs || exit 1 fi # Install packages install_container_packages || exit 1 # Download Nextcloud if [ ! -f "$LXC_ROOTFS/var/www/nextcloud/occ" ]; then install_nextcloud || exit 1 fi # Setup services setup_database || exit 1 setup_redis || exit 1 # Create startup script create_startup_script # Create LXC config lxc_create_config || exit 1 # Enable service uci_set main.enabled '1' /etc/init.d/nextcloud enable 2>/dev/null || true log_info "" log_info "Installation complete!" log_info "" log_info "Start with: /etc/init.d/nextcloud start" log_info "Web interface: http://:$http_port" log_info "" log_info "After first start, run Nextcloud setup:" log_info " nextcloudctl occ maintenance:install \\" log_info " --database mysql --database-name $db_name \\" log_info " --database-user $db_user --database-pass '$db_password' \\" log_info " --admin-user $admin_user --admin-pass '$admin_password'" log_info "" } cmd_uninstall() { require_root log_info "Uninstalling Nextcloud Platform..." # Stop service /etc/init.d/nextcloud stop 2>/dev/null || true /etc/init.d/nextcloud disable 2>/dev/null || true lxc_stop # Remove container (keep data) if [ -d "$LXC_PATH/$LXC_NAME" ]; then rm -rf "$LXC_PATH/$LXC_NAME" log_info "Container removed" fi uci_set main.enabled '0' log_info "Nextcloud Platform uninstalled" log_info "Data preserved in: $(uci_get main.data_path)" } cmd_update() { require_root load_config if ! lxc_exists; then log_error "Container not installed. Run: nextcloudctl install" return 1 fi log_info "Updating Nextcloud..." # Enable maintenance mode if lxc_running; then lxc_occ maintenance:mode --on fi # Download new Nextcloud install_nextcloud || exit 1 # Run upgrade if lxc_running; then lxc_occ upgrade lxc_occ maintenance:mode --off fi log_info "Update complete" } cmd_status() { load_config local enabled="$(uci_get main.enabled)" local running="false" local installed="false" local version="" local user_count=0 local disk_used="0" if lxc_exists; then installed="true" fi if lxc_running; then running="true" # Get Nextcloud version version=$(lxc_occ -V 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' || echo "") # Get user count user_count=$(lxc_occ user:list --output=json 2>/dev/null | jsonfilter -e '@[*]' 2>/dev/null | wc -l || echo 0) fi # Get disk usage if [ -d "$data_path/data" ]; then disk_used=$(du -sh "$data_path/data" 2>/dev/null | awk '{print $1}' || echo "0") fi # Get LAN IP local lan_ip lan_ip=$(uci -q get network.lan.ipaddr || echo "192.168.1.1") cat << EOF { "enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"), "running": $running, "installed": $installed, "version": "$version", "http_port": $http_port, "data_path": "$data_path", "memory_limit": "$memory_limit", "domain": "$domain", "user_count": $user_count, "disk_used": "$disk_used", "http_url": "http://${lan_ip}:${http_port}", "ssl_enabled": $([ "$ssl_enabled" = "1" ] && echo "true" || echo "false"), "ssl_domain": "$ssl_domain", "redis_enabled": $([ "$redis_enabled" = "1" ] && echo "true" || echo "false"), "container_name": "$LXC_NAME" } EOF } cmd_logs() { load_config if lxc_running; then if [ "$1" = "-f" ]; then lxc_exec tail -f /var/log/nginx/error.log /var/www/nextcloud/data/nextcloud.log 2>/dev/null else echo "=== Nginx Error Log ===" lxc_exec tail -50 /var/log/nginx/error.log 2>/dev/null || echo "No logs" echo "" echo "=== Nextcloud Log ===" lxc_exec tail -50 /var/www/nextcloud/data/nextcloud.log 2>/dev/null || echo "No logs" fi else echo "Container not running" fi } cmd_shell() { require_root if ! lxc_running; then log_error "Container not running" exit 1 fi lxc-attach -n "$LXC_NAME" -- /bin/bash } cmd_occ() { require_root lxc_occ "$@" } cmd_backup() { require_root load_config local name="${1:-$(date +%Y%m%d-%H%M%S)}" local backup_dir="$BACKUP_PATH" ensure_dir "$backup_dir" log_info "Creating backup: $name" # Enable maintenance mode if running local was_running=0 if lxc_running; then was_running=1 lxc_occ maintenance:mode --on fi # Dump database log_info "Dumping database..." if lxc_running; then lxc_exec mysqldump -u "$db_user" -p"$db_password" "$db_name" > "$backup_dir/$name-db.sql" || { log_error "Database dump failed" [ $was_running -eq 1 ] && lxc_occ maintenance:mode --off return 1 } fi # Backup data and config log_info "Backing up data..." tar -czf "$backup_dir/$name-data.tar.gz" \ -C "$data_path" data config 2>/dev/null || { log_error "Data backup failed" [ $was_running -eq 1 ] && lxc_occ maintenance:mode --off return 1 } # Disable maintenance mode if [ $was_running -eq 1 ]; then lxc_occ maintenance:mode --off fi local size=$(ls -lh "$backup_dir/$name-data.tar.gz" 2>/dev/null | awk '{print $5}') log_info "Backup created: $backup_dir/$name-* ($size)" # Cleanup old backups if [ "$backup_keep" -gt 0 ]; then ls -t "$backup_dir"/*-db.sql 2>/dev/null | tail -n +$((backup_keep + 1)) | while read f; do local base=$(basename "$f" -db.sql) rm -f "$backup_dir/$base-db.sql" "$backup_dir/$base-data.tar.gz" log_info "Removed old backup: $base" done fi echo "$name" } cmd_restore() { require_root load_config local name="$1" if [ -z "$name" ]; then log_error "Usage: nextcloudctl restore " log_error "Available backups:" cmd_list_backups return 1 fi local backup_dir="$BACKUP_PATH" if [ ! -f "$backup_dir/$name-db.sql" ]; then log_error "Backup not found: $name" return 1 fi log_info "Restoring from backup: $name" # Enable maintenance mode local was_running=0 if lxc_running; then was_running=1 lxc_occ maintenance:mode --on fi # Restore database log_info "Restoring database..." if lxc_running; then lxc_exec mysql -u "$db_user" -p"$db_password" "$db_name" < "$backup_dir/$name-db.sql" || { log_error "Database restore failed" return 1 } fi # Restore data log_info "Restoring data..." tar -xzf "$backup_dir/$name-data.tar.gz" -C "$data_path" || { log_error "Data restore failed" return 1 } # Fix permissions if lxc_running; then lxc_exec chown -R www-data:www-data /var/www/nextcloud lxc_occ maintenance:mode --off fi log_info "Restore complete" } cmd_list_backups() { load_config local backup_dir="$BACKUP_PATH" if [ ! -d "$backup_dir" ]; then echo "No backups found" return fi echo "Available backups:" ls -1 "$backup_dir"/*-db.sql 2>/dev/null | while read f; do local name=$(basename "$f" -db.sql) local data_file="$backup_dir/$name-data.tar.gz" local size="N/A" local date="N/A" if [ -f "$data_file" ]; then size=$(ls -lh "$data_file" | awk '{print $5}') fi date=$(stat -c '%y' "$f" 2>/dev/null | cut -d. -f1) echo " $name ($size) $date" done } cmd_ssl_enable() { require_root load_config local domain="$1" if [ -z "$domain" ]; then log_error "Usage: nextcloudctl ssl-enable " return 1 fi log_info "Enabling SSL for $domain via HAProxy + WAF..." # Check if HAProxy is available if [ ! -x /usr/sbin/haproxyctl ]; then log_error "HAProxy not installed. Install secubox-app-haproxy first." return 1 fi # Use haproxyctl vhost add - routes through mitmproxy_inspector by default log_info "Adding vhost via haproxyctl..." if ! /usr/sbin/haproxyctl vhost add "$domain" --acme; then log_error "Failed to add HAProxy vhost" return 1 fi # Add route to mitmproxy for WAF inspection # This ensures traffic is inspected before reaching Nextcloud # NOTE: Nextcloud uses host network (lxc.net.0.type=none) so it listens on 192.168.255.1 # mitmproxy runs in its own container, so it can't reach 127.0.0.1 on the host local routes_file="/srv/mitmproxy/haproxy-routes.json" local routes_file_in="/srv/mitmproxy-in/haproxy-routes.json" # Get the host's LAN IP (where Nextcloud is accessible from mitmproxy container) local host_ip host_ip=$(uci -q get network.lan.ipaddr || echo "192.168.255.1") if [ -f "$routes_file" ]; then log_info "Adding mitmproxy route for WAF inspection..." # Use jsonfilter to check if route exists, then add if not local existing existing=$(jsonfilter -i "$routes_file" -e "@[\"$domain\"]" 2>/dev/null || echo "") if [ -z "$existing" ]; then # Add route: domain -> [host_ip, port] local tmpfile="/tmp/routes-$$.json" # Read existing routes and add new one if command -v python3 >/dev/null 2>&1; then python3 -c " import json with open('$routes_file', 'r') as f: routes = json.load(f) routes['$domain'] = ['$host_ip', $http_port] with open('$tmpfile', 'w') as f: json.dump(routes, f, indent=2) " mv "$tmpfile" "$routes_file" # Also update the in-container copy [ -f "$routes_file_in" ] && cp "$routes_file" "$routes_file_in" log_info "Added WAF route: $domain -> $host_ip:$http_port" else log_info "Python3 not available, manual route config needed" log_info "Add to $routes_file: \"$domain\": [\"$host_ip\", $http_port]" fi else log_info "WAF route already exists for $domain" fi else log_info "mitmproxy routes file not found, creating..." mkdir -p "$(dirname "$routes_file")" echo "{\"$domain\": [\"$host_ip\", $http_port]}" > "$routes_file" [ -d "$(dirname "$routes_file_in")" ] && cp "$routes_file" "$routes_file_in" fi # Update Nextcloud UCI config uci_set ssl.enabled '1' uci_set ssl.domain "$domain" # Restart mitmproxy to load new routes if [ -x /etc/init.d/mitmproxy ]; then log_info "Reloading mitmproxy..." /etc/init.d/mitmproxy restart 2>/dev/null || true fi # Regenerate HAProxy config and reload log_info "Regenerating HAProxy config..." /usr/sbin/haproxyctl generate 2>/dev/null || true /etc/init.d/haproxy reload 2>/dev/null || true # Add trusted domain in Nextcloud if lxc_running; then log_info "Configuring Nextcloud trusted domains..." lxc_occ config:system:set trusted_domains 1 --value="$domain" lxc_occ config:system:set overwriteprotocol --value="https" lxc_occ config:system:set overwrite.cli.url --value="https://$domain" fi log_info "" log_info "SSL enabled for $domain (with WAF inspection)" log_info "Access Nextcloud at: https://$domain" log_info "" log_info "Traffic flow: Client -> HAProxy -> mitmproxy (WAF) -> Nextcloud" } cmd_ssl_disable() { require_root load_config log_info "Disabling SSL..." # Remove HAProxy config (simplified - just disable) uci_set ssl.enabled '0' log_info "SSL disabled" } cmd_setup_mail() { require_root load_config local smtp_host="${1:-}" local smtp_port="${2:-587}" local smtp_user="${3:-}" local smtp_pass="${4:-}" local smtp_from="${5:-}" if [ -z "$smtp_host" ]; then cat << 'EOF' Usage: nextcloudctl setup-mail [port] [user] [password] [from_address] Examples: # Gmail SMTP nextcloudctl setup-mail smtp.gmail.com 587 user@gmail.com "app-password" user@gmail.com # Local mailserver (secubox-app-mailserver) nextcloudctl setup-mail 127.0.0.1 25 # Mailcow/Mailinabox nextcloudctl setup-mail mail.example.com 587 noreply@example.com "password" noreply@example.com Note: For Gmail, use an App Password (not your regular password). EOF return 1 fi if ! lxc_running; then log_error "Nextcloud container not running" return 1 fi log_info "Configuring SMTP mail settings..." # Set mail mode to SMTP lxc_occ config:system:set mail_smtpmode --value="smtp" lxc_occ config:system:set mail_smtphost --value="$smtp_host" lxc_occ config:system:set mail_smtpport --value="$smtp_port" # Enable TLS for standard ports if [ "$smtp_port" = "587" ] || [ "$smtp_port" = "465" ]; then lxc_occ config:system:set mail_smtpsecure --value="tls" fi # Set authentication if provided if [ -n "$smtp_user" ]; then lxc_occ config:system:set mail_smtpauth --value="1" lxc_occ config:system:set mail_smtpauthtype --value="LOGIN" lxc_occ config:system:set mail_smtpname --value="$smtp_user" fi if [ -n "$smtp_pass" ]; then lxc_occ config:system:set mail_smtppassword --value="$smtp_pass" fi # Set from address if [ -n "$smtp_from" ]; then lxc_occ config:system:set mail_from_address --value="${smtp_from%@*}" lxc_occ config:system:set mail_domain --value="${smtp_from#*@}" elif [ -n "$smtp_user" ]; then lxc_occ config:system:set mail_from_address --value="${smtp_user%@*}" lxc_occ config:system:set mail_domain --value="${smtp_user#*@}" fi log_info "SMTP configured: $smtp_host:$smtp_port" log_info "" log_info "Test with: nextcloudctl occ notification:test-push " } cmd_setup_backup_cron() { require_root load_config local schedule="$(uci_get backup.schedule)" || schedule="daily" local cron_file="/etc/cron.d/nextcloud-backup" log_info "Setting up scheduled backups: $schedule" case "$schedule" in hourly) echo "0 * * * * root /usr/sbin/nextcloudctl backup auto-\$(date +\\%Y\\%m\\%d-\\%H) >/dev/null 2>&1" > "$cron_file" ;; daily) echo "0 3 * * * root /usr/sbin/nextcloudctl backup auto-\$(date +\\%Y\\%m\\%d) >/dev/null 2>&1" > "$cron_file" ;; weekly) echo "0 3 * * 0 root /usr/sbin/nextcloudctl backup auto-\$(date +\\%Y\\%m\\%d) >/dev/null 2>&1" > "$cron_file" ;; disabled|none) rm -f "$cron_file" log_info "Scheduled backups disabled" return 0 ;; *) log_error "Unknown schedule: $schedule (use: hourly, daily, weekly, disabled)" return 1 ;; esac chmod 644 "$cron_file" # Restart cron if running /etc/init.d/cron restart 2>/dev/null || true log_info "Backup cron job created: $cron_file" log_info "Backups will run $schedule and keep last $(uci_get backup.keep) copies" } cmd_connections() { load_config local lan_ip lan_ip=$(uci -q get network.lan.ipaddr || echo "192.168.1.1") local base_url="http://${lan_ip}:${http_port}" if [ "$ssl_enabled" = "1" ] && [ -n "$ssl_domain" ]; then base_url="https://${ssl_domain}" fi cat << EOF Nextcloud Connection URLs ========================= Web Interface: $base_url CalDAV (Calendar): $base_url/remote.php/dav/calendars// CardDAV (Contacts): $base_url/remote.php/dav/addressbooks/users//contacts/ WebDAV (Files): $base_url/remote.php/dav/files// iOS/macOS Calendar & Contacts: Server: $base_url Path: /remote.php/dav Android (DAVx5): Base URL: $base_url/remote.php/dav Login: Your Nextcloud username and password Thunderbird (TbSync + Provider for CalDAV & CardDAV): Server URL: $base_url Desktop Sync Client: Download from: https://nextcloud.com/install/#install-clients Server: $base_url Mobile Apps: iOS: https://apps.apple.com/app/nextcloud/id1125420102 Android: https://play.google.com/store/apps/details?id=com.nextcloud.client EOF } cmd_service_run() { require_root load_config lxc_check_prereqs || exit 1 lxc_run } cmd_service_stop() { require_root lxc_stop } # Main case "${1:-}" in install) shift; cmd_install "$@" ;; uninstall) shift; cmd_uninstall "$@" ;; update) shift; cmd_update "$@" ;; start) /etc/init.d/nextcloud start ;; stop) /etc/init.d/nextcloud stop ;; restart) /etc/init.d/nextcloud restart ;; status) shift; cmd_status "$@" ;; logs) shift; cmd_logs "$@" ;; shell) shift; cmd_shell "$@" ;; occ) shift; cmd_occ "$@" ;; backup) shift; cmd_backup "$@" ;; restore) shift; cmd_restore "$@" ;; list-backups) shift; cmd_list_backups "$@" ;; ssl-enable) shift; cmd_ssl_enable "$@" ;; ssl-disable) shift; cmd_ssl_disable "$@" ;; setup-mail) shift; cmd_setup_mail "$@" ;; setup-backup-cron) shift; cmd_setup_backup_cron "$@" ;; connections) shift; cmd_connections "$@" ;; service-run) shift; cmd_service_run "$@" ;; service-stop) shift; cmd_service_stop "$@" ;; *) usage ;; esac