- Create secubox-app-vhost-manager package for unified vhost orchestration - Single CLI tool (secubox-vhost) manages HAProxy, DNS, Tor, Mesh, mitmproxy - Unified UCI config (/etc/config/vhosts) as single source of truth - Backend adapters for each component (haproxy.sh, dns.sh, tor.sh, mesh.sh, mitmproxy.sh) - Centralized backend resolution function (backends.sh) - Import tool for existing HAProxy vhosts - Validation of backend reachability before creation Also includes: - FAQ-TROUBLESHOOTING.md with LXC cgroup v1/v2 fixes - Fix mitmproxyctl cgroup v1 -> v2 syntax for container compatibility - HAProxy backend resolution bugfixes CLI commands: secubox-vhost add <domain> <service> <port> [--ssl] [--tor] [--mesh] secubox-vhost remove/list/status/enable/disable/set/sync/validate/import Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
574 lines
17 KiB
Bash
574 lines
17 KiB
Bash
#!/bin/sh
|
|
# SecuBox Centralized VHost Manager
|
|
# Orchestrates HAProxy, DNS, Tor, Mesh, and mitmproxy from single interface
|
|
|
|
VERSION="1.0.0"
|
|
CONFIG="vhosts"
|
|
|
|
# Load adapter libraries
|
|
LIB_DIR="/usr/lib/vhost-manager"
|
|
. "$LIB_DIR/backends.sh"
|
|
. "$LIB_DIR/haproxy.sh"
|
|
. "$LIB_DIR/dns.sh"
|
|
. "$LIB_DIR/tor.sh"
|
|
. "$LIB_DIR/mesh.sh"
|
|
. "$LIB_DIR/mitmproxy.sh"
|
|
|
|
# Colors
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
CYAN='\033[0;36m'
|
|
NC='\033[0m'
|
|
|
|
log() { echo -e "${GREEN}[VHOST]${NC} $1"; }
|
|
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
|
error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
|
|
|
# ============================================================================
|
|
# Add VHost
|
|
# ============================================================================
|
|
|
|
cmd_add() {
|
|
local domain="$1"
|
|
local service="$2"
|
|
local port="$3"
|
|
shift 3
|
|
|
|
if [ -z "$domain" ] || [ -z "$service" ] || [ -z "$port" ]; then
|
|
echo "Usage: secubox-vhost add <domain> <service> <port> [options]"
|
|
echo ""
|
|
echo "Options:"
|
|
echo " --ssl Enable SSL (default: yes)"
|
|
echo " --no-ssl Disable SSL"
|
|
echo " --tor Enable Tor hidden service"
|
|
echo " --mesh Enable mesh P2P publishing"
|
|
echo " --mitm Enable mitmproxy WAF (default: yes)"
|
|
echo " --no-mitm Disable mitmproxy WAF"
|
|
return 1
|
|
fi
|
|
|
|
# Validate port
|
|
if ! echo "$port" | grep -qE '^[0-9]+$' || [ "$port" -lt 1 ] || [ "$port" -gt 65535 ]; then
|
|
error "Invalid port: $port (must be 1-65535)"
|
|
return 1
|
|
fi
|
|
|
|
# Parse options
|
|
local ssl=1
|
|
local tor=0
|
|
local mesh=0
|
|
local mitm=1
|
|
local host="127.0.0.1"
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--ssl) ssl=1 ;;
|
|
--no-ssl) ssl=0 ;;
|
|
--tor) tor=1 ;;
|
|
--mesh) mesh=1 ;;
|
|
--mitm) mitm=1 ;;
|
|
--no-mitm) mitm=0 ;;
|
|
--host=*) host="${1#*=}" ;;
|
|
*) warn "Unknown option: $1" ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
local section=$(sanitize_section_name "$domain")
|
|
|
|
# Check for duplicate
|
|
if uci -q get $CONFIG.$section >/dev/null 2>&1; then
|
|
error "VHost already exists: $domain"
|
|
return 1
|
|
fi
|
|
|
|
log "Creating vhost: $domain -> $host:$port"
|
|
|
|
# Store in UCI
|
|
uci set $CONFIG.$section=vhost
|
|
uci set $CONFIG.$section.domain="$domain"
|
|
uci set $CONFIG.$section.service="$service"
|
|
uci set $CONFIG.$section.backend_host="$host"
|
|
uci set $CONFIG.$section.backend_port="$port"
|
|
uci set $CONFIG.$section.enabled='1'
|
|
uci set $CONFIG.$section.haproxy='1'
|
|
uci set $CONFIG.$section.ssl="$ssl"
|
|
uci set $CONFIG.$section.ssl_redirect="$ssl"
|
|
uci set $CONFIG.$section.acme="$ssl"
|
|
uci set $CONFIG.$section.tor="$tor"
|
|
uci set $CONFIG.$section.mesh="$mesh"
|
|
uci set $CONFIG.$section.mitmproxy="$mitm"
|
|
uci set $CONFIG.$section.created="$(date -Iseconds)"
|
|
uci commit $CONFIG
|
|
|
|
# Apply to backends
|
|
log " [1/5] Creating HAProxy vhost..."
|
|
haproxy_add_vhost "$domain" "$service" "$host" "$port" "$ssl" "$ssl"
|
|
|
|
log " [2/5] Adding DNS record..."
|
|
dns_add_record "$domain" 2>/dev/null || warn "DNS: Not configured or failed"
|
|
|
|
if [ "$tor" = "1" ]; then
|
|
log " [3/5] Creating Tor hidden service..."
|
|
if tor_is_available; then
|
|
tor_add_service "$service" "$port"
|
|
local onion=$(tor_wait_for_onion "$service" 5)
|
|
if [ -n "$onion" ]; then
|
|
uci set $CONFIG.$section.onion_address="$onion"
|
|
uci commit $CONFIG
|
|
log " Onion: $onion"
|
|
fi
|
|
else
|
|
warn "Tor not available"
|
|
fi
|
|
else
|
|
log " [3/5] Tor: skipped"
|
|
fi
|
|
|
|
if [ "$mesh" = "1" ]; then
|
|
log " [4/5] Publishing to mesh..."
|
|
if mesh_is_available; then
|
|
mesh_publish "$service" "$port" "$domain"
|
|
else
|
|
warn "Mesh not available"
|
|
fi
|
|
else
|
|
log " [4/5] Mesh: skipped"
|
|
fi
|
|
|
|
if [ "$mitm" = "1" ]; then
|
|
log " [5/5] Adding mitmproxy route..."
|
|
if mitmproxy_is_available; then
|
|
mitmproxy_add_route "$domain" "$host" "$port"
|
|
else
|
|
warn "Mitmproxy not available"
|
|
fi
|
|
else
|
|
log " [5/5] Mitmproxy: skipped"
|
|
fi
|
|
|
|
# Reload HAProxy
|
|
log "Reloading HAProxy..."
|
|
haproxy_reload
|
|
|
|
echo ""
|
|
log "VHost created: https://$domain"
|
|
[ "$tor" = "1" ] && [ -n "$onion" ] && log "Tor: http://$onion"
|
|
}
|
|
|
|
# ============================================================================
|
|
# Remove VHost
|
|
# ============================================================================
|
|
|
|
cmd_remove() {
|
|
local domain="$1"
|
|
|
|
if [ -z "$domain" ]; then
|
|
echo "Usage: secubox-vhost remove <domain>"
|
|
return 1
|
|
fi
|
|
|
|
local section=$(sanitize_section_name "$domain")
|
|
|
|
if ! uci -q get $CONFIG.$section >/dev/null 2>&1; then
|
|
error "VHost not found: $domain"
|
|
return 1
|
|
fi
|
|
|
|
local service=$(uci -q get $CONFIG.$section.service)
|
|
local tor=$(uci -q get $CONFIG.$section.tor)
|
|
local mesh=$(uci -q get $CONFIG.$section.mesh)
|
|
local mitm=$(uci -q get $CONFIG.$section.mitmproxy)
|
|
|
|
log "Removing vhost: $domain"
|
|
|
|
# Remove from backends
|
|
log " [1/5] Removing HAProxy vhost..."
|
|
haproxy_remove_vhost "$domain"
|
|
|
|
log " [2/5] Removing DNS record..."
|
|
dns_remove_record "$domain" 2>/dev/null || true
|
|
|
|
if [ "$tor" = "1" ]; then
|
|
log " [3/5] Removing Tor hidden service..."
|
|
tor_remove_service "$service" 2>/dev/null || true
|
|
fi
|
|
|
|
if [ "$mesh" = "1" ]; then
|
|
log " [4/5] Unpublishing from mesh..."
|
|
mesh_unpublish "$service" 2>/dev/null || true
|
|
fi
|
|
|
|
if [ "$mitm" = "1" ]; then
|
|
log " [5/5] Removing mitmproxy route..."
|
|
mitmproxy_remove_route "$domain" 2>/dev/null || true
|
|
fi
|
|
|
|
# Remove from UCI
|
|
uci delete $CONFIG.$section
|
|
uci commit $CONFIG
|
|
|
|
# Reload HAProxy
|
|
haproxy_reload
|
|
|
|
log "VHost removed: $domain"
|
|
}
|
|
|
|
# ============================================================================
|
|
# List VHosts
|
|
# ============================================================================
|
|
|
|
cmd_list() {
|
|
local json=0
|
|
[ "$1" = "--json" ] && json=1
|
|
|
|
if [ "$json" = "1" ]; then
|
|
printf '{"vhosts":['
|
|
local first=1
|
|
for section in $(uci show $CONFIG 2>/dev/null | grep "=vhost$" | cut -d. -f2 | cut -d= -f1); do
|
|
local domain=$(uci -q get $CONFIG.$section.domain)
|
|
[ -z "$domain" ] && continue
|
|
|
|
[ $first -eq 0 ] && printf ','
|
|
first=0
|
|
|
|
local service=$(uci -q get $CONFIG.$section.service)
|
|
local port=$(uci -q get $CONFIG.$section.backend_port)
|
|
local enabled=$(uci -q get $CONFIG.$section.enabled)
|
|
local ssl=$(uci -q get $CONFIG.$section.ssl)
|
|
local tor=$(uci -q get $CONFIG.$section.tor)
|
|
local mesh=$(uci -q get $CONFIG.$section.mesh)
|
|
local mitm=$(uci -q get $CONFIG.$section.mitmproxy)
|
|
local onion=$(uci -q get $CONFIG.$section.onion_address)
|
|
|
|
printf '{"domain":"%s","service":"%s","port":%s,"enabled":%s,"ssl":%s,"tor":%s,"mesh":%s,"mitmproxy":%s,"onion":"%s"}' \
|
|
"$domain" "${service:-unknown}" "${port:-0}" \
|
|
"$([ "$enabled" = "1" ] && echo true || echo false)" \
|
|
"$([ "$ssl" = "1" ] && echo true || echo false)" \
|
|
"$([ "$tor" = "1" ] && echo true || echo false)" \
|
|
"$([ "$mesh" = "1" ] && echo true || echo false)" \
|
|
"$([ "$mitm" = "1" ] && echo true || echo false)" \
|
|
"${onion:-}"
|
|
done
|
|
printf ']}\n'
|
|
else
|
|
echo ""
|
|
echo "==========================================="
|
|
echo " SecuBox VHosts"
|
|
echo "==========================================="
|
|
echo ""
|
|
printf "%-30s %-12s %-6s %-4s %-4s %-4s %-4s\n" "DOMAIN" "SERVICE" "PORT" "SSL" "TOR" "MESH" "WAF"
|
|
printf "%-30s %-12s %-6s %-4s %-4s %-4s %-4s\n" "------" "-------" "----" "---" "---" "----" "---"
|
|
|
|
local count=0
|
|
for section in $(uci show $CONFIG 2>/dev/null | grep "=vhost$" | cut -d. -f2 | cut -d= -f1); do
|
|
local domain=$(uci -q get $CONFIG.$section.domain)
|
|
[ -z "$domain" ] && continue
|
|
|
|
local service=$(uci -q get $CONFIG.$section.service)
|
|
local port=$(uci -q get $CONFIG.$section.backend_port)
|
|
local ssl=$(uci -q get $CONFIG.$section.ssl)
|
|
local tor=$(uci -q get $CONFIG.$section.tor)
|
|
local mesh=$(uci -q get $CONFIG.$section.mesh)
|
|
local mitm=$(uci -q get $CONFIG.$section.mitmproxy)
|
|
|
|
printf "%-30s %-12s %-6s %-4s %-4s %-4s %-4s\n" \
|
|
"$domain" "${service:-?}" "${port:-?}" \
|
|
"$([ "$ssl" = "1" ] && echo "yes" || echo "no")" \
|
|
"$([ "$tor" = "1" ] && echo "yes" || echo "no")" \
|
|
"$([ "$mesh" = "1" ] && echo "yes" || echo "no")" \
|
|
"$([ "$mitm" = "1" ] && echo "yes" || echo "no")"
|
|
|
|
count=$((count + 1))
|
|
done
|
|
|
|
if [ "$count" -eq 0 ]; then
|
|
echo " No vhosts configured"
|
|
fi
|
|
echo ""
|
|
echo "Total: $count vhost(s)"
|
|
echo ""
|
|
fi
|
|
}
|
|
|
|
# ============================================================================
|
|
# Status
|
|
# ============================================================================
|
|
|
|
cmd_status() {
|
|
local domain="$1"
|
|
|
|
if [ -z "$domain" ]; then
|
|
echo "Usage: secubox-vhost status <domain>"
|
|
return 1
|
|
fi
|
|
|
|
local section=$(sanitize_section_name "$domain")
|
|
|
|
if ! uci -q get $CONFIG.$section >/dev/null 2>&1; then
|
|
error "VHost not found: $domain"
|
|
return 1
|
|
fi
|
|
|
|
echo ""
|
|
echo "VHost: $domain"
|
|
echo "========================================"
|
|
echo ""
|
|
echo "Service: $(uci -q get $CONFIG.$section.service)"
|
|
echo "Backend: $(uci -q get $CONFIG.$section.backend_host):$(uci -q get $CONFIG.$section.backend_port)"
|
|
echo "Enabled: $(uci -q get $CONFIG.$section.enabled)"
|
|
echo ""
|
|
echo "Channels:"
|
|
echo " HAProxy: yes ($(haproxy_get_status "$domain"))"
|
|
echo " SSL/ACME: $(uci -q get $CONFIG.$section.ssl)"
|
|
echo " Tor: $(uci -q get $CONFIG.$section.tor)"
|
|
[ "$(uci -q get $CONFIG.$section.tor)" = "1" ] && echo " Onion: $(uci -q get $CONFIG.$section.onion_address)"
|
|
echo " Mesh: $(uci -q get $CONFIG.$section.mesh)"
|
|
echo " Mitmproxy: $(uci -q get $CONFIG.$section.mitmproxy)"
|
|
echo ""
|
|
echo "Created: $(uci -q get $CONFIG.$section.created)"
|
|
echo ""
|
|
}
|
|
|
|
# ============================================================================
|
|
# Sync
|
|
# ============================================================================
|
|
|
|
cmd_sync() {
|
|
log "Syncing vhosts to all backends..."
|
|
|
|
for section in $(uci show $CONFIG 2>/dev/null | grep "=vhost$" | cut -d. -f2 | cut -d= -f1); do
|
|
local domain=$(uci -q get $CONFIG.$section.domain)
|
|
[ -z "$domain" ] && continue
|
|
|
|
local service=$(uci -q get $CONFIG.$section.service)
|
|
local host=$(uci -q get $CONFIG.$section.backend_host)
|
|
local port=$(uci -q get $CONFIG.$section.backend_port)
|
|
local ssl=$(uci -q get $CONFIG.$section.ssl)
|
|
local tor=$(uci -q get $CONFIG.$section.tor)
|
|
local mesh=$(uci -q get $CONFIG.$section.mesh)
|
|
local mitm=$(uci -q get $CONFIG.$section.mitmproxy)
|
|
|
|
log " Syncing: $domain"
|
|
|
|
# Ensure HAProxy
|
|
haproxy_add_vhost "$domain" "$service" "$host" "$port" "$ssl" "$ssl" 2>/dev/null
|
|
|
|
# Ensure mitmproxy route
|
|
[ "$mitm" = "1" ] && mitmproxy_add_route "$domain" "$host" "$port" 2>/dev/null
|
|
done
|
|
|
|
haproxy_reload
|
|
mitmproxy_sync_routes 2>/dev/null || true
|
|
|
|
log "Sync complete"
|
|
}
|
|
|
|
# ============================================================================
|
|
# Validate
|
|
# ============================================================================
|
|
|
|
cmd_validate() {
|
|
log "Validating vhosts..."
|
|
local errors=0
|
|
|
|
for section in $(uci show $CONFIG 2>/dev/null | grep "=vhost$" | cut -d. -f2 | cut -d= -f1); do
|
|
local domain=$(uci -q get $CONFIG.$section.domain)
|
|
[ -z "$domain" ] && continue
|
|
|
|
local host=$(uci -q get $CONFIG.$section.backend_host)
|
|
local port=$(uci -q get $CONFIG.$section.backend_port)
|
|
|
|
if validate_backend "$host:$port"; then
|
|
log " [OK] $domain -> $host:$port"
|
|
else
|
|
error " [FAIL] $domain -> $host:$port (unreachable)"
|
|
errors=$((errors + 1))
|
|
fi
|
|
done
|
|
|
|
if [ "$errors" -gt 0 ]; then
|
|
error "$errors vhost(s) have unreachable backends"
|
|
return 1
|
|
fi
|
|
|
|
log "All vhosts valid"
|
|
}
|
|
|
|
# ============================================================================
|
|
# Import
|
|
# ============================================================================
|
|
|
|
cmd_import() {
|
|
log "Importing vhosts from HAProxy UCI..."
|
|
local imported=0
|
|
|
|
for section in $(uci show haproxy 2>/dev/null | grep "=vhost$" | cut -d. -f2 | cut -d= -f1); do
|
|
local domain=$(uci -q get haproxy.$section.domain)
|
|
[ -z "$domain" ] && continue
|
|
|
|
local vhost_section=$(sanitize_section_name "$domain")
|
|
|
|
# Skip if already imported
|
|
uci -q get $CONFIG.$vhost_section >/dev/null 2>&1 && continue
|
|
|
|
local backend=$(uci -q get haproxy.$section.backend)
|
|
local addr=$(get_backend_address "" "$backend")
|
|
local host=$(echo "$addr" | cut -d: -f1)
|
|
local port=$(echo "$addr" | cut -d: -f2)
|
|
|
|
[ -z "$port" ] && port="80"
|
|
[ -z "$host" ] && host="127.0.0.1"
|
|
|
|
local ssl=$(uci -q get haproxy.$section.ssl)
|
|
local enabled=$(uci -q get haproxy.$section.enabled)
|
|
|
|
log " Importing: $domain ($backend -> $host:$port)"
|
|
|
|
uci set $CONFIG.$vhost_section=vhost
|
|
uci set $CONFIG.$vhost_section.domain="$domain"
|
|
uci set $CONFIG.$vhost_section.service="${backend#backend_}"
|
|
uci set $CONFIG.$vhost_section.backend_host="$host"
|
|
uci set $CONFIG.$vhost_section.backend_port="$port"
|
|
uci set $CONFIG.$vhost_section.enabled="${enabled:-1}"
|
|
uci set $CONFIG.$vhost_section.haproxy='1'
|
|
uci set $CONFIG.$vhost_section.ssl="${ssl:-1}"
|
|
uci set $CONFIG.$vhost_section.ssl_redirect="${ssl:-1}"
|
|
uci set $CONFIG.$vhost_section.acme="${ssl:-1}"
|
|
uci set $CONFIG.$vhost_section.tor='0'
|
|
uci set $CONFIG.$vhost_section.mesh='0'
|
|
uci set $CONFIG.$vhost_section.mitmproxy='1'
|
|
uci set $CONFIG.$vhost_section.imported='1'
|
|
|
|
imported=$((imported + 1))
|
|
done
|
|
|
|
uci commit $CONFIG
|
|
log "Imported $imported vhost(s)"
|
|
}
|
|
|
|
# ============================================================================
|
|
# Enable/Disable
|
|
# ============================================================================
|
|
|
|
cmd_enable() {
|
|
local domain="$1"
|
|
[ -z "$domain" ] && { echo "Usage: secubox-vhost enable <domain>"; return 1; }
|
|
|
|
local section=$(sanitize_section_name "$domain")
|
|
uci set $CONFIG.$section.enabled='1'
|
|
uci commit $CONFIG
|
|
haproxy_set_enabled "$domain" '1'
|
|
haproxy_reload
|
|
|
|
log "Enabled: $domain"
|
|
}
|
|
|
|
cmd_disable() {
|
|
local domain="$1"
|
|
[ -z "$domain" ] && { echo "Usage: secubox-vhost disable <domain>"; return 1; }
|
|
|
|
local section=$(sanitize_section_name "$domain")
|
|
uci set $CONFIG.$section.enabled='0'
|
|
uci commit $CONFIG
|
|
haproxy_set_enabled "$domain" '0'
|
|
haproxy_reload
|
|
|
|
log "Disabled: $domain"
|
|
}
|
|
|
|
# ============================================================================
|
|
# Set Channel
|
|
# ============================================================================
|
|
|
|
cmd_set() {
|
|
local domain="$1"
|
|
shift
|
|
|
|
[ -z "$domain" ] && { echo "Usage: secubox-vhost set <domain> --<channel>=<0|1>"; return 1; }
|
|
|
|
local section=$(sanitize_section_name "$domain")
|
|
|
|
if ! uci -q get $CONFIG.$section >/dev/null 2>&1; then
|
|
error "VHost not found: $domain"
|
|
return 1
|
|
fi
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--ssl=*) uci set $CONFIG.$section.ssl="${1#*=}" ;;
|
|
--tor=*) uci set $CONFIG.$section.tor="${1#*=}" ;;
|
|
--mesh=*) uci set $CONFIG.$section.mesh="${1#*=}" ;;
|
|
--mitmproxy=*|--mitm=*) uci set $CONFIG.$section.mitmproxy="${1#*=}" ;;
|
|
*) warn "Unknown option: $1" ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
uci commit $CONFIG
|
|
cmd_sync
|
|
log "Updated: $domain"
|
|
}
|
|
|
|
# ============================================================================
|
|
# Help
|
|
# ============================================================================
|
|
|
|
show_help() {
|
|
cat << EOF
|
|
SecuBox Centralized VHost Manager v${VERSION}
|
|
|
|
Usage: secubox-vhost <command> [options]
|
|
|
|
Commands:
|
|
add <domain> <service> <port> [opts] Create new vhost
|
|
remove <domain> Remove vhost
|
|
list [--json] List all vhosts
|
|
status <domain> Show vhost details
|
|
enable <domain> Enable vhost
|
|
disable <domain> Disable vhost
|
|
set <domain> --<channel>=<0|1> Toggle channel
|
|
sync Sync UCI to all backends
|
|
validate Validate all backends reachable
|
|
import Import from HAProxy UCI
|
|
|
|
Add Options:
|
|
--ssl / --no-ssl Enable/disable SSL (default: yes)
|
|
--tor Enable Tor hidden service
|
|
--mesh Enable mesh P2P publishing
|
|
--mitm / --no-mitm Enable/disable mitmproxy WAF (default: yes)
|
|
--host=<ip> Backend host (default: 127.0.0.1)
|
|
|
|
Examples:
|
|
secubox-vhost add git.example.com gitea 3000 --ssl --tor
|
|
secubox-vhost add api.example.com myapi 8080 --ssl --mesh --mitm
|
|
secubox-vhost remove git.example.com
|
|
secubox-vhost set api.example.com --tor=1
|
|
secubox-vhost list --json
|
|
secubox-vhost import
|
|
|
|
EOF
|
|
}
|
|
|
|
# ============================================================================
|
|
# Main
|
|
# ============================================================================
|
|
|
|
case "${1:-}" in
|
|
add) shift; cmd_add "$@" ;;
|
|
remove|rm) shift; cmd_remove "$@" ;;
|
|
list|ls) shift; cmd_list "$@" ;;
|
|
status) shift; cmd_status "$@" ;;
|
|
enable) shift; cmd_enable "$@" ;;
|
|
disable) shift; cmd_disable "$@" ;;
|
|
set) shift; cmd_set "$@" ;;
|
|
sync) shift; cmd_sync "$@" ;;
|
|
validate) shift; cmd_validate "$@" ;;
|
|
import) shift; cmd_import "$@" ;;
|
|
help|--help|-h|'') show_help ;;
|
|
*) error "Unknown command: $1"; show_help >&2; exit 1 ;;
|
|
esac
|
|
|
|
exit 0
|