- Add auto-republish to method_upload_file (was only in upload_finalize) - Add Gitea push to cmd_publish when gitea.enabled=1 - Use haproxyctl reload instead of init.d (container-aware) - Uploaded content now triggers full republish flow for emancipated sites This fixes the issue where uploading new HTML content didn't update the live site because republish wasn't triggered. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1058 lines
30 KiB
Bash
1058 lines
30 KiB
Bash
#!/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 <command> [options]
|
|
|
|
Site Commands:
|
|
list List all sites
|
|
create <name> <domain> [repo] Create new site
|
|
delete <name> Delete site
|
|
sync <name> Sync site from git repo
|
|
publish <name> Publish site (create HAProxy vhost)
|
|
gitea push <name> Create Gitea repo and push site content
|
|
gitea init-all Initialize Gitea repos for all existing sites
|
|
emancipate <name> 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 <uhttpd|nginx> 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" <<EOF
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>$name</title>
|
|
<meta property="og:title" content="$name">
|
|
<meta property="og:url" content="https://$domain">
|
|
<meta property="og:type" content="website">
|
|
<style>
|
|
body { font-family: system-ui; max-width: 800px; margin: 50px auto; padding: 20px; }
|
|
h1 { color: #3b82f6; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>$name</h1>
|
|
<p>Site published with MetaBlogizer</p>
|
|
<p><a href="https://$domain">https://$domain</a></p>
|
|
</body>
|
|
</html>
|
|
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
|
|
|
|
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" <<EOF
|
|
location /$name/ {
|
|
alias /srv/sites/$name/;
|
|
index index.html;
|
|
try_files \$uri \$uri/ /index.html;
|
|
}
|
|
EOF
|
|
|
|
# Reload nginx in container
|
|
lxc-attach -n "$NGINX_LXC" -- nginx -s reload 2>/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"
|
|
|
|
# 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" <<EOF
|
|
lxc.uts.name = $NGINX_LXC
|
|
lxc.rootfs.path = dir:$rootfs
|
|
lxc.net.0.type = none
|
|
lxc.mount.auto = proc:mixed sys:ro cgroup:mixed
|
|
lxc.mount.entry = $SITES_ROOT srv/sites none bind,create=dir 0 0
|
|
lxc.cap.drop = sys_admin sys_module mac_admin mac_override
|
|
lxc.init.cmd = /usr/sbin/nginx -g 'daemon off;'
|
|
EOF
|
|
|
|
log_info "nginx LXC installed"
|
|
log_info "Start with: lxc-start -n $NGINX_LXC -d"
|
|
}
|
|
|
|
# ===========================================
|
|
# KISS ULTIME MODE - Emancipate
|
|
# ===========================================
|
|
|
|
_emancipate_dns() {
|
|
local name="$1"
|
|
local domain="$2"
|
|
local default_zone=$(uci -q get dns-provider.main.zone)
|
|
local provider=$(uci -q get dns-provider.main.provider)
|
|
local vortex_wildcard=$(uci -q get vortex-dns.master.wildcard_domain)
|
|
|
|
# Check if dnsctl is available
|
|
if ! command -v dnsctl >/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"
|
|
|
|
log_info "[WAF] Syncing mitmproxy routes from HAProxy config"
|
|
|
|
# Use mitmproxyctl to sync routes from HAProxy UCI config
|
|
# This ensures all routes are in sync and mitmproxy will auto-reload
|
|
if command -v mitmproxyctl >/dev/null 2>&1; then
|
|
mitmproxyctl sync-routes 2>&1 | while read -r line; do
|
|
log_info "[WAF] $line"
|
|
done
|
|
log_info "[WAF] Routes synced - mitmproxy will auto-reload on next request"
|
|
else
|
|
log_warn "[WAF] mitmproxyctl not found, manually adding route"
|
|
# Fallback: directly add route to the routes file
|
|
local port=$(uci_get site_${name}.port)
|
|
local routes_file="/srv/mitmproxy-in/haproxy-routes.json"
|
|
|
|
if [ -f "$routes_file" ]; then
|
|
python3 -c "
|
|
import json
|
|
try:
|
|
with open('$routes_file') as f:
|
|
data = json.load(f)
|
|
data['$domain'] = ['192.168.255.1', $port]
|
|
with open('$routes_file', 'w') as f:
|
|
json.dump(data, f, indent=2)
|
|
print('[WAF] Route added: $domain -> 192.168.255.1:$port')
|
|
except Exception as e:
|
|
print(f'[WAF] Error: {e}')
|
|
" 2>/dev/null
|
|
fi
|
|
fi
|
|
}
|
|
|
|
_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 <domain>"
|
|
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 ""
|
|
}
|
|
|
|
# ===========================================
|
|
# 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 "$@" ;;
|
|
init-all) shift; cmd_gitea_init_all "$@" ;;
|
|
*) echo "Usage: metablogizerctl gitea {push|init-all} [name]"; exit 1 ;;
|
|
esac
|
|
;;
|
|
help|--help|-h) usage ;;
|
|
*) usage ;;
|
|
esac
|