#!/bin/sh # SPDX-License-Identifier: GPL-2.0-only # RPCD backend for MetaBlogizer Static Site Publisher # Copyright (C) 2025 CyberMind.fr . /lib/functions.sh . /usr/share/libubox/jshn.sh UCI_CONFIG="metablogizer" SITES_ROOT="/srv/metablogizer/sites" NGINX_CONTAINER="nginx" PORT_BASE=8900 # Helper: Get UCI value with default get_uci() { local section="$1" local option="$2" local default="$3" local value value=$(uci -q get "$UCI_CONFIG.$section.$option") echo "${value:-$default}" } # Runtime detection (uhttpd preferred, nginx fallback) detect_runtime() { local configured=$(get_uci main runtime "auto") case "$configured" in uhttpd) [ -x /etc/init.d/uhttpd ] && echo "uhttpd" || echo "none" ;; nginx) lxc-info -n "$NGINX_CONTAINER" >/dev/null 2>&1 && echo "nginx" || echo "none" ;; auto|*) [ -x /etc/init.d/uhttpd ] && echo "uhttpd" || \ (lxc-info -n "$NGINX_CONTAINER" >/dev/null 2>&1 && echo "nginx" || echo "none") ;; esac } # Get next available port for uhttpd 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 } # Fix permissions for web serving (755 for dirs, 644 for files) 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 {} \; } # Status method - get overall status and list all sites method_status() { local enabled runtime detected_runtime nginx_running site_count enabled=$(get_uci main enabled 0) runtime=$(get_uci main runtime "auto") SITES_ROOT=$(get_uci main sites_root "$SITES_ROOT") NGINX_CONTAINER=$(get_uci main nginx_container "$NGINX_CONTAINER") # Detect runtime detected_runtime=$(detect_runtime) # Check nginx container if using nginx nginx_running="0" if lxc-info -n "$NGINX_CONTAINER" -s 2>/dev/null | grep -q "RUNNING"; then nginx_running="1" fi # Count sites site_count=0 config_load "$UCI_CONFIG" config_foreach _count_site site site_count="$_site_count" json_init json_add_boolean "enabled" "$enabled" json_add_string "runtime" "$runtime" json_add_string "detected_runtime" "$detected_runtime" json_add_boolean "nginx_running" "$nginx_running" json_add_int "site_count" "$site_count" json_add_string "sites_root" "$SITES_ROOT" json_add_string "nginx_container" "$NGINX_CONTAINER" json_dump } _site_count=0 _count_site() { _site_count=$((_site_count + 1)) } # List all sites with their status method_list_sites() { SITES_ROOT=$(get_uci main sites_root "$SITES_ROOT") json_init json_add_array "sites" config_load "$UCI_CONFIG" config_foreach _add_site site json_close_array json_dump } _add_site() { local section="$1" local name domain gitea_repo ssl enabled description local has_content last_sync config_get name "$section" name "" config_get domain "$section" domain "" config_get gitea_repo "$section" gitea_repo "" config_get ssl "$section" ssl "1" config_get enabled "$section" enabled "1" config_get description "$section" description "" # Check if site has content has_content="0" if [ -d "$SITES_ROOT/$name" ] && [ -f "$SITES_ROOT/$name/index.html" ]; then has_content="1" fi # Get last sync time last_sync="" if [ -d "$SITES_ROOT/$name/.git" ]; then last_sync=$(cd "$SITES_ROOT/$name" && git log -1 --format="%ci" 2>/dev/null || echo "") fi json_add_object json_add_string "id" "$section" json_add_string "name" "$name" json_add_string "domain" "$domain" json_add_string "gitea_repo" "$gitea_repo" json_add_string "description" "$description" json_add_boolean "ssl" "$ssl" json_add_boolean "enabled" "$enabled" json_add_boolean "has_content" "$has_content" json_add_string "last_sync" "$last_sync" json_add_string "url" "https://$domain" json_close_object } # Create a new site with auto-vhost method_create_site() { local name domain gitea_repo ssl description local section_id gitea_url read -r input json_load "$input" json_get_var name name json_get_var domain domain json_get_var gitea_repo gitea_repo json_get_var ssl ssl "1" json_get_var description description "" # Validate required fields if [ -z "$name" ] || [ -z "$domain" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Name and domain are required" json_dump return fi # Sanitize name for section ID section_id="site_$(echo "$name" | sed 's/[^a-zA-Z0-9]/_/g')" # Check if site already exists if uci -q get "$UCI_CONFIG.$section_id" >/dev/null 2>&1; then json_init json_add_boolean "success" 0 json_add_string "error" "Site with this name already exists" json_dump return fi SITES_ROOT=$(get_uci main sites_root "$SITES_ROOT") NGINX_CONTAINER=$(get_uci main nginx_container "$NGINX_CONTAINER") # 1. Create UCI site config uci set "$UCI_CONFIG.$section_id=site" uci set "$UCI_CONFIG.$section_id.name=$name" uci set "$UCI_CONFIG.$section_id.domain=$domain" uci set "$UCI_CONFIG.$section_id.ssl=$ssl" uci set "$UCI_CONFIG.$section_id.enabled=1" [ -n "$gitea_repo" ] && uci set "$UCI_CONFIG.$section_id.gitea_repo=$gitea_repo" [ -n "$description" ] && uci set "$UCI_CONFIG.$section_id.description=$description" # 2. Create site directory mkdir -p "$SITES_ROOT/$name" # 3. Clone from Gitea if repo specified if [ -n "$gitea_repo" ]; then gitea_url=$(uci -q get gitea.main.url || echo "http://192.168.255.1:3000") git clone "$gitea_url/$gitea_repo.git" "$SITES_ROOT/$name" 2>/dev/null || true fi # 4. Fix permissions for web serving fix_permissions "$SITES_ROOT/$name" # 5. Create default index.html with OG tags if no content if [ ! -f "$SITES_ROOT/$name/index.html" ]; then cat > "$SITES_ROOT/$name/index.html" < $name

$name

Site published with MetaBlogizer

EOF fi # 6. Detect runtime and configure accordingly local current_runtime=$(detect_runtime) local port="" local server_address="192.168.255.1" local server_port="80" if [ "$current_runtime" = "uhttpd" ]; then # Create uhttpd instance port=$(get_next_port) uci set "uhttpd.metablog_${section_id}=uhttpd" uci set "uhttpd.metablog_${section_id}.listen_http=0.0.0.0:$port" uci set "uhttpd.metablog_${section_id}.home=$SITES_ROOT/$name" uci set "uhttpd.metablog_${section_id}.index_page=index.html" uci set "uhttpd.metablog_${section_id}.error_page=/index.html" uci commit uhttpd /etc/init.d/uhttpd reload 2>/dev/null server_port="$port" else # Configure nginx location in container _configure_nginx "$name" local nginx_ip nginx_ip=$(lxc-info -n "$NGINX_CONTAINER" -iH 2>/dev/null | head -1) [ -n "$nginx_ip" ] && server_address="$nginx_ip" fi # Save port to site config [ -n "$port" ] && uci set "$UCI_CONFIG.$section_id.port=$port" uci set "$UCI_CONFIG.$section_id.runtime=$current_runtime" # 7. Create HAProxy backend local backend_name="metablog_$(echo "$name" | sed 's/[^a-zA-Z0-9]/_/g')" 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=srv" uci set "haproxy.$server_name.address=$server_address" uci set "haproxy.$server_name.port=$server_port" uci set "haproxy.$server_name.weight=100" uci set "haproxy.$server_name.check=1" uci set "haproxy.$server_name.enabled=1" # 8. Create HAProxy vhost local vhost_name=$(echo "$domain" | sed 's/[^a-zA-Z0-9]/_/g') local acme_val="0" [ "$ssl" = "1" ] && acme_val="1" 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=$ssl" uci set "haproxy.$vhost_name.ssl_redirect=$ssl" uci set "haproxy.$vhost_name.acme=$acme_val" uci set "haproxy.$vhost_name.enabled=1" uci commit haproxy # Regenerate HAProxy config /usr/sbin/haproxyctl generate >/dev/null 2>&1 /etc/init.d/haproxy reload >/dev/null 2>&1 uci commit "$UCI_CONFIG" json_init json_add_boolean "success" 1 json_add_string "id" "$section_id" json_add_string "url" "https://$domain" json_add_string "name" "$name" json_add_string "domain" "$domain" json_dump } # Configure nginx location for a site _configure_nginx() { local site_name="$1" local nginx_conf="/var/lib/lxc/$NGINX_CONTAINER/rootfs/etc/nginx/sites.d" # Create nginx config for site mkdir -p "$nginx_conf" cat > "$nginx_conf/metablog-$site_name.conf" </dev/null || true } # Delete a site and cleanup method_delete_site() { local id read -r input json_load "$input" json_get_var id id if [ -z "$id" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Missing site id" json_dump return fi local name domain name=$(get_uci "$id" name "") domain=$(get_uci "$id" domain "") if [ -z "$name" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Site not found" json_dump return fi SITES_ROOT=$(get_uci main sites_root "$SITES_ROOT") NGINX_CONTAINER=$(get_uci main nginx_container "$NGINX_CONTAINER") # Get site runtime local site_runtime=$(get_uci "$id" runtime "") # 1. Delete HAProxy vhost local vhost_id=$(echo "$domain" | sed 's/[^a-zA-Z0-9]/_/g') uci delete "haproxy.$vhost_id" 2>/dev/null # 2. Delete HAProxy backend and server local backend_name="metablog_$(echo "$name" | sed 's/[^a-zA-Z0-9]/_/g')" uci delete "haproxy.$backend_name" 2>/dev/null uci delete "haproxy.${backend_name}_srv" 2>/dev/null uci commit haproxy /usr/sbin/haproxyctl generate >/dev/null 2>&1 /etc/init.d/haproxy reload >/dev/null 2>&1 # 3. Remove runtime config if [ "$site_runtime" = "uhttpd" ]; then # Remove uhttpd instance uci delete "uhttpd.metablog_$id" 2>/dev/null uci commit uhttpd /etc/init.d/uhttpd reload 2>/dev/null else # Remove nginx config rm -f "/var/lib/lxc/$NGINX_CONTAINER/rootfs/etc/nginx/sites.d/metablog-$name.conf" lxc-attach -n "$NGINX_CONTAINER" -- nginx -s reload 2>/dev/null || true fi # 4. Remove site directory rm -rf "$SITES_ROOT/$name" # 5. Delete UCI config uci delete "$UCI_CONFIG.$id" uci commit "$UCI_CONFIG" json_init json_add_boolean "success" 1 json_dump } # Sync site from Gitea (git pull) method_sync_site() { local id read -r input json_load "$input" json_get_var id id if [ -z "$id" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Missing site id" json_dump return fi local name gitea_repo name=$(get_uci "$id" name "") gitea_repo=$(get_uci "$id" gitea_repo "") if [ -z "$name" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Site not found" json_dump return fi SITES_ROOT=$(get_uci main sites_root "$SITES_ROOT") local site_path="$SITES_ROOT/$name" if [ -d "$site_path/.git" ]; then # Pull latest changes cd "$site_path" local result result=$(git pull 2>&1) local rc=$? # Fix permissions after pull fix_permissions "$site_path" json_init if [ $rc -eq 0 ]; then json_add_boolean "success" 1 json_add_string "message" "$result" else json_add_boolean "success" 0 json_add_string "error" "$result" fi json_dump elif [ -n "$gitea_repo" ]; then # Clone if not exists local gitea_url gitea_url=$(uci -q get gitea.main.url || echo "http://192.168.255.1:3000") rm -rf "$site_path" local result result=$(git clone "$gitea_url/$gitea_repo.git" "$site_path" 2>&1) local rc=$? # Fix permissions after clone fix_permissions "$site_path" json_init if [ $rc -eq 0 ]; then json_add_boolean "success" 1 json_add_string "message" "Cloned from $gitea_repo" else json_add_boolean "success" 0 json_add_string "error" "$result" fi json_dump else json_init json_add_boolean "success" 0 json_add_string "error" "No git repository configured for this site" json_dump fi } # Get publish info for QR code generation method_get_publish_info() { local id read -r input json_load "$input" json_get_var id id if [ -z "$id" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Missing site id" json_dump return fi local name domain description ssl name=$(get_uci "$id" name "") domain=$(get_uci "$id" domain "") description=$(get_uci "$id" description "") ssl=$(get_uci "$id" ssl "1") if [ -z "$name" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Site not found" json_dump return fi local protocol="http" [ "$ssl" = "1" ] && protocol="https" local url="${protocol}://${domain}" json_init json_add_boolean "success" 1 json_add_string "id" "$id" json_add_string "name" "$name" json_add_string "domain" "$domain" json_add_string "url" "$url" json_add_string "description" "$description" json_add_string "title" "$name - Published with SecuBox" json_dump } # Get site details method_get_site() { local id read -r input json_load "$input" json_get_var id id if [ -z "$id" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Missing site id" json_dump return fi local name domain gitea_repo ssl enabled description name=$(get_uci "$id" name "") domain=$(get_uci "$id" domain "") gitea_repo=$(get_uci "$id" gitea_repo "") ssl=$(get_uci "$id" ssl "1") enabled=$(get_uci "$id" enabled "1") description=$(get_uci "$id" description "") if [ -z "$name" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Site not found" json_dump return fi SITES_ROOT=$(get_uci main sites_root "$SITES_ROOT") local has_content last_sync has_content="0" if [ -d "$SITES_ROOT/$name" ] && [ -f "$SITES_ROOT/$name/index.html" ]; then has_content="1" fi last_sync="" if [ -d "$SITES_ROOT/$name/.git" ]; then last_sync=$(cd "$SITES_ROOT/$name" && git log -1 --format="%ci" 2>/dev/null || echo "") fi local protocol="http" [ "$ssl" = "1" ] && protocol="https" json_init json_add_boolean "success" 1 json_add_string "id" "$id" json_add_string "name" "$name" json_add_string "domain" "$domain" json_add_string "gitea_repo" "$gitea_repo" json_add_string "description" "$description" json_add_boolean "ssl" "$ssl" json_add_boolean "enabled" "$enabled" json_add_boolean "has_content" "$has_content" json_add_string "last_sync" "$last_sync" json_add_string "url" "${protocol}://${domain}" json_dump } # Update site settings method_update_site() { local id name domain gitea_repo ssl enabled description read -r input json_load "$input" json_get_var id id json_get_var name name json_get_var domain domain json_get_var gitea_repo gitea_repo json_get_var ssl ssl json_get_var enabled enabled json_get_var description description if [ -z "$id" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Missing site id" json_dump return fi # Check site exists local current_name current_name=$(get_uci "$id" name "") if [ -z "$current_name" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Site not found" json_dump return fi [ -n "$name" ] && uci set "$UCI_CONFIG.$id.name=$name" [ -n "$domain" ] && uci set "$UCI_CONFIG.$id.domain=$domain" [ -n "$gitea_repo" ] && uci set "$UCI_CONFIG.$id.gitea_repo=$gitea_repo" [ -n "$ssl" ] && uci set "$UCI_CONFIG.$id.ssl=$ssl" [ -n "$enabled" ] && uci set "$UCI_CONFIG.$id.enabled=$enabled" [ -n "$description" ] && uci set "$UCI_CONFIG.$id.description=$description" uci commit "$UCI_CONFIG" json_init json_add_boolean "success" 1 json_dump } # Upload file to site method_upload_file() { local id filename content read -r input json_load "$input" json_get_var id id json_get_var filename filename json_get_var content content if [ -z "$id" ] || [ -z "$filename" ] || [ -z "$content" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Missing required fields (id, filename, content)" json_dump return fi local name name=$(get_uci "$id" name "") if [ -z "$name" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Site not found" json_dump return fi SITES_ROOT=$(get_uci main sites_root "$SITES_ROOT") local site_path="$SITES_ROOT/$name" local file_path="$site_path/$filename" # Create directory structure if needed local dir_path=$(dirname "$file_path") mkdir -p "$dir_path" chmod 755 "$dir_path" # Decode base64 content and write file echo "$content" | base64 -d > "$file_path" 2>/dev/null local rc=$? if [ $rc -eq 0 ]; then chmod 644 "$file_path" # Ensure parent directories are also accessible chmod 755 "$site_path" json_init json_add_boolean "success" 1 json_add_string "filename" "$filename" json_add_string "path" "$file_path" json_dump else json_init json_add_boolean "success" 0 json_add_string "error" "Failed to write file" json_dump fi } # List files in a site method_list_files() { local id read -r input json_load "$input" json_get_var id id if [ -z "$id" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Missing site id" json_dump return fi local name name=$(get_uci "$id" name "") if [ -z "$name" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Site not found" json_dump return fi SITES_ROOT=$(get_uci main sites_root "$SITES_ROOT") local site_path="$SITES_ROOT/$name" json_init json_add_boolean "success" 1 json_add_array "files" if [ -d "$site_path" ]; then find "$site_path" -type f 2>/dev/null | while read -r file; do local rel_path="${file#$site_path/}" local size=$(stat -c%s "$file" 2>/dev/null || echo "0") json_add_object "" json_add_string "name" "$rel_path" json_add_int "size" "$size" json_close_object done fi json_close_array json_dump } # Get global settings method_get_settings() { json_init json_add_boolean "enabled" "$(get_uci main enabled 0)" json_add_string "runtime" "$(get_uci main runtime auto)" json_add_string "detected_runtime" "$(detect_runtime)" json_add_string "nginx_container" "$(get_uci main nginx_container nginx)" json_add_string "sites_root" "$(get_uci main sites_root /srv/metablogizer/sites)" json_add_string "gitea_url" "$(get_uci main gitea_url http://localhost:3000)" json_dump } # Save global settings method_save_settings() { local enabled runtime nginx_container sites_root gitea_url read -r input json_load "$input" json_get_var enabled enabled json_get_var runtime runtime json_get_var nginx_container nginx_container json_get_var sites_root sites_root json_get_var gitea_url gitea_url # Ensure main section exists uci -q get "$UCI_CONFIG.main" >/dev/null 2>&1 || uci set "$UCI_CONFIG.main=metablogizer" [ -n "$enabled" ] && uci set "$UCI_CONFIG.main.enabled=$enabled" [ -n "$runtime" ] && uci set "$UCI_CONFIG.main.runtime=$runtime" [ -n "$nginx_container" ] && uci set "$UCI_CONFIG.main.nginx_container=$nginx_container" [ -n "$sites_root" ] && uci set "$UCI_CONFIG.main.sites_root=$sites_root" [ -n "$gitea_url" ] && uci set "$UCI_CONFIG.main.gitea_url=$gitea_url" uci commit "$UCI_CONFIG" json_init json_add_boolean "success" 1 json_dump } # Main RPC interface case "$1" in list) cat <<'EOF' { "status": {}, "list_sites": {}, "get_site": { "id": "string" }, "create_site": { "name": "string", "domain": "string", "gitea_repo": "string", "ssl": "boolean", "description": "string" }, "update_site": { "id": "string", "name": "string", "domain": "string", "gitea_repo": "string", "ssl": "boolean", "enabled": "boolean", "description": "string" }, "delete_site": { "id": "string" }, "sync_site": { "id": "string" }, "get_publish_info": { "id": "string" }, "upload_file": { "id": "string", "filename": "string", "content": "string" }, "list_files": { "id": "string" }, "get_settings": {}, "save_settings": { "enabled": "boolean", "nginx_container": "string", "sites_root": "string" } } EOF ;; call) case "$2" in status) method_status ;; list_sites) method_list_sites ;; get_site) method_get_site ;; create_site) method_create_site ;; update_site) method_update_site ;; delete_site) method_delete_site ;; sync_site) method_sync_site ;; get_publish_info) method_get_publish_info ;; upload_file) method_upload_file ;; list_files) method_list_files ;; get_settings) method_get_settings ;; save_settings) method_save_settings ;; *) echo '{"error": "unknown method"}' ;; esac ;; esac