feat(secubox-core): Add secubox-landing page generator
- Add secubox-landing script to generate landing pages from HAProxy vhosts - Integrate landing command into secubox CLI - Add boot hook to regenerate landing pages on startup - Fix HAProxy multi-cert SNI using crt-list instead of directory mode - Fix backend IPs from 127.0.0.1 to 192.168.255.1 for LXC compatibility - Auto-convert localhost IPs in RPCD handler and CLI tools Landing page features: - Groups all services by zone with stats header - Shows SSL certificate status per domain - Categorizes by type: Streamlit, Blog, Admin, Media, Dev, etc. - Regenerates at boot (30s after startup) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
95f5022082
commit
ab34719f9f
@ -642,6 +642,13 @@ method_create_server() {
|
|||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Auto-convert localhost/127.0.0.1 to 192.168.255.1 (HAProxy runs in LXC)
|
||||||
|
case "$address" in
|
||||||
|
127.0.0.1|localhost|127.0.1.1)
|
||||||
|
address="192.168.255.1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
section_id="${backend}_$(echo "$name" | sed 's/[^a-zA-Z0-9]/_/g')"
|
section_id="${backend}_$(echo "$name" | sed 's/[^a-zA-Z0-9]/_/g')"
|
||||||
|
|
||||||
uci set "$UCI_CONFIG.$section_id=server"
|
uci set "$UCI_CONFIG.$section_id=server"
|
||||||
@ -686,6 +693,13 @@ method_update_server() {
|
|||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Auto-convert localhost/127.0.0.1 to 192.168.255.1 (HAProxy runs in LXC)
|
||||||
|
case "$address" in
|
||||||
|
127.0.0.1|localhost|127.0.1.1)
|
||||||
|
address="192.168.255.1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
# Check if this is an inline server (id format: backendname_servername)
|
# Check if this is an inline server (id format: backendname_servername)
|
||||||
# If so, we need to convert it to a proper server section
|
# If so, we need to convert it to a proper server section
|
||||||
if [ "$inline" = "1" ] || ! uci -q get "$UCI_CONFIG.$id" >/dev/null 2>&1; then
|
if [ "$inline" = "1" ] || ! uci -q get "$UCI_CONFIG.$id" >/dev/null 2>&1; then
|
||||||
|
|||||||
@ -82,7 +82,7 @@ use_backend backend_${name//-/_} if host_${name//-/_}_${zone//\./_}
|
|||||||
backend backend_${name//-/_}
|
backend backend_${name//-/_}
|
||||||
mode http
|
mode http
|
||||||
option httpchk GET /
|
option httpchk GET /
|
||||||
server ${name} 127.0.0.1:${backend_port} check inter 10s
|
server ${name} 192.168.255.1:${backend_port} check inter 10s
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Step 3: Register in UCI
|
# Step 3: Register in UCI
|
||||||
|
|||||||
@ -466,7 +466,7 @@ cmd_ssl_add() {
|
|||||||
uci set "haproxy.${backend_name}.mode=http"
|
uci set "haproxy.${backend_name}.mode=http"
|
||||||
uci set "haproxy.${backend_name}.balance=roundrobin"
|
uci set "haproxy.${backend_name}.balance=roundrobin"
|
||||||
uci set "haproxy.${backend_name}.enabled=1"
|
uci set "haproxy.${backend_name}.enabled=1"
|
||||||
uci add_list "haproxy.${backend_name}.server=${service} 127.0.0.1:${local_port} check"
|
uci add_list "haproxy.${backend_name}.server=${service} 192.168.255.1:${local_port} check"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if vhost already exists
|
# Check if vhost already exists
|
||||||
|
|||||||
@ -47,10 +47,8 @@ deploy() {
|
|||||||
|
|
||||||
echo "Certificate deployed: $HAPROXY_CERTS_DIR/$domain.pem"
|
echo "Certificate deployed: $HAPROXY_CERTS_DIR/$domain.pem"
|
||||||
|
|
||||||
# Reload HAProxy if running
|
# Regenerate certs.list for multi-certificate SNI support
|
||||||
if [ -x /etc/init.d/haproxy ]; then
|
/usr/sbin/haproxy-sync-certs 2>/dev/null || true
|
||||||
/etc/init.d/haproxy reload 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,8 +40,54 @@ done
|
|||||||
|
|
||||||
log_info "Certificate sync complete"
|
log_info "Certificate sync complete"
|
||||||
|
|
||||||
|
# Generate certs.list for multi-certificate SNI support
|
||||||
|
CERTS_LIST="$HAPROXY_CERTS_DIR/certs.list"
|
||||||
|
CONTAINER_CERTS_PATH="/opt/haproxy/certs"
|
||||||
|
|
||||||
|
log_info "Generating certificate list file..."
|
||||||
|
> "$CERTS_LIST"
|
||||||
|
|
||||||
|
for cert_file in "$HAPROXY_CERTS_DIR"/*.pem; do
|
||||||
|
[ -f "$cert_file" ] || continue
|
||||||
|
|
||||||
|
filename=$(basename "$cert_file")
|
||||||
|
container_cert_path="$CONTAINER_CERTS_PATH/$filename"
|
||||||
|
|
||||||
|
# Extract domain names from certificate SANs
|
||||||
|
domains=$(openssl x509 -in "$cert_file" -noout -text 2>/dev/null | \
|
||||||
|
grep -A1 "Subject Alternative Name" | \
|
||||||
|
tail -n1 | \
|
||||||
|
sed 's/DNS://g' | \
|
||||||
|
tr ',' '\n' | \
|
||||||
|
tr -d ' ')
|
||||||
|
|
||||||
|
# If no SANs, try to get CN
|
||||||
|
if [ -z "$domains" ]; then
|
||||||
|
domains=$(openssl x509 -in "$cert_file" -noout -subject 2>/dev/null | \
|
||||||
|
sed -n 's/.*CN *= *\([^,/]*\).*/\1/p')
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$domains" ]; then
|
||||||
|
echo "$domains" | while read -r domain; do
|
||||||
|
[ -n "$domain" ] || continue
|
||||||
|
echo "$container_cert_path $domain" >> "$CERTS_LIST"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
log_info "No domain found in certificate: $filename, adding without SNI filter"
|
||||||
|
echo "$container_cert_path" >> "$CERTS_LIST"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Deduplicate entries
|
||||||
|
if [ -f "$CERTS_LIST" ]; then
|
||||||
|
sort -u "$CERTS_LIST" > "$CERTS_LIST.tmp"
|
||||||
|
mv "$CERTS_LIST.tmp" "$CERTS_LIST"
|
||||||
|
count=$(wc -l < "$CERTS_LIST")
|
||||||
|
log_info "Generated certs.list with $count entries"
|
||||||
|
fi
|
||||||
|
|
||||||
# Reload HAProxy if running
|
# Reload HAProxy if running
|
||||||
if pgrep -x haproxy >/dev/null 2>&1 || lxc-info -n haproxy -s 2>/dev/null | grep -q RUNNING; then
|
if pgrep haproxy >/dev/null 2>&1 || lxc-info -n haproxy -s 2>/dev/null | grep -q RUNNING; then
|
||||||
log_info "Reloading HAProxy..."
|
log_info "Reloading HAProxy..."
|
||||||
/etc/init.d/haproxy reload 2>/dev/null || true
|
/etc/init.d/haproxy reload 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|||||||
@ -409,6 +409,69 @@ lxc_exec() {
|
|||||||
lxc-attach -n "$LXC_NAME" -- "$@"
|
lxc-attach -n "$LXC_NAME" -- "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# Certificate List Generation (for multi-cert SNI)
|
||||||
|
# ===========================================
|
||||||
|
|
||||||
|
# Generate crt-list file for multi-certificate SNI support
|
||||||
|
# This is more reliable than directory mode for multi-domain setups
|
||||||
|
generate_certs_list() {
|
||||||
|
local certs_list="$CERTS_PATH/certs.list"
|
||||||
|
local container_certs_path="/opt/haproxy/certs"
|
||||||
|
|
||||||
|
log_info "Generating certificate list file..."
|
||||||
|
|
||||||
|
# Start fresh
|
||||||
|
> "$certs_list"
|
||||||
|
|
||||||
|
# Process each .pem file
|
||||||
|
for cert_file in "$CERTS_PATH"/*.pem; do
|
||||||
|
[ -f "$cert_file" ] || continue
|
||||||
|
|
||||||
|
local filename=$(basename "$cert_file")
|
||||||
|
local container_cert_path="$container_certs_path/$filename"
|
||||||
|
|
||||||
|
# Extract domain names from certificate (CN and SANs)
|
||||||
|
local domains=$(openssl x509 -in "$cert_file" -noout -text 2>/dev/null | \
|
||||||
|
grep -A1 "Subject Alternative Name" | \
|
||||||
|
tail -n1 | \
|
||||||
|
sed 's/DNS://g' | \
|
||||||
|
tr ',' '\n' | \
|
||||||
|
tr -d ' ')
|
||||||
|
|
||||||
|
# If no SANs, try to get CN
|
||||||
|
if [ -z "$domains" ]; then
|
||||||
|
domains=$(openssl x509 -in "$cert_file" -noout -subject 2>/dev/null | \
|
||||||
|
sed -n 's/.*CN *= *\([^,/]*\).*/\1/p')
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$domains" ]; then
|
||||||
|
# For each domain, add an entry with SNI filter
|
||||||
|
# Format: /path/to/cert.pem [sni_filter]
|
||||||
|
echo "$domains" | while read -r domain; do
|
||||||
|
[ -n "$domain" ] || continue
|
||||||
|
# Handle wildcard certificates - the SNI filter should match the pattern
|
||||||
|
echo "$container_cert_path $domain" >> "$certs_list"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
# Fallback: add cert without SNI filter (will be default)
|
||||||
|
log_warn "No domain found in certificate: $filename"
|
||||||
|
echo "$container_cert_path" >> "$certs_list"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Deduplicate entries (same cert may have multiple SANs)
|
||||||
|
if [ -f "$certs_list" ]; then
|
||||||
|
sort -u "$certs_list" > "$certs_list.tmp"
|
||||||
|
mv "$certs_list.tmp" "$certs_list"
|
||||||
|
|
||||||
|
local count=$(wc -l < "$certs_list")
|
||||||
|
log_info "Generated certs.list with $count entries"
|
||||||
|
else
|
||||||
|
log_warn "No certificates found in $CERTS_PATH"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# Configuration Generation
|
# Configuration Generation
|
||||||
# ===========================================
|
# ===========================================
|
||||||
@ -420,6 +483,9 @@ generate_config() {
|
|||||||
|
|
||||||
log_info "Generating HAProxy configuration..."
|
log_info "Generating HAProxy configuration..."
|
||||||
|
|
||||||
|
# Generate certs.list for multi-certificate SNI support
|
||||||
|
generate_certs_list
|
||||||
|
|
||||||
# Global section
|
# Global section
|
||||||
cat > "$cfg_file" << EOF
|
cat > "$cfg_file" << EOF
|
||||||
# HAProxy Configuration - Generated by SecuBox
|
# HAProxy Configuration - Generated by SecuBox
|
||||||
@ -509,14 +575,28 @@ EOF
|
|||||||
# HTTPS Frontend (if certificates exist)
|
# HTTPS Frontend (if certificates exist)
|
||||||
# Use container path /opt/haproxy/certs/ (not host path)
|
# Use container path /opt/haproxy/certs/ (not host path)
|
||||||
local CONTAINER_CERTS_PATH="/opt/haproxy/certs"
|
local CONTAINER_CERTS_PATH="/opt/haproxy/certs"
|
||||||
|
local CERTS_LIST_FILE="$CERTS_PATH/certs.list"
|
||||||
if [ -d "$CERTS_PATH" ] && ls "$CERTS_PATH"/*.pem >/dev/null 2>&1; then
|
if [ -d "$CERTS_PATH" ] && ls "$CERTS_PATH"/*.pem >/dev/null 2>&1; then
|
||||||
cat << EOF
|
# Use crt-list for proper multi-certificate SNI matching
|
||||||
|
# Falls back to directory mode if certs.list doesn't exist
|
||||||
|
if [ -f "$CERTS_LIST_FILE" ] && [ -s "$CERTS_LIST_FILE" ]; then
|
||||||
|
cat << EOF
|
||||||
|
frontend https-in
|
||||||
|
bind *:$https_port,[::]:$https_port ssl crt-list $CONTAINER_CERTS_PATH/certs.list alpn h2,http/1.1
|
||||||
|
mode http
|
||||||
|
http-request set-header X-Forwarded-Proto https
|
||||||
|
http-request set-header X-Real-IP %[src]
|
||||||
|
EOF
|
||||||
|
else
|
||||||
|
# Fallback to directory mode if no certs.list
|
||||||
|
cat << EOF
|
||||||
frontend https-in
|
frontend https-in
|
||||||
bind *:$https_port,[::]:$https_port ssl crt $CONTAINER_CERTS_PATH/ alpn h2,http/1.1
|
bind *:$https_port,[::]:$https_port ssl crt $CONTAINER_CERTS_PATH/ alpn h2,http/1.1
|
||||||
mode http
|
mode http
|
||||||
http-request set-header X-Forwarded-Proto https
|
http-request set-header X-Forwarded-Proto https
|
||||||
http-request set-header X-Real-IP %[src]
|
http-request set-header X-Real-IP %[src]
|
||||||
EOF
|
EOF
|
||||||
|
fi
|
||||||
# Add vhost ACLs for HTTPS
|
# Add vhost ACLs for HTTPS
|
||||||
config_foreach _add_vhost_acl vhost "https"
|
config_foreach _add_vhost_acl vhost "https"
|
||||||
|
|
||||||
|
|||||||
@ -139,7 +139,7 @@ configure_haproxy() {
|
|||||||
uci -q add haproxy backend
|
uci -q add haproxy backend
|
||||||
uci -q set haproxy.@backend[-1].name='jellyfin_web'
|
uci -q set haproxy.@backend[-1].name='jellyfin_web'
|
||||||
uci -q set haproxy.@backend[-1].mode='http'
|
uci -q set haproxy.@backend[-1].mode='http'
|
||||||
uci -q add_list haproxy.@backend[-1].server="jellyfin 127.0.0.1:$port check"
|
uci -q add_list haproxy.@backend[-1].server="jellyfin 192.168.255.1:$port check"
|
||||||
|
|
||||||
# Add vhost
|
# Add vhost
|
||||||
uci -q add haproxy vhost
|
uci -q add haproxy vhost
|
||||||
|
|||||||
@ -268,7 +268,7 @@ configure_haproxy() {
|
|||||||
uci -q add haproxy backend
|
uci -q add haproxy backend
|
||||||
uci -q set haproxy.@backend[-1].name='jitsi_web'
|
uci -q set haproxy.@backend[-1].name='jitsi_web'
|
||||||
uci -q set haproxy.@backend[-1].mode='http'
|
uci -q set haproxy.@backend[-1].mode='http'
|
||||||
uci -q add_list haproxy.@backend[-1].server="jitsi 127.0.0.1:$web_port check"
|
uci -q add_list haproxy.@backend[-1].server="jitsi 192.168.255.1:$web_port check"
|
||||||
|
|
||||||
# Add vhost
|
# Add vhost
|
||||||
uci -q add haproxy vhost
|
uci -q add haproxy vhost
|
||||||
|
|||||||
@ -583,7 +583,7 @@ configure_haproxy() {
|
|||||||
uci -q add haproxy backend
|
uci -q add haproxy backend
|
||||||
uci -q set haproxy.@backend[-1].name='simplex_smp'
|
uci -q set haproxy.@backend[-1].name='simplex_smp'
|
||||||
uci -q set haproxy.@backend[-1].mode='tcp'
|
uci -q set haproxy.@backend[-1].mode='tcp'
|
||||||
uci -q add_list haproxy.@backend[-1].server="simplex-smp 127.0.0.1:$SMP_PORT check"
|
uci -q add_list haproxy.@backend[-1].server="simplex-smp 192.168.255.1:$SMP_PORT check"
|
||||||
|
|
||||||
uci commit haproxy
|
uci commit haproxy
|
||||||
/etc/init.d/haproxy reload 2>/dev/null
|
/etc/init.d/haproxy reload 2>/dev/null
|
||||||
|
|||||||
@ -228,7 +228,7 @@ if service_port and domain:
|
|||||||
run_cmd(f'uci set haproxy.{backend_name}=backend')
|
run_cmd(f'uci set haproxy.{backend_name}=backend')
|
||||||
run_cmd(f'uci set haproxy.{backend_name}.name="{backend_name}"')
|
run_cmd(f'uci set haproxy.{backend_name}.name="{backend_name}"')
|
||||||
run_cmd(f'uci set haproxy.{backend_name}.mode="http"')
|
run_cmd(f'uci set haproxy.{backend_name}.mode="http"')
|
||||||
run_cmd(f'uci set haproxy.{backend_name}.server="srv 127.0.0.1:{service_port} check"')
|
run_cmd(f'uci set haproxy.{backend_name}.server="srv 192.168.255.1:{service_port} check"')
|
||||||
run_cmd(f'uci set haproxy.{backend_name}.enabled="1"')
|
run_cmd(f'uci set haproxy.{backend_name}.enabled="1"')
|
||||||
|
|
||||||
run_cmd(f'uci set haproxy.{backend_name}_vhost=vhost')
|
run_cmd(f'uci set haproxy.{backend_name}_vhost=vhost')
|
||||||
|
|||||||
@ -62,4 +62,7 @@ service_triggers() {
|
|||||||
boot() {
|
boot() {
|
||||||
# Delay start on boot to allow network to initialize
|
# Delay start on boot to allow network to initialize
|
||||||
( sleep 10; start "$@"; ) &
|
( sleep 10; start "$@"; ) &
|
||||||
|
|
||||||
|
# Regenerate landing pages after network is up
|
||||||
|
( sleep 30; /usr/sbin/secubox-landing regenerate >/dev/null 2>&1; ) &
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,6 +31,7 @@ ${BOLD}Commands:${NC}
|
|||||||
${GREEN}device${NC} Device information and management
|
${GREEN}device${NC} Device information and management
|
||||||
${GREEN}net${NC} Network management
|
${GREEN}net${NC} Network management
|
||||||
${GREEN}diag${NC} Diagnostics and health checks
|
${GREEN}diag${NC} Diagnostics and health checks
|
||||||
|
${GREEN}landing${NC} Generate landing pages from vhosts
|
||||||
${GREEN}ai${NC} AI copilot (optional)
|
${GREEN}ai${NC} AI copilot (optional)
|
||||||
|
|
||||||
${BOLD}Examples:${NC}
|
${BOLD}Examples:${NC}
|
||||||
@ -392,6 +393,41 @@ EOF
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Landing page commands
|
||||||
|
cmd_landing() {
|
||||||
|
case "$1" in
|
||||||
|
generate|gen)
|
||||||
|
shift
|
||||||
|
/usr/sbin/secubox-landing generate "$@"
|
||||||
|
;;
|
||||||
|
list|ls)
|
||||||
|
/usr/sbin/secubox-landing list
|
||||||
|
;;
|
||||||
|
show)
|
||||||
|
/usr/sbin/secubox-landing show "$2"
|
||||||
|
;;
|
||||||
|
regenerate|regen)
|
||||||
|
/usr/sbin/secubox-landing regenerate
|
||||||
|
;;
|
||||||
|
help|*)
|
||||||
|
cat <<EOF
|
||||||
|
${BOLD}secubox landing${NC} - Landing page generator
|
||||||
|
|
||||||
|
${BOLD}Usage:${NC}
|
||||||
|
secubox landing list List all zones and service counts
|
||||||
|
secubox landing show <zone> Show services for a zone
|
||||||
|
secubox landing generate [zone] Generate landing page(s)
|
||||||
|
secubox landing regenerate Regenerate all landing pages
|
||||||
|
|
||||||
|
${BOLD}Examples:${NC}
|
||||||
|
secubox landing list
|
||||||
|
secubox landing generate gk2.secubox.in
|
||||||
|
secubox landing regenerate
|
||||||
|
EOF
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
# AI commands (optional)
|
# AI commands (optional)
|
||||||
cmd_ai() {
|
cmd_ai() {
|
||||||
# Check if AI is enabled
|
# Check if AI is enabled
|
||||||
@ -462,6 +498,10 @@ case "$1" in
|
|||||||
shift
|
shift
|
||||||
cmd_diag "$@"
|
cmd_diag "$@"
|
||||||
;;
|
;;
|
||||||
|
landing)
|
||||||
|
shift
|
||||||
|
cmd_landing "$@"
|
||||||
|
;;
|
||||||
ai)
|
ai)
|
||||||
shift
|
shift
|
||||||
cmd_ai "$@"
|
cmd_ai "$@"
|
||||||
|
|||||||
735
package/secubox/secubox-core/root/usr/sbin/secubox-landing
Normal file
735
package/secubox/secubox-core/root/usr/sbin/secubox-landing
Normal file
@ -0,0 +1,735 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# SecuBox Landing Page Generator
|
||||||
|
# Generates landing pages from HAProxy vhosts configuration
|
||||||
|
# Includes ALL vhosts across all zones, grouped by zone
|
||||||
|
#
|
||||||
|
|
||||||
|
VERSION="1.1.0"
|
||||||
|
SITES_DIR="/srv/metablogizer/sites"
|
||||||
|
CERTS_DIR="/etc/haproxy/certs"
|
||||||
|
|
||||||
|
# Colors
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log() { echo -e "${GREEN}[LANDING]${NC} $1"; }
|
||||||
|
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||||
|
error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||||
|
|
||||||
|
# Get public IP
|
||||||
|
get_public_ip() {
|
||||||
|
curl -s --connect-timeout 5 https://ipv4.icanhazip.com 2>/dev/null | tr -d '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get node name from UCI or hostname
|
||||||
|
get_node_name() {
|
||||||
|
local name=$(uci -q get secubox.main.node_name)
|
||||||
|
[ -z "$name" ] && name=$(uci -q get system.@system[0].hostname)
|
||||||
|
[ -z "$name" ] && name="SecuBox"
|
||||||
|
echo "$name"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get ALL vhosts (all domains)
|
||||||
|
get_all_vhosts() {
|
||||||
|
uci show haproxy 2>/dev/null | grep '\.domain=' | grep -v '_wildcard_' | grep -v 'cert_' | \
|
||||||
|
sed "s/.*domain='\\([^']*\\)'/\\1/" | sort -u
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract zone from domain (last 2 parts)
|
||||||
|
get_zone_from_domain() {
|
||||||
|
local domain="$1"
|
||||||
|
echo "$domain" | awk -F. '{if(NF>2) print $(NF-1)"."$NF; else print $0}'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract all unique zones from vhosts
|
||||||
|
get_zones() {
|
||||||
|
get_all_vhosts | while read domain; do
|
||||||
|
get_zone_from_domain "$domain"
|
||||||
|
done | sort -u
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get all vhosts for a specific zone
|
||||||
|
get_vhosts_for_zone() {
|
||||||
|
local zone="$1"
|
||||||
|
get_all_vhosts | grep "\.${zone}$"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if domain has valid certificate
|
||||||
|
has_valid_cert() {
|
||||||
|
local domain="$1"
|
||||||
|
local zone=$(get_zone_from_domain "$domain")
|
||||||
|
|
||||||
|
# Check for exact cert or wildcard
|
||||||
|
[ -f "${CERTS_DIR}/${domain}.pem" ] && return 0
|
||||||
|
[ -f "${CERTS_DIR}/_wildcard_.${zone}.pem" ] && return 0
|
||||||
|
|
||||||
|
# Check ACME certs
|
||||||
|
[ -f "/etc/acme/${domain}/fullchain.pem" ] && return 0
|
||||||
|
[ -f "/etc/acme/_wildcard_.${zone}/fullchain.pem" ] && return 0
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Detect service type from backend name
|
||||||
|
detect_service_type() {
|
||||||
|
local backend="$1"
|
||||||
|
local domain="$2"
|
||||||
|
|
||||||
|
case "$backend" in
|
||||||
|
streamlit_*|*streamlit*) echo "streamlit" ;;
|
||||||
|
metablog_*|*metablog*) echo "blog" ;;
|
||||||
|
luci|*luci*) echo "admin" ;;
|
||||||
|
*jellyfin*|*media*|*jitsi*) echo "media" ;;
|
||||||
|
*git*|*gitea*) echo "dev" ;;
|
||||||
|
*glances*|*status*|*monitor*|*uptime*) echo "infra" ;;
|
||||||
|
*)
|
||||||
|
# Try to guess from domain
|
||||||
|
case "$domain" in
|
||||||
|
console.*|admin.*|luci.*|c3box.*) echo "admin" ;;
|
||||||
|
git.*|dev.*|devel.*) echo "dev" ;;
|
||||||
|
blog.*|news.*|zine.*|gandalf.*|cyberzine.*) echo "blog" ;;
|
||||||
|
media.*|live.*|meet.*|jitsi.*) echo "media" ;;
|
||||||
|
status.*|glances.*|monitor.*) echo "infra" ;;
|
||||||
|
slides.*|sliders.*) echo "presentation" ;;
|
||||||
|
mail.*) echo "mail" ;;
|
||||||
|
*) echo "service" ;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get backend info for a vhost
|
||||||
|
get_vhost_backend() {
|
||||||
|
local domain="$1"
|
||||||
|
local backend=""
|
||||||
|
|
||||||
|
# Find the UCI section for this domain
|
||||||
|
for s in $(uci show haproxy 2>/dev/null | grep "\.domain='${domain}'" | cut -d. -f2 | cut -d= -f1); do
|
||||||
|
backend=$(uci -q get haproxy.${s}.backend)
|
||||||
|
[ -n "$backend" ] && break
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "$backend"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if vhost is enabled
|
||||||
|
is_vhost_enabled() {
|
||||||
|
local domain="$1"
|
||||||
|
|
||||||
|
for s in $(uci show haproxy 2>/dev/null | grep "\.domain='${domain}'" | cut -d. -f2 | cut -d= -f1); do
|
||||||
|
local enabled=$(uci -q get haproxy.${s}.enabled)
|
||||||
|
[ "$enabled" = "1" ] && return 0
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate HTML for a single service item
|
||||||
|
generate_service_html() {
|
||||||
|
local domain="$1"
|
||||||
|
local name="$2"
|
||||||
|
local desc="$3"
|
||||||
|
local type="$4"
|
||||||
|
local has_cert="$5"
|
||||||
|
local extra_class=""
|
||||||
|
local tags=""
|
||||||
|
|
||||||
|
case "$type" in
|
||||||
|
streamlit)
|
||||||
|
extra_class=" streamlit"
|
||||||
|
tags='<span class="tag">STREAMLIT</span>'
|
||||||
|
;;
|
||||||
|
admin)
|
||||||
|
extra_class=" highlight"
|
||||||
|
tags='<span class="tag">ADMIN</span>'
|
||||||
|
;;
|
||||||
|
blog)
|
||||||
|
tags='<span class="tag tag-blog">BLOG</span>'
|
||||||
|
;;
|
||||||
|
media)
|
||||||
|
tags='<span class="tag tag-media">MEDIA</span>'
|
||||||
|
;;
|
||||||
|
dev)
|
||||||
|
tags='<span class="tag tag-dev">DEV</span>'
|
||||||
|
;;
|
||||||
|
presentation)
|
||||||
|
tags='<span class="tag tag-slides">SLIDES</span>'
|
||||||
|
;;
|
||||||
|
mail)
|
||||||
|
tags='<span class="tag tag-mail">MAIL</span>'
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
tags=""
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Add SSL indicator
|
||||||
|
if [ "$has_cert" = "1" ]; then
|
||||||
|
tags="${tags}"'<span class="ssl-ok">🔒</span>'
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
<a href="https://${domain}" class="item${extra_class}">
|
||||||
|
<div class="item-left">
|
||||||
|
<span class="item-name">${name}</span>
|
||||||
|
<span class="item-desc">${desc}</span>
|
||||||
|
</div>
|
||||||
|
<div class="item-right">
|
||||||
|
${tags}
|
||||||
|
<span class="domain">${domain}</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate full landing page for a zone (includes ALL zones)
|
||||||
|
generate_landing_page() {
|
||||||
|
local primary_zone="$1"
|
||||||
|
local include_all="${2:-1}" # Include all zones by default
|
||||||
|
local output_dir="${SITES_DIR}/${primary_zone%%.*}"
|
||||||
|
local output_file="${output_dir}/index.html"
|
||||||
|
local node_name=$(get_node_name)
|
||||||
|
local public_ip=$(get_public_ip)
|
||||||
|
|
||||||
|
[ -z "$public_ip" ] && public_ip="N/A"
|
||||||
|
|
||||||
|
log "Generating landing page for zone: $primary_zone"
|
||||||
|
|
||||||
|
# Create output directory
|
||||||
|
mkdir -p "$output_dir"
|
||||||
|
|
||||||
|
# Collect zones to include
|
||||||
|
local zones_to_include=""
|
||||||
|
if [ "$include_all" = "1" ]; then
|
||||||
|
zones_to_include=$(get_zones)
|
||||||
|
else
|
||||||
|
zones_to_include="$primary_zone"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Count all services
|
||||||
|
local total_count=0
|
||||||
|
local cert_count=0
|
||||||
|
|
||||||
|
# Generate the HTML file header
|
||||||
|
cat > "$output_file" <<HTMLHEAD
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>${node_name} - ${primary_zone}</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #0a0a0f;
|
||||||
|
--card-bg: #12121a;
|
||||||
|
--accent: #00d4aa;
|
||||||
|
--accent2: #ff6b6b;
|
||||||
|
--accent3: #6b8aff;
|
||||||
|
--text: #e0e0e0;
|
||||||
|
--text-dim: #808090;
|
||||||
|
--border: #2a2a3a;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
.container { max-width: 1000px; margin: 0 auto; }
|
||||||
|
header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
background: linear-gradient(135deg, #1a1a2e 0%, #0a0a0f 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
background: linear-gradient(90deg, var(--accent), #00a0ff);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
margin-bottom: 0.3rem;
|
||||||
|
}
|
||||||
|
.subtitle { color: var(--text-dim); font-size: 0.95rem; }
|
||||||
|
.node-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.node-info span {
|
||||||
|
background: var(--card-bg);
|
||||||
|
padding: 0.3rem 0.8rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
.stats {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 2rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
.stat {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.stat-value {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
.stat-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
.zone-section {
|
||||||
|
margin-top: 2rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: var(--card-bg);
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.zone-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.zone-name {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.zone-count {
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
.category {
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
padding-bottom: 0.3rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
.list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
background: rgba(255,255,255,0.02);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.5rem 0.8rem;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.item:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
transform: translateX(4px);
|
||||||
|
background: rgba(0,212,170,0.05);
|
||||||
|
}
|
||||||
|
.item.streamlit:hover { border-color: var(--accent2); background: rgba(255,107,107,0.05); }
|
||||||
|
.item-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.6rem;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.item-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--accent);
|
||||||
|
min-width: 100px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.item.streamlit .item-name { color: var(--accent2); }
|
||||||
|
.item-desc {
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
.item-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
.tag {
|
||||||
|
background: var(--accent)22;
|
||||||
|
color: var(--accent);
|
||||||
|
padding: 0.1rem 0.3rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.6rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
.item.streamlit .tag {
|
||||||
|
background: var(--accent2)22;
|
||||||
|
color: var(--accent2);
|
||||||
|
}
|
||||||
|
.tag-blog { background: #9b59b622; color: #9b59b6; }
|
||||||
|
.tag-media { background: #e74c3c22; color: #e74c3c; }
|
||||||
|
.tag-dev { background: #3498db22; color: #3498db; }
|
||||||
|
.tag-slides { background: #f39c1222; color: #f39c12; }
|
||||||
|
.tag-mail { background: #1abc9c22; color: #1abc9c; }
|
||||||
|
.ssl-ok { font-size: 0.7rem; }
|
||||||
|
.domain {
|
||||||
|
font-family: monospace;
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 2rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
footer a { color: var(--accent); }
|
||||||
|
.highlight {
|
||||||
|
border-color: var(--accent) !important;
|
||||||
|
background: linear-gradient(135deg, rgba(0,212,170,0.05) 0%, transparent 100%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1>${node_name}</h1>
|
||||||
|
<p class="subtitle">Services distribués sur le noeud Vortex</p>
|
||||||
|
<div class="node-info">
|
||||||
|
<span>*.${primary_zone}</span>
|
||||||
|
<span>${public_ip}</span>
|
||||||
|
<span>Mesh: Enabled</span>
|
||||||
|
</div>
|
||||||
|
HTMLHEAD
|
||||||
|
|
||||||
|
# Process each zone
|
||||||
|
local zone_html=""
|
||||||
|
for zone in $zones_to_include; do
|
||||||
|
local vhosts=$(get_vhosts_for_zone "$zone")
|
||||||
|
local zone_count=0
|
||||||
|
|
||||||
|
# Group vhosts by category
|
||||||
|
local streamlit_items=""
|
||||||
|
local blog_items=""
|
||||||
|
local admin_items=""
|
||||||
|
local media_items=""
|
||||||
|
local dev_items=""
|
||||||
|
local infra_items=""
|
||||||
|
local presentation_items=""
|
||||||
|
local mail_items=""
|
||||||
|
local service_items=""
|
||||||
|
|
||||||
|
for domain in $vhosts; do
|
||||||
|
# Skip base zone domains
|
||||||
|
[ "$domain" = "$zone" ] && continue
|
||||||
|
|
||||||
|
# Skip disabled vhosts
|
||||||
|
is_vhost_enabled "$domain" || continue
|
||||||
|
|
||||||
|
local backend=$(get_vhost_backend "$domain")
|
||||||
|
local type=$(detect_service_type "$backend" "$domain")
|
||||||
|
|
||||||
|
# Extract name from subdomain
|
||||||
|
local subdomain=$(echo "$domain" | sed "s/\\.${zone}$//")
|
||||||
|
local name=$(echo "$subdomain" | sed 's/[-_]/ /g' | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) tolower(substr($i,2))}1')
|
||||||
|
|
||||||
|
# Generate description based on type/backend
|
||||||
|
local desc=""
|
||||||
|
case "$type" in
|
||||||
|
streamlit) desc="Streamlit App" ;;
|
||||||
|
blog) desc="Blog / Site" ;;
|
||||||
|
admin) desc="Administration" ;;
|
||||||
|
media) desc="Media / Live" ;;
|
||||||
|
dev) desc="Development" ;;
|
||||||
|
infra) desc="Monitoring" ;;
|
||||||
|
presentation) desc="Slides" ;;
|
||||||
|
mail) desc="Mail Service" ;;
|
||||||
|
*) desc="Service" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Check certificate
|
||||||
|
local has_cert=0
|
||||||
|
has_valid_cert "$domain" && has_cert=1
|
||||||
|
[ "$has_cert" = "1" ] && cert_count=$((cert_count + 1))
|
||||||
|
|
||||||
|
local item_html=$(generate_service_html "$domain" "$name" "$desc" "$type" "$has_cert")
|
||||||
|
zone_count=$((zone_count + 1))
|
||||||
|
total_count=$((total_count + 1))
|
||||||
|
|
||||||
|
case "$type" in
|
||||||
|
streamlit) streamlit_items="${streamlit_items}${item_html}\n" ;;
|
||||||
|
blog) blog_items="${blog_items}${item_html}\n" ;;
|
||||||
|
admin) admin_items="${admin_items}${item_html}\n" ;;
|
||||||
|
media) media_items="${media_items}${item_html}\n" ;;
|
||||||
|
dev) dev_items="${dev_items}${item_html}\n" ;;
|
||||||
|
infra) infra_items="${infra_items}${item_html}\n" ;;
|
||||||
|
presentation) presentation_items="${presentation_items}${item_html}\n" ;;
|
||||||
|
mail) mail_items="${mail_items}${item_html}\n" ;;
|
||||||
|
*) service_items="${service_items}${item_html}\n" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Skip empty zones
|
||||||
|
[ "$zone_count" -eq 0 ] && continue
|
||||||
|
|
||||||
|
# Build zone section
|
||||||
|
zone_html="${zone_html}
|
||||||
|
<div class=\"zone-section\">
|
||||||
|
<div class=\"zone-header\">
|
||||||
|
<span class=\"zone-name\">*.${zone}</span>
|
||||||
|
<span class=\"zone-count\">${zone_count} services</span>
|
||||||
|
</div>"
|
||||||
|
|
||||||
|
# Add categories with content
|
||||||
|
[ -n "$streamlit_items" ] && zone_html="${zone_html}
|
||||||
|
<h3 class=\"category\">Apps Streamlit</h3>
|
||||||
|
<div class=\"list\">
|
||||||
|
$(printf "%b" "$streamlit_items") </div>"
|
||||||
|
|
||||||
|
[ -n "$blog_items" ] && zone_html="${zone_html}
|
||||||
|
<h3 class=\"category\">Sites et Blogs</h3>
|
||||||
|
<div class=\"list\">
|
||||||
|
$(printf "%b" "$blog_items") </div>"
|
||||||
|
|
||||||
|
[ -n "$media_items" ] && zone_html="${zone_html}
|
||||||
|
<h3 class=\"category\">Médias et Live</h3>
|
||||||
|
<div class=\"list\">
|
||||||
|
$(printf "%b" "$media_items") </div>"
|
||||||
|
|
||||||
|
[ -n "$presentation_items" ] && zone_html="${zone_html}
|
||||||
|
<h3 class=\"category\">Présentations</h3>
|
||||||
|
<div class=\"list\">
|
||||||
|
$(printf "%b" "$presentation_items") </div>"
|
||||||
|
|
||||||
|
if [ -n "$admin_items" ] || [ -n "$infra_items" ]; then
|
||||||
|
zone_html="${zone_html}
|
||||||
|
<h3 class=\"category\">Infrastructure</h3>
|
||||||
|
<div class=\"list\">"
|
||||||
|
[ -n "$admin_items" ] && zone_html="${zone_html}
|
||||||
|
$(printf "%b" "$admin_items")"
|
||||||
|
[ -n "$infra_items" ] && zone_html="${zone_html}
|
||||||
|
$(printf "%b" "$infra_items")"
|
||||||
|
zone_html="${zone_html}
|
||||||
|
</div>"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -n "$dev_items" ] && zone_html="${zone_html}
|
||||||
|
<h3 class=\"category\">Développement</h3>
|
||||||
|
<div class=\"list\">
|
||||||
|
$(printf "%b" "$dev_items") </div>"
|
||||||
|
|
||||||
|
[ -n "$mail_items" ] && zone_html="${zone_html}
|
||||||
|
<h3 class=\"category\">Services Mail</h3>
|
||||||
|
<div class=\"list\">
|
||||||
|
$(printf "%b" "$mail_items") </div>"
|
||||||
|
|
||||||
|
[ -n "$service_items" ] && zone_html="${zone_html}
|
||||||
|
<h3 class=\"category\">Autres Services</h3>
|
||||||
|
<div class=\"list\">
|
||||||
|
$(printf "%b" "$service_items") </div>"
|
||||||
|
|
||||||
|
zone_html="${zone_html}
|
||||||
|
</div>"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Add stats to header
|
||||||
|
local zone_count=$(echo "$zones_to_include" | wc -w)
|
||||||
|
cat >> "$output_file" <<STATS
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-value">${total_count}</div>
|
||||||
|
<div class="stat-label">Services</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-value">${zone_count}</div>
|
||||||
|
<div class="stat-label">Zones</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-value">${cert_count}</div>
|
||||||
|
<div class="stat-label">SSL Certs</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
STATS
|
||||||
|
|
||||||
|
# Add zone sections
|
||||||
|
printf "%s" "$zone_html" >> "$output_file"
|
||||||
|
|
||||||
|
# Footer
|
||||||
|
cat >> "$output_file" <<HTMLFOOTER
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p>Propulsé par <a href="https://secubox.in">SecuBox</a> | Vortex DNS Mesh</p>
|
||||||
|
<p style="margin-top: 0.3rem;">Generated: $(date "+%Y-%m-%d %H:%M:%S")</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
HTMLFOOTER
|
||||||
|
|
||||||
|
log " Generated: $output_file (${total_count} services, ${zone_count} zones, ${cert_count} certs)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Commands
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
cmd_generate() {
|
||||||
|
local zone="$1"
|
||||||
|
local single_zone="${2:-}" # --single to only include that zone
|
||||||
|
|
||||||
|
if [ -n "$zone" ]; then
|
||||||
|
if [ "$single_zone" = "--single" ]; then
|
||||||
|
generate_landing_page "$zone" 0
|
||||||
|
else
|
||||||
|
generate_landing_page "$zone" 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Generate for all zones
|
||||||
|
log "Generating landing pages for all zones..."
|
||||||
|
for z in $(get_zones); do
|
||||||
|
generate_landing_page "$z" 1
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Done!"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_list() {
|
||||||
|
echo ""
|
||||||
|
echo "Available Zones:"
|
||||||
|
echo "================"
|
||||||
|
local total=0
|
||||||
|
for z in $(get_zones); do
|
||||||
|
local count=$(get_vhosts_for_zone "$z" | wc -l)
|
||||||
|
printf " %-30s %3d services\n" "$z" "$count"
|
||||||
|
total=$((total + count))
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
echo "Total: $total services across $(get_zones | wc -w) zones"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_show() {
|
||||||
|
local zone="$1"
|
||||||
|
|
||||||
|
if [ -z "$zone" ]; then
|
||||||
|
# Show all vhosts
|
||||||
|
echo ""
|
||||||
|
echo "All Vhosts:"
|
||||||
|
echo "==========="
|
||||||
|
for domain in $(get_all_vhosts); do
|
||||||
|
local backend=$(get_vhost_backend "$domain")
|
||||||
|
local type=$(detect_service_type "$backend" "$domain")
|
||||||
|
local enabled="✓"
|
||||||
|
is_vhost_enabled "$domain" || enabled="✗"
|
||||||
|
local cert="🔒"
|
||||||
|
has_valid_cert "$domain" || cert=" "
|
||||||
|
printf " %s %s %-40s [%-10s] → %s\n" "$enabled" "$cert" "$domain" "$type" "${backend:-inline}"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo "Services for zone: $zone"
|
||||||
|
echo "========================="
|
||||||
|
for domain in $(get_vhosts_for_zone "$zone"); do
|
||||||
|
local backend=$(get_vhost_backend "$domain")
|
||||||
|
local type=$(detect_service_type "$backend" "$domain")
|
||||||
|
local enabled="✓"
|
||||||
|
is_vhost_enabled "$domain" || enabled="✗"
|
||||||
|
local cert="🔒"
|
||||||
|
has_valid_cert "$domain" || cert=" "
|
||||||
|
printf " %s %s %-40s [%-10s] → %s\n" "$enabled" "$cert" "$domain" "$type" "${backend:-inline}"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_regenerate_all() {
|
||||||
|
log "Regenerating all landing pages..."
|
||||||
|
for z in $(get_zones); do
|
||||||
|
generate_landing_page "$z" 1
|
||||||
|
done
|
||||||
|
log "All landing pages regenerated!"
|
||||||
|
}
|
||||||
|
|
||||||
|
show_help() {
|
||||||
|
cat <<EOF
|
||||||
|
SecuBox Landing Page Generator v${VERSION}
|
||||||
|
|
||||||
|
Usage: secubox-landing <command> [options]
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
generate [zone] [--single]
|
||||||
|
Generate landing page(s)
|
||||||
|
If zone specified, generate for that zone
|
||||||
|
--single: only include services from that zone
|
||||||
|
Default: includes ALL zones on each landing
|
||||||
|
|
||||||
|
list List all detected zones and service counts
|
||||||
|
|
||||||
|
show [zone] Show vhosts (optionally filtered by zone)
|
||||||
|
Displays: enabled, SSL, domain, type, backend
|
||||||
|
|
||||||
|
regenerate Regenerate all landing pages
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
secubox-landing list
|
||||||
|
secubox-landing show
|
||||||
|
secubox-landing show gk2.secubox.in
|
||||||
|
secubox-landing generate gk2.secubox.in
|
||||||
|
secubox-landing generate gk2.secubox.in --single
|
||||||
|
secubox-landing regenerate
|
||||||
|
|
||||||
|
Environment:
|
||||||
|
SITES_DIR Output directory (default: /srv/metablogizer/sites)
|
||||||
|
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Main
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
generate|gen)
|
||||||
|
shift
|
||||||
|
cmd_generate "$@"
|
||||||
|
;;
|
||||||
|
list|ls)
|
||||||
|
cmd_list
|
||||||
|
;;
|
||||||
|
show)
|
||||||
|
shift
|
||||||
|
cmd_show "$@"
|
||||||
|
;;
|
||||||
|
regenerate|regen)
|
||||||
|
cmd_regenerate_all
|
||||||
|
;;
|
||||||
|
help|--help|-h|'')
|
||||||
|
show_help
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
error "Unknown command: $1"
|
||||||
|
show_help >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
exit 0
|
||||||
Loading…
Reference in New Issue
Block a user