From 2b8fb1cd62ef31316e0c9d7dceeba9df3fe89373 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Sat, 14 Feb 2026 09:07:33 +0100 Subject: [PATCH] feat(apps): Convert Docker-based apps to LXC Converted secubox-app-jellyfin, secubox-app-mailserver, and added secubox-app-roundcube to use LXC containers instead of Docker. Changes: - jellyfinctl: Now uses LXC at 192.168.255.31 - mailserverctl: New controller for Alpine LXC with Postfix/Dovecot - roundcubectl: New package for Roundcube webmail LXC All controllers support: - Bootstrap Alpine rootfs using static apk - LXC configuration generation - HAProxy integration with waf_bypass - Start/stop/status commands Co-Authored-By: Claude Opus 4.5 --- package/secubox/secubox-app-jellyfin/Makefile | 34 +- .../files/usr/sbin/jellyfinctl | 615 ++++++------------ .../secubox/secubox-app-mailserver/Makefile | 31 +- .../files/usr/sbin/mailserverctl | 570 ++++++++++++++++ .../secubox/secubox-app-roundcube/Makefile | 34 + .../files/usr/sbin/roundcubectl | 437 +++++++++++++ 6 files changed, 1255 insertions(+), 466 deletions(-) create mode 100644 package/secubox/secubox-app-mailserver/files/usr/sbin/mailserverctl create mode 100644 package/secubox/secubox-app-roundcube/Makefile create mode 100644 package/secubox/secubox-app-roundcube/files/usr/sbin/roundcubectl diff --git a/package/secubox/secubox-app-jellyfin/Makefile b/package/secubox/secubox-app-jellyfin/Makefile index 0e2f0121..5995dfbd 100644 --- a/package/secubox/secubox-app-jellyfin/Makefile +++ b/package/secubox/secubox-app-jellyfin/Makefile @@ -1,27 +1,26 @@ include $(TOPDIR)/rules.mk PKG_NAME:=secubox-app-jellyfin -PKG_VERSION:=1.0.0 +PKG_VERSION:=3.0.0 PKG_RELEASE:=1 -PKG_ARCH:=all -PKG_MAINTAINER:=CyberMind Studio -PKG_LICENSE:=Apache-2.0 + +PKG_MAINTAINER:=SecuBox +PKG_LICENSE:=AGPL-3.0 include $(INCLUDE_DIR)/package.mk define Package/secubox-app-jellyfin - SECTION:=utils - CATEGORY:=Utilities + SECTION:=secubox + CATEGORY:=SecuBox + SUBMENU:=Apps PKGARCH:=all - SUBMENU:=SecuBox Apps - TITLE:=SecuBox Jellyfin media server - DEPENDS:=+dockerd +docker +containerd + TITLE:=SecuBox Jellyfin Media Server (LXC) + DEPENDS:=+lxc +curl endef define Package/secubox-app-jellyfin/description -Installer, configuration, and service manager for running Jellyfin -inside Docker on SecuBox-powered OpenWrt systems. Free media server -for streaming movies, TV shows, music, and photos. + Jellyfin media server running in LXC container. + Free media server for streaming movies, TV shows, music, and photos. endef define Package/secubox-app-jellyfin/conffiles @@ -47,16 +46,15 @@ define Package/secubox-app-jellyfin/postinst [ -n "$${IPKG_INSTROOT}" ] || { echo "" echo "============================================" - echo " Jellyfin Media Server Installed" + echo " Jellyfin Media Server (LXC) Installed" echo "============================================" echo "" echo "Quick Start:" - echo " 1. Install: jellyfinctl install" - echo " 2. Configure media: uci add_list jellyfin.media.media_path='/path/to/media'" - echo " 3. Commit: uci commit jellyfin" - echo " 4. Start: /etc/init.d/jellyfin start" + echo " 1. Extract rootfs from Docker image (see README)" + echo " 2. Install: jellyfinctl install" + echo " 3. Start: jellyfinctl start" echo "" - echo "Web UI: http://:8096" + echo "Web UI: http://192.168.255.31:8096" echo "" } exit 0 diff --git a/package/secubox/secubox-app-jellyfin/files/usr/sbin/jellyfinctl b/package/secubox/secubox-app-jellyfin/files/usr/sbin/jellyfinctl index 8251ff28..aa48bf47 100644 --- a/package/secubox/secubox-app-jellyfin/files/usr/sbin/jellyfinctl +++ b/package/secubox/secubox-app-jellyfin/files/usr/sbin/jellyfinctl @@ -1,11 +1,11 @@ #!/bin/sh # SecuBox Jellyfin Media Server manager -# Full integration: Docker, HAProxy, Firewall, Mesh P2P, Backup/Restore +# LXC-based deployment with HAProxy, Firewall, Mesh P2P integration -VERSION="2.0.0" +VERSION="3.0.0" CONFIG="jellyfin" -CONTAINER="secbx-jellyfin" -OPKG_UPDATED=0 +CONTAINER="jellyfin" +LXC_PATH="/srv/lxc/jellyfin" # Colors RED='\033[0;31m' @@ -29,83 +29,68 @@ require_root() { } defaults() { - image="$(uci_get main.image)" - [ -z "$image" ] && image="jellyfin/jellyfin:latest" data_path="$(uci_get main.data_path)" [ -z "$data_path" ] && data_path="/srv/jellyfin" + media_path="$(uci_get main.media_path)" + [ -z "$media_path" ] && media_path="/srv/SHARE" port="$(uci_get main.port)" [ -z "$port" ] && port="8096" - timezone="$(uci_get main.timezone)" - [ -z "$timezone" ] && timezone="Europe/Paris" - hw_accel="$(uci_get transcoding.hw_accel)" - [ -z "$hw_accel" ] && hw_accel="0" - gpu_device="$(uci_get transcoding.gpu_device)" + ip_address="$(uci_get main.ip_address)" + [ -z "$ip_address" ] && ip_address="192.168.255.31" domain="$(uci_get network.domain)" [ -z "$domain" ] && domain="jellyfin.secubox.local" } ensure_dir() { [ -d "$1" ] || mkdir -p "$1"; } -ensure_packages() { - for pkg in "$@"; do - if ! opkg status "$pkg" 2>/dev/null | grep -q "Status:.*installed"; then - if [ "$OPKG_UPDATED" -eq 0 ]; then - opkg update || return 1 - OPKG_UPDATED=1 - fi - opkg install "$pkg" || return 1 - fi - done +# ============================================================================ +# LXC Helpers +# ============================================================================ + +lxc_running() { + lxc-info -n "$CONTAINER" 2>/dev/null | grep -q "State:.*RUNNING" } -# ============================================================================ -# Prerequisite Checks -# ============================================================================ +lxc_exists() { + [ -d "$LXC_PATH/rootfs" ] +} -check_prereqs() { +create_lxc_config() { defaults - ensure_dir "$data_path" - [ -d /sys/fs/cgroup ] || { error "/sys/fs/cgroup missing"; return 1; } - ensure_packages dockerd docker containerd - /etc/init.d/dockerd enable >/dev/null 2>&1 - /etc/init.d/dockerd start >/dev/null 2>&1 + cat > "$LXC_PATH/config" << EOF +lxc.uts.name = jellyfin +lxc.rootfs.path = dir:${LXC_PATH}/rootfs +lxc.net.0.type = veth +lxc.net.0.link = br-lan +lxc.net.0.flags = up +lxc.net.0.ipv4.address = ${ip_address}/24 +lxc.net.0.ipv4.gateway = 192.168.255.1 +lxc.mount.auto = proc:mixed sys:ro cgroup:mixed +lxc.mount.entry = ${media_path} srv/SHARE none bind,ro 0 0 +lxc.mount.entry = ${data_path}/config config none bind 0 0 +lxc.mount.entry = ${data_path}/cache cache none bind 0 0 +lxc.cap.drop = sys_module mac_admin mac_override sys_time +lxc.seccomp.profile = +lxc.tty.max = 0 +lxc.pty.max = 256 +lxc.cgroup2.memory.max = 2048000000 +lxc.init.cmd = /opt/start-jellyfin.sh +EOF } -# ============================================================================ -# Docker Helpers -# ============================================================================ +create_startup_script() { + cat > "$LXC_PATH/rootfs/opt/start-jellyfin.sh" << 'EOF' +#!/bin/bash +export PATH=/usr/lib/jellyfin-ffmpeg:$PATH +export LD_LIBRARY_PATH=/usr/lib/jellyfin-ffmpeg/lib:$LD_LIBRARY_PATH -pull_image() { - defaults - docker pull "$image" -} - -stop_container() { - docker stop "$CONTAINER" >/dev/null 2>&1 || true - docker rm "$CONTAINER" >/dev/null 2>&1 || true -} - -build_media_mounts() { - local mounts="" - local paths - paths=$(uci -q get ${CONFIG}.media.media_path) - if [ -n "$paths" ]; then - for p in $paths; do - [ -d "$p" ] && mounts="$mounts -v ${p}:${p}:ro" - done - fi - echo "$mounts" -} - -build_gpu_args() { - if [ "$hw_accel" = "1" ]; then - local dev="${gpu_device:-/dev/dri}" - if [ -e "$dev" ]; then - echo "--device=${dev}:${dev}" - else - echo "" - fi - fi +echo "Starting Jellyfin..." +exec /jellyfin/jellyfin \ + --datadir=/config \ + --cachedir=/cache \ + --webdir=/jellyfin/jellyfin-web +EOF + chmod +x "$LXC_PATH/rootfs/opt/start-jellyfin.sh" } # ============================================================================ @@ -123,155 +108,39 @@ configure_haproxy() { defaults - local ssl=$(uci_get network.haproxy_ssl) - local ssl_redirect=$(uci_get network.haproxy_ssl_redirect) - - # Check if vhost already exists (idempotent) - local existing=$(uci show haproxy 2>/dev/null | grep "\.domain='$domain'" | head -1) - if [ -n "$existing" ]; then - log "HAProxy vhost for $domain already configured" - return 0 - fi - - log "Configuring HAProxy for $domain..." - - # Add backend - uci -q add haproxy backend - uci -q set haproxy.@backend[-1].name='jellyfin_web' - uci -q set haproxy.@backend[-1].mode='http' - uci -q add_list haproxy.@backend[-1].server="jellyfin 192.168.255.1:$port check" - - # Add vhost - uci -q add haproxy vhost - uci -q set haproxy.@vhost[-1].enabled='1' - uci -q set haproxy.@vhost[-1].domain="$domain" - uci -q set haproxy.@vhost[-1].backend='jellyfin_web' - uci -q set haproxy.@vhost[-1].ssl="${ssl:-1}" - uci -q set haproxy.@vhost[-1].ssl_redirect="${ssl_redirect:-1}" - uci -q set haproxy.@vhost[-1].websocket='1' - - uci commit haproxy - /etc/init.d/haproxy reload 2>/dev/null - - log "HAProxy configured for $domain" -} - -remove_haproxy() { - if ! command -v haproxyctl >/dev/null 2>&1; then - return 0 - fi - - defaults - - log "Removing HAProxy configuration for $domain..." - - # Find and remove backend - local idx=0 - while uci -q get haproxy.@backend[$idx] >/dev/null 2>&1; do - local name=$(uci -q get haproxy.@backend[$idx].name) - if [ "$name" = "jellyfin_web" ]; then - uci delete haproxy.@backend[$idx] - break - fi - idx=$((idx + 1)) - done - - # Find and remove vhost - idx=0 - while uci -q get haproxy.@vhost[$idx] >/dev/null 2>&1; do - local vdomain=$(uci -q get haproxy.@vhost[$idx].domain) - if [ "$vdomain" = "$domain" ]; then - uci delete haproxy.@vhost[$idx] - break - fi - idx=$((idx + 1)) - done - - uci commit haproxy - /etc/init.d/haproxy reload 2>/dev/null - - log "HAProxy configuration removed" -} - -# ============================================================================ -# Firewall -# ============================================================================ - -configure_firewall() { - local fw_wan=$(uci_get network.firewall_wan) - [ "$fw_wan" != "1" ] && { log "WAN firewall rule disabled in UCI"; return 0; } - - defaults - - # Idempotent: check if rule already exists - if uci show firewall 2>/dev/null | grep -q "Jellyfin-HTTP"; then - log "Firewall rule for Jellyfin already exists" - return 0 - fi - - log "Configuring firewall for port $port..." - - uci add firewall rule - uci set firewall.@rule[-1].name='Jellyfin-HTTP' - uci set firewall.@rule[-1].src='wan' - uci set firewall.@rule[-1].dest_port="$port" - uci set firewall.@rule[-1].proto='tcp' - uci set firewall.@rule[-1].target='ACCEPT' - uci set firewall.@rule[-1].enabled='1' - - uci commit firewall - /etc/init.d/firewall reload 2>/dev/null - - log "Firewall configured" -} - -remove_firewall() { - log "Removing firewall rules..." - - local idx=0 - while uci -q get firewall.@rule[$idx] >/dev/null 2>&1; do - local name=$(uci -q get firewall.@rule[$idx].name) - if [ "$name" = "Jellyfin-HTTP" ]; then - uci delete firewall.@rule[$idx] - uci commit firewall - /etc/init.d/firewall reload 2>/dev/null - log "Firewall rule removed" - return 0 - fi - idx=$((idx + 1)) - done -} - -# ============================================================================ -# Mesh Integration -# ============================================================================ - -register_mesh_service() { - local mesh_enabled=$(uci_get mesh.enabled) - [ "$mesh_enabled" != "1" ] && return 0 - - defaults - - if [ -x /usr/sbin/secubox-p2p ]; then - /usr/sbin/secubox-p2p register-service jellyfin "$port" 2>/dev/null - log "Registered Jellyfin with mesh network" + # Check if backend already exists + if uci -q get haproxy.jellyfin_web >/dev/null 2>&1; then + log "HAProxy backend jellyfin_web already exists, updating..." + uci set haproxy.jellyfin_web.server="media ${ip_address}:${port} weight 100 check" else - warn "secubox-p2p not found, skipping mesh registration" + log "Creating HAProxy backend..." + uci set haproxy.jellyfin_web=backend + uci set haproxy.jellyfin_web.name='jellyfin_web' + uci set haproxy.jellyfin_web.mode='http' + uci set haproxy.jellyfin_web.balance='roundrobin' + uci set haproxy.jellyfin_web.enabled='1' + uci set haproxy.jellyfin_web.server="media ${ip_address}:${port} weight 100 check" fi - local dns_enabled=$(uci -q get secubox-p2p.dns.enabled || echo "0") - if [ "$dns_enabled" = "1" ]; then - local dns_domain=$(uci -q get secubox-p2p.dns.base_domain || echo "mesh.local") - local hostname=$(echo "$domain" | cut -d'.' -f1) - log "Mesh DNS: $hostname.$dns_domain" + # Check if vhost already exists + local vhost_name=$(echo "$domain" | tr '.' '_') + if ! uci -q get haproxy.${vhost_name} >/dev/null 2>&1; then + log "Creating HAProxy vhost for $domain..." + uci set haproxy.${vhost_name}=vhost + uci set haproxy.${vhost_name}.domain="$domain" + uci set haproxy.${vhost_name}.backend='jellyfin_web' + uci set haproxy.${vhost_name}.ssl='1' + uci set haproxy.${vhost_name}.ssl_redirect='1' + uci set haproxy.${vhost_name}.acme='1' + uci set haproxy.${vhost_name}.waf_bypass='1' + uci set haproxy.${vhost_name}.enabled='1' fi -} -unregister_mesh_service() { - if [ -x /usr/sbin/secubox-p2p ]; then - /usr/sbin/secubox-p2p unregister-service jellyfin 2>/dev/null - log "Unregistered Jellyfin from mesh network" - fi + uci commit haproxy + haproxyctl generate 2>/dev/null + haproxyctl reload 2>/dev/null + + log "HAProxy configured for $domain -> ${ip_address}:${port}" } # ============================================================================ @@ -280,83 +149,94 @@ unregister_mesh_service() { cmd_install() { require_root - log "Installing Jellyfin Media Server..." + log "Installing Jellyfin LXC container..." - check_prereqs || exit 1 defaults - + ensure_dir "$LXC_PATH" + ensure_dir "$LXC_PATH/rootfs/srv/SHARE" + ensure_dir "$LXC_PATH/rootfs/config" + ensure_dir "$LXC_PATH/rootfs/cache" + ensure_dir "$LXC_PATH/rootfs/opt" ensure_dir "$data_path/config" ensure_dir "$data_path/cache" - log "Pulling Docker image..." - pull_image || exit 1 + if [ ! -f "$LXC_PATH/rootfs/jellyfin/jellyfin" ]; then + log "Jellyfin rootfs not found. Please extract from Docker image:" + echo " docker pull jellyfin/jellyfin:latest" + echo " docker create --name temp-jellyfin jellyfin/jellyfin:latest" + echo " docker export temp-jellyfin > /tmp/jellyfin.tar" + echo " tar -xf /tmp/jellyfin.tar -C $LXC_PATH/rootfs/" + echo " docker rm temp-jellyfin" + return 1 + fi + + create_lxc_config + create_startup_script + + # Symlink ffmpeg if needed + if [ -d "$LXC_PATH/rootfs/usr/lib/jellyfin-ffmpeg" ]; then + ln -sf /usr/lib/jellyfin-ffmpeg/ffmpeg "$LXC_PATH/rootfs/usr/bin/ffmpeg" 2>/dev/null + ln -sf /usr/lib/jellyfin-ffmpeg/ffprobe "$LXC_PATH/rootfs/usr/bin/ffprobe" 2>/dev/null + fi uci_set main.enabled '1' uci commit ${CONFIG} - /etc/init.d/jellyfin enable - # Integrate with HAProxy if configured configure_haproxy - # Configure firewall if WAN access requested - configure_firewall - - # Register with mesh if enabled - register_mesh_service - - log "Jellyfin installed successfully!" - echo "" - echo "Next steps:" - echo " 1. Add media: uci add_list jellyfin.media.media_path='/path/to/media'" - echo " 2. Set domain: uci set jellyfin.network.domain='media.example.com'" - echo " 3. Commit: uci commit jellyfin" - echo " 4. Start: /etc/init.d/jellyfin start" - echo " Web UI: http://:${port}" - echo "" + log "Jellyfin LXC installed successfully!" + log "Start with: jellyfinctl start" } -cmd_uninstall() { +# ============================================================================ +# Service Control +# ============================================================================ + +cmd_start() { require_root - log "Uninstalling Jellyfin..." - /etc/init.d/jellyfin stop 2>/dev/null - /etc/init.d/jellyfin disable 2>/dev/null - stop_container + if lxc_running; then + log "Jellyfin already running" + return 0 + fi - # Remove integrations - remove_haproxy - remove_firewall - unregister_mesh_service + if ! lxc_exists; then + error "Jellyfin not installed. Run 'jellyfinctl install' first" + return 1 + fi - # Remove image defaults - docker rmi "$image" 2>/dev/null + create_lxc_config - uci_set main.enabled '0' - uci commit ${CONFIG} + log "Starting Jellyfin LXC..." + lxc-start -n "$CONTAINER" -d + sleep 5 - log "Jellyfin uninstalled. Data preserved at $data_path" - log "To remove data: rm -rf $data_path" + if lxc_running; then + log "Jellyfin started at http://${ip_address}:${port}" + else + error "Failed to start Jellyfin" + return 1 + fi } -# ============================================================================ -# Check & Update -# ============================================================================ - -cmd_check() { - check_prereqs - echo "Prerequisite check completed." -} - -cmd_update() { +cmd_stop() { require_root - log "Pulling latest image..." - pull_image || exit 1 - log "Restarting service..." - /etc/init.d/jellyfin restart - # Prune old images - docker image prune -f 2>/dev/null - log "Update complete" + + if ! lxc_running; then + log "Jellyfin is not running" + return 0 + fi + + log "Stopping Jellyfin..." + lxc-stop -n "$CONTAINER" + log "Jellyfin stopped" +} + +cmd_restart() { + cmd_stop + sleep 2 + cmd_start } # ============================================================================ @@ -368,86 +248,43 @@ cmd_status() { echo "" echo "========================================" - echo " Jellyfin Media Server v$VERSION" + echo " Jellyfin Media Server v$VERSION (LXC)" echo "========================================" echo "" local enabled=$(uci_get main.enabled) echo "Configuration:" echo " Enabled: $([ "$enabled" = "1" ] && echo -e "${GREEN}Yes${NC}" || echo -e "${RED}No${NC}")" - echo " Image: $image" + echo " IP Address: $ip_address" echo " Port: $port" echo " Data: $data_path" + echo " Media: $media_path" echo " Domain: $domain" echo "" - # Docker check - if ! command -v docker >/dev/null 2>&1; then - echo -e "Docker: ${RED}Not installed${NC}" - return - fi - - # Container status echo "Container:" - local state=$(docker inspect -f '{{.State.Status}}' "$CONTAINER" 2>/dev/null) - if [ "$state" = "running" ]; then + if lxc_running; then echo -e " Status: ${GREEN}Running${NC}" - local uptime=$(docker ps --filter "name=$CONTAINER" --format '{{.Status}}' 2>/dev/null) - echo " Uptime: $uptime" - elif [ -n "$state" ]; then - echo -e " Status: ${YELLOW}$state${NC}" + local pid=$(lxc-info -n "$CONTAINER" 2>/dev/null | grep PID | awk '{print $2}') + echo " PID: $pid" + + # Health check + local health=$(curl -s "http://${ip_address}:${port}/health" 2>/dev/null) + if [ "$health" = "Healthy" ]; then + echo -e " Health: ${GREEN}Healthy${NC}" + else + echo -e " Health: ${YELLOW}Starting...${NC}" + fi + elif lxc_exists; then + echo -e " Status: ${YELLOW}Stopped${NC}" else echo -e " Status: ${RED}Not installed${NC}" fi echo "" - # Media paths - local paths=$(uci -q get ${CONFIG}.media.media_path) - if [ -n "$paths" ]; then - echo "Media Libraries:" - for p in $paths; do - if [ -d "$p" ]; then - local count=$(ls -1 "$p" 2>/dev/null | wc -l) - echo " $p ($count items)" - else - echo -e " $p ${RED}(not found)${NC}" - fi - done - echo "" - fi - - # Integration status - echo "Integrations:" - local haproxy_enabled=$(uci_get network.haproxy) - if [ "$haproxy_enabled" = "1" ]; then - local vhost_exists=$(uci show haproxy 2>/dev/null | grep "\.domain='$domain'" | head -1) - if [ -n "$vhost_exists" ]; then - echo -e " HAProxy: ${GREEN}Configured${NC} ($domain)" - else - echo -e " HAProxy: ${YELLOW}Enabled but not configured${NC}" - fi - else - echo " HAProxy: Disabled" - fi - - local mesh_enabled=$(uci_get mesh.enabled) - if [ "$mesh_enabled" = "1" ]; then - echo -e " Mesh P2P: ${GREEN}Enabled${NC}" - else - echo " Mesh P2P: Disabled" - fi - - local fw_wan=$(uci_get network.firewall_wan) - if [ "$fw_wan" = "1" ]; then - echo -e " Firewall: ${GREEN}WAN access on port $port${NC}" - else - echo " Firewall: LAN only" - fi - - # Disk usage + # Storage if [ -d "$data_path" ]; then local disk=$(du -sh "$data_path" 2>/dev/null | cut -f1) - echo "" echo "Storage:" echo " Data size: ${disk:-unknown}" fi @@ -459,84 +296,21 @@ cmd_status() { # Logs & Shell # ============================================================================ -cmd_logs() { docker logs "$@" "$CONTAINER" 2>&1; } +cmd_logs() { + defaults + if [ -f "$data_path/config/log/log_$(date +%Y%m%d).log" ]; then + tail "${@:--100}" "$data_path/config/log/log_$(date +%Y%m%d).log" + else + ls -la "$data_path/config/log/" 2>/dev/null || echo "No logs found" + fi +} cmd_shell() { - docker exec -it "$CONTAINER" /bin/bash 2>/dev/null || docker exec -it "$CONTAINER" /bin/sh -} - -# ============================================================================ -# Backup / Restore -# ============================================================================ - -cmd_backup() { - local backup_file="${1:-/tmp/jellyfin-backup-$(date +%Y%m%d-%H%M%S).tar.gz}" - - defaults - log "Creating backup..." - - tar -czf "$backup_file" \ - -C / \ - etc/config/jellyfin \ - "${data_path#/}/config" \ - 2>/dev/null - - if [ -f "$backup_file" ]; then - local size=$(ls -lh "$backup_file" | awk '{print $5}') - log "Backup created: $backup_file ($size)" - else - error "Backup failed" + if ! lxc_running; then + error "Container not running" return 1 fi -} - -cmd_restore() { - local backup_file="$1" - - if [ -z "$backup_file" ] || [ ! -f "$backup_file" ]; then - echo "Usage: jellyfinctl restore " - return 1 - fi - - require_root - log "Restoring from $backup_file..." - - /etc/init.d/jellyfin stop 2>/dev/null - tar -xzf "$backup_file" -C / - /etc/init.d/jellyfin start - - log "Restore complete" -} - -# ============================================================================ -# Service Run (procd integration) -# ============================================================================ - -cmd_service_run() { - require_root - check_prereqs || exit 1 - defaults - stop_container - - local media_mounts - media_mounts=$(build_media_mounts) - - local gpu_args - gpu_args=$(build_gpu_args) - - local docker_args="--name $CONTAINER" - docker_args="$docker_args -p ${port}:8096" - docker_args="$docker_args -v ${data_path}/config:/config" - docker_args="$docker_args -v ${data_path}/cache:/cache" - docker_args="$docker_args -e TZ=${timezone}" - - # shellcheck disable=SC2086 - exec docker run --rm $docker_args $media_mounts $gpu_args "$image" -} - -cmd_service_stop() { - require_root - stop_container + lxc-attach -n "$CONTAINER" -- /bin/bash 2>/dev/null || lxc-attach -n "$CONTAINER" -- /bin/sh } # ============================================================================ @@ -545,61 +319,42 @@ cmd_service_stop() { show_help() { cat << EOF -Jellyfin Media Server Control v$VERSION +Jellyfin Media Server Control v$VERSION (LXC) Usage: jellyfinctl [options] Commands: - install Install prerequisites, pull image, configure integrations - uninstall Stop service, remove container and integrations - check Run prerequisite checks - update Pull latest image and restart - status Show service and integration status + install Install LXC container + start Start Jellyfin + stop Stop Jellyfin + restart Restart Jellyfin + status Show status - logs [-f] [--tail N] Show container logs + logs [-f] [--tail N] Show logs shell Open shell inside container - configure-haproxy Configure/update HAProxy vhost - remove-haproxy Remove HAProxy configuration - configure-fw Configure firewall rules - remove-fw Remove firewall rules - register-mesh Register with mesh P2P network - unregister-mesh Unregister from mesh network - - backup [file] Create configuration backup - restore Restore from backup - - service-run Internal: run container via procd - service-stop Internal: stop container + configure-haproxy Configure HAProxy vhost Examples: jellyfinctl install + jellyfinctl start jellyfinctl status - jellyfinctl logs --tail 100 - jellyfinctl backup /tmp/jellyfin.tar.gz - jellyfinctl configure-haproxy + jellyfinctl logs -100 EOF } case "${1:-}" in install) shift; cmd_install "$@" ;; - uninstall) shift; cmd_uninstall "$@" ;; - check) shift; cmd_check "$@" ;; - update) shift; cmd_update "$@" ;; + start) shift; cmd_start "$@" ;; + stop) shift; cmd_stop "$@" ;; + restart) shift; cmd_restart "$@" ;; status) shift; cmd_status "$@" ;; logs) shift; cmd_logs "$@" ;; shell) shift; cmd_shell "$@" ;; configure-haproxy) configure_haproxy ;; - remove-haproxy) remove_haproxy ;; - configure-fw) configure_firewall ;; - remove-fw) remove_firewall ;; - register-mesh) register_mesh_service ;; - unregister-mesh) unregister_mesh_service ;; - backup) shift; cmd_backup "$@" ;; - restore) shift; cmd_restore "$@" ;; - service-run) shift; cmd_service_run "$@" ;; - service-stop) shift; cmd_service_stop "$@" ;; + service-run) cmd_start ;; + service-stop) cmd_stop ;; help|--help|-h|'') show_help ;; *) error "Unknown command: $1"; show_help >&2; exit 1 ;; esac diff --git a/package/secubox/secubox-app-mailserver/Makefile b/package/secubox/secubox-app-mailserver/Makefile index 988474d6..a110d421 100644 --- a/package/secubox/secubox-app-mailserver/Makefile +++ b/package/secubox/secubox-app-mailserver/Makefile @@ -1,39 +1,34 @@ include $(TOPDIR)/rules.mk PKG_NAME:=secubox-app-mailserver -PKG_VERSION:=1.0.0 -PKG_RELEASE:=2 +PKG_VERSION:=2.0.0 +PKG_RELEASE:=1 -PKG_MAINTAINER:=SecuBox Team -PKG_LICENSE:=MIT +PKG_MAINTAINER:=SecuBox +PKG_LICENSE:=AGPL-3.0 include $(INCLUDE_DIR)/package.mk define Package/$(PKG_NAME) SECTION:=secubox CATEGORY:=SecuBox - TITLE:=SecuBox Mail Server Manager - DEPENDS:=+lxc +secubox-app-dns-provider + SUBMENU:=Apps + TITLE:=SecuBox Mail Server (LXC) + DEPENDS:=+lxc +curl +openssl-util PKGARCH:=all endef define Package/$(PKG_NAME)/description - Custom mail server (Postfix + Dovecot) in LXC with mesh backup support. - Integrates with dnsctl for MX/SPF/DKIM/DMARC management. + Postfix + Dovecot mail server running in LXC container. + Supports IMAP/SMTP with SSL/TLS. +endef + +define Build/Compile endef define Package/$(PKG_NAME)/install - $(INSTALL_DIR) $(1)/etc/config - $(INSTALL_CONF) ./files/etc/config/mailserver $(1)/etc/config/ - - $(INSTALL_DIR) $(1)/etc/init.d - $(INSTALL_BIN) ./files/etc/init.d/mailserver $(1)/etc/init.d/ - $(INSTALL_DIR) $(1)/usr/sbin - $(INSTALL_BIN) ./files/usr/sbin/mailctl $(1)/usr/sbin/ - - $(INSTALL_DIR) $(1)/usr/lib/mailserver - $(INSTALL_DATA) ./files/usr/lib/mailserver/*.sh $(1)/usr/lib/mailserver/ + $(INSTALL_BIN) ./files/usr/sbin/mailserverctl $(1)/usr/sbin/ endef $(eval $(call BuildPackage,$(PKG_NAME))) diff --git a/package/secubox/secubox-app-mailserver/files/usr/sbin/mailserverctl b/package/secubox/secubox-app-mailserver/files/usr/sbin/mailserverctl new file mode 100644 index 00000000..5cf7294f --- /dev/null +++ b/package/secubox/secubox-app-mailserver/files/usr/sbin/mailserverctl @@ -0,0 +1,570 @@ +#!/bin/sh +# SecuBox Mailserver Controller +# LXC-based Postfix + Dovecot mail server + +VERSION="2.0.0" +CONFIG="mailserver" +CONTAINER="mailserver" +LXC_PATH="/srv/lxc/mailserver" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +log() { echo -e "${GREEN}[MAILSERVER]${NC} $1"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +error() { echo -e "${RED}[ERROR]${NC} $1"; } + +# ============================================================================ +# Configuration Helpers +# ============================================================================ + +uci_get() { uci -q get ${CONFIG}.$1; } +uci_set() { uci -q set ${CONFIG}.$1="$2"; } + +require_root() { + [ "$(id -u)" -eq 0 ] || { error "Root required"; exit 1; } +} + +defaults() { + ip_address="$(uci_get main.ip_address)" + [ -z "$ip_address" ] && ip_address="192.168.255.30" + domain="$(uci_get main.domain)" + [ -z "$domain" ] && domain="secubox.in" + hostname="$(uci_get main.hostname)" + [ -z "$hostname" ] && hostname="mail.$domain" +} + +ensure_dir() { [ -d "$1" ] || mkdir -p "$1"; } + +# ============================================================================ +# LXC Helpers +# ============================================================================ + +lxc_running() { + lxc-info -n "$CONTAINER" 2>/dev/null | grep -q "State:.*RUNNING" +} + +lxc_exists() { + [ -d "$LXC_PATH/rootfs" ] +} + +create_lxc_config() { + defaults + cat > "$LXC_PATH/config" << EOF +lxc.uts.name = mailserver +lxc.rootfs.path = dir:${LXC_PATH}/rootfs +lxc.net.0.type = veth +lxc.net.0.link = br-lan +lxc.net.0.flags = up +lxc.net.0.ipv4.address = ${ip_address}/24 +lxc.net.0.ipv4.gateway = 192.168.255.1 +lxc.mount.auto = proc:mixed sys:ro cgroup:mixed +lxc.cap.drop = sys_module mac_admin mac_override sys_time +lxc.seccomp.profile = +lxc.tty.max = 0 +lxc.pty.max = 256 +lxc.cgroup2.memory.max = 512000000 +lxc.init.cmd = /opt/start-mail.sh +EOF +} + +create_startup_script() { + cat > "$LXC_PATH/rootfs/opt/start-mail.sh" << 'EOF' +#!/bin/sh +# Mailserver startup script + +# Start services +/usr/sbin/rsyslogd +sleep 1 +/usr/sbin/postfix start +/usr/sbin/dovecot +echo "Mail services started" + +# Keep container running +exec tail -f /var/log/dovecot.log /var/log/messages 2>/dev/null || exec sleep infinity +EOF + chmod +x "$LXC_PATH/rootfs/opt/start-mail.sh" +} + +# ============================================================================ +# Alpine Bootstrap +# ============================================================================ + +bootstrap_alpine() { + require_root + defaults + + log "Bootstrapping Alpine Linux rootfs..." + + ensure_dir "$LXC_PATH" + + # Download apk-tools-static + cd "$LXC_PATH" + if [ ! -f sbin/apk.static ]; then + log "Downloading apk-tools-static..." + curl -L -o apk-tools-static.apk \ + "https://dl-cdn.alpinelinux.org/alpine/v3.21/main/aarch64/apk-tools-static-2.14.6-r3.apk" + tar -xzf apk-tools-static.apk sbin/apk.static + rm -f apk-tools-static.apk + fi + + # Bootstrap rootfs + log "Installing base system..." + ./sbin/apk.static -X https://dl-cdn.alpinelinux.org/alpine/v3.21/main \ + -U --allow-untrusted --root rootfs --initdb add \ + alpine-base alpine-baselayout busybox musl openrc + + # Set up repositories + mkdir -p rootfs/etc/apk + cat > rootfs/etc/apk/repositories << 'REPOEOF' +https://dl-cdn.alpinelinux.org/alpine/v3.21/main +https://dl-cdn.alpinelinux.org/alpine/v3.21/community +REPOEOF + + # Set up DNS + cat > rootfs/etc/resolv.conf << 'DNSEOF' +nameserver 8.8.8.8 +nameserver 1.1.1.1 +DNSEOF + + # Set hostname + echo "mailserver" > rootfs/etc/hostname + + log "Base system installed" +} + +install_mail_packages() { + require_root + + if ! lxc_running; then + log "Starting container for package installation..." + lxc-start -n "$CONTAINER" -d + sleep 3 + fi + + log "Installing mail packages..." + lxc-attach -n "$CONTAINER" -- apk update + lxc-attach -n "$CONTAINER" -- apk add --no-cache \ + postfix \ + dovecot \ + dovecot-lmtpd \ + ca-certificates \ + openssl \ + rsyslog + + log "Packages installed" +} + +# ============================================================================ +# Mail Configuration +# ============================================================================ + +configure_postfix() { + defaults + local rootfs="$LXC_PATH/rootfs" + + log "Configuring Postfix..." + + cat > "$rootfs/etc/postfix/main.cf" << EOF +# Basic config +myhostname = $hostname +mydomain = $domain +myorigin = \$mydomain +mydestination = \$myhostname, localhost.\$mydomain, localhost +mynetworks = 127.0.0.0/8 [::1]/128 192.168.255.0/24 + +# Virtual mailbox +virtual_mailbox_domains = $domain +virtual_mailbox_base = /var/mail +virtual_mailbox_maps = hash:/etc/postfix/vmailbox +virtual_uid_maps = static:102 +virtual_gid_maps = static:105 +virtual_transport = lmtp:unix:private/dovecot-lmtp + +# SASL auth via Dovecot +smtpd_sasl_auth_enable = yes +smtpd_sasl_type = dovecot +smtpd_sasl_path = private/auth +smtpd_sasl_security_options = noanonymous +smtpd_sasl_local_domain = \$mydomain +broken_sasl_auth_clients = yes + +# TLS +smtpd_tls_cert_file = /etc/ssl/certs/mail.crt +smtpd_tls_key_file = /etc/ssl/private/mail.key +smtpd_tls_security_level = may +smtp_tls_security_level = may + +# Restrictions +smtpd_recipient_restrictions = permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination +smtpd_sender_restrictions = permit_sasl_authenticated, permit_mynetworks + +# Limits +mailbox_size_limit = 0 +message_size_limit = 52428800 +inet_interfaces = all +inet_protocols = ipv4 +EOF + + cat > "$rootfs/etc/postfix/master.cf" << 'EOF' +smtp inet n - n - - smtpd +submission inet n - n - - smtpd + -o syslog_name=postfix/submission + -o smtpd_tls_security_level=encrypt + -o smtpd_sasl_auth_enable=yes + -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject +smtps inet n - n - - smtpd + -o syslog_name=postfix/smtps + -o smtpd_tls_wrappermode=yes + -o smtpd_sasl_auth_enable=yes + -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject +pickup unix n - n 60 1 pickup +cleanup unix n - n - 0 cleanup +qmgr unix n - n 300 1 qmgr +tlsmgr unix - - n 1000? 1 tlsmgr +rewrite unix - - n - - trivial-rewrite +bounce unix - - n - 0 bounce +defer unix - - n - 0 bounce +trace unix - - n - 0 bounce +verify unix - - n - 1 verify +flush unix n - n 1000? 0 flush +proxymap unix - - n - - proxymap +proxywrite unix - - n - 1 proxymap +smtp unix - - n - - smtp +relay unix - - n - - smtp +showq unix n - n - - showq +error unix - - n - - error +retry unix - - n - - error +discard unix - - n - - discard +local unix - n n - - local +virtual unix - n n - - virtual +lmtp unix - - n - - lmtp +anvil unix - - n - 1 anvil +scache unix - - n - 1 scache +EOF + + log "Postfix configured" +} + +configure_dovecot() { + defaults + local rootfs="$LXC_PATH/rootfs" + + log "Configuring Dovecot..." + + cat > "$rootfs/etc/dovecot/dovecot.conf" << 'EOF' +protocols = imap lmtp +listen = * +mail_location = maildir:/var/mail/%d/%n +mail_uid = 102 +mail_gid = 105 +first_valid_uid = 102 +last_valid_uid = 102 + +# Auth +auth_mechanisms = plain login +passdb { + driver = passwd-file + args = /etc/dovecot/users +} +userdb { + driver = static + args = uid=102 gid=105 home=/var/mail/%d/%n +} + +# SSL +ssl = yes +ssl_cert = /dev/null + + chmod 600 "$rootfs/etc/ssl/private/mail.key" + log "SSL certificate generated" +} + +# ============================================================================ +# User Management +# ============================================================================ + +cmd_add_user() { + local email="$1" + local password="$2" + + if [ -z "$email" ] || [ -z "$password" ]; then + echo "Usage: mailserverctl add-user " + return 1 + fi + + local user=$(echo "$email" | cut -d@ -f1) + local domain=$(echo "$email" | cut -d@ -f2) + local rootfs="$LXC_PATH/rootfs" + + # Create mailbox entry + echo "$email ${domain}/${user}/" >> "$rootfs/etc/postfix/vmailbox" + + # Generate password hash and add to users file + if lxc_running; then + local pass_hash=$(lxc-attach -n "$CONTAINER" -- doveadm pw -s SHA512-CRYPT -p "$password") + echo "${email}:${pass_hash}:102:105::/var/mail/${domain}/${user}::" >> "$rootfs/etc/dovecot/users" + else + error "Container not running. Start it first." + return 1 + fi + + # Create mail directory + mkdir -p "$rootfs/var/mail/${domain}/${user}" + lxc-attach -n "$CONTAINER" -- chown -R vmail:vmail "/var/mail/${domain}" + + # Rebuild postfix maps + lxc-attach -n "$CONTAINER" -- postmap /etc/postfix/vmailbox + + log "User $email added" +} + +# ============================================================================ +# Service Control +# ============================================================================ + +cmd_install() { + require_root + log "Installing Mailserver LXC..." + + defaults + + if lxc_exists; then + log "Container already exists" + else + bootstrap_alpine + fi + + create_lxc_config + create_startup_script + + # Start for package installation + lxc-start -n "$CONTAINER" -d + sleep 3 + + if lxc_running; then + install_mail_packages + lxc-stop -n "$CONTAINER" + fi + + configure_postfix + configure_dovecot + generate_ssl_cert + + # Create vmail user directories + local rootfs="$LXC_PATH/rootfs" + mkdir -p "$rootfs/var/mail" + mkdir -p "$rootfs/var/spool/postfix/private" + + # Create minimal interfaces file + cat > "$rootfs/etc/network/interfaces" << 'EOF' +auto lo +iface lo inet loopback +EOF + + uci_set main.enabled '1' + uci commit ${CONFIG} + + log "Mailserver installed!" + log "Add users with: mailserverctl add-user user@domain.com password" + log "Start with: mailserverctl start" +} + +cmd_start() { + require_root + + if lxc_running; then + log "Mailserver already running" + return 0 + fi + + if ! lxc_exists; then + error "Mailserver not installed. Run 'mailserverctl install' first" + return 1 + fi + + defaults + create_lxc_config + + log "Starting Mailserver LXC..." + lxc-start -n "$CONTAINER" -d + sleep 5 + + if lxc_running; then + log "Mailserver started at $ip_address" + log "IMAP: ${ip_address}:993 (SSL)" + log "SMTP: ${ip_address}:465 (SSL)" + else + error "Failed to start Mailserver" + return 1 + fi +} + +cmd_stop() { + require_root + + if ! lxc_running; then + log "Mailserver is not running" + return 0 + fi + + log "Stopping Mailserver..." + lxc-stop -n "$CONTAINER" + log "Mailserver stopped" +} + +cmd_restart() { + cmd_stop + sleep 2 + cmd_start +} + +cmd_status() { + defaults + + echo "" + echo "========================================" + echo " SecuBox Mailserver v$VERSION (LXC)" + echo "========================================" + echo "" + + echo "Configuration:" + echo " Domain: $domain" + echo " Hostname: $hostname" + echo " IP Address: $ip_address" + echo "" + + echo "Container:" + if lxc_running; then + echo -e " Status: ${GREEN}Running${NC}" + local pid=$(lxc-info -n "$CONTAINER" 2>/dev/null | grep PID | awk '{print $2}') + echo " PID: $pid" + + # Test IMAP + local imap_test=$(echo "a LOGOUT" | openssl s_client -connect ${ip_address}:993 -quiet 2>/dev/null | head -1) + if echo "$imap_test" | grep -q "OK"; then + echo -e " IMAP: ${GREEN}OK${NC}" + else + echo -e " IMAP: ${YELLOW}Not responding${NC}" + fi + elif lxc_exists; then + echo -e " Status: ${YELLOW}Stopped${NC}" + else + echo -e " Status: ${RED}Not installed${NC}" + fi + + echo "" + echo "Ports:" + echo " SMTP: 25, 465 (SSL), 587 (submission)" + echo " IMAP: 143, 993 (SSL)" + echo "" +} + +cmd_shell() { + if ! lxc_running; then + error "Container not running" + return 1 + fi + lxc-attach -n "$CONTAINER" -- /bin/sh +} + +# ============================================================================ +# Main +# ============================================================================ + +show_help() { + cat << EOF +SecuBox Mailserver Control v$VERSION (LXC) + +Usage: mailserverctl [options] + +Commands: + install Install LXC mail server + start Start mail server + stop Stop mail server + restart Restart mail server + status Show status + + add-user Add mail user + shell Open shell in container + +Examples: + mailserverctl install + mailserverctl add-user admin@example.com MyPassword123 + mailserverctl start + +EOF +} + +case "${1:-}" in + install) shift; cmd_install "$@" ;; + start) shift; cmd_start "$@" ;; + stop) shift; cmd_stop "$@" ;; + restart) shift; cmd_restart "$@" ;; + status) shift; cmd_status "$@" ;; + add-user) shift; cmd_add_user "$@" ;; + shell) shift; cmd_shell "$@" ;; + service-run) cmd_start ;; + service-stop) cmd_stop ;; + help|--help|-h|'') show_help ;; + *) error "Unknown command: $1"; show_help >&2; exit 1 ;; +esac + +exit 0 diff --git a/package/secubox/secubox-app-roundcube/Makefile b/package/secubox/secubox-app-roundcube/Makefile new file mode 100644 index 00000000..2dddb108 --- /dev/null +++ b/package/secubox/secubox-app-roundcube/Makefile @@ -0,0 +1,34 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=secubox-app-roundcube +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 + +PKG_MAINTAINER:=SecuBox +PKG_LICENSE:=AGPL-3.0 + +include $(INCLUDE_DIR)/package.mk + +define Package/secubox-app-roundcube + SECTION:=secubox + CATEGORY:=SecuBox + SUBMENU:=Apps + TITLE:=Roundcube Webmail (LXC) + DEPENDS:=+lxc +curl + PKGARCH:=all +endef + +define Package/secubox-app-roundcube/description + Roundcube Webmail running in LXC container with nginx and PHP-FPM. + Provides web-based email client for IMAP servers. +endef + +define Build/Compile +endef + +define Package/secubox-app-roundcube/install + $(INSTALL_DIR) $(1)/usr/sbin + $(INSTALL_BIN) ./files/usr/sbin/roundcubectl $(1)/usr/sbin/ +endef + +$(eval $(call BuildPackage,secubox-app-roundcube)) diff --git a/package/secubox/secubox-app-roundcube/files/usr/sbin/roundcubectl b/package/secubox/secubox-app-roundcube/files/usr/sbin/roundcubectl new file mode 100644 index 00000000..4837a215 --- /dev/null +++ b/package/secubox/secubox-app-roundcube/files/usr/sbin/roundcubectl @@ -0,0 +1,437 @@ +#!/bin/sh +# SecuBox Roundcube Webmail Controller +# LXC-based nginx + PHP-FPM + Roundcube + +VERSION="1.0.0" +CONFIG="roundcube" +CONTAINER="roundcube" +LXC_PATH="/srv/lxc/roundcube" +ROUNDCUBE_VERSION="1.6.12" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +log() { echo -e "${GREEN}[ROUNDCUBE]${NC} $1"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +error() { echo -e "${RED}[ERROR]${NC} $1"; } + +# ============================================================================ +# Configuration +# ============================================================================ + +uci_get() { uci -q get ${CONFIG}.$1; } +uci_set() { uci -q set ${CONFIG}.$1="$2"; } + +require_root() { + [ "$(id -u)" -eq 0 ] || { error "Root required"; exit 1; } +} + +defaults() { + port="$(uci_get main.port)" + [ -z "$port" ] && port="8027" + mail_host="$(uci_get main.mail_host)" + [ -z "$mail_host" ] && mail_host="192.168.255.30" + domain="$(uci_get main.domain)" + [ -z "$domain" ] && domain="webmail.gk2.secubox.in" +} + +ensure_dir() { [ -d "$1" ] || mkdir -p "$1"; } + +# ============================================================================ +# LXC Helpers +# ============================================================================ + +lxc_running() { + lxc-info -n "$CONTAINER" 2>/dev/null | grep -q "State:.*RUNNING" +} + +lxc_exists() { + [ -d "$LXC_PATH/rootfs" ] +} + +create_lxc_config() { + defaults + cat > "$LXC_PATH/config" << EOF +lxc.uts.name = roundcube +lxc.rootfs.path = dir:${LXC_PATH}/rootfs +lxc.net.0.type = none +lxc.mount.auto = proc:mixed sys:ro cgroup:mixed +lxc.cap.drop = sys_module mac_admin mac_override sys_time +lxc.seccomp.profile = +lxc.tty.max = 0 +lxc.pty.max = 256 +lxc.cgroup2.memory.max = 128000000 +lxc.init.cmd = /opt/start-roundcube.sh +EOF +} + +create_startup_script() { + cat > "$LXC_PATH/rootfs/opt/start-roundcube.sh" << 'EOF' +#!/bin/sh +set -e + +# Initialize SQLite database if not exists +if [ ! -f /var/www/roundcube/db/roundcube.db ]; then + echo "Initializing Roundcube database..." + mkdir -p /var/www/roundcube/db + cd /var/www/roundcube + sqlite3 db/roundcube.db < SQL/sqlite.initial.sql + chown nginx:nginx db/roundcube.db + chmod 640 db/roundcube.db + echo "Database initialized" +fi + +# Start PHP-FPM +echo "Starting PHP-FPM..." +php-fpm84 -D + +# Start nginx in foreground +echo "Starting nginx..." +exec nginx -g "daemon off;" +EOF + chmod +x "$LXC_PATH/rootfs/opt/start-roundcube.sh" +} + +# ============================================================================ +# Installation +# ============================================================================ + +bootstrap_alpine() { + require_root + log "Bootstrapping Alpine Linux rootfs..." + + ensure_dir "$LXC_PATH" + cd "$LXC_PATH" + + if [ ! -f sbin/apk.static ]; then + log "Downloading apk-tools-static..." + curl -L -o apk-tools-static.apk \ + "https://dl-cdn.alpinelinux.org/alpine/v3.21/main/aarch64/apk-tools-static-2.14.6-r3.apk" + tar -xzf apk-tools-static.apk sbin/apk.static + rm -f apk-tools-static.apk + fi + + log "Installing base system..." + ./sbin/apk.static -X https://dl-cdn.alpinelinux.org/alpine/v3.21/main \ + -U --allow-untrusted --root rootfs --initdb add \ + alpine-base alpine-baselayout busybox musl + + mkdir -p rootfs/etc/apk + cat > rootfs/etc/apk/repositories << 'EOF' +https://dl-cdn.alpinelinux.org/alpine/v3.21/main +https://dl-cdn.alpinelinux.org/alpine/v3.21/community +EOF + + cat > rootfs/etc/resolv.conf << 'EOF' +nameserver 8.8.8.8 +nameserver 1.1.1.1 +EOF + + log "Base system installed" +} + +install_packages() { + require_root + + if ! lxc_running; then + log "Starting container for package installation..." + lxc-start -n "$CONTAINER" -d + sleep 3 + fi + + log "Installing packages..." + lxc-attach -n "$CONTAINER" -- apk update + lxc-attach -n "$CONTAINER" -- apk add --no-cache \ + nginx \ + php84 php84-fpm php84-imap php84-mbstring php84-openssl \ + php84-session php84-pdo php84-pdo_sqlite php84-sqlite3 \ + php84-xml php84-dom php84-intl php84-zip php84-gd \ + php84-ctype php84-json php84-fileinfo php84-ldap \ + sqlite curl + + log "Packages installed" +} + +download_roundcube() { + local rootfs="$LXC_PATH/rootfs" + + log "Downloading Roundcube $ROUNDCUBE_VERSION..." + + mkdir -p "$rootfs/var/www" + curl -L -o "$rootfs/tmp/roundcube.tar.gz" \ + "https://github.com/roundcube/roundcubemail/releases/download/${ROUNDCUBE_VERSION}/roundcubemail-${ROUNDCUBE_VERSION}-complete.tar.gz" + + tar -xzf "$rootfs/tmp/roundcube.tar.gz" -C "$rootfs/var/www/" + mv "$rootfs/var/www/roundcubemail-${ROUNDCUBE_VERSION}" "$rootfs/var/www/roundcube" + rm -f "$rootfs/tmp/roundcube.tar.gz" + + log "Roundcube downloaded" +} + +configure_roundcube() { + defaults + local rootfs="$LXC_PATH/rootfs" + + log "Configuring Roundcube..." + + cat > "$rootfs/var/www/roundcube/config/config.inc.php" << EOF + ["verify_peer" => false, "verify_peer_name" => false] +]; +\$config["smtp_conn_options"] = [ + "ssl" => ["verify_peer" => false, "verify_peer_name" => false] +]; +\$config["support_url"] = ""; +\$config["product_name"] = "SecuBox Webmail"; +\$config["des_key"] = "rcmail-!24ByteDESKey*Sym"; +\$config["plugins"] = ["archive", "zipdownload"]; +\$config["skin"] = "elastic"; +\$config["language"] = "fr_FR"; +EOF + + # Configure nginx + cat > "$rootfs/etc/nginx/http.d/roundcube.conf" << EOF +server { + listen ${port}; + server_name _; + root /var/www/roundcube; + index index.php; + + location / { + try_files \$uri \$uri/ /index.php?\$args; + } + + location ~ \.php\$ { + fastcgi_pass unix:/run/php-fpm.sock; + fastcgi_index index.php; + include fastcgi.conf; + } + + location ~ /\. { + deny all; + } +} +EOF + + # Configure PHP-FPM + mkdir -p "$rootfs/etc/php84/php-fpm.d" + cat > "$rootfs/etc/php84/php-fpm.d/www.conf" << 'EOF' +[www] +user = nginx +group = nginx +listen = /run/php-fpm.sock +listen.owner = nginx +listen.group = nginx +pm = dynamic +pm.max_children = 5 +pm.start_servers = 2 +pm.min_spare_servers = 1 +pm.max_spare_servers = 3 +EOF + + # Set permissions + lxc-attach -n "$CONTAINER" -- chown -R nginx:nginx /var/www/roundcube 2>/dev/null || true + + log "Roundcube configured" +} + +configure_haproxy() { + defaults + + if ! command -v haproxyctl >/dev/null 2>&1; then + warn "haproxyctl not found" + return 0 + fi + + local vhost_name=$(echo "$domain" | tr '.' '_') + + if ! uci -q get haproxy.roundcube >/dev/null 2>&1; then + log "Creating HAProxy backend..." + uci set haproxy.roundcube=backend + uci set haproxy.roundcube.name='roundcube' + uci set haproxy.roundcube.mode='http' + uci set haproxy.roundcube.balance='roundrobin' + uci set haproxy.roundcube.enabled='1' + uci set haproxy.roundcube.option='forwardfor' + uci add_list haproxy.roundcube.http_request='set-header X-Forwarded-Proto https' + uci add_list haproxy.roundcube.http_request='set-header X-Real-IP %[src]' + + uci set haproxy.roundcube_srv=server + uci set haproxy.roundcube_srv.backend='roundcube' + uci set haproxy.roundcube_srv.name='roundcube' + uci set haproxy.roundcube_srv.address='192.168.255.1' + uci set haproxy.roundcube_srv.port="$port" + uci set haproxy.roundcube_srv.weight='100' + uci set haproxy.roundcube_srv.check='1' + uci set haproxy.roundcube_srv.enabled='1' + fi + + if ! uci -q get haproxy.${vhost_name} >/dev/null 2>&1; then + log "Creating HAProxy vhost for $domain..." + uci set haproxy.${vhost_name}=vhost + uci set haproxy.${vhost_name}.domain="$domain" + uci set haproxy.${vhost_name}.backend='roundcube' + uci set haproxy.${vhost_name}.ssl='1' + uci set haproxy.${vhost_name}.ssl_redirect='1' + uci set haproxy.${vhost_name}.acme='1' + uci set haproxy.${vhost_name}.waf_bypass='1' + uci set haproxy.${vhost_name}.enabled='1' + fi + + uci commit haproxy + haproxyctl generate 2>/dev/null + haproxyctl reload 2>/dev/null + + log "HAProxy configured for $domain" +} + +cmd_install() { + require_root + log "Installing Roundcube LXC..." + + defaults + + if ! lxc_exists; then + bootstrap_alpine + fi + + create_lxc_config + create_startup_script + + lxc-start -n "$CONTAINER" -d + sleep 3 + + if lxc_running; then + install_packages + download_roundcube + configure_roundcube + lxc-stop -n "$CONTAINER" + else + error "Failed to start container" + return 1 + fi + + configure_haproxy + + uci_set main.enabled '1' + uci commit ${CONFIG} + + log "Roundcube installed!" + log "Start with: roundcubectl start" +} + +cmd_start() { + require_root + + if lxc_running; then + log "Roundcube already running" + return 0 + fi + + if ! lxc_exists; then + error "Roundcube not installed" + return 1 + fi + + defaults + create_lxc_config + + log "Starting Roundcube LXC..." + lxc-start -n "$CONTAINER" -d + sleep 3 + + if lxc_running; then + log "Roundcube started on port $port" + else + error "Failed to start Roundcube" + return 1 + fi +} + +cmd_stop() { + require_root + + if ! lxc_running; then + log "Roundcube is not running" + return 0 + fi + + log "Stopping Roundcube..." + lxc-stop -n "$CONTAINER" + log "Roundcube stopped" +} + +cmd_restart() { + cmd_stop + sleep 2 + cmd_start +} + +cmd_status() { + defaults + + echo "" + echo "========================================" + echo " SecuBox Roundcube v$VERSION (LXC)" + echo "========================================" + echo "" + + echo "Configuration:" + echo " Port: $port" + echo " Mail Host: $mail_host" + echo " Domain: $domain" + echo "" + + echo "Container:" + if lxc_running; then + echo -e " Status: ${GREEN}Running${NC}" + local test=$(curl -sI "http://127.0.0.1:$port/" 2>/dev/null | head -1) + if echo "$test" | grep -q "200"; then + echo -e " Web: ${GREEN}OK${NC}" + else + echo -e " Web: ${YELLOW}Starting...${NC}" + fi + elif lxc_exists; then + echo -e " Status: ${YELLOW}Stopped${NC}" + else + echo -e " Status: ${RED}Not installed${NC}" + fi + echo "" +} + +show_help() { + cat << EOF +SecuBox Roundcube Webmail v$VERSION (LXC) + +Usage: roundcubectl + +Commands: + install Install LXC container + start Start Roundcube + stop Stop Roundcube + restart Restart Roundcube + status Show status + +EOF +} + +case "${1:-}" in + install) shift; cmd_install "$@" ;; + start) shift; cmd_start "$@" ;; + stop) shift; cmd_stop "$@" ;; + restart) shift; cmd_restart "$@" ;; + status) shift; cmd_status "$@" ;; + service-run) cmd_start ;; + service-stop) cmd_stop ;; + help|--help|-h|'') show_help ;; + *) error "Unknown command: $1"; show_help >&2; exit 1 ;; +esac + +exit 0