#!/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 . /lib/functions.sh log_info() { echo "[INFO] $*"; logger -t metablogizer "$*"; } log_warn() { echo "[WARN] $*" >&2; } log_error() { echo "[ERROR] $*" >&2; } 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 emancipate KISS ULTIME MODE - Full exposure workflow: 1. DNS A record (Gandi/OVH) 2. Vortex DNS mesh publication 3. HAProxy vhost with SSL 4. ACME certificate 5. Zero-downtime reload Runtime Commands: runtime Show current runtime runtime set Set runtime preference Management: status Show overall status 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) Examples: metablogizerctl create myblog blog.example.com metablogizerctl emancipate myblog # Full exposure in one command 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 # =========================================== get_next_port() { local port=$PORT_BASE while uci show uhttpd 2>/dev/null | grep -q "listen_http='0.0.0.0:$port'"; do port=$((port + 1)) done echo $port } # 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" 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; } log_info "Publishing $name to $domain" # 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 /usr/sbin/haproxyctl generate 2>/dev/null /etc/init.d/haproxy 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-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 } 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" } 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 local gitea_enabled=$(uci_get main.gitea_enabled) local gitea_url=$(uci_get main.gitea_url) local gitea_user=$(uci_get main.gitea_user) local gitea_token=$(uci_get main.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.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 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\":false,\"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}" } 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" # 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_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 } 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: SSL Certificate _emancipate_ssl "$domain" # Step 5: 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 "" } # =========================================== # 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 "$@" ;; install-nginx) shift; cmd_install_nginx "$@" ;; gitea) shift case "${1:-}" in push) shift; cmd_gitea_push "$@" ;; *) echo "Usage: metablogizerctl gitea push "; exit 1 ;; esac ;; help|--help|-h) usage ;; *) usage ;; esac