- Add mitmproxy restart after _add_mitmproxy_route() to load new routes - mitmproxy loads routes at startup only, so restart is required - Run restart in background to avoid blocking publish command Also fixed on router: - Disabled health check for mitmproxy_inspector backend - HAProxy health check fails because mitmproxy returns 404 for requests without valid Host header Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1651 lines
46 KiB
Bash
1651 lines
46 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
|
|
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 <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
|
|
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 <name> Generate README.nfo manifest
|
|
nfo info <name> Show NFO summary
|
|
nfo edit <name> Edit manifest in editor
|
|
nfo validate <name> Validate NFO structure
|
|
nfo sync <name> Sync NFO fields from UCI config
|
|
nfo json <name> 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" <<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
|
|
|
|
# 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" <<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
|
|
}
|
|
|
|
# Add mitmproxy route for WAF forwarding
|
|
_add_mitmproxy_route() {
|
|
local domain="$1"
|
|
local port="$2"
|
|
local routes_file="/srv/mitmproxy-in/haproxy-routes.json"
|
|
|
|
# mitmproxy reaches uhttpd on host via 127.0.0.1
|
|
local host_ip="127.0.0.1"
|
|
|
|
# Direct JSON update
|
|
if [ -f "$routes_file" ] && command -v python3 >/dev/null 2>&1; then
|
|
python3 -c "
|
|
import json
|
|
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)
|
|
except Exception as e:
|
|
pass
|
|
" 2>/dev/null
|
|
fi
|
|
|
|
# Also update the main mitmproxy routes file
|
|
local main_routes="/srv/mitmproxy/haproxy-routes.json"
|
|
if [ -f "$main_routes" ] && command -v python3 >/dev/null 2>&1; then
|
|
python3 -c "
|
|
import json
|
|
try:
|
|
with open('$main_routes', 'r') as f:
|
|
routes = json.load(f)
|
|
routes['$domain'] = ['$host_ip', $port]
|
|
with open('$main_routes', 'w') as f:
|
|
json.dump(routes, f, indent=2)
|
|
except Exception as e:
|
|
pass
|
|
" 2>/dev/null
|
|
fi
|
|
|
|
# Trigger mitmproxy reload to pick up new route
|
|
# mitmproxy loads routes at startup, so we need to restart it
|
|
# Run in background to avoid blocking publish
|
|
( sleep 1 && /etc/init.d/mitmproxy restart ) >/dev/null 2>&1 &
|
|
}
|
|
|
|
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 (use 127.0.0.1 for mitmproxy to reach uhttpd on host)
|
|
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="127.0.0.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 - route through mitmproxy for WAF inspection
|
|
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="mitmproxy_inspector"
|
|
uci set haproxy.${vhost_name}.original_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
|
|
|
|
# Add mitmproxy route for WAF forwarding
|
|
_add_mitmproxy_route "$domain" "$port"
|
|
|
|
# Regenerate HAProxy config and reload container (in background - takes ~90s with many vhosts)
|
|
(
|
|
/usr/sbin/haproxyctl generate >/dev/null 2>&1
|
|
/usr/sbin/haproxyctl reload >/dev/null 2>&1
|
|
) &
|
|
|
|
log_info "Site published!"
|
|
log_info "HAProxy config regenerating in background..."
|
|
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
|
|
# Background HAProxy regeneration (takes ~90s with many vhosts)
|
|
(/usr/sbin/haproxyctl generate >/dev/null 2>&1 && /etc/init.d/haproxy reload >/dev/null 2>&1) &
|
|
log_info "HAProxy config regenerating in background..."
|
|
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 (use 127.0.0.1 for mitmproxy to reach uhttpd on host)
|
|
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="127.0.0.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 - route through mitmproxy for WAF inspection
|
|
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="mitmproxy_inspector"
|
|
uci set haproxy.${vhost_name}.original_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 (in background - takes ~90s with many vhosts)
|
|
if command -v haproxyctl >/dev/null 2>&1; then
|
|
haproxyctl generate >/dev/null 2>&1 &
|
|
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 <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 ""
|
|
}
|
|
|
|
# ===========================================
|
|
# 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 <<EOF
|
|
{
|
|
"identity": {
|
|
"id": "$id",
|
|
"name": "$nfo_name",
|
|
"version": "$version"
|
|
},
|
|
"tags": {
|
|
"category": "$category"
|
|
},
|
|
"description": {
|
|
"short": "$short"
|
|
}
|
|
}
|
|
EOF
|
|
}
|
|
|
|
cmd_nfo() {
|
|
local action="$1"
|
|
local name="$2"
|
|
|
|
case "$action" in
|
|
init)
|
|
[ -z "$name" ] && { log_error "Site name required"; return 1; }
|
|
site_exists "$name" || { log_error "Site '$name' not found"; return 1; }
|
|
_nfo_init "$name"
|
|
;;
|
|
init-all)
|
|
log_info "Generating NFO manifests for all MetaBlog sites..."
|
|
echo ""
|
|
|
|
# Use temp files for counters (config_foreach runs in subshell)
|
|
local tmp_success="/tmp/nfo_success_$$"
|
|
local tmp_skipped="/tmp/nfo_skipped_$$"
|
|
local tmp_failed="/tmp/nfo_failed_$$"
|
|
echo 0 > "$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 <init|init-all|info|edit|validate|sync|json> [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
|