#!/bin/sh # SecuBox MetaBlogizer - Static Site Publisher # Supports uhttpd (default) and nginx LXC runtime # Copyright (C) 2025 CyberMind.fr CONFIG="metablogizer" SITES_ROOT="/srv/metablogizer/sites" NGINX_LXC="metablogizer-nginx" LXC_PATH="/srv/lxc" PORT_BASE=8900 NFO_TEMPLATE="/usr/share/metablogizer/nfo-template.nfo" . /lib/functions.sh # Load NFO parser library if available NFO_PARSER="/usr/share/streamlit-forge/lib/nfo-parser.sh" [ -f "$NFO_PARSER" ] && . "$NFO_PARSER" log_info() { echo "[INFO] $*"; logger -t metablogizer "$*"; } log_warn() { echo "[WARN] $*" >&2; } log_error() { echo "[ERROR] $*" >&2; } log_ok() { echo "[OK] $*"; } uci_get() { uci -q get ${CONFIG}.$1; } uci_set() { uci set ${CONFIG}.$1="$2" && uci commit ${CONFIG}; } fix_permissions() { local dir="$1" [ -d "$dir" ] || return 1 chmod 755 "$dir" find "$dir" -type d -exec chmod 755 {} \; find "$dir" -type f -exec chmod 644 {} \; } usage() { cat <<'EOF' MetaBlogizer - Static Site Publisher Usage: metablogizerctl [options] Site Commands: list List all sites create [repo] Create new site delete Delete site sync Sync site from git repo publish Publish site (create HAProxy vhost) gitea push Create Gitea repo and push site content gitea init-all Initialize Gitea repos for all existing sites emancipate KISS ULTIME MODE - Full exposure workflow: 1. DNS A record (Gandi/OVH) 2. Vortex DNS mesh publication 3. HAProxy vhost with SSL 4. WAF/mitmproxy integration 5. Path ACL (secubox.in/gk2/{name}) 6. SSL certificate (or wildcard) 7. Zero-downtime reload Runtime Commands: runtime Show current runtime runtime set Set runtime preference Management: status Show overall status check-ports Check for duplicate port assignments fix-ports Auto-fix duplicate ports install-nginx Install nginx LXC container (optional) Runtime Selection: auto - Auto-detect (uhttpd preferred) uhttpd - Use uhttpd instances (lightweight) nginx - Use nginx LXC container (more features) NFO Module Manifest: nfo init Generate README.nfo manifest nfo info Show NFO summary nfo edit Edit manifest in editor nfo validate Validate NFO structure nfo sync Sync NFO fields from UCI config nfo json Export NFO as JSON Examples: metablogizerctl create myblog blog.example.com metablogizerctl emancipate myblog # Full exposure in one command metablogizerctl nfo init myblog # Generate NFO manifest metablogizerctl nfo info myblog # View manifest summary EOF } # =========================================== # Runtime Detection # =========================================== has_uhttpd() { [ -x /etc/init.d/uhttpd ]; } has_nginx_lxc() { command -v lxc-info >/dev/null 2>&1 && \ [ -d "$LXC_PATH/$NGINX_LXC/rootfs" ] } detect_runtime() { local configured=$(uci_get main.runtime) case "$configured" in uhttpd) if has_uhttpd; then echo "uhttpd" else log_error "uhttpd requested but not available" return 1 fi ;; nginx) if has_nginx_lxc; then echo "nginx" else log_error "nginx LXC requested but not installed" return 1 fi ;; auto|*) # Prefer uhttpd (lighter), fall back to nginx if has_uhttpd; then echo "uhttpd" elif has_nginx_lxc; then echo "nginx" else log_error "No runtime available" return 1 fi ;; esac } # =========================================== # Site Management # =========================================== port_in_use() { local port="$1" # Check uhttpd config uci show uhttpd 2>/dev/null | grep -q "listen_http='0.0.0.0:$port'" && return 0 # Check metablogizer config (in case uhttpd hasn't loaded the site yet) uci show ${CONFIG} 2>/dev/null | grep -q "\.port='$port'" && return 0 return 1 } get_next_port() { local port=$PORT_BASE while port_in_use "$port"; do port=$((port + 1)) done echo $port } # Check for duplicate ports across all sites cmd_check_ports() { echo "Checking for duplicate ports..." echo "" local duplicates=0 local ports_file=$(mktemp) # Collect all ports with their sites uci show ${CONFIG} 2>/dev/null | grep "\.port=" | while read line; do local section=$(echo "$line" | cut -d. -f2) local port=$(echo "$line" | cut -d= -f2 | tr -d "'") local name=$(uci_get "${section}.name" 2>/dev/null) local domain=$(uci_get "${section}.domain" 2>/dev/null) echo "$port|$name|$domain" >> "$ports_file" done # Find duplicates local seen_ports="" while IFS='|' read port name domain; do if echo "$seen_ports" | grep -q ":$port:"; then echo "DUPLICATE: Port $port used by $name ($domain)" duplicates=$((duplicates + 1)) else seen_ports="$seen_ports:$port:" fi done < "$ports_file" rm -f "$ports_file" if [ "$duplicates" -eq 0 ]; then echo "No duplicate ports found." else echo "" echo "Found $duplicates duplicate port(s)!" echo "Run 'metablogizerctl fix-ports' to auto-assign new ports." return 1 fi } # Auto-fix duplicate ports cmd_fix_ports() { echo "Scanning for duplicate ports..." local ports_file=$(mktemp) local seen_ports="" local fixed=0 # Collect all ports uci show ${CONFIG} 2>/dev/null | grep "\.port=" | while read line; do local section=$(echo "$line" | cut -d. -f2) local port=$(echo "$line" | cut -d= -f2 | tr -d "'") echo "$section|$port" >> "$ports_file" done # Process and fix duplicates while IFS='|' read section port; do if echo "$seen_ports" | grep -q ":$port:"; then local name=$(uci_get "${section}.name" 2>/dev/null) local new_port=$(get_next_port) echo "Fixing $name: port $port -> $new_port" # Update metablogizer config uci set ${CONFIG}.${section}.port="$new_port" # Update uhttpd config uci set uhttpd.metablog_${section}.listen_http="0.0.0.0:$new_port" # Update HAProxy backend if exists uci set haproxy.metablog_${section}_srv.port="$new_port" 2>/dev/null seen_ports="$seen_ports:$new_port:" fixed=$((fixed + 1)) else seen_ports="$seen_ports:$port:" fi done < "$ports_file" rm -f "$ports_file" if [ "$fixed" -gt 0 ]; then uci commit ${CONFIG} uci commit uhttpd uci commit haproxy 2>/dev/null echo "" echo "Fixed $fixed duplicate port(s)." echo "Restarting services..." /etc/init.d/uhttpd restart haproxyctl generate 2>/dev/null && haproxyctl reload 2>/dev/null else echo "No duplicate ports found." fi } # Convert site name to UCI section name (hyphens -> underscores) get_section() { echo "site_$(echo "$1" | tr '-' '_')" } site_exists() { local name="$1" local section=$(get_section "$name") uci -q get ${CONFIG}.${section} >/dev/null 2>&1 } cmd_list() { echo "MetaBlogizer Sites:" echo "===================" local runtime=$(detect_runtime 2>/dev/null) echo "Runtime: ${runtime:-none}" echo "" config_load "$CONFIG" local found=0 _print_site() { local section="$1" local name domain port enabled gitea_repo config_get name "$section" name config_get domain "$section" domain config_get port "$section" port config_get enabled "$section" enabled "0" config_get gitea_repo "$section" gitea_repo "" [ -z "$name" ] && return local status="disabled" [ "$enabled" = "1" ] && status="enabled" local dir_status="missing" [ -d "$SITES_ROOT/$name" ] && dir_status="exists" printf " %-15s %-25s :%-5s [%s] %s\n" "$name" "$domain" "$port" "$status" "$dir_status" found=1 } config_foreach _print_site site [ "$found" = "0" ] && echo " No sites configured" } cmd_create() { local name="$1" local domain="$2" local gitea_repo="$3" [ -z "$name" ] && { log_error "Site name required"; return 1; } [ -z "$domain" ] && { log_error "Domain required"; return 1; } # Sanitize name name=$(echo "$name" | tr -cd 'a-z0-9_-') if site_exists "$name"; then log_error "Site '$name' already exists" return 1 fi local runtime=$(detect_runtime) || return 1 local port=$(get_next_port) log_info "Creating site: $name ($domain) on port $port using $runtime" # Create site directory with proper permissions mkdir -p "$SITES_ROOT/$name" chmod 755 "$SITES_ROOT/$name" # Create placeholder index cat > "$SITES_ROOT/$name/index.html" < $name

$name

Site published with MetaBlogizer

https://$domain

EOF chmod 644 "$SITES_ROOT/$name/index.html" # Clone from Gitea if repo specified if [ -n "$gitea_repo" ]; then local gitea_url=$(uci_get main.gitea_url) [ -z "$gitea_url" ] && gitea_url="http://localhost:3000" log_info "Cloning from $gitea_url/$gitea_repo..." rm -rf "$SITES_ROOT/$name" git clone "$gitea_url/$gitea_repo.git" "$SITES_ROOT/$name" 2>/dev/null || { log_warn "Git clone failed, using placeholder" mkdir -p "$SITES_ROOT/$name" } fi # Always fix permissions for web serving fix_permissions "$SITES_ROOT/$name" # Configure runtime case "$runtime" in uhttpd) _create_uhttpd_site "$name" "$port" ;; nginx) _create_nginx_site "$name" ;; esac # Save site config uci set ${CONFIG}.site_${name}=site uci set ${CONFIG}.site_${name}.name="$name" uci set ${CONFIG}.site_${name}.domain="$domain" uci set ${CONFIG}.site_${name}.port="$port" uci set ${CONFIG}.site_${name}.runtime="$runtime" [ -n "$gitea_repo" ] && uci set ${CONFIG}.site_${name}.gitea_repo="$gitea_repo" uci set ${CONFIG}.site_${name}.enabled="1" uci commit ${CONFIG} log_info "Site created: $name" log_info "Directory: $SITES_ROOT/$name" log_info "Local URL: http://localhost:$port" # Auto-push to Gitea if enabled local gitea_token_cfg=$(uci_get main.gitea_token) if [ -n "$gitea_token_cfg" ]; then log_info "Auto-pushing to Gitea..." cmd_gitea_push "$name" fi # Auto-generate NFO manifest if template exists if [ -f "$NFO_TEMPLATE" ]; then log_info "Generating NFO manifest..." _nfo_init "$name" 2>/dev/null || true fi echo "" echo "Next: Run 'metablogizerctl publish $name' to create HAProxy vhost" } _create_uhttpd_site() { local name="$1" local port="$2" log_info "Creating uhttpd instance for $name on port $port" uci set uhttpd.metablog_${name}=uhttpd uci set uhttpd.metablog_${name}.listen_http="0.0.0.0:$port" uci set uhttpd.metablog_${name}.home="$SITES_ROOT/$name" uci set uhttpd.metablog_${name}.index_page="index.html" uci set uhttpd.metablog_${name}.error_page="/index.html" uci commit uhttpd /etc/init.d/uhttpd reload 2>/dev/null || /etc/init.d/uhttpd restart } _create_nginx_site() { local name="$1" if ! has_nginx_lxc; then log_error "nginx LXC not installed. Run: metablogizerctl install-nginx" return 1 fi log_info "Creating nginx config for $name" local nginx_conf="$LXC_PATH/$NGINX_LXC/rootfs/etc/nginx/sites.d" mkdir -p "$nginx_conf" cat > "$nginx_conf/metablog-$name.conf" </dev/null || true } cmd_publish() { local name="$1" [ -z "$name" ] && { log_error "Site name required"; return 1; } if ! site_exists "$name"; then log_error "Site '$name' not found" return 1 fi local domain=$(uci_get site_${name}.domain) local port=$(uci_get site_${name}.port) [ -z "$domain" ] && { log_error "Site domain not configured"; return 1; } # Validate NFO manifest if exists local site_dir="$SITES_ROOT/$name" local nfo_file="$site_dir/README.nfo" if [ -f "$nfo_file" ]; then log_info "Validating NFO manifest..." if ! _nfo_validate "$name" >/dev/null 2>&1; then log_warn "NFO manifest has validation errors" log_warn "Fix with: metablogizerctl nfo edit $name" fi fi log_info "Publishing $name to $domain" # Ensure uhttpd instance exists local existing_uhttpd=$(uci -q get uhttpd.metablogizer_${name}) if [ -z "$existing_uhttpd" ]; then log_info "Creating uhttpd instance for $name on port $port" _create_uhttpd_site "$name" "$port" fi # Create HAProxy backend local backend_name="metablog_${name}" uci set haproxy.${backend_name}=backend uci set haproxy.${backend_name}.name="$backend_name" uci set haproxy.${backend_name}.mode="http" uci set haproxy.${backend_name}.balance="roundrobin" uci set haproxy.${backend_name}.enabled="1" # Create HAProxy server local server_name="${backend_name}_srv" uci set haproxy.${server_name}=server uci set haproxy.${server_name}.backend="$backend_name" uci set haproxy.${server_name}.name="uhttpd" uci set haproxy.${server_name}.address="192.168.255.1" uci set haproxy.${server_name}.port="$port" uci set haproxy.${server_name}.weight="100" uci set haproxy.${server_name}.check="1" uci set haproxy.${server_name}.enabled="1" # Create HAProxy vhost local vhost_name=$(echo "$domain" | tr '.-' '_') uci set haproxy.${vhost_name}=vhost uci set haproxy.${vhost_name}.domain="$domain" uci set haproxy.${vhost_name}.backend="$backend_name" 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}.enabled="1" uci commit haproxy # Regenerate HAProxy config and reload container /usr/sbin/haproxyctl generate 2>/dev/null /usr/sbin/haproxyctl reload 2>/dev/null log_info "Site published!" echo "" echo "URL: https://$domain" echo "" echo "To request SSL certificate:" echo " haproxyctl cert add $domain" # Auto-push to Gitea if configured local gitea_enabled=$(uci_get gitea.enabled) if [ "$gitea_enabled" = "1" ]; then log_info "Pushing to Gitea repository..." cmd_gitea_push "$name" 2>/dev/null & fi # Auto-package for P2P distribution if [ -x /usr/sbin/secubox-content-pkg ]; then log_info "Packaging site for P2P distribution..." /usr/sbin/secubox-content-pkg site "$name" "$domain" 2>/dev/null && \ log_info "Site packaged for mesh distribution" fi # Regenerate GK2 Hub landing page if generator exists [ -x /usr/bin/gk2hub-generate ] && /usr/bin/gk2hub-generate >/dev/null 2>&1 & } cmd_delete() { local name="$1" [ -z "$name" ] && { log_error "Site name required"; return 1; } log_info "Deleting site: $name" # Remove uhttpd instance uci delete uhttpd.metablog_${name} 2>/dev/null uci commit uhttpd /etc/init.d/uhttpd reload 2>/dev/null # Remove HAProxy config local domain=$(uci_get site_${name}.domain) if [ -n "$domain" ]; then local vhost_name=$(echo "$domain" | tr '.-' '_') uci delete haproxy.${vhost_name} 2>/dev/null uci delete haproxy.metablog_${name} 2>/dev/null uci delete haproxy.metablog_${name}_srv 2>/dev/null uci commit haproxy /usr/sbin/haproxyctl generate 2>/dev/null /etc/init.d/haproxy reload 2>/dev/null fi # Remove site config uci delete ${CONFIG}.site_${name} 2>/dev/null uci commit ${CONFIG} # Optionally remove files if [ -d "$SITES_ROOT/$name" ]; then echo "Site directory: $SITES_ROOT/$name" echo "Remove manually if desired: rm -rf $SITES_ROOT/$name" fi log_info "Site deleted" # Regenerate GK2 Hub landing page if generator exists [ -x /usr/bin/gk2hub-generate ] && /usr/bin/gk2hub-generate >/dev/null 2>&1 & } cmd_sync() { local name="$1" [ -z "$name" ] && { log_error "Site name required"; return 1; } local section=$(get_section "$name") local gitea_repo=$(uci_get ${section}.gitea_repo) [ -z "$gitea_repo" ] && { log_error "No git repo configured for $name"; return 1; } local site_dir="$SITES_ROOT/$name" [ ! -d "$site_dir" ] && { log_error "Site directory not found"; return 1; } log_info "Syncing $name from git..." cd "$site_dir" if [ -d ".git" ]; then git pull origin main 2>/dev/null || git pull origin master 2>/dev/null || git pull else local gitea_url=$(uci_get main.gitea_url) [ -z "$gitea_url" ] && gitea_url="http://localhost:3000" git clone "$gitea_url/$gitea_repo.git" /tmp/metablog-sync-$$ cp -r /tmp/metablog-sync-$$/* "$site_dir/" rm -rf /tmp/metablog-sync-$$ fi # Fix permissions for web serving fix_permissions "$site_dir" log_info "Sync complete" } # Create Gitea repo via API and push local site content cmd_gitea_push() { local name="$1" [ -z "$name" ] && { log_error "Site name required"; return 1; } if ! site_exists "$name"; then log_error "Site '$name' not found" return 1 fi # Load Gitea config from dedicated gitea section local gitea_enabled=$(uci_get gitea.enabled) local gitea_url=$(uci_get gitea.url) local gitea_user=$(uci_get gitea.user) local gitea_token=$(uci_get gitea.token) [ -z "$gitea_url" ] && gitea_url="http://localhost:3000" if [ -z "$gitea_token" ]; then log_error "Gitea token not configured" log_info "Configure with:" log_info " uci set metablogizer.gitea=gitea" log_info " uci set metablogizer.gitea.enabled=1" log_info " uci set metablogizer.gitea.url='http://192.168.255.1:3001'" log_info " uci set metablogizer.gitea.user='admin'" log_info " uci set metablogizer.gitea.token='your-token'" log_info " uci commit metablogizer" return 1 fi local site_dir="$SITES_ROOT/$name" if [ ! -d "$site_dir" ]; then log_error "Site '$name' not found at $site_dir" return 1 fi local gitea_host=$(echo "$gitea_url" | sed 's|^https\?://||' | sed 's|/.*||') local gitea_proto=$(echo "$gitea_url" | grep -q '^https' && echo "https" || echo "http") local repo_name="metablog-$name" log_info "Creating Gitea repository: $repo_name" # Check if repo exists, create if not local repo_check=$(curl -s -o /dev/null -w "%{http_code}" \ -H "Authorization: token $gitea_token" \ "${gitea_url}/api/v1/repos/${gitea_user}/${repo_name}" 2>/dev/null) if [ "$repo_check" != "200" ]; then log_info "Repository doesn't exist, creating..." local create_result=$(curl -s -X POST \ -H "Authorization: token $gitea_token" \ -H "Content-Type: application/json" \ -d "{\"name\":\"${repo_name}\",\"description\":\"MetaBlogizer site: ${name}\",\"private\":true,\"auto_init\":false}" \ "${gitea_url}/api/v1/user/repos" 2>/dev/null) if ! echo "$create_result" | grep -q "\"name\":"; then log_error "Failed to create repository" log_error "Response: $create_result" return 1 fi log_info "Repository created: ${gitea_user}/${repo_name}" else log_info "Repository exists: ${gitea_user}/${repo_name}" fi # Initialize git in site directory if needed cd "$site_dir" if [ ! -d ".git" ]; then log_info "Initializing git repository..." git init git config user.name "$gitea_user" git config user.email "${gitea_user}@localhost" fi # Set remote local remote_url="${gitea_proto}://${gitea_user}:${gitea_token}@${gitea_host}/${gitea_user}/${repo_name}.git" git remote remove origin 2>/dev/null git remote add origin "$remote_url" # Add, commit and push log_info "Adding files and committing..." git add -A git commit -m "Auto-push from SecuBox MetaBlogizer at $(date -Iseconds)" 2>/dev/null || \ log_info "No changes to commit" log_info "Pushing to Gitea..." git push -u origin HEAD:main --force 2>&1 || { # Try master branch as fallback git push -u origin HEAD:master --force 2>&1 || { log_error "Failed to push to Gitea" return 1 } } # Save repo reference in UCI local section=$(get_section "$name") uci set "${CONFIG}.${section}.gitea_repo=${gitea_user}/${repo_name}" uci set "${CONFIG}.${section}.gitea_synced=$(date -Iseconds)" uci commit "$CONFIG" log_info "Push complete: ${gitea_url}/${gitea_user}/${repo_name}" } # Initialize Gitea for all existing sites cmd_gitea_init_all() { local gitea_token=$(uci_get main.gitea_token) if [ -z "$gitea_token" ]; then log_error "Gitea token not configured" log_info "Configure with:" log_info " uci set metablogizer.main.gitea_url='http://192.168.255.1:3000'" log_info " uci set metablogizer.main.gitea_user='admin'" log_info " uci set metablogizer.main.gitea_token='your-token'" log_info " uci commit metablogizer" return 1 fi log_info "Initializing Gitea repositories for all sites..." echo "" local success=0 local failed=0 # Load config and iterate over sites config_load "$CONFIG" _init_site_gitea() { local section="$1" local name config_get name "$section" name [ -z "$name" ] && return # Check if site directory exists if [ ! -d "$SITES_ROOT/$name" ]; then log_warn "[$name] Site directory not found, skipping" return fi # Check if already has a repo configured local existing_repo config_get existing_repo "$section" gitea_repo if [ -n "$existing_repo" ]; then log_info "[$name] Already linked to $existing_repo, syncing..." else log_info "[$name] Creating Gitea repository..." fi if cmd_gitea_push "$name"; then success=$((success + 1)) else failed=$((failed + 1)) fi echo "" } config_foreach _init_site_gitea site echo "========================================" echo "Gitea initialization complete" echo " Success: $success" echo " Failed: $failed" echo "========================================" } cmd_runtime() { local action="$1" local value="$2" if [ "$action" = "set" ]; then case "$value" in uhttpd|nginx|auto) uci_set main.runtime "$value" log_info "Runtime set to: $value" ;; *) log_error "Invalid runtime: $value (use uhttpd, nginx, or auto)" return 1 ;; esac else local configured=$(uci_get main.runtime) local detected=$(detect_runtime 2>/dev/null) echo "Configured: ${configured:-auto}" echo "Detected: ${detected:-none}" echo "" echo "Available:" has_uhttpd && echo " - uhttpd (installed)" || echo " - uhttpd (not available)" has_nginx_lxc && echo " - nginx LXC (installed)" || echo " - nginx LXC (not installed)" fi } cmd_status() { echo "MetaBlogizer Status" echo "===================" local enabled=$(uci_get main.enabled) local runtime=$(detect_runtime 2>/dev/null) local sites_count=$(uci show $CONFIG 2>/dev/null | grep -c "=site") echo "Enabled: $([ "$enabled" = "1" ] && echo "yes" || echo "no")" echo "Runtime: ${runtime:-none}" echo "Sites: $sites_count" echo "Sites Root: $SITES_ROOT" echo "" cmd_list } cmd_install_nginx() { log_info "Installing nginx LXC container..." command -v lxc-start >/dev/null 2>&1 || { log_error "LXC not installed. Install with: opkg install lxc lxc-common" return 1 } local rootfs="$LXC_PATH/$NGINX_LXC/rootfs" mkdir -p "$LXC_PATH/$NGINX_LXC" # Download Alpine local arch="aarch64" case "$(uname -m)" in x86_64) arch="x86_64" ;; armv7l) arch="armv7" ;; esac log_info "Downloading Alpine Linux..." wget -q -O /tmp/alpine-nginx.tar.gz \ "https://dl-cdn.alpinelinux.org/alpine/v3.19/releases/$arch/alpine-minirootfs-3.19.0-$arch.tar.gz" || { log_error "Failed to download Alpine" return 1 } mkdir -p "$rootfs" tar xzf /tmp/alpine-nginx.tar.gz -C "$rootfs" rm -f /tmp/alpine-nginx.tar.gz # Configure echo "nameserver 8.8.8.8" > "$rootfs/etc/resolv.conf" # Install nginx chroot "$rootfs" /bin/sh -c "apk update && apk add --no-cache nginx" # Create LXC config cat > "$LXC_PATH/$NGINX_LXC/config" </dev/null 2>&1; then log_warn "[DNS] dnsctl not found, skipping external DNS" return 1 fi # Get public IP local public_ip=$(curl -s --connect-timeout 5 https://ipv4.icanhazip.com 2>/dev/null | tr -d '\n') [ -z "$public_ip" ] && { log_warn "[DNS] Cannot detect public IP, skipping DNS"; return 1; } # Detect zone from domain suffix (try known zones) local zone="" local subdomain="" for z in "secubox.in" "maegia.tv" "cybermind.fr"; do if echo "$domain" | grep -q "\.${z}$"; then zone="$z" subdomain=$(echo "$domain" | sed "s/\.${z}$//") break elif [ "$domain" = "$z" ]; then zone="$z" subdomain="@" break fi done # Fallback to default zone if no match if [ -z "$zone" ]; then zone="$default_zone" subdomain=$(echo "$domain" | sed "s/\.${zone}$//") fi [ -z "$zone" ] && { log_warn "[DNS] No zone detected, skipping external DNS"; return 1; } log_info "[DNS] Registering $subdomain.$zone -> $public_ip via $provider" # Register on the published domain's zone dnsctl -z "$zone" add A "$subdomain" "$public_ip" 3600 # Also register on vortex node subdomain (e.g., bday.gk2.secubox.in) if [ -n "$vortex_wildcard" ]; then local vortex_zone=$(echo "$vortex_wildcard" | sed 's/^[^.]*\.//') local vortex_node=$(echo "$vortex_wildcard" | cut -d. -f1) local vortex_subdomain="${name}.${vortex_node}" log_info "[DNS] Registering $vortex_subdomain.$vortex_zone -> $public_ip (vortex node)" dnsctl -z "$vortex_zone" add A "$vortex_subdomain" "$public_ip" 3600 fi log_info "[DNS] Verify with: dnsctl verify $domain" } _emancipate_vortex() { local name="$1" local domain="$2" # Check if vortexctl is available if ! command -v vortexctl >/dev/null 2>&1; then log_info "[VORTEX] vortexctl not found, skipping mesh publication" return 0 fi # Check if vortex-dns is enabled local vortex_enabled=$(uci -q get vortex-dns.main.enabled) if [ "$vortex_enabled" = "1" ]; then log_info "[VORTEX] Publishing $name as $domain to mesh" vortexctl mesh publish "$name" "$domain" 2>/dev/null else log_info "[VORTEX] Vortex DNS disabled, skipping mesh publication" fi } _emancipate_haproxy() { local name="$1" local domain="$2" local port=$(uci_get site_${name}.port) log_info "[HAPROXY] Creating vhost for $domain" # Ensure uhttpd instance exists local existing_uhttpd=$(uci -q get uhttpd.metablogizer_${name}) if [ -z "$existing_uhttpd" ]; then log_info "[HAPROXY] Creating uhttpd instance for $name on port $port" _create_uhttpd_site "$name" "$port" fi # Create backend local backend_name="metablog_${name}" uci set haproxy.${backend_name}=backend uci set haproxy.${backend_name}.name="$backend_name" uci set haproxy.${backend_name}.mode="http" uci set haproxy.${backend_name}.balance="roundrobin" uci set haproxy.${backend_name}.enabled="1" # Create server local server_name="${backend_name}_srv" uci set haproxy.${server_name}=server uci set haproxy.${server_name}.backend="$backend_name" uci set haproxy.${server_name}.name="uhttpd" uci set haproxy.${server_name}.address="192.168.255.1" uci set haproxy.${server_name}.port="$port" uci set haproxy.${server_name}.weight="100" uci set haproxy.${server_name}.check="1" uci set haproxy.${server_name}.enabled="1" # Create vhost with SSL local vhost_name=$(echo "$domain" | tr '.-' '_') uci set haproxy.${vhost_name}=vhost uci set haproxy.${vhost_name}.domain="$domain" uci set haproxy.${vhost_name}.backend="$backend_name" 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}.enabled="1" uci commit haproxy # Generate HAProxy config if command -v haproxyctl >/dev/null 2>&1; then haproxyctl generate 2>/dev/null fi } _emancipate_mitmproxy() { local name="$1" local domain="$2" local port=$(uci_get site_${name}.port) local routes_file="/srv/mitmproxy-in/haproxy-routes.json" # Get the host's LAN IP (mitmproxy runs in container, can't reach 127.0.0.1 on host) local host_ip host_ip=$(uci -q get network.lan.ipaddr || echo "192.168.255.1") log_info "[WAF] Adding route: $domain -> $host_ip:$port" # Direct JSON update - most reliable method if [ -f "$routes_file" ] && command -v python3 >/dev/null 2>&1; then python3 -c " import json import sys try: with open('$routes_file', 'r') as f: routes = json.load(f) routes['$domain'] = ['$host_ip', $port] with open('$routes_file', 'w') as f: json.dump(routes, f, indent=2) print('Route added successfully') except Exception as e: print(f'Error: {e}', file=sys.stderr) sys.exit(1) " 2>&1 && { log_info "[WAF] Route registered in $routes_file" return 0 } fi # Fallback: Use centralized secubox-route if available if command -v secubox-route >/dev/null 2>&1; then if secubox-route add "$domain" "$host_ip" "$port" "metablogizer" 2>&1; then log_info "[WAF] Route registered via secubox-route" return 0 fi fi # Fallback: Sync via mitmproxyctl if command -v mitmproxyctl >/dev/null 2>&1; then log_warn "[WAF] Direct update failed, trying mitmproxyctl" mitmproxyctl sync-routes >/dev/null 2>&1 && { log_info "[WAF] Routes synced via mitmproxyctl" return 0 } fi log_error "[WAF] Failed to register route - manual intervention required" log_error "[WAF] Add manually to $routes_file" return 1 } _emancipate_path_acl() { local name="$1" local backend_name="metablog_${name}" log_info "[PATH] Adding /gk2/$name path ACL to secubox.in" # Create path ACL for secubox.in/gk2/{name} local acl_name="path_gk2_${name}" uci set haproxy.${acl_name}=acl uci set haproxy.${acl_name}.type="path_beg" uci set haproxy.${acl_name}.pattern="/gk2/${name}" uci set haproxy.${acl_name}.backend="$backend_name" uci set haproxy.${acl_name}.host="secubox.in" uci set haproxy.${acl_name}.enabled="1" uci set haproxy.${acl_name}.waf_bypass="1" uci commit haproxy log_info "[PATH] Path ACL created: secubox.in/gk2/$name -> $backend_name" } _emancipate_ssl() { local domain="$1" log_info "[SSL] Requesting certificate for $domain" # Check if haproxyctl is available if ! command -v haproxyctl >/dev/null 2>&1; then log_warn "[SSL] haproxyctl not found, skipping SSL" return 1 fi # haproxyctl cert add handles ACME webroot mode (no HAProxy restart needed) haproxyctl cert add "$domain" 2>&1 | while read line; do echo " $line" done if [ -f "/srv/haproxy/certs/$domain.pem" ]; then log_info "[SSL] Certificate obtained successfully" else log_warn "[SSL] Certificate request may still be pending" log_warn "[SSL] Check with: haproxyctl cert verify $domain" fi } _emancipate_reload() { log_info "[RELOAD] Applying HAProxy configuration" # Generate fresh config haproxyctl generate 2>/dev/null # Always restart for clean state with new vhosts/certs log_info "[RELOAD] Restarting HAProxy for clean state..." /etc/init.d/haproxy restart 2>/dev/null sleep 1 # Verify HAProxy is running if pgrep haproxy >/dev/null 2>&1; then log_info "[RELOAD] HAProxy restarted successfully" else log_warn "[RELOAD] HAProxy may not have started properly" fi # Regenerate GK2 Hub landing page if generator exists [ -x /usr/bin/gk2hub-generate ] && /usr/bin/gk2hub-generate >/dev/null 2>&1 & } cmd_emancipate() { local name="$1" [ -z "$name" ] && { log_error "Site name required"; usage; return 1; } if ! site_exists "$name"; then log_error "Site '$name' not found" log_error "Create first: metablogizerctl create $name " return 1 fi local section=$(get_section "$name") local domain=$(uci_get ${section}.domain) [ -z "$domain" ] && { log_error "Site domain not configured"; return 1; } echo "" echo "==============================================" echo " KISS ULTIME MODE: Emancipating $name" echo "==============================================" echo "" # Step 1: DNS Registration (external provider) _emancipate_dns "$name" "$domain" # Step 2: Vortex DNS (mesh registration) _emancipate_vortex "$name" "$domain" # Step 3: HAProxy vhost + backend _emancipate_haproxy "$name" "$domain" # Step 4: WAF/mitmproxy integration _emancipate_mitmproxy "$name" "$domain" # Step 5: Path ACL for secubox.in/gk2/{name} _emancipate_path_acl "$name" # Step 6: SSL Certificate (wildcard covers *.gk2.secubox.in) # Only request if not covered by wildcard case "$domain" in *.gk2.secubox.in) log_info "[SSL] Using wildcard certificate *.gk2.secubox.in" ;; *) _emancipate_ssl "$domain" ;; esac # Step 7: Reload HAProxy _emancipate_reload # Mark site as emancipated uci set ${CONFIG}.${section}.emancipated="1" uci set ${CONFIG}.${section}.emancipated_at="$(date -Iseconds)" uci commit ${CONFIG} echo "" echo "==============================================" echo " EMANCIPATION COMPLETE" echo "==============================================" echo "" echo " Site: https://$domain" echo " Status: Published and SSL-protected" echo " Mesh: $(uci -q get vortex-dns.main.enabled | grep -q 1 && echo 'Published' || echo 'Disabled')" echo "" echo " Verify:" echo " curl -v https://$domain" echo " dnsctl verify $domain" echo " haproxyctl cert verify $domain" echo "" } # =========================================== # NFO Module Manifest # =========================================== _nfo_init() { local name="$1" local site_dir="$SITES_ROOT/$name" local nfo_file="$site_dir/README.nfo" if [ -f "$nfo_file" ]; then log_warn "NFO already exists: $nfo_file" echo "Use 'metablogizerctl nfo edit $name' to modify" return 1 fi if [ ! -f "$NFO_TEMPLATE" ]; then log_error "NFO template not found: $NFO_TEMPLATE" return 1 fi # Get site info from UCI local section=$(get_section "$name") local domain=$(uci_get ${section}.domain) local short_desc="MetaBlog site: $name" # Generate NFO from template local date_now=$(date +%Y-%m-%d) sed -e "s/{{APP_ID}}/$name/g" \ -e "s/{{APP_NAME}}/$name/g" \ -e "s/{{VERSION}}/1.0.0/g" \ -e "s/{{DATE}}/$date_now/g" \ -e "s/{{SHORT_DESC}}/$short_desc/g" \ "$NFO_TEMPLATE" > "$nfo_file" log_info "NFO manifest created: $nfo_file" echo "" echo "Edit with: metablogizerctl nfo edit $name" } _nfo_info() { local name="$1" local site_dir="$SITES_ROOT/$name" local nfo_file="$site_dir/README.nfo" if [ ! -f "$nfo_file" ]; then log_error "No NFO manifest found for $name" log_info "Create one with: metablogizerctl nfo init $name" return 1 fi # Check if nfo_parser is available if ! type nfo_parse >/dev/null 2>&1; then log_warn "NFO parser not available, showing raw file" cat "$nfo_file" return 0 fi echo "" echo "╔══════════════════════════════════════════════════════════════╗" echo "║ NFO Manifest: $name" echo "╚══════════════════════════════════════════════════════════════╝" echo "" # Parse and display key sections echo "IDENTITY" echo " ID: $(nfo_get "$nfo_file" identity id)" echo " Name: $(nfo_get "$nfo_file" identity name)" echo " Version: $(nfo_get "$nfo_file" identity version)" echo " Author: $(nfo_get "$nfo_file" identity author)" echo "" echo "DESCRIPTION" echo " $(nfo_get "$nfo_file" description short)" echo "" echo "CLASSIFICATION" echo " Category: $(nfo_get "$nfo_file" tags category)" echo " Keywords: $(nfo_get "$nfo_file" tags keywords)" echo " Audience: $(nfo_get "$nfo_file" tags audience)" echo "" echo "RUNTIME" echo " Type: $(nfo_get "$nfo_file" runtime type)" echo " Framework: $(nfo_get "$nfo_file" runtime framework)" echo " Generator: $(nfo_get "$nfo_file" runtime generator)" echo "" echo "EXPOSURE" echo " Auto-expose: $(nfo_get "$nfo_file" exposure auto_expose)" echo " SSL: $(nfo_get "$nfo_file" exposure ssl)" echo " WAF: $(nfo_get "$nfo_file" exposure waf_enabled)" echo "" # Show AI context if available if nfo_has_ai_context "$nfo_file" 2>/dev/null; then echo "DYNAMICS (AI)" echo " Capabilities: $(nfo_get "$nfo_file" dynamics capabilities)" echo "" fi echo "File: $nfo_file" } _nfo_edit() { local name="$1" local site_dir="$SITES_ROOT/$name" local nfo_file="$site_dir/README.nfo" if [ ! -f "$nfo_file" ]; then log_warn "No NFO manifest found, creating one first..." _nfo_init "$name" || return 1 fi # Create backup before editing if type nfo_backup >/dev/null 2>&1; then nfo_backup "$nfo_file" else cp "$nfo_file" "${nfo_file}.bak" fi # Use available editor local editor="${EDITOR:-vi}" command -v nano >/dev/null 2>&1 && editor="nano" command -v vim >/dev/null 2>&1 && editor="vim" "$editor" "$nfo_file" # Validate after edit if type nfo_validate >/dev/null 2>&1; then if nfo_validate "$nfo_file"; then log_info "NFO manifest is valid" else log_warn "NFO manifest has validation warnings" echo "Restore backup with: cp ${nfo_file}.bak $nfo_file" fi fi } _nfo_validate() { local name="$1" local site_dir="$SITES_ROOT/$name" local nfo_file="$site_dir/README.nfo" if [ ! -f "$nfo_file" ]; then log_error "No NFO manifest found for $name" return 1 fi echo "Validating NFO manifest: $nfo_file" echo "" local errors=0 local warnings=0 # Use nfo_validate if available if type nfo_validate >/dev/null 2>&1; then nfo_validate "$nfo_file" return $? fi # Fallback: basic validation # Check required sections for section in identity description tags runtime; do if ! grep -q "^\[$section\]" "$nfo_file"; then echo "[ERROR] Missing required section: [$section]" errors=$((errors + 1)) fi done # Check required identity fields local id=$(grep "^id=" "$nfo_file" | head -1 | cut -d= -f2) local nfo_name=$(grep "^name=" "$nfo_file" | head -1 | cut -d= -f2) local version=$(grep "^version=" "$nfo_file" | head -1 | cut -d= -f2) [ -z "$id" ] && { echo "[ERROR] Missing identity.id"; errors=$((errors + 1)); } [ -z "$nfo_name" ] && { echo "[ERROR] Missing identity.name"; errors=$((errors + 1)); } [ -z "$version" ] && { echo "[ERROR] Missing identity.version"; errors=$((errors + 1)); } # Check recommended fields local category=$(grep "^category=" "$nfo_file" | head -1 | cut -d= -f2) local short=$(grep "^short=" "$nfo_file" | head -1 | cut -d= -f2) [ -z "$category" ] && { echo "[WARN] Missing tags.category"; warnings=$((warnings + 1)); } [ -z "$short" ] && { echo "[WARN] Missing description.short"; warnings=$((warnings + 1)); } echo "" if [ $errors -eq 0 ] && [ $warnings -eq 0 ]; then echo "✓ NFO manifest is valid" return 0 elif [ $errors -eq 0 ]; then echo "⚠ NFO valid with $warnings warning(s)" return 0 else echo "✗ NFO invalid: $errors error(s), $warnings warning(s)" return 1 fi } _nfo_sync() { local name="$1" local site_dir="$SITES_ROOT/$name" local nfo_file="$site_dir/README.nfo" if [ ! -f "$nfo_file" ]; then log_error "No NFO manifest found for $name" log_info "Create one first: metablogizerctl nfo init $name" return 1 fi # Check if nfo_sync_from_uci is available if ! type nfo_sync_from_uci >/dev/null 2>&1; then log_error "NFO parser with sync support not available" return 1 fi log_info "Syncing NFO from UCI config..." # Get site UCI section local section=$(get_section "$name") # Build UCI prefix for sync local uci_prefix="${CONFIG}.${section}" # Sync using parser function nfo_sync_from_uci "$nfo_file" "$uci_prefix" log_info "NFO synced from UCI" echo "View with: metablogizerctl nfo info $name" } _nfo_json() { local name="$1" local site_dir="$SITES_ROOT/$name" local nfo_file="$site_dir/README.nfo" if [ ! -f "$nfo_file" ]; then log_error "No NFO manifest found for $name" return 1 fi # Use nfo_to_json if available if type nfo_to_json >/dev/null 2>&1; then nfo_to_json "$nfo_file" return $? fi # Fallback: basic JSON output log_warn "NFO parser not available, using basic JSON export" local id=$(grep "^id=" "$nfo_file" | head -1 | cut -d= -f2) local nfo_name=$(grep "^name=" "$nfo_file" | head -1 | cut -d= -f2) local version=$(grep "^version=" "$nfo_file" | head -1 | cut -d= -f2) local category=$(grep "^category=" "$nfo_file" | head -1 | cut -d= -f2) local short=$(grep "^short=" "$nfo_file" | head -1 | cut -d= -f2) cat < "$tmp_success" echo 0 > "$tmp_skipped" echo 0 > "$tmp_failed" config_load "$CONFIG" _generate_nfo_site() { local section="$1" local site_name config_get site_name "$section" name [ -z "$site_name" ] && return local site_dir="$SITES_ROOT/$site_name" [ ! -d "$site_dir" ] && { log_warn "[$site_name] Site directory not found, skipping" return } local nfo_file="$site_dir/README.nfo" if [ -f "$nfo_file" ]; then log_info "[$site_name] NFO already exists, skipping" echo $(($(cat "$tmp_skipped") + 1)) > "$tmp_skipped" return fi log_info "[$site_name] Generating README.nfo..." if _nfo_init "$site_name"; then log_ok " Created: $nfo_file" echo $(($(cat "$tmp_success") + 1)) > "$tmp_success" else log_error " Failed to create NFO" echo $(($(cat "$tmp_failed") + 1)) > "$tmp_failed" fi } config_foreach _generate_nfo_site site local success=$(cat "$tmp_success") local skipped=$(cat "$tmp_skipped") local failed=$(cat "$tmp_failed") rm -f "$tmp_success" "$tmp_skipped" "$tmp_failed" echo "" echo "========================================" echo "NFO generation complete" echo " Created: $success" echo " Skipped: $skipped (already exist)" echo " Failed: $failed" echo "========================================" ;; info) [ -z "$name" ] && { log_error "Site name required"; return 1; } site_exists "$name" || { log_error "Site '$name' not found"; return 1; } _nfo_info "$name" ;; edit) [ -z "$name" ] && { log_error "Site name required"; return 1; } site_exists "$name" || { log_error "Site '$name' not found"; return 1; } _nfo_edit "$name" ;; validate) [ -z "$name" ] && { log_error "Site name required"; return 1; } site_exists "$name" || { log_error "Site '$name' not found"; return 1; } _nfo_validate "$name" ;; sync) [ -z "$name" ] && { log_error "Site name required"; return 1; } site_exists "$name" || { log_error "Site '$name' not found"; return 1; } _nfo_sync "$name" ;; json) [ -z "$name" ] && { log_error "Site name required"; return 1; } site_exists "$name" || { log_error "Site '$name' not found"; return 1; } _nfo_json "$name" ;; *) echo "Usage: metablogizerctl nfo [name]" return 1 ;; esac } # =========================================== # Main # =========================================== case "${1:-}" in list) shift; cmd_list "$@" ;; create) shift; cmd_create "$@" ;; delete) shift; cmd_delete "$@" ;; sync) shift; cmd_sync "$@" ;; publish) shift; cmd_publish "$@" ;; emancipate) shift; cmd_emancipate "$@" ;; runtime) shift; cmd_runtime "$@" ;; status) shift; cmd_status "$@" ;; check-ports) shift; cmd_check_ports "$@" ;; fix-ports) shift; cmd_fix_ports "$@" ;; install-nginx) shift; cmd_install_nginx "$@" ;; gitea) shift case "${1:-}" in push) shift; cmd_gitea_push "$@" ;; init-all) shift; cmd_gitea_init_all "$@" ;; *) echo "Usage: metablogizerctl gitea {push|init-all} [name]"; exit 1 ;; esac ;; nfo) shift cmd_nfo "$@" ;; help|--help|-h) usage ;; *) usage ;; esac