npm-api.sh
Rama main · réplica de lectura del repositorio original.
#!/bin/bash
# Nginx Proxy Manager CLI Script
# Github [ https://github.com/Erreur32/nginx-proxy-manager-Bash-API ]
# By Erreur32 - July 2024
# NPM api https://github.com/NginxProxyManager/nginx-proxy-manager/tree/develop/backend/schema
# https://github.com/NginxProxyManager/nginx-proxy-manager/tree/develop/backend/schema/components
VERSION="3.6.9"
#################################
# This script allows you to manage Nginx Proxy Manager via the API. It provides
# functionalities such as creating proxy hosts, managing users, listing hosts,
# backing up configurations, and more.
#
#################################
# PERSISTENT Config
#################################
# Create config file $SCRIPT_DIR/npm-api.conf and Edit Variables (required)
# NGINX_IP="127.0.0.1"
# NGINX_PORT="81"
# API_USER="admin@example.com"
# API_PASS="changeme"
#
# Optional (only if you want in other placer than script directory)
# DATA_DIR="/path/nginx_backup/dir"
#
################################
# Optional (for a future version, not use)
# NGINX_PATH_DOCKER="/home/docker/nginx_proxy/nginx"
#################################
# Common Examples
#
# 1. Create a new proxy host:
# ./npm-api.sh --host-create example.com -i 192.168.1.10 -p 8080
#
# 2. Enable SSL for a host:
# ./npm-api.sh --host-ssl-enable 1
#
# 3. Create a new user:
# ./npm-api.sh --user-create admin admin@example.com password123
#
# 4. List all proxy hosts:
# ./npm-api.sh --host-list
#
# 5. Generate SSL certificate:
# ./npm-api.sh --cert-generate example.com admin@example.com
# ./npm-api.sh --cert-generate "*.example.com" \
# --dns-provider cloudflare --dns-credentials '{"dns_cloudflare_api_token":"your_token"}'
#
# 6. Show host details:
# ./npm-api.sh --host-show 1
#
# debug version
set -eu -o pipefail
#set -x # Active dbog
#set -eu -o pipefail
# Check if config file npm-api.conf exist
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="$SCRIPT_DIR/npm-api.conf"
################################
# Variables to Edit (required) #
# or create a config file #
################################
# set default variables
DEFAULT_NGINX_IP="127.0.0.1"
DEFAULT_NGINX_PORT="81"
DEFAULT_API_USER="admin@example.com"
DEFAULT_API_PASS="changeme"
DEFAULT_NGINX_PATH_DOCKER="/path/docker/npm"
# default backup directoy (You can override with your path)
DEFAULT_DATA_DIR="$SCRIPT_DIR/data"
################################
# DON'T TOUCH BELOW !!! ;)
################################
# Colors Custom
COLOR_GREEN="\033[32m"
COLOR_GREEN_b="\e[92m"
COLOR_RED="\033[91m"
COLOR_RED_FULL="\033[41;1m"
COLOR_ORANGE="\033[38;5;202m"
COLOR_YELLOW="\033[93m"
COLOR_CYAN="\033[36m"
COLOR_GREY="\e[90m"
COLOR_GRAY=" \e[100m"
WHITE_ON_GREEN="\033[30;48;5;83m"
CoR="\033[0m"
# load config file if exists
if [ -f "$CONFIG_FILE" ]; then
# First set default values
NGINX_IP="$DEFAULT_NGINX_IP"
NGINX_PORT="$DEFAULT_NGINX_PORT"
API_USER="$DEFAULT_API_USER"
API_PASS="$DEFAULT_API_PASS"
DATA_DIR="$DEFAULT_DATA_DIR"
NGINX_PATH_DOCKER="$DEFAULT_NGINX_PATH_DOCKER"
# Then load config file which will override defaults
source "$CONFIG_FILE"
# Finally set variables as read only
declare -r NGINX_IP
declare -r NGINX_PORT
declare -r API_USER
declare -r API_PASS
#declare -r DATA_DIR
# NGINX_PATH_DOCKER is optional, only make it readonly if it was set in config
if [ -n "${NGINX_PATH_DOCKER+x}" ]; then
declare -r NGINX_PATH_DOCKER
fi
else
# Use default values
NGINX_IP="$DEFAULT_NGINX_IP"
NGINX_PORT="$DEFAULT_NGINX_PORT"
API_USER="$DEFAULT_API_USER"
API_PASS="$DEFAULT_API_PASS"
DATA_DIR="$DEFAULT_DATA_DIR"
NGINX_PATH_DOCKER="$DEFAULT_NGINX_PATH_DOCKER"
# Check if using default API user
if [ "$API_USER" = "$DEFAULT_API_USER" ]; then
echo -e "\n⚠️ ${COLOR_RED}Using default API credentials - Please configure the script!${CoR}"
echo -e "\n📝 Create configuration file: $CONFIG_FILE with content:"
echo -e "${COLOR_GREY}NGINX_IP=\"$NGINX_IP\"${CoR} ${COLOR_YELLOW}(current default)${CoR}"
echo -e "${COLOR_GREY}NGINX_PORT=\"$NGINX_PORT\"${CoR} ${COLOR_YELLOW}(current default)${CoR}"
echo -e "${COLOR_RED}API_USER=\"admin@example.com\"${CoR} ${COLOR_RED}(required)${CoR}"
echo -e "${COLOR_RED}API_PASS=\"your_password\"${CoR} ${COLOR_RED}(required)${CoR}"
echo -e "${COLOR_GREY}DATA_DIR=\"$DATA_DIR\"${CoR} ${COLOR_YELLOW}(current default)${CoR}"
echo -e "\n❌ ${COLOR_RED}Cannot continue with default API credentials${CoR}\n"
exit 1
fi
fi
# API Endpoints
BASE_URL="http://$NGINX_IP:$NGINX_PORT/api"
# Set Token duration validity.
#TOKEN_EXPIRY="365d"
#TOKEN_EXPIRY="31536000s"
TOKEN_EXPIRY="1y"
# Default variables for creating a new proxy host (adapt to your needs)
# Note: These variables must match the names used in argument parsing and JSON payload generation
CACHING_ENABLED=false
BLOCK_EXPLOITS=true
ALLOW_WEBSOCKET_UPGRADE=false # Fixed: was '1' instead of 'false' (Issue #23)
HTTP2_SUPPORT=false # consistent true/false convention (Issue #23 class)
ADVANCED_CONFIG=""
LETS_ENCRYPT_AGREE=false
LETS_ENCRYPT_EMAIL=""
FORWARD_SCHEME="http"
FORCE_CERT_CREATION=false
SSL_FORCED=false
HSTS_ENABLED=false
HSTS_SUBDOMAINS=false
#DNS_PROVIDER=""
#DNS_API_KEY=""
# Don't touch below that line (or you know ...)
DEFAULT_EMAIL="$API_USER"
DOMAIN=""
DOMAIN_NAMES=""
FORWARD_HOST=""
FORWARD_PORT=""
CUSTOM_LOCATIONS=""
USERNAME=""
PASSWORD=""
EMAIL=""
HOST_ID=""
FIELD=""
VALUE=""
FIELD_VALUE=""
# Control variables
AUTO_YES=false
JSON_OUTPUT=false
LIST_JSON_COMMANDS=false
CHECK_TOKEN=false
CHECK_UPDATE=false
EXAMPLES=false
INFO=false
SHOW_HELP=false
SHOW_DEFAULT=false
USER_CREATE=false
USER_DELETE=false
USER_IS_ADMIN=false
USER_LIST=false
HOST_SHOW=false
HOST_LIST=false
HOST_LIST_FULL=false
HOST_SEARCH=false
HOST_UPDATE=false
HOST_ENABLE=false
HOST_SEARCHNAME=""
HOST_DISABLE=false
HOST_DELETE=false
HOST_ACL_ENABLE=false
HOST_ACL_DISABLE=false
HOST_CREATE=false
LIST_CERT_ALL=false
CERT_SHOW=false
CERT_GENERATE=false
CERT_DELETE=false
CERT_PURGE=false
CERT_DOWNLOAD=false
CERT_DOMAIN=""
CERT_EMAIL=""
# IDs / search term resolved by command parsers; initialized for set -u safety
CERT_ID=""
GENERATED_CERT_ID=""
USER_ID=""
search_term=""
# Cached API token (populated by check_token_notverbose) to avoid re-reading
# the token file on every curl. Curl headers use ${TOKEN:-$(cat "$TOKEN_FILE")}
# so an empty cache safely falls back to reading the file.
TOKEN=""
DNS_PROVIDER=""
CERT_DNS_PROVIDER=""
CERT_DNS_CREDENTIALS=""
DOMAIN_EXISTS=""
HOST_SSL_ENABLE=false
HOST_SSL_DISABLE=false
SSL_RESTORE=false
ACCESS_LIST=false
ACCESS_LIST_CREATE=false
ACCESS_LIST_UPDATE=false
ACCESS_LIST_DELETE=false
ACCESS_LIST_SHOW=false
ACCESS_LIST_ID=""
ACCESS_LIST_UPDATE_ARGS=()
ACCESS_LIST_CREATE_ARGS=()
BACKUP=false
BACKUP_LIST=false
### in progress
BACKUP_HOST=false
CLEAN_HOSTS=false
REDIRECT_HOST_LIST=false
REDIRECT_HOST_CREATE=false
REDIRECT_HOST_DELETE=false
REDIRECT_HOST_ENABLE=false
REDIRECT_HOST_DISABLE=false
REDIRECT_HOST_ID=""
FORWARD_DOMAIN=""
FORWARD_HTTP_CODE="301"
PRESERVE_PATH=false
STREAM_HOST_LIST=false
STREAM_HOST_CREATE=false
STREAM_HOST_DELETE=false
STREAM_HOST_ENABLE=false
STREAM_HOST_DISABLE=false
STREAM_HOST_ID=""
INCOMING_PORT=""
TCP_FORWARDING=false
UDP_FORWARDING=false
DEAD_HOST_LIST=false
DEAD_HOST_CREATE=false
DEAD_HOST_DELETE=false
DEAD_HOST_ENABLE=false
DEAD_HOST_DISABLE=false
DEAD_HOST_ID=""
if [ $# -eq 0 ]; then
INFO=true
#SHOW_HELP=true
fi
###############################################
# Check if necessary dependencies are installed
################################
# Function to check and create required directories
# Usage: check_dependencies
# Returns: 0 if successful, exits with 1 if error
################################
check_dependencies() {
#echo -e "\n 🔍 ${COLOR_CYAN}Checking system dependencies and directories...${CoR}"
################################
# 1. Check System Dependencies #
################################
local dependencies=("curl" "jq")
local missing=()
for dep in "${dependencies[@]}"; do
if ! command -v "$dep" &>/dev/null; then
missing+=("$dep")
fi
done
if [ ${#missing[@]} -gt 0 ]; then
echo -e " ⛔ ${COLOR_RED}Missing dependencies. Please install:${CoR}"
printf " - %s\n" "${missing[@]}"
exit 1
fi
#return 0
}
#check_dependencies
# Check if the Nginx Proxy Manager API is accessible
check_nginx_access() {
local max_retries=3
local retry_count=0
local timeout=5
#echo -e "\n🔍 Checking NPM availability..."
#echo -e " • Attempting to connect to: ${COLOR_YELLOW}$BASE_URL${CoR}"
#echo -e "\n ✅ Loading variables from file $CONFIG_FILE"
while [ $retry_count -lt $max_retries ]; do
# Try to connect with timeout
if curl --output /dev/null --silent --head --fail --connect-timeout $timeout --max-redirs 0 "$BASE_URL"; then
#echo -e " ✅ NPM is accessible at: ${COLOR_GREEN}$BASE_URL${CoR}"
local version_info=$(curl -s --max-redirs 0 "${BASE_URL%/api}/version")
return 0
fi
retry_count=$((retry_count + 1))
# Show retry message if not last attempt
if [ $retry_count -lt $max_retries ]; then
echo -e " ⏳ Attempt $retry_count/$max_retries - Retrying in ${timeout}s..."
sleep $timeout
fi
done
# If we get here, all attempts failed
echo -e "\n❌ ${COLOR_RED}ERROR: Cannot connect to NPM${CoR}"
echo -e "🔍 Details:"
echo -e " • URL: ${COLOR_YELLOW}$BASE_URL${CoR}"
echo -e " • Host: ${COLOR_YELLOW}$NGINX_IP${CoR}"
echo -e " • Port: ${COLOR_YELLOW}${NGINX_PORT}${CoR}"
echo -e "\n📋 Troubleshooting:"
echo -e " 1. Check if NPM is running"
echo -e " 2. Verify network connectivity"
echo -e " 3. Confirm NPM IP and port settings"
echo -e " 4. Check NPM logs for errors"
echo -e "\n💡 Command to check NPM status:"
echo -e " $ docker ps | grep nginx-proxy-manager"
echo -e " $ docker logs nginx-proxy-manager\n"
exit 1
}
################################
# Generate and/or validate token
# $1: boolean - true to display messages, false for silent mode
check_token() {
local verbose=${1:-false}
# PATH
###############################
[ "$verbose" = true ] && echo -e "\n 🔍 ${COLOR_CYAN}Checking system dependencies and directories...${CoR}"
check_dependencies
local ip_port_dir="${NGINX_IP//[.:]/_}_${NGINX_PORT}"
DATA_DIR_ID="$DATA_DIR/$ip_port_dir"
BACKUP_DIR="$DATA_DIR_ID/backups"
TOKEN_DIR="$DATA_DIR_ID/token"
TOKEN_FILE="$TOKEN_DIR/token.txt"
EXPIRY_FILE="$TOKEN_DIR/expiry.txt"
################################
# Create directories if they don't exist
################################
for dir in "$DATA_DIR_ID" "$TOKEN_DIR" "$BACKUP_DIR"; do
if [ ! -d "$dir" ]; then
[ "$verbose" = true ] && echo -e " 📢 ${COLOR_YELLOW}Creating directory: $dir${CoR}"
if ! mkdir -p "$dir" 2>/dev/null; then
echo -e "\n ${COLOR_RED}Error: Failed to create directory $dir${CoR}"
exit 1
fi
# Set proper permissions
chmod 755 "$dir" 2>/dev/null
fi
done
[ "$verbose" = true ] && echo -e " ✅ ${COLOR_GREEN}All dependencies and directories are properly set up${CoR}"
[ "$verbose" = true ] && echo -e " ${COLOR_GREY}├── System tools: OK${CoR}"
[ "$verbose" = true ] && echo -e " ${COLOR_GREY}├── Directories : OK${CoR}"
[ "$verbose" = true ] && echo -e " ${COLOR_GREY}└── Permissions : OK${CoR}"
[ "$verbose" = true ] && echo -e "\n 🔑 Checking token validity..."
# Check if token files exist and are readable
if [ ! -f "$TOKEN_FILE" ] || [ ! -f "$EXPIRY_FILE" ] ||
[ ! -r "$TOKEN_FILE" ] || [ ! -r "$EXPIRY_FILE" ] ||
[ ! -s "$TOKEN_FILE" ] || [ ! -s "$EXPIRY_FILE" ]; then
[ "$verbose" = true ] && echo -e " ⛔ ${COLOR_RED}Token files missing or unreadable. Generating new token...${CoR}"
generate_new_token
# Ensure files were created and are readable
if [ ! -f "$TOKEN_FILE" ] || [ ! -f "$EXPIRY_FILE" ] ||
[ ! -r "$TOKEN_FILE" ] || [ ! -r "$EXPIRY_FILE" ]; then
echo -e "\n ${COLOR_RED}Error: Failed to create or read token files${CoR}"
exit 1
fi
return
fi
# Read token and expiry
token=$(cat "$TOKEN_FILE" 2>/dev/null)
expires=$(cat "$EXPIRY_FILE" 2>/dev/null)
current_time=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
# Validate expiry date format
if ! date -d "$expires" >/dev/null 2>&1; then
[ "$verbose" = true ] && echo -e " ⛔ ${COLOR_RED}Invalid expiry date. Generating new token...${CoR}"
generate_new_token
return
fi
# Check if token is expired or will expire soon (1 hour)
local expiry_timestamp=$(date -d "$expires" +%s)
local current_timestamp=$(date -d "$current_time" +%s)
local time_diff=$((expiry_timestamp - current_timestamp))
if [ $time_diff -lt 3600 ]; then
[ "$verbose" = true ] && echo -e " ⚠️ ${COLOR_YELLOW}Token expires soon. Generating new token...${CoR}"
generate_new_token
return
fi
# Test token with API call
local test_response
test_response=$(curl -s -w "HTTPSTATUS:%{http_code}" \
-H "Authorization: Bearer $token" \
--connect-timeout 5 \
"$BASE_URL/tokens")
local http_status=$(echo "$test_response" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
if [ "${http_status:-0}" -eq 200 ]; then
[ "$verbose" = true ] && echo -e " ✅ ${COLOR_GREEN}Token is valid${CoR}"
[ "$verbose" = true ] && echo -e " 📅 Expires: ${COLOR_YELLOW}$expires${CoR}\n"
else
[ "$verbose" = true ] && echo -e " ⛔ ${COLOR_RED}Token is invalid. Generating new token...${CoR}\n"
generate_new_token
fi
return 0
}
# validate_token not verbose
check_token_notverbose() {
check_token false
# Cache the validated token once so subsequent curls reuse it (see $TOKEN)
TOKEN=$(cat "$TOKEN_FILE" 2>/dev/null)
}
################################
# Generate a new API token
generate_new_token() {
echo -e "\n 🔄 ${COLOR_YELLOW}Generating a new API token...${CoR}"
# check if API credentials are missing
if [ -z "$API_USER" ] || [ -z "$API_PASS" ]; then
echo -e " ❌ ${COLOR_RED}Error: API credentials are missing.${CoR}"
exit 1
fi
# check if NPM is accessible
if ! curl --output /dev/null --silent --head --fail --connect-timeout 5 "$BASE_URL"; then
echo -e "\n❌ ${COLOR_RED}ERROR: Cannot connect to NPM to generate token${CoR}"
echo -e "🔍 Please check if NPM is running and accessible at ${COLOR_YELLOW}$BASE_URL${CoR}\n"
exit 1
fi
# First get a temporary token
local temp_response=$(curl -s -w "\nHTTPSTATUS:%{http_code}" -X POST "$BASE_URL/tokens" \
-H "Content-Type: application/json" \
--data-raw "{\"identity\":\"$API_USER\",\"secret\":\"$API_PASS\"}")
local temp_body=$(echo "$temp_response" | sed -e 's/HTTPSTATUS\:.*//g')
local temp_status=$(echo "$temp_response" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
if [ "${temp_status:-0}" -ne 200 ]; then
echo -e " ❌ ${COLOR_RED}Failed to generate temporary token. Status: $temp_status${CoR}"
echo -e " 📝 Response: $temp_body"
exit 1
fi
# Extract the temporary token
local temp_token=$(echo "$temp_body" | jq -r '.token')
# Now get a long-term token using the temporary one
local response=$(curl -s -w "\nHTTPSTATUS:%{http_code}" -X GET "$BASE_URL/tokens?expiry=$TOKEN_EXPIRY" \
-H "Authorization: Bearer $temp_token" \
-H "Accept: application/json")
local body=$(echo "$response" | sed -e 's/HTTPSTATUS\:.*//g')
local status=$(echo "$response" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
if [ "${status:-0}" -ne 200 ]; then
echo -e " ❌ ${COLOR_RED}Failed to generate long-term token. Status: $status${CoR}"
echo -e " 📝 Response: $body"
exit 1
fi
# Extract token and expiry
local token=$(echo "$body" | jq -r '.token')
local expiry=$(echo "$body" | jq -r '.expires')
# Ensure token directory exists with proper permissions
if [ ! -d "$TOKEN_DIR" ]; then
mkdir -p "$TOKEN_DIR" 2>/dev/null
chmod 755 "$TOKEN_DIR" 2>/dev/null
fi
# Save token and expiry with proper permissions
echo "$token" >"$TOKEN_FILE"
echo "$expiry" >"$EXPIRY_FILE"
chmod 644 "$TOKEN_FILE" "$EXPIRY_FILE" 2>/dev/null
# Verify files were created successfully
if [ ! -f "$TOKEN_FILE" ] || [ ! -f "$EXPIRY_FILE" ] ||
[ ! -r "$TOKEN_FILE" ] || [ ! -r "$EXPIRY_FILE" ]; then
echo -e " ❌ ${COLOR_RED}Failed to save token files${CoR}"
exit 1
fi
echo -e " ✅ ${COLOR_GREEN}New token successfully generated and stored.${CoR}"
echo -e " 📅 Token expiry date: ${COLOR_YELLOW}$expiry${CoR}"
}
# Function to pad strings to a certain length
pad() {
local str="$1"
local len="$2"
local str_len=${#str}
local pad_len=$((len - str_len))
local padding=$(printf '%*s' "$pad_len" "")
echo "$str$padding"
}
################################
# Like pad() but truncates with an ellipsis when the string is longer than
# the requested width, so columns stay aligned even with long names.
# Always returns a string of exactly $len visible characters.
truncate_pad() {
local str="$1"
local len="$2"
if [ "${#str}" -gt "$len" ]; then
str="${str:0:$((len - 1))}…"
fi
local pad_len=$((len - ${#str}))
[ "$pad_len" -lt 0 ] && pad_len=0
printf '%s%*s' "$str" "$pad_len" ""
}
################################
# Colorize boolean values for display
colorize_boolean() {
local value="$1"
if [ "$value" = "true" ]; then
echo "${COLOR_GREEN}true${CoR}"
elif [ "$value" = "false" ]; then
echo "${COLOR_RED}false${CoR}"
else
# If value is neither true nor false, convert to boolean
if [ "$value" = "1" ] || [ "$value" = "yes" ] || [ "$value" = "on" ]; then
echo "${COLOR_GREEN}true${CoR}"
else
echo "${COLOR_RED}false${CoR}"
fi
fi
}
################################
# Colorize other boolean values for display
colorize_booleanh() {
local value=$1
if [ "$value" = https ]; then
echo -e "${COLOR_GREEN}https${CoR}"
else
echo -e "${COLOR_YELLOW}http${CoR}"
fi
}
# Function to sanitize names for directory
sanitize_name() {
local name=$1
echo "${name//[^a-zA-Z0-9]/_}"
}
################################
# Function to validate JSON files
validate_json() {
local file=$1
if ! jq empty "$file" 2>/dev/null; then
echo -e "\n ⛔ Invalid JSON detected in file: $file"
cat "$file" # Show file content for debug
return 1
fi
return 0
}
################################
# Display help
# Print one help line with its description aligned to a fixed column.
# Visible width ignores literal ANSI color sequences (\033[..m / \e[..m) and
# counts wide emoji (1 code point but 2 terminal cells) as 2.
HELP_DESC_COL=42
help_row() {
local left="$1" desc="$2"
local plain width pad g tmp
plain=$(printf '%s' "$left" | sed -E 's/\\(033|e)\[[0-9;]*m//g')
width=${#plain}
for g in 🆔 💾 🔖 🌍 🔗 🔒 🔄 ✨ 📥 📦 📜; do
tmp=${plain//"$g"/}
width=$((width + ${#plain} - ${#tmp}))
done
pad=$((HELP_DESC_COL - width))
if [ "$pad" -lt 1 ]; then
# Left part is wider than the description column: wrap the description onto
# the next line, aligned under the same column so everything stays tidy.
echo -e "${left}"
echo -e "$(printf '%*s' "$HELP_DESC_COL" '')${desc}"
else
echo -e "${left}$(printf '%*s' "$pad" '')${desc}"
fi
}
show_help() {
echo -e "\n Options available: ${COLOR_GREY}(see --examples for more details)${CoR}"
help_row " -y" "Automatic ${COLOR_YELLOW}yes${CoR} prompts!"
help_row " --json" "${COLOR_GREEN}🤖 ${CoR}Modifier: force ${COLOR_GREY}pure JSON output${CoR} on read commands (list/show), for scripts"
help_row " --json --list" "${COLOR_GREEN}🤖 ${CoR}List ${COLOR_GREY}the commands compatible with --json${CoR}"
help_row " --info" "Display ${COLOR_GREY}Script Variables Information${CoR}"
help_row " --show-default" "Show ${COLOR_GREY}Default settings for host creation${CoR}"
help_row " --check-token" "Check ${COLOR_GREY}current token info${CoR}"
help_row " --check-update" "${COLOR_GREEN}🆕 ${CoR}Check ${COLOR_GREY}GitHub for a newer script version${CoR}"
help_row " --backup" "${COLOR_GREEN}💾 ${CoR}Backup ${COLOR_GREY}All configurations to a different files in \$DATA_DIR${CoR}"
#echo -e " --clean-hosts ${COLOR_GREEN}📥 ${CoR}Reimport${CoR} ${COLOR_GREY}Clean Proxy ID and SSL ID in sqlite database ;)${CoR}"
help_row " --backup-host ${COLOR_CYAN}[id]${CoR}" "${COLOR_GREEN}📦 ${CoR}Backup ${COLOR_GREY}One proxy host (with SSL cert), or all if no id${CoR}"
help_row " --backup-host-list" "${COLOR_GREEN}📋 ${CoR}List ${COLOR_GREY}Available per-host proxy backups${CoR}"
#echo -e " --host-list-full > backup.txt 💾 ${COLOR_YELLOW}Export${CoR} Full host configuration to file"
echo ""
echo -e " Proxy Host Management:"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
help_row " --host-search ${COLOR_CYAN}domain${CoR}" "Search ${COLOR_GREY}Proxy host by ${COLOR_YELLOW}domain name${CoR}"
help_row " --host-list" "List ${COLOR_GREY}All Proxy hosts (to find ID)${CoR}"
help_row " --host-list-full" "List ${COLOR_GREY}All Proxy hosts with full details (${COLOR_YELLOW}JSON${COLOR_GREY})${CoR}"
help_row " --host-show ${COLOR_CYAN}🆔${CoR}" "Show ${COLOR_GREY}Full details for a specific host by ${COLOR_YELLOW}ID${CoR}"
echo ""
echo -e " --host-create ${COLOR_ORANGE}domain${CoR} ${COLOR_CYAN}-i ${COLOR_ORANGE}forward_host${CoR} ${COLOR_CYAN}-p ${COLOR_ORANGE}forward_port${CoR} [options]\n"
echo -e " ${COLOR_RED}Required:${CoR}"
help_row " domain" "Domain name (${COLOR_RED}required${CoR})"
help_row " ${COLOR_CYAN}-i${CoR} forward-host" "IP address or domain name of the target server (${COLOR_RED}required${CoR})"
help_row " ${COLOR_CYAN}-p${CoR} forward-port" "Port of the target server (${COLOR_RED}required${CoR})"
echo ""
echo -e " optional: ${COLOR_GREY}(Check default settings,no argument needed if already set!)${CoR}"
help_row " ${COLOR_CYAN}-f ${COLOR_GREY}FORWARD_SCHEME${CoR}" "Scheme for forwarding (http/https, default: $(colorize_booleanh "$FORWARD_SCHEME"))"
help_row " ${COLOR_CYAN}-c ${COLOR_GREY}CACHING_ENABLED${CoR}" "Enable caching (true/false, default: $(colorize_boolean "$CACHING_ENABLED"))"
help_row " ${COLOR_CYAN}-b ${COLOR_GREY}BLOCK_EXPLOITS${CoR}" "Block exploits (true/false, default: $(colorize_boolean "$BLOCK_EXPLOITS"))"
help_row " ${COLOR_CYAN}-w ${COLOR_GREY}ALLOW_WEBSOCKET_UPGRADE${CoR}" "Allow WebSocket upgrade (true/false, default: $(colorize_boolean "$ALLOW_WEBSOCKET_UPGRADE"))"
help_row " ${COLOR_CYAN}-l ${COLOR_GREY}CUSTOM_LOCATIONS${CoR}" "Custom locations (${COLOR_YELLOW}JSON array${CoR} of location objects)"
help_row " ${COLOR_CYAN}-a ${COLOR_GREY}ADVANCED_CONFIG${CoR}" "Advanced configuration (${COLOR_YELLOW}string${CoR})"
#echo -e " ${COLOR_CYAN}-h ${COLOR_GREY}HTTP2_SUPPORT${CoR} HTTP2 (true/false, default: $(colorize_boolean "$HTTP2_SUPPORT"))"
echo ""
help_row " --host-enable ${COLOR_CYAN}🆔${CoR}" "Enable Proxy host by ${COLOR_YELLOW}ID${CoR}"
help_row " --host-disable ${COLOR_CYAN}🆔${CoR}" "Disable Proxy host by ${COLOR_YELLOW}ID${CoR}"
help_row " --host-delete ${COLOR_CYAN}🆔${CoR}" "Delete Proxy host by ${COLOR_YELLOW}ID${CoR}"
help_row " --host-update ${COLOR_CYAN}🆔${CoR} ${COLOR_CYAN}[field]=value${CoR}" "Update One specific field of an existing proxy host by ${COLOR_YELLOW}ID${CoR}"
echo -e " (eg., --host-update 42 forward_host=foobar.local)${CoR}"
echo ""
help_row " --host-acl-enable ${COLOR_CYAN}🆔${CoR} ${COLOR_CYAN}access_list_id${CoR}" "Enable ACL for Proxy host by ${COLOR_YELLOW}ID${CoR} with Access List ID"
help_row " --host-acl-disable ${COLOR_CYAN}🆔${CoR}" "Disable ACL for Proxy host by ${COLOR_YELLOW}ID${CoR}"
help_row " --host-ssl-enable ${COLOR_CYAN}🆔${CoR} ${COLOR_CYAN}[cert_id]${CoR}" "Enable SSL for host ID optionally using specific certificate ID"
help_row " --host-ssl-disable ${COLOR_CYAN}🆔${CoR}" "Disable SSL, HTTP/2, and HSTS for a proxy host"
echo ""
help_row " --cert-list" "List ALL SSL certificates"
help_row " --cert-show ${COLOR_CYAN}domain${CoR} Or ${COLOR_CYAN}🆔${CoR}" "List SSL certificates filtered by [domain name] (add ${COLOR_YELLOW}--json${CoR} for raw JSON)"
help_row " --cert-delete ${COLOR_CYAN}domain${CoR} Or ${COLOR_CYAN}🆔${CoR} ${COLOR_CYAN}[--purge]${CoR}" "Delete Certificate for the given '${COLOR_YELLOW}domain${CoR}' (${COLOR_GREY}--purge${CoR} also removes on-disk files)"
help_row " --cert-download ${COLOR_CYAN}🆔${CoR} ${COLOR_CYAN}[output_dir]${CoR} ${COLOR_CYAN}[cert_name]${CoR}" "Download certificate as ZIP with fallback support"
help_row " --cert-generate ${COLOR_CYAN}domain${CoR} ${COLOR_CYAN}[email]${CoR}" "Generate Let's Encrypt Certificate or others Providers."
echo -e " • ${COLOR_YELLOW}Email:${CoR} positional [email] or ${COLOR_CYAN}--cert-email${CoR} email (default: \$DEFAULT_EMAIL), NPM < 2.13 only"
echo -e " - NPM >= 2.13 uses the email of the NPM user account"
echo -e " • ${COLOR_YELLOW}Standard domains:${CoR} example.com, sub.example.com"
echo -e " • ${COLOR_YELLOW}Wildcard domains:${CoR} *.example.com (requires DNS challenge)${CoR}"
echo -e " • DNS Challenge:${CoR} Required for wildcard certificates"
echo -e " - ${COLOR_YELLOW}Format:${CoR} --dns-provider PROVIDER --dns-credentials 'JSON' (or certbot INI text)"
echo -e " - ${COLOR_YELLOW}Providers:${CoR} dynu, cloudflare, digitalocean, godaddy, namecheap, route53, ovh, gcloud, hostinger, rcodezero, hoster.by, lws, tencentcloud-edgeone, ..."
echo -e " - ${COLOR_YELLOW}Cloudflare:${CoR} '{\"dns_cloudflare_api_token\":\"TOKEN\"}' (recommended, Zone:DNS:Edit)"
echo -e " or '{\"dns_cloudflare_email\":\"EMAIL\",\"dns_cloudflare_api_key\":\"GLOBAL_KEY\"}'"
echo -e " • ${COLOR_YELLOW}Reuse:${CoR} one wildcard cert for many hosts: --host-ssl-enable HOST_ID CERT_ID"
echo ""
echo ""
echo -e " Redirection Host Management:"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
help_row " --redirect-host-list" "List all Redirection Hosts"
echo -e " --redirect-host-create ${COLOR_ORANGE}domain${CoR} --forward-domain ${COLOR_ORANGE}target${CoR} [options]"
echo -e " Required:"
help_row " domain" "Source domain name"
help_row " --forward-domain ${COLOR_ORANGE}target${CoR}" "Target domain to redirect to"
echo -e " Optional:"
help_row " --forward-scheme ${COLOR_GREY}http|https${CoR}" "Scheme (default: http)"
help_row " --http-code ${COLOR_GREY}301|302|307...${CoR}" "HTTP redirect code (default: 301)"
help_row " --preserve-path ${COLOR_GREY}true|false${CoR}" "Keep URI path (default: false)"
help_row " --redirect-host-enable ${COLOR_CYAN}🆔${CoR}" "Enable Redirection Host by ${COLOR_YELLOW}ID${CoR}"
help_row " --redirect-host-disable ${COLOR_CYAN}🆔${CoR}" "Disable Redirection Host by ${COLOR_YELLOW}ID${CoR}"
help_row " --redirect-host-delete ${COLOR_CYAN}🆔${CoR}" "Delete Redirection Host by ${COLOR_YELLOW}ID${CoR}"
echo ""
echo ""
echo -e " Stream Host Management (TCP/UDP forwarding):"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
help_row " --stream-host-list" "List all Stream Hosts"
echo -e " --stream-host-create ${COLOR_ORANGE}incoming_port${CoR} --forward-host ${COLOR_ORANGE}host${CoR} --forward-port ${COLOR_ORANGE}port${CoR} [options]"
echo -e " Required:"
help_row " incoming_port" "Port to listen on"
help_row " --forward-host ${COLOR_ORANGE}host${CoR}" "Target host or IP"
help_row " --forward-port ${COLOR_ORANGE}port${CoR}" "Target port"
echo -e " Optional:"
help_row " --tcp" "Enable TCP forwarding"
help_row " --udp" "Enable UDP forwarding"
help_row " --cert-id ${COLOR_GREY}id${CoR}" "Certificate ID (default: 0)"
help_row " --stream-host-enable ${COLOR_CYAN}🆔${CoR}" "Enable Stream Host by ${COLOR_YELLOW}ID${CoR}"
help_row " --stream-host-disable ${COLOR_CYAN}🆔${CoR}" "Disable Stream Host by ${COLOR_YELLOW}ID${CoR}"
help_row " --stream-host-delete ${COLOR_CYAN}🆔${CoR}" "Delete Stream Host by ${COLOR_YELLOW}ID${CoR}"
echo ""
echo ""
echo -e " Dead Host (404) Management:"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
help_row " --dead-host-list" "List all Dead Hosts (404)"
echo -e " --dead-host-create ${COLOR_ORANGE}domain${CoR} [options]"
echo -e " Required:"
help_row " domain" "Domain name to serve 404 for"
echo -e " Optional:"
help_row " --cert-id ${COLOR_GREY}id${CoR}" "Certificate ID (default: 0)"
help_row " --ssl-forced ${COLOR_GREY}true|false${CoR}" "Force SSL (default: false)"
help_row " --http2 ${COLOR_GREY}true|false${CoR}" "Enable HTTP/2 (default: false)"
help_row " --hsts ${COLOR_GREY}true|false${CoR}" "Enable HSTS (default: false)"
help_row " --hsts-subdomains ${COLOR_GREY}true|false${CoR}" "HSTS subdomains (default: false)"
help_row " --advanced-config ${COLOR_GREY}string${CoR}" "Custom nginx config"
help_row " --dead-host-enable ${COLOR_CYAN}🆔${CoR}" "Enable Dead Host by ${COLOR_YELLOW}ID${CoR}"
help_row " --dead-host-disable ${COLOR_CYAN}🆔${CoR}" "Disable Dead Host by ${COLOR_YELLOW}ID${CoR}"
help_row " --dead-host-delete ${COLOR_CYAN}🆔${CoR}" "Delete Dead Host by ${COLOR_YELLOW}ID${CoR}"
echo ""
help_row " --user-list" "List All Users"
help_row " --user-create ${COLOR_CYAN}username${CoR} ${COLOR_CYAN}password${CoR} ${COLOR_CYAN}email${CoR} ${COLOR_CYAN}[--admin]${CoR}" "Create User (${COLOR_GREY}--admin${CoR} grants admin role; default standard)"
help_row " --user-delete ${COLOR_CYAN}🆔${CoR}" "Delete User by ${COLOR_YELLOW}username${CoR}"
echo ""
help_row " --access-list" "List All available Access Lists (ID and Name)"
help_row " --access-list-show ${COLOR_CYAN}🆔${CoR}" "Show detailed information for specific access list"
help_row " --access-list-create" "Create Access Lists with options:"
echo -e " • ${COLOR_YELLOW}--satisfy [any|all]${CoR} Set access list satisfaction mode"
echo -e " • ${COLOR_YELLOW}--pass-auth [true|false]${CoR} Enable/disable password authentication"
echo -e " • ${COLOR_YELLOW}--users \"user1,user2\"${CoR} List of users (comma-separated)"
echo -e " • ${COLOR_YELLOW}--allow \"ip1,ip2\"${CoR} List of allowed IPs/ranges"
echo -e " • ${COLOR_YELLOW}--deny \"ip1,ip2\"${CoR} List of denied IPs/ranges"
help_row " --access-list-delete ${COLOR_CYAN}🆔${CoR}" "Delete Access List by access ID"
help_row " --access-list-update ${COLOR_CYAN}🆔${CoR}" "Update Access List by access ID with options:"
echo -e " • ${COLOR_YELLOW}--name \"new_name\"${CoR} New name for the access list"
echo -e " • ${COLOR_YELLOW}--satisfy [any|all]${CoR} Update satisfaction mode"
echo -e " • ${COLOR_YELLOW}--pass-auth [true|false]${CoR} Update password authentication"
echo -e " • ${COLOR_YELLOW}--users \"user1,user2\"${CoR} Update list of users"
echo -e " • ${COLOR_YELLOW}--allow \"ip1,ip2\"${CoR} Update allowed IPs/ranges"
echo -e " • ${COLOR_YELLOW}--deny \"ip1,ip2\"${CoR} Update denied IPs/ranges\n"
#echo -e "\n ${COLOR_CYAN}🆔${CoR} = ID Host Proxy"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
help_row " --examples" "${COLOR_ORANGE}🔖 ${CoR}Examples ${COLOR_GREY}commands, more explicits${CoR}"
echo -e " --help ${COLOR_YELLOW} 👉 ${COLOR_GREY}It's me${CoR}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
exit 0
}
# List read commands compatible with the global --json modifier.
# Combine with --json (e.g. "--json --list") to get this list as raw JSON.
list_json_commands() {
local cmds=(
"--host-list|List all proxy hosts|./npm-api.sh --host-list --json"
"--host-list-full|List all proxy hosts with full details (always pure JSON)|./npm-api.sh --host-list-full"
"--host-show <id>|Show one proxy host|./npm-api.sh --host-show 42 --json"
"--cert-list|List all SSL certificates|./npm-api.sh --cert-list --json"
"--cert-show <domain_or_id>|Show certificate(s) filtered by domain or id|./npm-api.sh --cert-show example.com --json"
"--redirect-host-list|List all redirection hosts|./npm-api.sh --redirect-host-list --json"
"--stream-host-list|List all stream hosts|./npm-api.sh --stream-host-list --json"
"--dead-host-list|List all dead hosts|./npm-api.sh --dead-host-list --json"
"--access-list|List all access lists|./npm-api.sh --access-list --json"
"--access-list-show <id>|Show one access list|./npm-api.sh --access-list-show 5 --json"
)
if [ "$JSON_OUTPUT" = true ]; then
printf '%s\n' "${cmds[@]}" | jq -R -c 'split("|") | {command: .[0], description: .[1], example: .[2]}' | jq -s '.'
else
echo -e "\n${COLOR_ORANGE} 🤖 Commands compatible with --json:${CoR}\n"
local c flag desc example
for c in "${cmds[@]}"; do
IFS='|' read -r flag desc example <<<"$c"
help_row " $flag" "$desc ${COLOR_GREY}, e.g. $example${CoR}"
done
echo ""
fi
exit 0
}
################################
# Examples CLI Commands
examples_cli() {
echo -e "\n${COLOR_YELLOW}💡 Tips:${CoR}"
echo -e " • Use -y flag to skip confirmation prompts"
echo -e " • Check --help for complete command list"
echo -e " • Always backup before making major changes"
echo -e " • Use --host-list OR --host-search to find host IDs\n"
echo -e "\n${COLOR_YELLOW}🔰 Common Usage Examples:${CoR}"
# Basic commands
echo -e "\n${COLOR_GREEN}📋 Basic Commands:${CoR}"
echo -e "${COLOR_GREY} # List all hosts in table format${CoR}"
echo -e " $0 --host-list"
echo -e "${COLOR_GREY} # Show detailed information about a specific host${CoR}"
echo -e " $0 --host-show 42"
echo -e "${COLOR_GREY} # Display default settings${CoR}"
echo -e " $0 --show-default"
# Host Management
echo -e "\n${COLOR_GREEN}🌐 Host Management:${CoR}"
echo -e "${COLOR_GREY} # Create new proxy host (basic)${CoR}"
echo -e " $0 --host-create example.com -i 192.168.1.10 -p 8080"
echo -e "${COLOR_GREY} # Create host with SSL and advanced config${CoR}"
echo -e " $0 --host-create example.com -i 127.0.0.1 -p 6666 -f https -b true -c true --cert-generate example.com --host-ssl-enable -y"
echo -e "${COLOR_GREY} # Create host with custom locations${CoR}"
echo -e " $0 --host-create example.com -i 192.168.1.10 -p 8080 -l '[{\"path\":\"/api\",\"forward_host\":\"192.168.1.11\",\"forward_port\":8081}]'"
# SSL Management
echo -e "\n${COLOR_GREEN}🔒 SSL Certificate Management:${CoR}"
echo -e "${COLOR_GREY} # Generate new SSL certificate${CoR}"
echo -e " $0 --cert-generate example.com admin@example.com"
echo -e "${COLOR_GREY} # Enable SSL for existing host${CoR}"
echo -e " $0 --host-ssl-enable 42"
echo -e "${COLOR_GREY} # List all SSL certificates${CoR}"
echo -e " $0 --cert-show"
echo -e "${COLOR_GREY} # List all certificates${CoR}"
echo -e " $0 --cert-list"
echo -e "${COLOR_GREY} # Download certificate as ZIP${CoR}"
echo -e " $0 --cert-download 123"
echo -e "${COLOR_GREY} # Generate SSL certificate${CoR}"
echo -e " $0 --cert-generate example.com --cert-email admin@example.com"
# Add new section for Wildcard certificates
echo -e "\n ${COLOR_GREEN}🌟 Wildcard SSL Certificates with DNS Providers:${CoR}"
echo -e "${COLOR_GREY} # Generate wildcard certificate with ${COLOR_CYAN}Cloudflare${COLOR_GREY} API token (recommended, permission Zone:DNS:Edit)${CoR}"
echo -e " $0 --cert-generate \"*.example.com\" \\"
echo -e " --dns-provider cloudflare \\"
echo -e " --dns-credentials '{\"dns_cloudflare_api_token\":\"your_token\"}'"
echo -e "${COLOR_GREY} # Same with the Cloudflare Global API Key (37 hex chars)${CoR}"
echo -e " $0 --cert-generate \"*.example.com\" \\"
echo -e " --dns-provider cloudflare \\"
echo -e " --dns-credentials '{\"dns_cloudflare_email\":\"your@email.com\",\"dns_cloudflare_api_key\":\"your_global_api_key\"}'"
echo -e "\n${COLOR_GREY} # Generate wildcard certificate with ${COLOR_CYAN}DigitalOcean${CoR}"
echo -e " $0 --cert-generate \"*.example.com\" \\"
echo -e " --dns-provider digitalocean \\"
echo -e " --dns-credentials '{\"dns_digitalocean_token\":\"your_token\"}'"
echo -e "${COLOR_GREY} # Generate wildcard certificate with ${COLOR_CYAN}GoDaddy${CoR}"
echo -e " $0 --cert-generate \"*.example.com\" \\"
echo -e " --dns-provider godaddy \\"
echo -e " --dns-credentials '{\"dns_godaddy_key\":\"your_key\",\"dns_godaddy_secret\":\"your_secret\"}'"
echo -e "${COLOR_GREY} # Generate wildcard certificate with ${COLOR_CYAN}OVH${CoR}"
echo -e " $0 --cert-generate \"*.example.com\" \\"
echo -e " --dns-provider ovh \\"
echo -e " --dns-credentials '{\"dns_ovh_endpoint\":\"ovh-eu\",\"dns_ovh_application_key\":\"key\",\"dns_ovh_application_secret\":\"secret\",\"dns_ovh_consumer_key\":\"consumer_key\"}'"
echo -e "${COLOR_GREY} # Generate wildcard certificate with ${COLOR_CYAN}Dynu${CoR}"
echo -e " $0 --cert-generate \"*.example.com\" \\"
echo -e " --dns-provider dynu \\"
echo -e " --dns-credentials '{\"dns_dynu_auth_token\":\"your_token\"}'"
echo -e "\n${COLOR_GREY} # Reuse an existing wildcard certificate for a new host (no DNS credentials needed)${CoR}"
echo -e " $0 --cert-show example.com ${COLOR_GREY}# get the certificate ID${CoR}"
echo -e " $0 --host-create app.example.com -i 192.168.1.10 -p 8080 -y"
echo -e " $0 --host-ssl-enable <host_id> <cert_id>"
echo -e "${COLOR_GREY} # Note: NPM >= 2.13 registers Let's Encrypt with the email of the NPM user account (--cert-email is ignored)${CoR}"
# Redirection Host Management
echo -e "\n${COLOR_GREEN}🔀 Redirection Host Management:${CoR}"
echo -e "${COLOR_GREY} # List all redirection hosts${CoR}"
echo -e " $0 --redirect-host-list"
echo -e "${COLOR_GREY} # Create a simple 301 redirect${CoR}"
echo -e " $0 --redirect-host-create old.example.com --forward-domain new.example.com"
echo -e "${COLOR_GREY} # Create a 302 redirect preserving path${CoR}"
echo -e " $0 --redirect-host-create old.example.com --forward-domain new.example.com --http-code 302 --preserve-path true"
echo -e "${COLOR_GREY} # Delete a redirection host (with auto-confirm)${CoR}"
echo -e " $0 --redirect-host-delete 5 -y"
echo -e "${COLOR_GREY} # Enable / disable a redirection host${CoR}"
echo -e " $0 --redirect-host-enable 5"
echo -e " $0 --redirect-host-disable 5"
echo
# Stream Host Management
echo -e "\n${COLOR_GREEN}🔌 Stream Hosts:${CoR}"
echo -e "${COLOR_GREY} # List all stream hosts${CoR}"
echo -e " $0 --stream-host-list"
echo -e "${COLOR_GREY} # Forward DNS (TCP+UDP) port 53 to a Pi-hole container${CoR}"
echo -e " $0 --stream-host-create 53 --forward-host pihole --forward-port 53 --tcp --udp"
echo -e "${COLOR_GREY} # TCP-only forward${CoR}"
echo -e " $0 --stream-host-create 2222 --forward-host 192.168.1.10 --forward-port 22 --tcp"
echo -e "${COLOR_GREY} # Delete a stream host (with auto-confirm)${CoR}"
echo -e " $0 --stream-host-delete 5 -y"
echo -e "${COLOR_GREY} # Enable / disable a stream host${CoR}"
echo -e " $0 --stream-host-enable 5"
echo -e " $0 --stream-host-disable 5"
echo
# Dead Host (404) Management
echo -e "\n${COLOR_GREEN}🚫 Dead Hosts (404):${CoR}"
echo -e "${COLOR_GREY} # List all dead hosts${CoR}"
echo -e " $0 --dead-host-list"
echo -e "${COLOR_GREY} # Create a 404 host for a decommissioned domain${CoR}"
echo -e " $0 --dead-host-create old.example.com"
echo -e "${COLOR_GREY} # Create with SSL forced and a certificate${CoR}"
echo -e " $0 --dead-host-create old.example.com --cert-id 3 --ssl-forced true --http2 true"
echo -e "${COLOR_GREY} # Delete a dead host (with auto-confirm)${CoR}"
echo -e " $0 --dead-host-delete 5 -y"
echo -e "${COLOR_GREY} # Enable / disable a dead host${CoR}"
echo -e " $0 --dead-host-enable 5"
echo -e " $0 --dead-host-disable 5"
echo
# User Management
echo -e "\n${COLOR_GREEN}👤 User Management:${CoR}"
echo -e "${COLOR_GREY} # Create new user${CoR}"
echo -e " $0 --user-create john.doe secretpass john.doe@example.com"
echo -e "${COLOR_GREY} # List all users${CoR}"
echo -e " $0 --user-list"
# Access Control
echo -e "\n${COLOR_GREEN}🛡️ Access List Management Examples:${CoR}"
echo -e "${COLOR_GREY} # List all access lists${CoR}"
echo -e " $0 --access-list"
echo
echo -e "${COLOR_GREY} # Show detailed information for specific access list${CoR}"
echo -e " $0 --access-list-show 123"
echo
echo -e "${COLOR_GREY} # Create a basic access list${CoR}"
echo -e " $0 --access-list-create \"office\" --satisfy any"
echo
echo -e "${COLOR_GREY} # Create access list with authentication${CoR}"
echo -e " $0 --access-list-create \"secure_area\" --satisfy all --pass-auth true"
echo
echo -e "${COLOR_GREY} # Create access list with users${CoR}"
echo -e " $0 --access-list-create \"dev_team\" --users \"john,jane,bob\" --pass-auth true"
echo
echo -e "${COLOR_GREY} # Create access list with IP rules${CoR}"
echo -e " $0 --access-list-create \"internal\" --allow \"192.168.1.0/24\" --deny \"192.168.1.100\""
echo
echo -e "${COLOR_GREY} # Create comprehensive access list${CoR}"
echo -e " $0 --access-list-create \"full_config\" \\"
echo -e " --satisfy all \\"
echo -e " --pass-auth true \\"
echo -e " --users \"admin1,admin2\" \\"
echo -e " --allow \"10.0.0.0/8,172.16.0.0/12\" \\"
echo -e " --deny \"10.0.0.50,172.16.1.100\""
echo
echo -e "${COLOR_GREY} # Update existing access list${CoR}"
echo -e " $0 --access-list-update 123 --name \"new_name\" --satisfy any"
echo
echo -e "${COLOR_GREY} # Update access list with new allow rules${CoR}"
echo -e " $0 --access-list-update 123 --allow \"172.0.0.0/8,10.0.0.0/8\""
echo
echo -e "${COLOR_GREY} # Delete access list${CoR}"
echo -e " $0 --access-list-delete 123"
echo -e "${COLOR_GREY} # Enable ACL for a host${CoR}"
echo -e " $0 --host-acl-enable 42 2"
# Advanced Configuration
echo -e "${COLOR_GREY} # Create host with custom headers${CoR}"
echo -e " $0 --host-create example.com -i 192.168.1.10 -p 8080 -a 'proxy_set_header X-Real-IP \$remote_addr;'"
echo -e "${COLOR_GREY} # Update specific field of existing host${CoR}"
echo -e " $0 --host-update 42 forward_host=new.example.com"
echo -e "\n${COLOR_GREEN}⚙️ Advanced Configuration:${CoR}"
echo -e "${COLOR_GREY} # Create host with all options${CoR}"
echo -e " $0 --host-create example.com -i 192.168.1.10 -p 8080 \\"
echo -e " -f https \\"
echo -e " -c true \\"
echo -e " -b true \\"
echo -e " -w true \\"
echo -e " -h true \\"
echo -e " -s true \\"
echo -e " -a 'proxy_set_header X-Real-IP \$remote_addr;' \\"
echo -e " -l '[{\"path\":\"/api\",\"forward_host\":\"api.local\",\"forward_port\":3000}]'"
echo -e "\n${COLOR_YELLOW}📝 Command Parameters:${CoR}"
echo -e " domain : Domain name for the proxy host"
echo -e " -i, --forward-host : Target server (IP/hostname)"
echo -e " -p, --forward-port : Target port"
echo -e " -f, --forward-scheme : http/https"
echo -e " -c, --cache : Enable cache"
echo -e " -b, --block-exploits : Protection against exploits"
echo -e " -w, --websocket : WebSocket support"
echo -e " -h, --http2 : HTTP/2 support"
echo -e " -s, --ssl-force : Force SSL"
echo -e " -a, --advanced-config : Custom Nginx configuration"
echo -e " -l, --locations : Custom location rules (JSON)\n"
exit 0
}
################################
# Display script variables info
display_info() {
check_token true
echo -e "${COLOR_YELLOW}\n Script Info: ${COLOR_GREEN}${VERSION}${CoR}"
echo -e " ${COLOR_YELLOW}Script Variables Information:${CoR}"
#echo -e "\n ${COLOR_GREEN}DATA_DIR${CoR} ${DATA_DIR}"
echo -e " ${COLOR_GREEN}Config${CoR} : ${SCRIPT_DIR}/npm-api.conf"
echo -e " ${COLOR_GREEN}BASE URL${CoR} : ${BASE_URL}"
echo -e " ${COLOR_GREEN}NGINX IP${CoR} : ${NGINX_IP}"
echo -e " ${COLOR_GREEN}USER NPM${CoR} : ${API_USER}"
echo -e " ${COLOR_GREEN}BACKUP DIR ${CoR} : ${DATA_DIR_ID}"
#echo -e " ${COLOR_GREEN}DOCKER Path${CoR} : ${NGINX_PATH_DOCKER}"
display_dashboard
DATE=$(date +"_%Y_%m_%d__%H_%M_%S")
BACKUP_PATH="$BACKUP_DIR"
# Check if directory exists and contains files
if [ -d "$BACKUP_PATH" ] && [ -n "$(find "$BACKUP_PATH" -mindepth 1 -print -quit 2>/dev/null)" ]; then
# Count backup files by type
local total_files=$(find "$BACKUP_PATH" -type f \( \
-name "*.json" -o \
-name "*.conf" -o \
-name "*.log" -o \
-name "*.pem" -o \
-name "*.key" \
\) 2>/dev/null | wc -l)
if [ "$total_files" -gt 0 ]; then
echo -e "\n ${COLOR_CYAN}💾 Backup Statistics 📦${CoR}"
echo -e " ${COLOR_GREY}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${CoR}"
local config_files=$(find "$BACKUP_PATH" -maxdepth 1 -type f -name "full_config*.json" 2>/dev/null | wc -l)
local proxy_files=$(find "$BACKUP_PATH/.Proxy_Hosts" -type f -name "proxy_config.json" 2>/dev/null | wc -l)
local redirect_files=$(find "$BACKUP_PATH/.Redirection_Hosts" -type f -name "*.json" 2>/dev/null | wc -l)
local stream_files=$(find "$BACKUP_PATH/.Stream_Hosts" -type f -name "*.json" 2>/dev/null | wc -l)
local dead_files=$(find "$BACKUP_PATH/.Dead_Hosts" -type f -name "*.json" 2>/dev/null | wc -l)
local ssl_files=$(find "$BACKUP_PATH" -type f \( \
-name "certificate*.json" -o \
-name "*.pem" -o \
-name "*.key" \
\) 2>/dev/null | wc -l)
local access_files=$(find "$BACKUP_PATH/.access_lists" -type f -name "*.json" 2>/dev/null | wc -l)
local settings_files=$(find "$BACKUP_PATH/.settings" -type f -name "*.json" 2>/dev/null | wc -l)
local user_files=$(find "$BACKUP_PATH/.user" -type f -name "*.json" 2>/dev/null | wc -l)
# Show statistics only if there are files
[ "$config_files" -gt 0 ] && echo -e " • Full Config Files : ${COLOR_CYAN}$config_files${CoR}"
[ "$proxy_files" -gt 0 ] && echo -e " • Proxy Host Files : ${COLOR_CYAN}$proxy_files${CoR}"
[ "$redirect_files" -gt 0 ] && echo -e " • Redirection Files : ${COLOR_CYAN}$redirect_files${CoR}"
[ "$stream_files" -gt 0 ] && echo -e " • Stream Host Files : ${COLOR_CYAN}$stream_files${CoR}"
[ "$dead_files" -gt 0 ] && echo -e " • 404 Host Files : ${COLOR_CYAN}$dead_files${CoR}"
[ "$ssl_files" -gt 0 ] && echo -e " • SSL Files : ${COLOR_CYAN}$ssl_files${CoR}"
[ "$access_files" -gt 0 ] && echo -e " • Access Lists : ${COLOR_CYAN}$access_files${CoR}"
[ "$settings_files" -gt 0 ] && echo -e " • Settings Files : ${COLOR_CYAN}$settings_files${CoR}"
[ "$user_files" -gt 0 ] && echo -e " • User Files : ${COLOR_CYAN}$user_files${CoR}"
echo -e " • Total Files : ${COLOR_CYAN}$total_files${CoR}"
fi
fi
# Display backup locations
echo -e "\n 📂 ${COLOR_YELLOW}Backup Locations:${CoR}"
echo -e " • Backup: ${COLOR_GREY}$BACKUP_PATH${CoR}"
echo -e " • Token: ${COLOR_GREY}$BACKUP_PATH/token/${CoR}"
echo -e "\n ${COLOR_YELLOW}💡 Use --help to see available commands${CoR}"
echo -e " ${COLOR_GREY} Check --examples for more help examples${CoR}\n"
}
################################
# Check GitHub for a newer release of this script
# Usage: check_update
################################
check_update() {
check_dependencies
local repo="Erreur32/nginx-proxy-manager-Bash-API"
echo -e "\n 🔍 ${COLOR_CYAN}Checking for script updates...${CoR}"
local latest_json
latest_json=$(curl -s --max-time 5 "https://api.github.com/repos/$repo/releases/latest" 2>/dev/null || true)
local latest_version
latest_version=$(echo "$latest_json" | jq -r '.tag_name // empty' 2>/dev/null)
latest_version="${latest_version#v}"
if [ -z "$latest_version" ]; then
echo -e " ⛔ ${COLOR_RED}Unable to check for updates (no network or GitHub API unavailable).${CoR}\n"
return 1
fi
echo -e " • Current version : ${COLOR_YELLOW}${VERSION}${CoR}"
echo -e " • Latest version : ${COLOR_YELLOW}${latest_version}${CoR}"
if [ "$latest_version" = "$VERSION" ]; then
echo -e "\n ✅ ${COLOR_GREEN}You are running the latest version!${CoR}\n"
else
local html_url
html_url=$(echo "$latest_json" | jq -r '.html_url // empty' 2>/dev/null)
echo -e "\n 🆕 ${COLOR_ORANGE}A new version is available: ${COLOR_GREEN}${latest_version}${CoR}"
[ -n "$html_url" ] && echo -e " 🔗 ${COLOR_CYAN}${html_url}${CoR}"
echo -e " 💡 ${COLOR_GREY}Update: git -C \"$SCRIPT_DIR\" pull${CoR} ${COLOR_GREY}(or download the release manually)${CoR}\n"
fi
}
# Function to display dashboard
display_dashboard() {
#check_token_notverbose
echo -e "\n ${COLOR_CYAN}📊 NGINX - Proxy Manager - Dashboard 🔧 ${CoR}"
echo -e " ${COLOR_GREY}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${CoR}"
# Get all data first
local proxy_hosts=$(curl -s --max-redirs 0 -X GET "$BASE_URL/nginx/proxy-hosts" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
local redirection_hosts=$(curl -s --max-redirs 0 -X GET "$BASE_URL/nginx/redirection-hosts" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
local stream_hosts=$(curl -s --max-redirs 0 -X GET "$BASE_URL/nginx/streams" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
local dead_hosts=$(curl -s --max-redirs 0 -X GET "$BASE_URL/nginx/dead-hosts" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
local access_lists=$(curl -s --max-redirs 0 -X GET "$BASE_URL/nginx/access-lists" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
local certificates=$(curl -s --max-redirs 0 -X GET "$BASE_URL/nginx/certificates" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
local users=$(curl -s --max-redirs 0 -X GET "$BASE_URL/users" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
# Calculate counts with error checking
local proxy_count=0
local enabled_proxy_count=0
local redirect_count=0
local enabled_redirect_count=0
local stream_count=0
local enabled_stream_count=0
local dead_count=0
local enabled_dead_count=0
local cert_count=0
local expired_cert_count=0
local user_count=0
local access_list_count=0
# Check and calculate proxy hosts
if [ "$(echo "$proxy_hosts" | jq -r 'type')" == "array" ]; then
proxy_count=$(echo "$proxy_hosts" | jq '. | length')
enabled_proxy_count=$(echo "$proxy_hosts" | jq '[.[] | select(.enabled == true)] | length')
fi
local disabled_proxy_count=$((proxy_count - enabled_proxy_count))
# Check and calculate redirection hosts
if [ "$(echo "$redirection_hosts" | jq -r 'type')" == "array" ]; then
redirect_count=$(echo "$redirection_hosts" | jq '. | length')
enabled_redirect_count=$(echo "$redirection_hosts" | jq '[.[] | select(.enabled == true)] | length')
fi
local disabled_redirect_count=$((redirect_count - enabled_redirect_count))
# Check and calculate stream hosts
if [ "$(echo "$stream_hosts" | jq -r 'type')" == "array" ]; then
stream_count=$(echo "$stream_hosts" | jq '. | length')
enabled_stream_count=$(echo "$stream_hosts" | jq '[.[] | select(.enabled == true)] | length')
fi
local disabled_stream_count=$((stream_count - enabled_stream_count))
# Check and calculate 404 hosts (dead hosts)
if [ "$(echo "$dead_hosts" | jq -r 'type')" == "array" ]; then
dead_count=$(echo "$dead_hosts" | jq '. | length')
enabled_dead_count=$(echo "$dead_hosts" | jq '[.[] | select(.enabled == true)] | length')
fi
local disabled_dead_count=$((dead_count - enabled_dead_count))
# Check and calculate access lists
if [ "$(echo "$access_lists" | jq -r 'type')" == "array" ]; then
access_list_count=$(echo "$access_lists" | jq '. | length')
fi
# Check and calculate certificates
if [ "$(echo "$certificates" | jq -r 'type')" == "array" ]; then
cert_count=$(echo "$certificates" | jq '. | length')
expired_cert_count=$(echo "$certificates" | jq "$CERT_JQ_EXPIRED"' [.[] | select(is_expired)] | length')
fi
local valid_cert_count=$((cert_count - expired_cert_count))
# Check and calculate users
if [ "$(echo "$users" | jq -r 'type')" == "array" ]; then
user_count=$(echo "$users" | jq '. | length')
fi
# Get version and uptime
local uptime=$(uptime | sed 's/.*up \([^,]*\),.*/\1/')
local version_info=$(curl -s --max-redirs 0 -X GET "$BASE_URL/")
local npm_version="Unknown"
if echo "$version_info" | jq -e '.version' >/dev/null 2>&1; then
npm_version=$(echo "$version_info" | jq -r '"\(.version.major).\(.version.minor).\(.version.revision)"')
fi
print_row() {
local component="$1"
local status="$2"
local force_color="${3:-}" # Initialize with empty value by default
local status_color=""
# If a color is forced, use it
if [ -n "$force_color" ]; then
status_color="$force_color"
# Otherwise, apply automatic coloring logic
else
# Components that should not be colored green
case "$component" in
*"Disabled"* | *"Expired"* | *"Uptime"* | *"Version"*)
status_color=""
;;
*)
# For others, color green if > 0
if [[ "$status" =~ ^[0-9]+$ ]] && [ "$status" -gt 0 ]; then
status_color="$COLOR_GREEN"
fi
;;
esac
fi
# Calculate padding needed (20 is the max width of status column)
local status_length=${#status}
local padding=$((8 - status_length))
local spaces=$(printf '%*s' "$padding" '')
echo -e " ${COLOR_GREY}│${CoR} $component ${COLOR_GREY}│${CoR} ${status_color}$status${spaces}${CoR}${COLOR_GREY}│${CoR}"
}
echo -e " ${COLOR_GREY}┌─────────────────┬─────────┐${CoR}"
echo -e " ${COLOR_GREY}│${CoR} COMPONENT ${COLOR_GREY}│${CoR} STATUS ${COLOR_GREY}│${CoR}"
echo -e " ${COLOR_GREY}├─────────────────┼─────────┤${CoR}"
# --- Hosts (same order as the NPM web UI: Proxy / Redirection / Streams / 404) ---
# Proxy Hosts
print_row "🌐 Proxy Hosts " "$proxy_count" "$COLOR_YELLOW"
print_row "├─ Enabled " "$enabled_proxy_count"
print_row "└─ Disabled " "$disabled_proxy_count" "$COLOR_RED"
echo -e " ${COLOR_GREY}├─────────────────┼─────────┤${CoR}"
# Redirection Hosts
print_row "🔄 Redirections" "$redirect_count" "$COLOR_YELLOW"
print_row "├─ Enabled " "$enabled_redirect_count"
print_row "└─ Disabled " "$disabled_redirect_count" "$COLOR_RED"
echo -e " ${COLOR_GREY}├─────────────────┼─────────┤${CoR}"
# Stream Hosts
print_row "🔌 Stream Hosts" "$stream_count" "$COLOR_YELLOW"
print_row "├─ Enabled " "$enabled_stream_count"
print_row "└─ Disabled " "$disabled_stream_count" "$COLOR_RED"
echo -e " ${COLOR_GREY}├─────────────────┼─────────┤${CoR}"
# 404 Hosts (Dead Hosts)
print_row "🚫 404 Hosts " "$dead_count" "$COLOR_YELLOW"
print_row "├─ Enabled " "$enabled_dead_count"
print_row "└─ Disabled " "$disabled_dead_count" "$COLOR_RED"
echo -e " ${COLOR_GREY}├─────────────────┼─────────┤${CoR}"
# Access Lists
print_row "🔒 Access Lists" "$access_list_count"
echo -e " ${COLOR_GREY}├─────────────────┼─────────┤${CoR}"
# SSL Certificates
print_row "🔒 Certificates" "$cert_count" "$COLOR_YELLOW"
print_row "├─ Valid " "$valid_cert_count"
print_row "└─ Expired " "$expired_cert_count" "$COLOR_RED"
echo -e " ${COLOR_GREY}├─────────────────┼─────────┤${CoR}"
# Users
print_row "👥 Users " "$user_count"
echo -e " ${COLOR_GREY}├─────────────────┼─────────┤${CoR}"
# System
print_row "⏱️ Uptime " "$uptime" "$COLOR_YELLOW"
print_row "📦 NPM Version " "$npm_version" "$COLOR_YELLOW"
echo -e " ${COLOR_GREY}└─────────────────┴─────────┘${CoR}"
}
################################
# show_default
# Display default settings for creating hosts
show_default() {
check_token_notverbose
echo -e "\n📝 ${COLOR_YELLOW}Default Settings for Creating Hosts:${CoR}"
echo -e "\n ${COLOR_GREEN}Basic Settings:${CoR}"
echo -e " Forward Scheme: $(colorize_booleanh "$FORWARD_SCHEME")"
echo -e " Caching Enabled: $(colorize_boolean "$CACHING_ENABLED")"
echo -e " Block Exploits: $(colorize_boolean "$BLOCK_EXPLOITS")"
echo -e " Allow Websocket Upgrade: $(colorize_boolean "$ALLOW_WEBSOCKET_UPGRADE")"
echo -e "\n ${COLOR_GREEN}SSL Settings:${CoR}"
echo -e " HTTP/2 Support: $(colorize_boolean "$HTTP2_SUPPORT")"
echo -e " SSL Forced: $(colorize_boolean "$SSL_FORCED")"
echo -e " HSTS Enabled: $(colorize_boolean "$HSTS_ENABLED")"
echo -e " HSTS Subdomains: $(colorize_boolean "$HSTS_SUBDOMAINS")"
echo -e "\n ${COLOR_GREEN}Advanced Settings:${CoR}"
if [ -n "$ADVANCED_CONFIG" ]; then
echo -e " Advanced Config: ${COLOR_YELLOW}$ADVANCED_CONFIG${CoR}"
else
echo -e " Advanced Config: ${COLOR_GREY}Not configured${CoR}"
fi
if [ -n "$CUSTOM_LOCATIONS" ]; then
echo -e " Custom Locations: ${COLOR_YELLOW}$CUSTOM_LOCATIONS${CoR}"
else
echo -e " Custom Locations: ${COLOR_GREY}Not configured${CoR}"
fi
echo -e "\n ${COLOR_GREEN}System Settings:${CoR}"
echo -e " Base URL: $BASE_URL"
echo -e " Base Directory: $DATA_DIR"
echo -e " Token Directory: $TOKEN_DIR"
echo -e " Backup Directory: $BACKUP_DIR"
echo -e " Token Expiry: $TOKEN_EXPIRY\n"
exit 0
}
#######################################################################################
################################
# List all users
user_list() {
check_token_notverbose
echo -e "\n 👉 List of users..."
RESPONSE=$(curl -s -X GET "$BASE_URL/users" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
echo -e "\n $RESPONSE" | jq
exit 0
}
################################
# Create a new user
user_create() {
check_token_notverbose
if [ -z "$USERNAME" ] || [ -z "$PASSWORD" ] || [ -z "$EMAIL" ]; then
echo -e "\n 👤 ${COLOR_RED}The --user-create option requires username, password, and email.${CoR}"
echo -e " Usage: ${COLOR_ORANGE}$0 --user-create username password email [--admin]${CoR}"
echo -e " Example:"
echo -e " ${COLOR_GREEN}$0 --user-create john secretpass john@domain.com${CoR}\n"
return 1
fi
# check if user already exists
EXISTING_USERS=$(curl -s -X GET "$BASE_URL/users" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
# check if email already exists
if echo "$EXISTING_USERS" | jq -e --arg email "$EMAIL" '.[] | select(.email == $email)' >/dev/null; then
echo -e "\n ⛔ ${COLOR_RED}Error: A user with email $EMAIL already exists.${CoR}"
return 1
fi
# check if username already exists
if echo "$EXISTING_USERS" | jq -e --arg name "$USERNAME" '.[] | select(.name == $name)' >/dev/null; then
echo -e "\n ⛔ ${COLOR_RED}Error: A user with name $USERNAME already exists.${CoR}"
return 1
fi
# create user
# Standard user by default; admin only when --admin was passed
if [ "$USER_IS_ADMIN" = true ]; then
ROLES_JSON='["admin"]'
echo -e "\n 👤 Creating ${COLOR_YELLOW}admin${CoR} user ${COLOR_GREEN}$USERNAME${CoR}..."
else
ROLES_JSON='[]'
echo -e "\n 👤 Creating user ${COLOR_GREEN}$USERNAME${CoR}..."
fi
DATA=$(jq -n \
--arg username "$USERNAME" \
--arg password "$PASSWORD" \
--arg email "$EMAIL" \
--arg name "$USERNAME" \
--arg nickname "$USERNAME" \
--arg secret "$PASSWORD" \
--argjson roles "$ROLES_JSON" \
'{
name: $name,
nickname: $nickname,
email: $email,
roles: $roles,
is_disabled: false,
auth: {
type: "password",
secret: $secret
}
}')
# send data to API
RESPONSE=$(curl -s -w "HTTPSTATUS:%{http_code}" -X POST "$BASE_URL/users" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" \
-H "Content-Type: application/json; charset=UTF-8" \
--data-raw "$DATA")
HTTP_BODY=${RESPONSE//HTTPSTATUS:*/}
HTTP_STATUS=${RESPONSE##*HTTPSTATUS:}
if [ "${HTTP_STATUS:-0}" -eq 201 ]; then
# get user id
USERS_RESPONSE=$(curl -s -X GET "$BASE_URL/users" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
USER_ID=$(echo "$USERS_RESPONSE" | jq -r --arg email "$EMAIL" --arg name "$USERNAME" \
'.[] | select(.email == $email and .name == $name) | .id')
if [ -n "$USER_ID" ]; then
echo -e " ✅ ${COLOR_GREEN}User $USERNAME created successfully!${CoR}"
echo -e " 📧 Email: ${COLOR_YELLOW}$EMAIL${CoR}"
echo -e " 🆔 User ID: ${COLOR_YELLOW}$USER_ID${CoR}\n"
else
echo -e " ⚠️ ${COLOR_GREEN}User created but couldn't fetch ID${CoR}"
echo -e " 📧 Email: ${COLOR_YELLOW}$EMAIL${CoR}\n"
fi
return 0
else
echo -e " ⛔ ${COLOR_RED}Failed to create user. Status: $HTTP_STATUS${CoR}"
echo -e " Error: ${COLOR_RED}$HTTP_BODY${CoR}\n"
return 1
fi
}
################################
# Delete a user
# $1: user_id - ID of the user to delete
user_delete() {
local USER_ID="$1"
if [ -z "$USER_ID" ]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: User ID is required${CoR}"
echo -e " Usage : ${COLOR_ORANGE}$0 --user-delete <user_id> [-y]${CoR}"
echo -e " Example: ${COLOR_GREEN}$0 --user-delete 42${CoR}"
exit 1
fi
# Check if USER_ID is a number
if ! [[ "$USER_ID" =~ ^[0-9]+$ ]]; then
echo -e " ⛔ ${COLOR_RED}ERROR: Invalid user ID '$USER_ID' - must be a number${CoR}"
echo -e " Usage : ${COLOR_ORANGE}$0 --user-delete <user_id> [-y]${CoR}"
echo -e " Example: ${COLOR_GREEN}$0 --user-delete 42${CoR}"
exit 1
fi
check_token_notverbose
# Get user details first
local RESPONSE=$(curl -s -X GET "$BASE_URL/users/$USER_ID" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
if [ "$(echo "$RESPONSE" | jq -r '.error.code // empty')" = "404" ]; then
echo -e " ⛔ ${COLOR_RED}User ID $USER_ID not found${CoR}"
exit 1
fi
local USERNAME=$(echo "$RESPONSE" | jq -r '.name')
local EMAIL=$(echo "$RESPONSE" | jq -r '.email')
if [ "$AUTO_YES" = true ]; then
echo -e " 🔔 Auto-confirming deletion of user '$USERNAME' (ID: $USER_ID)..."
else
echo -e " ┌───────────────────────────────────────────"
echo -e " │ ID: ${COLOR_YELLOW}$USER_ID${CoR}"
echo -e " │ Name: ${COLOR_GREEN}$USERNAME${CoR}"
echo -e " │ Email: ${COLOR_CYAN}$EMAIL${CoR}"
echo -e " └───────────────────────────────────────────"
echo -e " ⚠️ ${COLOR_RED}WARNING: This action cannot be undone!${CoR}"
read -n 1 -r -p " 🔔 Confirm deletion? (y/n): " CONFIRM
echo
if [[ ! $CONFIRM =~ ^[Yy]$ ]]; then
echo -e " ❌ ${COLOR_RED}Operation cancelled${CoR}"
exit 1
fi
fi
# Delete user
RESPONSE=$(curl -s -X DELETE "$BASE_URL/users/$USER_ID" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
if [ "$RESPONSE" = "true" ]; then
echo -e " ✅ ${COLOR_GREEN}User '$USERNAME' (ID: $USER_ID) deleted successfully!${CoR}"
else
echo -e " ⛔ ${COLOR_RED}Failed to delete user.${CoR}"
if [ -n "$RESPONSE" ]; then
echo -e " Error: $RESPONSE"
fi
exit 1
fi
}
################################
# Check if a certificate exists and is valid
check_certificate_exists() {
local DOMAIN="$1"
local RESPONSE
local CERT_ID
RESPONSE=$(curl -s -X GET "$BASE_URL/nginx/certificates" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
if [[ "$DOMAIN" == \** ]]; then
CERT_ID=$(echo "$RESPONSE" | jq -r --arg domain "$DOMAIN" \
'.[] | select(.domain_names[] == $domain) | .id' | sort -n | tail -n1)
else
local BASE_DOMAIN="${DOMAIN#\*\.}"
CERT_ID=$(echo "$RESPONSE" | jq -r --arg domain "$BASE_DOMAIN" \
'.[] | select(
(.domain_names[] == $domain) or
(.domain_names[] | startswith("*.") and ($domain | endswith(.[2:]))) or
($domain | startswith("*.") and (.domain_names[] | endswith(.[2:])))
) | .id' | sort -n | tail -n1)
fi
if [ -n "$CERT_ID" ]; then
return 0
else
return 1
fi
}
##############################################################
# Function to delete all existing proxy hosts # DEBUG
##############################################################
# Delete all existing proxy hosts
delete_all_proxy_hosts() {
check_token_notverbose
echo -e "\n 🗑️ ${COLOR_ORANGE}Deleting all existing proxy hosts...${CoR}"
# Get all host IDs
local existing_hosts=$(curl -s -X GET "$BASE_URL/nginx/proxy-hosts" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" | jq -r '.[].id')
local count=0
for host_id in $existing_hosts; do
echo -e " • Deleting host ID ${COLOR_CYAN}$host_id${CoR}...${COLOR_GREEN}✓${CoR}"
local response=$(curl -s -w "HTTPSTATUS:%{http_code}" -X DELETE "$BASE_URL/nginx/proxy-hosts/$host_id" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
local http_body=$(echo "$response" | sed -e 's/HTTPSTATUS\:.*//g')
local http_status=$(echo "$response" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
if [ "${http_status:-0}" -ne 200 ]; then
echo -e " ⛔ ${COLOR_RED}Failed to delete host ID $host_id. HTTP status: $http_status. Response: $http_body${CoR}"
return 1
fi
count=$((count + 1))
done
echo -e " ✅ ${COLOR_GREEN}Successfully deleted $count proxy hosts!${CoR}"
return 0
}
################################
# --clean_hosts from_backup_file
# Function to reimport hosts from backup file
# Create a safety backup before major operations
create_safety_backup() {
check_token_notverbose
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
SAFETY_BACKUP="$BACKUP_DIR/pre_reimport_SAFETY_BACKUP_${TIMESTAMP}.json"
echo -e "📦 Creating safety backup..."
# Get the list of IDs
local host_ids=$(curl -s -X GET "$BASE_URL/nginx/proxy-hosts" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" | jq -r '.[].id')
# Create a table to store the complete configurations
echo "[" >"$SAFETY_BACKUP"
local first=true
# Get the detailed configuration of each host
for id in $host_ids; do
if [ "$first" = true ]; then
first=false
else
echo "," >>"$SAFETY_BACKUP"
fi
curl -s -X GET "$BASE_URL/nginx/proxy-hosts/$id" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" >>"$SAFETY_BACKUP"
done
echo "]" >>"$SAFETY_BACKUP"
# Verify that the backup is valid
if ! jq empty "$SAFETY_BACKUP" 2>/dev/null; then
echo -e " ❌ ${COLOR_RED}Failed to create valid safety backup${CoR}"
exit 1
fi
echo -e " ✅ ${COLOR_GREEN}Safety backup created: ${COLOR_CYAN}$SAFETY_BACKUP${CoR}"
return 0
}
################################
# Function to display import summary
display_import_summary() {
echo -e "\n📊 ${COLOR_YELLOW}Import Summary:${CoR}"
echo -e " • Total hosts processed: ${COLOR_CYAN}$total${CoR}"
echo -e " • Successfully imported: ${COLOR_GREEN}$success${CoR}"
echo -e " • Failed imports: ${COLOR_RED}$failed${CoR}"
echo -e "\n🔒 SSL Certificates:"
echo -e " • Successfully configured: ${COLOR_GREEN}$ssl_success${CoR}"
echo -e " • Failed configurations: ${COLOR_RED}$ssl_failed${CoR}"
if [ ${#failed_ssl_domains[@]} -gt 0 ]; then
echo -e "\n⚠️ ${COLOR_YELLOW}Domains with failed SSL setup:${CoR}"
for domain in "${failed_ssl_domains[@]}"; do
echo -e " • $domain"
done
fi
echo -e "\n✨ ${COLOR_GREEN}Import process completed${CoR}"
}
################################
# jq helper: the NPM API has no `expired` field, derive it from expires_on
# ("YYYY-MM-DD HH:MM:SS" on SQLite, ISO 8601 on MySQL/Postgres)
CERT_JQ_EXPIRED='def is_expired: if has("expired") then .expired == true else ((.expires_on // "")[0:19] | sub(" "; "T") + "Z" | try fromdateiso8601 catch null) as $t | ($t != null and $t < now) end;'
################################
# Shared jq format + colorizer for certificate listings (used by cert_show and
# list_cert_all). CERT_JQ_FMT formats one certificate object; cert_colorize
# reads that output on stdin and colors the Status line.
CERT_JQ_FMT='" 🔒 ID: \(.id)\n • Domain(s): \(.domain_names | join(", "))\n • Provider: \(.provider)\n • Created on: \(.created_on // "N/A")\n • Expires on: \(.expires_on // "N/A")\n • Status: \(if is_expired then "❌ EXPIRED" else if .expires_on then "✅ VALID" else "⚠️ PENDING" end end)"'
cert_colorize() {
while IFS= read -r line; do
if [[ $line == *"❌ EXPIRED"* ]]; then
echo -e "${line/❌ EXPIRED/${COLOR_RED}❌ EXPIRED${CoR}}"
elif [[ $line == *"✅ VALID"* ]]; then
echo -e "${line/✅ VALID/${COLOR_GREEN}✅ VALID${CoR}}"
elif [[ $line == *"⚠️ PENDING"* ]]; then
echo -e "${line/⚠️ PENDING/${COLOR_YELLOW}⚠️ PENDING${CoR}}"
else
echo -e "$line"
fi
done
}
################################
# Function to list SSL certificates by ID or domain
cert_show() {
local search_term="$1"
check_token_notverbose
# If no search term is provided, show usage
if [ -z "$search_term" ]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: Missing argument${CoR}"
echo -e " Usage: "
echo -e " ${COLOR_ORANGE}$0 --cert-show <domain>${CoR} 🔍 Search by domain name"
echo -e " ${COLOR_ORANGE}$0 --cert-show <id>${CoR} 🔢 Search by ID"
echo -e " ${COLOR_ORANGE}$0 --cert-show-all${CoR} 📜 List all certificates\n"
exit 1
fi
# Get all certificates
RESPONSE=$(curl -s -X GET "$BASE_URL/nginx/certificates" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
if [ -z "$RESPONSE" ] || [ "$RESPONSE" == "null" ]; then
echo -e " ⛔ ${COLOR_RED}Error: Unable to retrieve certificates${CoR}"
exit 1
fi
# Search by ID if numeric
if [[ "$search_term" =~ ^[0-9]+$ ]]; then
[ "$JSON_OUTPUT" = true ] || echo -e "\n 🔍 Searching for certificate with ID: ${COLOR_YELLOW}$search_term${CoR}"
# Get specific certificate by ID
CERT_RESPONSE=$(curl -s -X GET "$BASE_URL/nginx/certificates/$search_term" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
if echo "$CERT_RESPONSE" | jq -e '.error' >/dev/null; then
if [ "$JSON_OUTPUT" = true ]; then
echo '{"error": "Certificate not found with ID: '"$search_term"'"}'
exit 1
fi
echo -e " ⛔ ${COLOR_RED}Certificate not found with ID: $search_term${CoR}"
else
if [ "$JSON_OUTPUT" = true ]; then
echo "$CERT_RESPONSE" | jq .
exit 0
fi
echo "$CERT_RESPONSE" | jq -r "$CERT_JQ_EXPIRED $CERT_JQ_FMT" | cert_colorize
fi
echo ""
return 0
fi
# Search by domain name (partial match)
if [ "$JSON_OUTPUT" = true ]; then
echo "$RESPONSE" | jq --arg domain "$search_term" '[.[] | select(.domain_names[] | contains($domain))]'
exit 0
fi
echo -e "\n 🔍 Searching certificates for domain: ${COLOR_YELLOW}$search_term${CoR}"
DOMAIN_CERTS=$(echo "$RESPONSE" | jq -r --arg domain "$search_term" \
'.[] | select(.domain_names[] | contains($domain))')
if [ -z "$DOMAIN_CERTS" ]; then
echo -e " ℹ️ ${COLOR_YELLOW}No certificates found for domain: $search_term${CoR}"
else
echo "$DOMAIN_CERTS" | jq -r "$CERT_JQ_EXPIRED $CERT_JQ_FMT" | cert_colorize
echo ""
fi
}
################################
# Function to list all SSL certificates
list_cert_all() {
check_token_notverbose
# Get all certificates
RESPONSE=$(curl -s -X GET "$BASE_URL/nginx/certificates" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
if [ -z "$RESPONSE" ] || [ "$RESPONSE" == "null" ]; then
echo -e " ⛔ ${COLOR_RED}Error: Unable to retrieve certificates${CoR}"
exit 1
fi
if [ "$JSON_OUTPUT" = true ]; then
echo "$RESPONSE" | jq .
exit 0
fi
echo -e "\n 📜 SSL Certificates List:"
# Check if there are any certificates
if [ "$RESPONSE" = "[]" ]; then
echo -e " ℹ️ ${COLOR_YELLOW}No certificates found${CoR}"
return 0
fi
# Process and display all certificates
echo "$RESPONSE" | jq -r "$CERT_JQ_EXPIRED .[] | $CERT_JQ_FMT" | cert_colorize
# Display statistics
TOTAL_CERTS=$(echo "$RESPONSE" | jq '. | length')
EXPIRED_CERTS=$(echo "$RESPONSE" | jq "$CERT_JQ_EXPIRED"' [.[] | select(is_expired)] | length')
VALID_CERTS=$((TOTAL_CERTS - EXPIRED_CERTS))
echo -e "\n 📊 Statistics"
echo -e " Total certs: ${COLOR_YELLOW}$TOTAL_CERTS${CoR}"
echo -e " • ${COLOR_GREEN}Valid${CoR} : ${COLOR_GREEN}$VALID_CERTS${CoR}"
echo -e " • ${COLOR_RED}Expired${CoR} : ${COLOR_RED}$EXPIRED_CERTS${CoR}\n"
}
################################
# Function to download certificate as ZIP with fallback support
cert_download() {
local cert_id="$1"
local output_dir="${2:-./certificates}"
local cert_name="${3:-certificate_$cert_id}"
check_token_notverbose
if [ -z "$cert_id" ]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: Certificate ID is required${CoR}"
echo -e " Usage: "
echo -e " ${COLOR_ORANGE}$0 --cert-download <cert_id> [output_dir] [cert_name]${CoR}"
echo -e " ${COLOR_ORANGE}$0 --cert-download 123 ./certs mydomain${CoR}\n"
exit 1
fi
# Create output directory
mkdir -p "$output_dir"
echo -e "\n 🔒 Downloading certificate (ID: ${COLOR_YELLOW}$cert_id${CoR})"
echo -e " Output directory: ${COLOR_CYAN}$output_dir${CoR}"
echo -e " Certificate name: ${COLOR_CYAN}$cert_name${CoR}"
# Get certificate metadata first
local CERT_META
CERT_META=$(curl -s -X GET "$BASE_URL/nginx/certificates/$cert_id" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
if [ -z "$CERT_META" ] || ! echo "$CERT_META" | jq empty 2>/dev/null; then
echo -e " ⛔ ${COLOR_RED}Error: Unable to retrieve certificate metadata${CoR}"
exit 1
fi
# Check if certificate exists
if echo "$CERT_META" | jq -e '.error' >/dev/null; then
echo -e " ⛔ ${COLOR_RED}Certificate not found with ID: $cert_id${CoR}"
exit 1
fi
# Extract certificate information
local domain_names=$(echo "$CERT_META" | jq -r '.domain_names | join(", ")')
local provider=$(echo "$CERT_META" | jq -r '.provider // "Unknown"')
local expires_on=$(echo "$CERT_META" | jq -r '.expires_on // "N/A"')
echo -e " Domain(s): ${COLOR_YELLOW}$domain_names${CoR}"
echo -e " Provider: ${COLOR_YELLOW}$provider${CoR}"
echo -e " Expires: ${COLOR_YELLOW}$expires_on${CoR}"
# Try new API first (JSON format)
echo -e "\n 🔄 Trying new API format..."
local CERT_CONTENT
CERT_CONTENT=$(curl -s -X GET "$BASE_URL/nginx/certificates/$cert_id/certificates" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" \
-H "Accept: application/json")
if [ -n "$CERT_CONTENT" ] && echo "$CERT_CONTENT" | jq empty 2>/dev/null; then
# Check if we got valid certificate data
local cert_data=$(echo "$CERT_CONTENT" | jq -r '.certificate // empty')
local private_data=$(echo "$CERT_CONTENT" | jq -r '.private // empty')
if [ -n "$cert_data" ] && [ -n "$private_data" ]; then
echo -e " ✅ ${COLOR_GREEN}New API format successful${CoR}"
# Save certificate files
echo "$cert_data" >"$output_dir/${cert_name}.crt"
echo "$private_data" >"$output_dir/${cert_name}.key"
chmod 600 "$output_dir/${cert_name}.key"
# Save intermediate certificate if available
local intermediate_data=$(echo "$CERT_CONTENT" | jq -r '.intermediate // empty')
if [ -n "$intermediate_data" ]; then
echo "$intermediate_data" >"$output_dir/${cert_name}.chain.crt"
fi
# Save metadata
echo "$CERT_META" | jq '.' >"$output_dir/${cert_name}_metadata.json"
# Create ZIP file
local zip_file="$output_dir/${cert_name}_certificate.zip"
cd "$output_dir" || exit 1
zip -q "$zip_file" "${cert_name}.crt" "${cert_name}.key" "${cert_name}_metadata.json"
[ -f "${cert_name}.chain.crt" ] && zip -q "$zip_file" "${cert_name}.chain.crt"
cd - >/dev/null || exit 1
echo -e " ✅ ${COLOR_GREEN}Certificate downloaded successfully${CoR}"
echo -e " Files created:"
echo -e " • ${COLOR_CYAN}${cert_name}.crt${CoR} (Certificate)"
echo -e " • ${COLOR_CYAN}${cert_name}.key${CoR} (Private Key)"
[ -f "$output_dir/${cert_name}.chain.crt" ] && echo -e " • ${COLOR_CYAN}${cert_name}.chain.crt${CoR} (Chain)"
echo -e " • ${COLOR_CYAN}${cert_name}_metadata.json${CoR} (Metadata)"
echo -e " • ${COLOR_CYAN}${cert_name}_certificate.zip${CoR} (ZIP Archive)"
return 0
fi
fi
# Fallback to old API (ZIP download)
echo -e " ⚠️ ${COLOR_YELLOW}New API failed, trying legacy ZIP download...${CoR}"
local TMP_DIR=$(mktemp -d)
local zip_file="$TMP_DIR/certificate.zip"
# Download ZIP file
local download_response
download_response=$(curl -s -w "%{http_code}" -X GET "$BASE_URL/nginx/certificates/$cert_id/download" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" \
-o "$zip_file")
local http_code="${download_response: -3}"
if [ "$http_code" = "200" ] && [ -f "$zip_file" ] && [ -s "$zip_file" ]; then
echo -e " ✅ ${COLOR_GREEN}Legacy ZIP download successful${CoR}"
# Extract ZIP file
if command -v unzip >/dev/null 2>&1; then
unzip -q "$zip_file" -d "$TMP_DIR"
# Find certificate files with wildcards (supporting cert8.pem, cert9.pem, etc.)
local cert_file=$(find "$TMP_DIR" -name "cert*.pem" | head -1)
local key_file=$(find "$TMP_DIR" -name "privkey*.pem" | head -1)
local chain_file=$(find "$TMP_DIR" -name "chain*.pem" | head -1)
local fullchain_file=$(find "$TMP_DIR" -name "fullchain*.pem" | head -1)
if [ -n "$cert_file" ] && [ -n "$key_file" ]; then
# Copy files to output directory
cp "$cert_file" "$output_dir/${cert_name}.crt"
cp "$key_file" "$output_dir/${cert_name}.key"
chmod 600 "$output_dir/${cert_name}.key"
# Copy chain file if available
if [ -n "$chain_file" ]; then
cp "$chain_file" "$output_dir/${cert_name}.chain.crt"
fi
# Copy fullchain if available
if [ -n "$fullchain_file" ]; then
cp "$fullchain_file" "$output_dir/${cert_name}.fullchain.crt"
fi
# Save metadata
echo "$CERT_META" | jq '.' >"$output_dir/${cert_name}_metadata.json"
# Create new ZIP file with standardized names
local final_zip="$output_dir/${cert_name}_certificate.zip"
cd "$output_dir" || exit 1
zip -q "$final_zip" "${cert_name}.crt" "${cert_name}.key" "${cert_name}_metadata.json"
[ -f "${cert_name}.chain.crt" ] && zip -q "$final_zip" "${cert_name}.chain.crt"
[ -f "${cert_name}.fullchain.crt" ] && zip -q "$final_zip" "${cert_name}.fullchain.crt"
cd - >/dev/null || exit 1
echo -e " ✅ ${COLOR_GREEN}Certificate downloaded successfully (legacy method)${CoR}"
echo -e " Files created:"
echo -e " • ${COLOR_CYAN}${cert_name}.crt${CoR} (Certificate)"
echo -e " • ${COLOR_CYAN}${cert_name}.key${CoR} (Private Key)"
[ -f "$output_dir/${cert_name}.chain.crt" ] && echo -e " • ${COLOR_CYAN}${cert_name}.chain.crt${CoR} (Chain)"
[ -f "$output_dir/${cert_name}.fullchain.crt" ] && echo -e " • ${COLOR_CYAN}${cert_name}.fullchain.crt${CoR} (Full Chain)"
echo -e " • ${COLOR_CYAN}${cert_name}_metadata.json${CoR} (Metadata)"
echo -e " • ${COLOR_CYAN}${cert_name}_certificate.zip${CoR} (ZIP Archive)"
# Cleanup
rm -rf "$TMP_DIR"
return 0
else
echo -e " ⛔ ${COLOR_RED}Error: Could not find certificate files in ZIP${CoR}"
fi
else
echo -e " ⛔ ${COLOR_RED}Error: unzip command not found${CoR}"
fi
else
echo -e " ⛔ ${COLOR_RED}Error: Failed to download certificate ZIP (HTTP $http_code)${CoR}"
fi
# Cleanup on failure
rm -rf "$TMP_DIR"
echo -e " ⛔ ${COLOR_RED}Certificate download failed${CoR}"
exit 1
}
# Verify Cloudflare API Key validity
verify_cloudflare_api_key() {
local api_key="$1"
local email="$2"
echo -e " 🔍 Verifying Cloudflare API Key..."
# Test API call to Cloudflare
local response=$(curl -s -X GET "https://api.cloudflare.com/client/v4/user" \
-H "X-Auth-Email: $email" \
-H "X-Auth-Key: $api_key" \
-H "Content-Type: application/json")
# Check if the API call was successful
if [ "$(echo "$response" | jq -r '.success')" = "true" ]; then
local username=$(echo "$response" | jq -r '.result.username')
echo -e " ✅ ${COLOR_GREEN}Cloudflare API Key is valid${CoR}"
echo -e " 👤 Connected as: ${COLOR_CYAN}$username${CoR}"
return 0
else
local error_msg=$(echo "$response" | jq -r '.errors[0].message')
echo -e " ❌ ${COLOR_RED}Invalid Cloudflare API Key${CoR}"
echo -e " ⛔ Error: $error_msg"
return 1
fi
}
###############################
# Create or update a proxy host based on the existence of the domain
create_or_update_proxy_host() {
# Add static variable to track execution
if [ "${FUNCTION_CALLED:-0}" -eq 1 ]; then
return 0
fi
FUNCTION_CALLED=1
# Check for wildcard domains in host creation
if [[ "$DOMAIN_NAMES" == \** ]]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: Wildcard domains (*.domain.com) are not allowed for host creation${CoR}"
echo -e " Wildcards are only supported for SSL certificates"
exit 1
fi
check_token_notverbose
# Check if the host already exists
#echo -e "\n 🔎 Checking if the host ${COLOR_RED}$DOMAIN_NAMES${CoR} already exists..."
RESPONSE=$(curl -s -X GET "$BASE_URL/nginx/proxy-hosts" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
EXISTING_HOST=$(echo "$RESPONSE" | jq -r --arg DOMAIN "$DOMAIN_NAMES" '.[] | select(.domain_names[] == $DOMAIN)')
HOST_ID=$(echo "$EXISTING_HOST" | jq -r '.id // empty')
# Prepare JSON data for API
if [[ -z "$CUSTOM_LOCATIONS" || "$CUSTOM_LOCATIONS" == "null" ]]; then
CUSTOM_LOCATIONS_ESCAPED="[]"
else
CUSTOM_LOCATIONS_ESCAPED=$(echo "$CUSTOM_LOCATIONS" | jq -c . 2>/dev/null || echo '[]')
fi
# Convert boolean to JSON
CACHING_ENABLED_JSON=$([ "$CACHING_ENABLED" == "true" ] && echo true || echo false)
BLOCK_EXPLOITS_JSON=$([ "$BLOCK_EXPLOITS" == "true" ] && echo true || echo false)
ALLOW_WEBSOCKET_UPGRADE_JSON=$([ "$ALLOW_WEBSOCKET_UPGRADE" == "true" ] && echo true || echo false)
HTTP2_SUPPORT_JSON=$([ "$HTTP2_SUPPORT" == "true" ] && echo true || echo false)
# Generate JSON
DATA=$(jq -n \
--arg domain "$DOMAIN_NAMES" \
--arg host "$FORWARD_HOST" \
--arg port "$FORWARD_PORT" \
--arg scheme "$FORWARD_SCHEME" \
--argjson caching "$CACHING_ENABLED_JSON" \
--argjson block_exploits "$BLOCK_EXPLOITS_JSON" \
--arg advanced_config "$ADVANCED_CONFIG" \
--argjson websocket_upgrade "$ALLOW_WEBSOCKET_UPGRADE_JSON" \
--argjson http2_support "$HTTP2_SUPPORT_JSON" \
--argjson enabled true \
--argjson locations "$CUSTOM_LOCATIONS_ESCAPED" \
'{
domain_names: [$domain],
forward_host: $host,
forward_port: ($port | tonumber),
access_list_id: null,
certificate_id: null,
ssl_forced: false,
caching_enabled: $caching,
block_exploits: $block_exploits,
advanced_config: $advanced_config,
meta: { dns_challenge: null },
allow_websocket_upgrade: $websocket_upgrade,
http2_support: $http2_support,
forward_scheme: $scheme,
enabled: $enabled,
locations: $locations
}')
# Check if the JSON is valid
if ! echo "$DATA" | jq empty >/dev/null 2>&1; then
echo -e " ${COLOR_RED}⛔ ERROR: Invalid JSON generated:\n$DATA${CoR}"
exit 1
fi
if [ -n "$HOST_ID" ]; then
# Update existing HOST
#echo -e "\n ${COLOR_CYAN}🔄${CoR} Updating the proxy host for ${COLOR_GREEN}$DOMAIN_NAMES${CoR}"
if [ "$AUTO_YES" != "true" ]; then
echo -e " ${COLOR_YELLOW}👉 Do you want to update this host ${CoR} $DOMAIN_NAMES ${COLOR_YELLOW}?${CoR}"
read -n 1 -r -p " (y/n): " answer
echo
if [[ ! $answer =~ ^[Yy]$ ]]; then
echo -e " ${COLOR_YELLOW}🚫 No changes made.${CoR}\n"
return 0
fi
fi
echo -e "${CoR}"
METHOD="PUT"
URL="$BASE_URL/nginx/proxy-hosts/$HOST_ID"
else
# Create NEW HOST
echo -e "\n ${COLOR_CYAN}🌍${CoR} Creating a new proxy host: ${COLOR_GREEN}$DOMAIN_NAMES${CoR}"
METHOD="POST"
URL="$BASE_URL/nginx/proxy-hosts"
fi
# Send API request
RESPONSE=$(curl -s -X "$METHOD" "$URL" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" \
-H "Content-Type: application/json; charset=UTF-8" \
--data-raw "$DATA")
# Check API response
ERROR_MSG=$(echo "$RESPONSE" | jq -r '.error.message // empty')
if [ -z "$ERROR_MSG" ]; then
PROXY_ID=$(echo "$RESPONSE" | jq -r '.id // "unknown"')
# If certificate generation is requested
if [ "$CERT_GENERATE" = true ]; then
#echo -e " ${COLOR_YELLOW}🔐 Generate SSL certificate CREATE_OR_${CoR}"
# Check if it's a wildcard certificate (the host itself can't be a wildcard)
if [[ "$CERT_DOMAIN" == \** ]]; then
# Use --dns-provider / --dns-credentials, prompt only when missing
if [ -z "$CERT_DNS_PROVIDER" ] || [ -z "$CERT_DNS_CREDENTIALS" ]; then
echo -e " ⚠️ ${COLOR_YELLOW}Wildcard certificate requires DNS challenge${CoR}"
if [ "$AUTO_YES" = true ]; then
echo -e " ⛔ ${COLOR_RED}Missing --dns-provider / --dns-credentials${CoR}\n"
exit 1
fi
echo -e " 👉 Please provide DNS provider and credentials:"
[ -z "$CERT_DNS_PROVIDER" ] && read -r -p " DNS Provider (cloudflare, dynu, etc.): " CERT_DNS_PROVIDER
[ -z "$CERT_DNS_CREDENTIALS" ] && read -r -p " DNS Credentials (JSON): " CERT_DNS_CREDENTIALS
fi
# Verify Cloudflare Global API key if provided (API tokens are not checked)
local cf_key cf_email
cf_key=$(echo "$CERT_DNS_CREDENTIALS" | jq -r '.dns_cloudflare_api_key // empty' 2>/dev/null || true)
cf_email=$(echo "$CERT_DNS_CREDENTIALS" | jq -r '.dns_cloudflare_email // empty' 2>/dev/null || true)
cf_email=${cf_email:-$CERT_EMAIL}
if [[ "${CERT_DNS_PROVIDER,,}" == "cloudflare" ]] && [[ "$cf_key" =~ ^[0-9a-fA-F]{37}$ ]]; then
if ! verify_cloudflare_api_key "$cf_key" "$cf_email"; then
echo -e " ⛔ ${COLOR_RED}Cannot proceed with invalid Cloudflare API Key${CoR}"
return 1
fi
# Verify domain is managed by Cloudflare
local domain=${CERT_DOMAIN#\*.}
local zone_check=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=$domain" \
-H "X-Auth-Email: $cf_email" \
-H "X-Auth-Key: $cf_key" \
-H "Content-Type: application/json")
if [ "$(echo "$zone_check" | jq -r '.result | length')" -eq 0 ]; then
echo -e " ⛔ ${COLOR_RED}Domain $domain is not managed by Cloudflare${CoR}"
echo -e " 👉 Please make sure your domain is added to your Cloudflare account"
return 1
fi
echo -e " ✅ ${COLOR_GREEN}Domain verification successful${CoR}"
fi
fi
# Generate the certificate
cert_generate "$CERT_DOMAIN" "$CERT_EMAIL" "$CERT_DNS_PROVIDER" "$CERT_DNS_CREDENTIALS"
# Check SSL creation
CERT_CHECK=$(curl -s -X GET "$BASE_URL/nginx/certificates" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
CERT_ID=$(echo "$CERT_CHECK" | jq -r --arg domain "$CERT_DOMAIN" \
'.[] | select(.domain_names[] == $domain) | .id' | sort -n | tail -n1)
if [ -n "$CERT_ID" ]; then
#echo -e "\n ${COLOR_YELLOW}✨ Automatic SSL Activation${CoR} $CERT_DOMAIN "
# update certificat with HOST ID
UPDATE_DATA=$(jq -n \
--arg cert_id "$CERT_ID" \
'{
certificate_id: $cert_id,
ssl_forced: true,
http2_support: true,
hsts_enabled: false,
hsts_subdomains: false,
enabled: true
}')
UPDATE_RESPONSE=$(curl -s -w "HTTPSTATUS:%{http_code}" -X PUT \
"$BASE_URL/nginx/proxy-hosts/$PROXY_ID" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" \
-H "Content-Type: application/json" \
--data "$UPDATE_DATA")
UPDATE_STATUS=${UPDATE_RESPONSE##*HTTPSTATUS:}
# Check if successfull
if [ "$UPDATE_STATUS" -eq 200 ]; then
echo -e "\n ✅ ${COLOR_GREEN}SSL Configuration Complete ${CoR}🎉"
echo -e " 📋 CHECK SSL Status for ${COLOR_GREEN}$DOMAIN_NAMES${CoR}:"
echo -e " ├─ 🔒 SSL: ${COLOR_GREEN}Enabled${CoR}"
echo -e " ├─ 📜 Certificate ID: $CERT_ID"
echo -e " ├─ 🚀 HTTP/2: ${COLOR_GREEN}Active${CoR}"
echo -e " ├─ 🛡️ HSTS: ${COLOR_RED}Disabled${CoR}"
echo -e " └─ 🌐 HSTS Subdomains: ${COLOR_RED}Disabled${CoR}\n"
exit 0
else
echo -e " ⛔ ${COLOR_RED}Failed to enable SSL. Status code: $UPDATE_STATUS${CoR}\n"
exit 1
fi
else
echo -e " ⛔ ${COLOR_RED}Certificate not found after generation${CoR}\n"
exit 1
fi
fi
if [ "$METHOD" = "PUT" ]; then
echo -e " ✅ ${COLOR_GREEN}Proxy host 🔗$DOMAIN_NAMES (ID: ${COLOR_YELLOW}$PROXY_ID${COLOR_GREEN}) OK${CoR}\n"
else
echo -e " ✅ ${COLOR_GREEN}Proxy host 🔗$DOMAIN_NAMES (ID: ${COLOR_YELLOW}$PROXY_ID${COLOR_GREEN}) created successfully! 🎉${CoR}\n"
fi
else
echo -e " ⛔ ${COLOR_RED}Operation failed. Error: $ERROR_MSG${CoR}\n"
exit 1
fi
}
# List all proxy hosts with basic details, including SSL certificate status and associated domain
host_list() {
check_token_notverbose
RESPONSE=$(curl -s -X GET "$BASE_URL/nginx/proxy-hosts" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
if [ "$JSON_OUTPUT" = true ]; then
echo "$RESPONSE" | jq .
exit 0
fi
echo -e "\n${COLOR_ORANGE} 👉 List of proxy hosts ${CoR}\n"
printf " %4s %-36s %-9s %-6s %-30s %-36s\n" "ID" " DOMAIN" " STATUS" " SSL" " TARGET" " CERT DOMAIN"
# Clean the response to remove control characters
CLEANED_RESPONSE=$(echo "$RESPONSE" | tr -d '\000-\031')
# Fetch all certificates once and build an "id -> domain1, domain2" map, so we
# don't make one API call per proxy host to resolve the CERT DOMAIN column.
ALL_CERTS=$(curl -s -X GET "$BASE_URL/nginx/certificates" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
declare -A CERT_DOMAINS=()
while IFS=$'\t' read -r _cid _cdoms; do
[ -n "$_cid" ] && CERT_DOMAINS["$_cid"]="$_cdoms"
done < <(echo "$ALL_CERTS" | jq -r '.[]? | select(.domain_names != null) | "\(.id)\t\(.domain_names | join(", "))"')
# Fix PR #28: Use tab delimiter to safely handle multiple domain names
# Fix PR #29: Extract forward_host, forward_port, forward_scheme for TARGET column
echo "$CLEANED_RESPONSE" | jq -r '.[] | "\(.id)\t\(.domain_names | join(", "))\t\(.enabled)\t\(.certificate_id)\t\(.forward_scheme)\t\(.forward_host)\t\(.forward_port)"' | while IFS=$'\t' read -r id domain enabled certificate_id forward_scheme forward_host forward_port; do
if [ "$enabled" = "true" ]; then
status="$(echo -e "${WHITE_ON_GREEN} enabled ${CoR}")"
else
status="$(echo -e "${COLOR_RED} disable ${CoR}")"
fi
# Default SSL status
ssl_status="$(pad "✘" 6)"
ssl_color="${COLOR_RED}"
cert_domain=""
# Resolve the certificate domain from the prefetched map (no per-host call)
if [ "$certificate_id" != "null" ] && [ -n "$certificate_id" ]; then
cert_domain="${CERT_DOMAINS[$certificate_id]:-}"
if [ -n "$cert_domain" ]; then
ssl_status="$(pad "$certificate_id" 6)"
ssl_color="${COLOR_CYAN}"
fi
fi
# Build TARGET column (PR #29): scheme://host:port
if [ -n "$forward_host" ] && [ "$forward_host" != "null" ] && [ -n "$forward_port" ] && [ "$forward_port" != "null" ]; then
target="${forward_scheme:-http}://${forward_host}:${forward_port}"
else
target="N/A"
fi
# Split the domain list: the first domain goes on the main row, any extra
# domains are printed below, indented under the DOMAIN column. This keeps
# all columns aligned even with long or multiple domain names.
IFS=', ' read -ra domain_arr <<<"$domain"
primary_domain="${domain_arr[0]}"
# Print the row with colors, TARGET column, and certificate domain (if available)
printf " ${COLOR_YELLOW}%4s${CoR} ${COLOR_GREEN}%s${CoR} %-9s ${ssl_color}%-6s${CoR} ${COLOR_CYAN}%s${CoR} ${COLOR_CYAN}%s${CoR}\n" \
"$id" "$(truncate_pad "$primary_domain" 36)" "$status" "$ssl_status" "$(truncate_pad "$target" 30)" "$cert_domain"
# Continuation lines for the remaining domains (indented under DOMAIN, col 8)
for ((i = 1; i < ${#domain_arr[@]}; i++)); do
printf " ${COLOR_GREEN}%s${CoR}\n" "$(truncate_pad "${domain_arr[i]}" 36)"
done
done
echo ""
exit 0
}
################################
# List all proxy hosts with full details
host_list_full() {
check_token_notverbose
RESPONSE=$(curl -s -X GET "$BASE_URL/nginx/proxy-hosts" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
echo "$RESPONSE" | jq -c '.[]' | while read -r proxy; do
echo "$proxy" | jq .
done
echo ""
exit 0
}
################################
# List all redirection hosts
redirect_host_list() {
check_token_notverbose
RESPONSE=$(curl -s -X GET "$BASE_URL/nginx/redirection-hosts" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
if [ "$JSON_OUTPUT" = true ]; then
echo "$RESPONSE" | jq .
exit 0
fi
echo -e "\n${COLOR_ORANGE} 👉 List of Redirection hosts ${CoR}\n"
printf " %4s %-36s %-9s %-7s %-7s %-36s\n" "ID" " DOMAIN" " STATUS" " CODE" " SSL" " FORWARD DOMAIN"
CLEANED_RESPONSE=$(echo "$RESPONSE" | tr -d '\000-\031')
echo "$CLEANED_RESPONSE" | jq -r '.[] | "\(.id)\t\(.domain_names | join(", "))\t\(.enabled)\t\(.forward_http_code)\t\(.certificate_id)\t\(.forward_domain_name)"' |
while IFS=$'\t' read -r id domain enabled http_code certificate_id forward_domain; do
if [ "$enabled" = "true" ]; then
status="$(echo -e "${WHITE_ON_GREEN} enabled ${CoR}")"
else
status="$(echo -e "${COLOR_RED} disable ${CoR}")"
fi
ssl_status="$(pad "✘" 7)"
ssl_color="${COLOR_RED}"
if [ "$certificate_id" != "null" ] && [ -n "$certificate_id" ]; then
ssl_status="$(pad "$certificate_id" 7)"
ssl_color="${COLOR_CYAN}"
fi
# Split the domain list: the first domain goes on the main row, any extra
# domains are printed below, indented under the DOMAIN column. Keeps all
# columns aligned even with long or multiple domains (same as host_list).
IFS=', ' read -ra domain_arr <<<"$domain"
primary_domain="${domain_arr[0]}"
printf " ${COLOR_YELLOW}%4s${CoR} ${COLOR_GREEN}%s${CoR} %-9s ${COLOR_CYAN}%-7s${CoR} ${ssl_color}%-7s${CoR} ${COLOR_CYAN}%s${CoR}\n" \
"$id" "$(truncate_pad "$primary_domain" 36)" "$status" "$http_code" "$ssl_status" "$(truncate_pad "$forward_domain" 36)"
# Continuation lines for the remaining domains (indented under DOMAIN, col 8)
for ((i = 1; i < ${#domain_arr[@]}; i++)); do
printf " ${COLOR_GREEN}%s${CoR}\n" "$(truncate_pad "${domain_arr[i]}" 36)"
done
done
echo ""
exit 0
}
################################
# Create or Update a redirection host
# Reads globals: DOMAIN_NAMES, FORWARD_DOMAIN, FORWARD_SCHEME, FORWARD_HTTP_CODE,
# PRESERVE_PATH, BLOCK_EXPLOITS, ADVANCED_CONFIG, AUTO_YES
create_or_update_redirect_host() {
if [ "${REDIRECT_FUNCTION_CALLED:-0}" -eq 1 ]; then
return 0
fi
REDIRECT_FUNCTION_CALLED=1
if [ -z "$DOMAIN_NAMES" ] || [ -z "$FORWARD_DOMAIN" ]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: Missing required parameters (domain, forward domain).${CoR}"
echo -e " Usage: ${COLOR_ORANGE}$0 --redirect-host-create domain.com --forward-domain target.com${CoR}"
exit 1
fi
# Validate HTTP code
if ! [[ "$FORWARD_HTTP_CODE" =~ ^(301|302|303|307|308)$ ]]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: Invalid HTTP code '$FORWARD_HTTP_CODE'. Must be 301, 302, 303, 307, or 308.${CoR}"
exit 1
fi
# Validate forward scheme
if ! [[ "$FORWARD_SCHEME" =~ ^(http|https)$ ]]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: Invalid forward scheme '$FORWARD_SCHEME'. Must be 'http' or 'https'.${CoR}"
exit 1
fi
check_token_notverbose
# Check if a redirect host for this domain already exists
RESPONSE=$(curl -s -X GET "$BASE_URL/nginx/redirection-hosts" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
EXISTING=$(echo "$RESPONSE" | jq -r --arg DOMAIN "$DOMAIN_NAMES" '.[] | select(.domain_names[] == $DOMAIN)')
REDIRECT_ID=$(echo "$EXISTING" | jq -r '.id // empty')
# Convert booleans to JSON
PRESERVE_PATH_JSON=$([ "$PRESERVE_PATH" == "true" ] && echo true || echo false)
BLOCK_EXPLOITS_JSON=$([ "$BLOCK_EXPLOITS" == "true" ] && echo true || echo false)
DATA=$(jq -n \
--arg domain "$DOMAIN_NAMES" \
--arg forward_scheme "$FORWARD_SCHEME" \
--argjson forward_http_code "$FORWARD_HTTP_CODE" \
--arg forward_domain_name "$FORWARD_DOMAIN" \
--argjson preserve_path "$PRESERVE_PATH_JSON" \
--argjson block_exploits "$BLOCK_EXPLOITS_JSON" \
--arg advanced_config "$ADVANCED_CONFIG" \
'{
domain_names: [$domain],
forward_scheme: $forward_scheme,
forward_http_code: $forward_http_code,
forward_domain_name: $forward_domain_name,
preserve_path: $preserve_path,
block_exploits: $block_exploits,
advanced_config: $advanced_config,
meta: {}
}')
if ! echo "$DATA" | jq empty >/dev/null 2>&1; then
echo -e " ${COLOR_RED}⛔ ERROR: Invalid JSON generated:\n$DATA${CoR}"
exit 1
fi
if [ -n "$REDIRECT_ID" ]; then
if [ "$AUTO_YES" != "true" ]; then
echo -e " ${COLOR_YELLOW}👉 Do you want to update this redirect host ${CoR} $DOMAIN_NAMES ${COLOR_YELLOW}?${CoR}"
read -n 1 -r -p " (y/n): " answer
echo
if [[ ! $answer =~ ^[Yy]$ ]]; then
echo -e " ${COLOR_YELLOW}🚫 No changes made.${CoR}\n"
return 0
fi
fi
echo -e "\n ${COLOR_CYAN}🔄${CoR} Updating redirect host: ${COLOR_GREEN}$DOMAIN_NAMES${CoR}"
METHOD="PUT"
URL="$BASE_URL/nginx/redirection-hosts/$REDIRECT_ID"
else
echo -e "\n ${COLOR_CYAN}🔀${CoR} Creating redirect host: ${COLOR_GREEN}$DOMAIN_NAMES${CoR} → ${COLOR_YELLOW}$FORWARD_DOMAIN${CoR}"
METHOD="POST"
URL="$BASE_URL/nginx/redirection-hosts"
fi
RESPONSE=$(curl -s -X "$METHOD" "$URL" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" \
-H "Content-Type: application/json; charset=UTF-8" \
--data-raw "$DATA")
ERROR_MSG=$(echo "$RESPONSE" | jq -r '.error.message // empty')
if [ -z "$ERROR_MSG" ]; then
RID=$(echo "$RESPONSE" | jq -r '.id // "unknown"')
if [ "$METHOD" = "PUT" ]; then
echo -e " ✅ ${COLOR_GREEN}Redirect host 🔗$DOMAIN_NAMES (ID: ${COLOR_YELLOW}$RID${COLOR_GREEN}) updated successfully! 🎉${CoR}\n"
else
echo -e " ✅ ${COLOR_GREEN}Redirect host 🔗$DOMAIN_NAMES (ID: ${COLOR_YELLOW}$RID${COLOR_GREEN}) created successfully! 🎉${CoR}\n"
fi
else
echo -e " ⛔ ${COLOR_RED}Operation failed. Error: $ERROR_MSG${CoR}\n"
exit 1
fi
}
################################
# Delete a redirection host by ID
redirect_host_delete() {
local host_id="$1"
if [ -z "$host_id" ]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: The --redirect-host-delete option requires a host 🆔.${CoR}"
echo -e " Usage : ${COLOR_ORANGE}$0 --redirect-host-delete <id>${CoR}"
echo -e " Example: ${COLOR_GREEN}$0 --redirect-host-delete 5${CoR}"
exit 1
fi
check_token_notverbose
CHECK_RESPONSE=$(curl -s -w "HTTPSTATUS:%{http_code}" \
"$BASE_URL/nginx/redirection-hosts/$host_id" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" 2>/dev/null)
CHECK_BODY=${CHECK_RESPONSE//HTTPSTATUS:*/}
CHECK_STATUS=${CHECK_RESPONSE##*HTTPSTATUS:}
if [ "$CHECK_STATUS" -eq 404 ]; then
echo -e " ⛔ ${COLOR_RED}ERROR: Redirect host ID ${COLOR_YELLOW}$host_id${COLOR_RED} not found!${CoR}"
exit 1
elif [ "$CHECK_STATUS" -ne 200 ]; then
echo -e " ⛔ ${COLOR_RED}ERROR: Failed to check redirect host. Status: $CHECK_STATUS${CoR}"
exit 1
fi
DOMAIN_NAME=$(echo "$CHECK_BODY" | jq -r '.domain_names[0] // "unknown"')
if [ "$AUTO_YES" != true ]; then
echo -e " ┌───────────────────────────────────────────"
echo -e " │ ID: ${COLOR_YELLOW}$host_id${CoR}"
echo -e " │ Domain: ${COLOR_GREEN}$DOMAIN_NAME${CoR}"
echo -e " └───────────────────────────────────────────"
echo -e " ⚠️ ${COLOR_RED}WARNING: This action cannot be undone!${CoR}"
read -n 1 -r -p " 🔔 Confirm deletion? (y/n): " CONFIRM
echo
if [[ ! $CONFIRM =~ ^[Yy]$ ]]; then
echo -e " ❌ ${COLOR_RED}Operation cancelled${CoR}"
exit 1
fi
else
echo -e "\n ${COLOR_YELLOW}🔔 -y Auto-confirming deletion${CoR}"
fi
echo -e " 🗑️ Deleting redirect host ${COLOR_GREEN}$DOMAIN_NAME${CoR} (ID: ${COLOR_GREEN}$host_id${CoR})..."
RESPONSE=$(curl -s -w "HTTPSTATUS:%{http_code}" -X DELETE \
"$BASE_URL/nginx/redirection-hosts/$host_id" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" 2>/dev/null)
HTTP_STATUS=${RESPONSE##*HTTPSTATUS:}
if [ "${HTTP_STATUS:-0}" -eq 200 ]; then
echo -e " ✅ ${COLOR_GREEN}Redirect host ${COLOR_YELLOW}$DOMAIN_NAME${CoR} (ID: $host_id) deleted successfully!${CoR}\n"
exit 0
else
echo -e " ⛔ ${COLOR_RED}Failed to delete redirect host. Status: $HTTP_STATUS${CoR}"
exit 1
fi
}
################################
# Enable a redirection host by ID
redirect_host_enable() {
local host_id="$1"
if [ -z "$host_id" ]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: The --redirect-host-enable option requires a host 🆔.${CoR}"
echo -e " Usage : ${COLOR_ORANGE}$0 --redirect-host-enable <id>${CoR}"
echo -e " Example: ${COLOR_GREEN}$0 --redirect-host-enable 5${CoR}"
return 1
fi
check_token_notverbose
CHECK_RESPONSE=$(curl -s -X GET "$BASE_URL/nginx/redirection-hosts/$host_id" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
if echo "$CHECK_RESPONSE" | jq -e '.error' >/dev/null 2>&1; then
echo -e " ⛔ ${COLOR_RED}Redirect host with ID $host_id does not exist${CoR}"
return 1
fi
DOMAIN_NAME=$(echo "$CHECK_RESPONSE" | jq -r '.domain_names[0]')
echo -e "\n ${COLOR_YELLOW}🔄 Enabling redirect host ${CoR} 🆔${COLOR_CYAN}$host_id${CoR} 🌐Domain: ${COLOR_CYAN}$DOMAIN_NAME${CoR}"
RESPONSE=$(curl -s -X POST "$BASE_URL/nginx/redirection-hosts/$host_id/enable" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" \
-H "Content-Type: application/json; charset=UTF-8")
if ! echo "$RESPONSE" | jq -e '.error' >/dev/null 2>&1; then
echo -e " ✅ ${COLOR_GREEN}Successfully enabled ${CoR}🆔${COLOR_CYAN}$host_id${CoR} 🌐Domain: ${COLOR_CYAN}$DOMAIN_NAME${CoR}"
else
ERROR_MSG=$(echo "$RESPONSE" | jq -r '.error.message // "Unknown error"')
echo -e " ⛔ ${COLOR_RED}Failed to enable redirect host: $ERROR_MSG${CoR}"
return 1
fi
}
################################
# Disable a redirection host by ID
redirect_host_disable() {
local host_id="$1"
if [ -z "$host_id" ]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: The --redirect-host-disable option requires a host 🆔.${CoR}"
echo -e " Usage : ${COLOR_ORANGE}$0 --redirect-host-disable <id>${CoR}"
echo -e " Example: ${COLOR_GREEN}$0 --redirect-host-disable 5${CoR}"
return 1
fi
check_token_notverbose
CHECK_RESPONSE=$(curl -s -X GET "$BASE_URL/nginx/redirection-hosts/$host_id" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
if echo "$CHECK_RESPONSE" | jq -e '.error' >/dev/null 2>&1; then
echo -e " ⛔ ${COLOR_RED}Redirect host with ID $host_id does not exist${CoR}"
return 1
fi
DOMAIN_NAME=$(echo "$CHECK_RESPONSE" | jq -r '.domain_names[0]')
echo -e "\n ${COLOR_YELLOW}🔄 Disabling redirect host ${CoR} 🆔${COLOR_CYAN}$host_id${CoR} 🌐Domain: ${COLOR_CYAN}$DOMAIN_NAME${CoR}"
RESPONSE=$(curl -s -X POST "$BASE_URL/nginx/redirection-hosts/$host_id/disable" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" \
-H "Content-Type: application/json; charset=UTF-8")
if ! echo "$RESPONSE" | jq -e '.error' >/dev/null 2>&1; then
echo -e " ✅ ${COLOR_GREEN}Successfully disabled ${CoR}🆔${COLOR_CYAN}$host_id${CoR} 🌐Domain: ${COLOR_CYAN}$DOMAIN_NAME${CoR}"
else
ERROR_MSG=$(echo "$RESPONSE" | jq -r '.error.message // "Unknown error"')
echo -e " ⛔ ${COLOR_RED}Failed to disable redirect host: $ERROR_MSG${CoR}"
return 1
fi
}
################################
# List all stream hosts (TCP/UDP forwarding)
stream_host_list() {
check_token_notverbose
RESPONSE=$(curl -s -X GET "$BASE_URL/nginx/streams" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
if [ "$JSON_OUTPUT" = true ]; then
echo "$RESPONSE" | jq .
exit 0
fi
echo -e "\n${COLOR_ORANGE} 👉 List of Stream hosts ${CoR}\n"
printf " %4s %-8s %-9s %-4s %-4s %-36s\n" "ID" " PORT" " STATUS" " TCP" " UDP" " FORWARD HOST:PORT"
CLEANED_RESPONSE=$(echo "$RESPONSE" | tr -d '\000-\031')
echo "$CLEANED_RESPONSE" | jq -r '.[] | "\(.id)\t\(.incoming_port)\t\(.enabled)\t\(.tcp_forwarding)\t\(.udp_forwarding)\t\(.forwarding_host):\(.forwarding_port)"' |
while IFS=$'\t' read -r id port enabled tcp udp forward; do
if [ "$enabled" = "true" ]; then
status="$(echo -e "${WHITE_ON_GREEN} enabled ${CoR}")"
else
status="$(echo -e "${COLOR_RED} disable ${CoR}")"
fi
tcp_flag="✘"
[ "$tcp" = "true" ] && tcp_flag="✔"
udp_flag="✘"
[ "$udp" = "true" ] && udp_flag="✔"
printf " ${COLOR_YELLOW}%4s${CoR} ${COLOR_GREEN}%-7s${CoR} %-9s ${COLOR_CYAN}%-4s${CoR} ${COLOR_CYAN}%-4s${CoR} ${COLOR_CYAN}%s${CoR}\n" \
"$id" "$port" "$status" "$tcp_flag" "$udp_flag" "$(truncate_pad "$forward" 36)"
done
echo ""
exit 0
}
################################
# Create or Update a stream host
# Reads globals: INCOMING_PORT, FORWARD_HOST, FORWARD_PORT, TCP_FORWARDING,
# UDP_FORWARDING, CERT_ID, AUTO_YES
create_or_update_stream_host() {
if [ -z "$INCOMING_PORT" ] || [ -z "$FORWARD_HOST" ] || [ -z "$FORWARD_PORT" ]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: Missing required parameters (incoming port, forward host, forward port).${CoR}"
echo -e " Usage: ${COLOR_ORANGE}$0 --stream-host-create <incoming_port> --forward-host <host> --forward-port <port> [--tcp] [--udp]${CoR}"
exit 1
fi
if [ "$TCP_FORWARDING" != "true" ] && [ "$UDP_FORWARDING" != "true" ]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: At least one of --tcp or --udp must be enabled.${CoR}"
exit 1
fi
check_token_notverbose
# Check if a stream host for this incoming port already exists
RESPONSE=$(curl -s -X GET "$BASE_URL/nginx/streams" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
EXISTING=$(echo "$RESPONSE" | jq -r --argjson PORT "$INCOMING_PORT" '.[] | select(.incoming_port == $PORT)')
STREAM_ID=$(echo "$EXISTING" | jq -r '.id // empty')
DATA=$(jq -n \
--argjson incoming_port "$INCOMING_PORT" \
--arg forwarding_host "$FORWARD_HOST" \
--argjson forwarding_port "$FORWARD_PORT" \
--argjson tcp_forwarding "$TCP_FORWARDING" \
--argjson udp_forwarding "$UDP_FORWARDING" \
--argjson certificate_id "${CERT_ID:-0}" \
'{
incoming_port: $incoming_port,
forwarding_host: $forwarding_host,
forwarding_port: $forwarding_port,
tcp_forwarding: $tcp_forwarding,
udp_forwarding: $udp_forwarding,
certificate_id: $certificate_id,
meta: {}
}')
if ! echo "$DATA" | jq empty >/dev/null 2>&1; then
echo -e " ${COLOR_RED}⛔ ERROR: Invalid JSON generated:\n$DATA${CoR}"
exit 1
fi
if [ -n "$STREAM_ID" ]; then
if [ "$AUTO_YES" != "true" ]; then
echo -e " ${COLOR_YELLOW}👉 Do you want to update this stream host ${CoR} port $INCOMING_PORT ${COLOR_YELLOW}?${CoR}"
read -n 1 -r -p " (y/n): " answer
echo
if [[ ! $answer =~ ^[Yy]$ ]]; then
echo -e " ${COLOR_YELLOW}🚫 No changes made.${CoR}\n"
return 0
fi
fi
echo -e "\n ${COLOR_CYAN}🔄${CoR} Updating stream host: ${COLOR_GREEN}port $INCOMING_PORT${CoR}"
METHOD="PUT"
URL="$BASE_URL/nginx/streams/$STREAM_ID"
else
echo -e "\n ${COLOR_CYAN}🔌${CoR} Creating stream host: ${COLOR_GREEN}port $INCOMING_PORT${CoR} → ${COLOR_YELLOW}$FORWARD_HOST:$FORWARD_PORT${CoR}"
METHOD="POST"
URL="$BASE_URL/nginx/streams"
fi
RESPONSE=$(curl -s -X "$METHOD" "$URL" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" \
-H "Content-Type: application/json; charset=UTF-8" \
--data-raw "$DATA")
ERROR_MSG=$(echo "$RESPONSE" | jq -r '.error.message // empty')
if [ -z "$ERROR_MSG" ]; then
RID=$(echo "$RESPONSE" | jq -r '.id // "unknown"')
if [ "$METHOD" = "PUT" ]; then
echo -e " ✅ ${COLOR_GREEN}Stream host 🔌port $INCOMING_PORT (ID: ${COLOR_YELLOW}$RID${COLOR_GREEN}) updated successfully! 🎉${CoR}\n"
else
echo -e " ✅ ${COLOR_GREEN}Stream host 🔌port $INCOMING_PORT (ID: ${COLOR_YELLOW}$RID${COLOR_GREEN}) created successfully! 🎉${CoR}\n"
fi
else
echo -e " ⛔ ${COLOR_RED}Operation failed. Error: $ERROR_MSG${CoR}\n"
exit 1
fi
}
################################
# Delete a stream host by ID
stream_host_delete() {
local host_id="$1"
if [ -z "$host_id" ]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: The --stream-host-delete option requires a host 🆔.${CoR}"
echo -e " Usage : ${COLOR_ORANGE}$0 --stream-host-delete <id>${CoR}"
echo -e " Example: ${COLOR_GREEN}$0 --stream-host-delete 5${CoR}"
exit 1
fi
check_token_notverbose
CHECK_RESPONSE=$(curl -s -w "HTTPSTATUS:%{http_code}" \
"$BASE_URL/nginx/streams/$host_id" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" 2>/dev/null)
CHECK_BODY=${CHECK_RESPONSE//HTTPSTATUS:*/}
CHECK_STATUS=${CHECK_RESPONSE##*HTTPSTATUS:}
if [ "$CHECK_STATUS" -eq 404 ]; then
echo -e " ⛔ ${COLOR_RED}ERROR: Stream host ID ${COLOR_YELLOW}$host_id${COLOR_RED} not found!${CoR}"
exit 1
elif [ "$CHECK_STATUS" -ne 200 ]; then
echo -e " ⛔ ${COLOR_RED}ERROR: Failed to check stream host. Status: $CHECK_STATUS${CoR}"
exit 1
fi
STREAM_PORT=$(echo "$CHECK_BODY" | jq -r '.incoming_port // "unknown"')
if [ "$AUTO_YES" != true ]; then
echo -e " ┌───────────────────────────────────────────"
echo -e " │ ID: ${COLOR_YELLOW}$host_id${CoR}"
echo -e " │ Port: ${COLOR_GREEN}$STREAM_PORT${CoR}"
echo -e " └───────────────────────────────────────────"
echo -e " ⚠️ ${COLOR_RED}WARNING: This action cannot be undone!${CoR}"
read -n 1 -r -p " 🔔 Confirm deletion? (y/n): " CONFIRM
echo
if [[ ! $CONFIRM =~ ^[Yy]$ ]]; then
echo -e " ❌ ${COLOR_RED}Operation cancelled${CoR}"
exit 1
fi
else
echo -e "\n ${COLOR_YELLOW}🔔 -y Auto-confirming deletion${CoR}"
fi
echo -e " 🗑️ Deleting stream host ${COLOR_GREEN}port $STREAM_PORT${CoR} (ID: ${COLOR_GREEN}$host_id${CoR})..."
RESPONSE=$(curl -s -w "HTTPSTATUS:%{http_code}" -X DELETE \
"$BASE_URL/nginx/streams/$host_id" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" 2>/dev/null)
HTTP_STATUS=${RESPONSE##*HTTPSTATUS:}
if [ "${HTTP_STATUS:-0}" -eq 200 ]; then
echo -e " ✅ ${COLOR_GREEN}Stream host ${COLOR_YELLOW}port $STREAM_PORT${CoR} (ID: $host_id) deleted successfully!${CoR}\n"
exit 0
else
echo -e " ⛔ ${COLOR_RED}Failed to delete stream host. Status: $HTTP_STATUS${CoR}"
exit 1
fi
}
################################
# Enable a stream host by ID
stream_host_enable() {
local host_id="$1"
if [ -z "$host_id" ]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: The --stream-host-enable option requires a host 🆔.${CoR}"
echo -e " Usage : ${COLOR_ORANGE}$0 --stream-host-enable <id>${CoR}"
echo -e " Example: ${COLOR_GREEN}$0 --stream-host-enable 5${CoR}"
return 1
fi
check_token_notverbose
CHECK_RESPONSE=$(curl -s -X GET "$BASE_URL/nginx/streams/$host_id" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
if echo "$CHECK_RESPONSE" | jq -e '.error' >/dev/null 2>&1; then
echo -e " ⛔ ${COLOR_RED}Stream host with ID $host_id does not exist${CoR}"
return 1
fi
STREAM_PORT=$(echo "$CHECK_RESPONSE" | jq -r '.incoming_port')
echo -e "\n ${COLOR_YELLOW}🔄 Enabling stream host ${CoR} 🆔${COLOR_CYAN}$host_id${CoR} 🔌Port: ${COLOR_CYAN}$STREAM_PORT${CoR}"
RESPONSE=$(curl -s -X POST "$BASE_URL/nginx/streams/$host_id/enable" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" \
-H "Content-Type: application/json; charset=UTF-8")
if ! echo "$RESPONSE" | jq -e '.error' >/dev/null 2>&1; then
echo -e " ✅ ${COLOR_GREEN}Successfully enabled ${CoR}🆔${COLOR_CYAN}$host_id${CoR} 🔌Port: ${COLOR_CYAN}$STREAM_PORT${CoR}"
else
ERROR_MSG=$(echo "$RESPONSE" | jq -r '.error.message // "Unknown error"')
echo -e " ⛔ ${COLOR_RED}Failed to enable stream host: $ERROR_MSG${CoR}"
return 1
fi
}
################################
# Disable a stream host by ID
stream_host_disable() {
local host_id="$1"
if [ -z "$host_id" ]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: The --stream-host-disable option requires a host 🆔.${CoR}"
echo -e " Usage : ${COLOR_ORANGE}$0 --stream-host-disable <id>${CoR}"
echo -e " Example: ${COLOR_GREEN}$0 --stream-host-disable 5${CoR}"
return 1
fi
check_token_notverbose
CHECK_RESPONSE=$(curl -s -X GET "$BASE_URL/nginx/streams/$host_id" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
if echo "$CHECK_RESPONSE" | jq -e '.error' >/dev/null 2>&1; then
echo -e " ⛔ ${COLOR_RED}Stream host with ID $host_id does not exist${CoR}"
return 1
fi
STREAM_PORT=$(echo "$CHECK_RESPONSE" | jq -r '.incoming_port')
echo -e "\n ${COLOR_YELLOW}🔄 Disabling stream host ${CoR} 🆔${COLOR_CYAN}$host_id${CoR} 🔌Port: ${COLOR_CYAN}$STREAM_PORT${CoR}"
RESPONSE=$(curl -s -X POST "$BASE_URL/nginx/streams/$host_id/disable" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" \
-H "Content-Type: application/json; charset=UTF-8")
if ! echo "$RESPONSE" | jq -e '.error' >/dev/null 2>&1; then
echo -e " ✅ ${COLOR_GREEN}Successfully disabled ${CoR}🆔${COLOR_CYAN}$host_id${CoR} 🔌Port: ${COLOR_CYAN}$STREAM_PORT${CoR}"
else
ERROR_MSG=$(echo "$RESPONSE" | jq -r '.error.message // "Unknown error"')
echo -e " ⛔ ${COLOR_RED}Failed to disable stream host: $ERROR_MSG${CoR}"
return 1
fi
}
################################
# List all dead hosts (404 Hosts)
dead_host_list() {
check_token_notverbose
RESPONSE=$(curl -s -X GET "$BASE_URL/nginx/dead-hosts" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
if [ "$JSON_OUTPUT" = true ]; then
echo "$RESPONSE" | jq .
exit 0
fi
echo -e "\n${COLOR_ORANGE} 👉 List of Dead hosts (404) ${CoR}\n"
printf " %4s %-36s %-9s %-7s\n" "ID" " DOMAIN" " STATUS" " SSL"
CLEANED_RESPONSE=$(echo "$RESPONSE" | tr -d '\000-\031')
echo "$CLEANED_RESPONSE" | jq -r '.[] | "\(.id)\t\(.domain_names | join(", "))\t\(.enabled)\t\(.certificate_id)"' |
while IFS=$'\t' read -r id domain enabled certificate_id; do
if [ "$enabled" = "true" ]; then
status="$(echo -e "${WHITE_ON_GREEN} enabled ${CoR}")"
else
status="$(echo -e "${COLOR_RED} disable ${CoR}")"
fi
ssl_status="$(pad "✘" 7)"
ssl_color="${COLOR_RED}"
if [ "$certificate_id" != "null" ] && [ -n "$certificate_id" ] && [ "$certificate_id" != "0" ]; then
ssl_status="$(pad "$certificate_id" 7)"
ssl_color="${COLOR_CYAN}"
fi
IFS=', ' read -ra domain_arr <<<"$domain"
primary_domain="${domain_arr[0]}"
printf " ${COLOR_YELLOW}%4s${CoR} ${COLOR_GREEN}%s${CoR} %-9s ${ssl_color}%-7s${CoR}\n" \
"$id" "$(truncate_pad "$primary_domain" 36)" "$status" "$ssl_status"
for ((i = 1; i < ${#domain_arr[@]}; i++)); do
printf " ${COLOR_GREEN}%s${CoR}\n" "$(truncate_pad "${domain_arr[i]}" 36)"
done
done
echo ""
exit 0
}
################################
# Create or Update a dead host (404 host)
# Reads globals: DOMAIN_NAMES, CERT_ID, SSL_FORCED, HSTS_ENABLED, HSTS_SUBDOMAINS,
# HTTP2_SUPPORT, ADVANCED_CONFIG, AUTO_YES
create_or_update_dead_host() {
if [ -z "$DOMAIN_NAMES" ]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: Missing required parameter (domain).${CoR}"
echo -e " Usage: ${COLOR_ORANGE}$0 --dead-host-create domain.com [options]${CoR}"
exit 1
fi
check_token_notverbose
# Check if a dead host for this domain already exists
RESPONSE=$(curl -s -X GET "$BASE_URL/nginx/dead-hosts" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
EXISTING=$(echo "$RESPONSE" | jq -r --arg DOMAIN "$DOMAIN_NAMES" '.[] | select(.domain_names[] == $DOMAIN)')
DEAD_ID=$(echo "$EXISTING" | jq -r '.id // empty')
SSL_FORCED_JSON=$([ "$SSL_FORCED" == "true" ] && echo true || echo false)
HSTS_ENABLED_JSON=$([ "$HSTS_ENABLED" == "true" ] && echo true || echo false)
HSTS_SUBDOMAINS_JSON=$([ "$HSTS_SUBDOMAINS" == "true" ] && echo true || echo false)
HTTP2_SUPPORT_JSON=$([ "$HTTP2_SUPPORT" == "true" ] && echo true || echo false)
DATA=$(jq -n \
--arg domain "$DOMAIN_NAMES" \
--argjson certificate_id "${CERT_ID:-0}" \
--argjson ssl_forced "$SSL_FORCED_JSON" \
--argjson hsts_enabled "$HSTS_ENABLED_JSON" \
--argjson hsts_subdomains "$HSTS_SUBDOMAINS_JSON" \
--argjson http2_support "$HTTP2_SUPPORT_JSON" \
--arg advanced_config "$ADVANCED_CONFIG" \
'{
domain_names: [$domain],
certificate_id: $certificate_id,
ssl_forced: $ssl_forced,
hsts_enabled: $hsts_enabled,
hsts_subdomains: $hsts_subdomains,
http2_support: $http2_support,
advanced_config: $advanced_config,
meta: {}
}')
if ! echo "$DATA" | jq empty >/dev/null 2>&1; then
echo -e " ${COLOR_RED}⛔ ERROR: Invalid JSON generated:\n$DATA${CoR}"
exit 1
fi
if [ -n "$DEAD_ID" ]; then
if [ "$AUTO_YES" != "true" ]; then
echo -e " ${COLOR_YELLOW}👉 Do you want to update this dead host ${CoR} $DOMAIN_NAMES ${COLOR_YELLOW}?${CoR}"
read -n 1 -r -p " (y/n): " answer
echo
if [[ ! $answer =~ ^[Yy]$ ]]; then
echo -e " ${COLOR_YELLOW}🚫 No changes made.${CoR}\n"
return 0
fi
fi
echo -e "\n ${COLOR_CYAN}🔄${CoR} Updating dead host: ${COLOR_GREEN}$DOMAIN_NAMES${CoR}"
METHOD="PUT"
URL="$BASE_URL/nginx/dead-hosts/$DEAD_ID"
else
echo -e "\n ${COLOR_CYAN}🚫${CoR} Creating dead host (404): ${COLOR_GREEN}$DOMAIN_NAMES${CoR}"
METHOD="POST"
URL="$BASE_URL/nginx/dead-hosts"
fi
RESPONSE=$(curl -s -X "$METHOD" "$URL" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" \
-H "Content-Type: application/json; charset=UTF-8" \
--data-raw "$DATA")
ERROR_MSG=$(echo "$RESPONSE" | jq -r '.error.message // empty')
if [ -z "$ERROR_MSG" ]; then
RID=$(echo "$RESPONSE" | jq -r '.id // "unknown"')
if [ "$METHOD" = "PUT" ]; then
echo -e " ✅ ${COLOR_GREEN}Dead host 🚫$DOMAIN_NAMES (ID: ${COLOR_YELLOW}$RID${COLOR_GREEN}) updated successfully! 🎉${CoR}\n"
else
echo -e " ✅ ${COLOR_GREEN}Dead host 🚫$DOMAIN_NAMES (ID: ${COLOR_YELLOW}$RID${COLOR_GREEN}) created successfully! 🎉${CoR}\n"
fi
else
echo -e " ⛔ ${COLOR_RED}Operation failed. Error: $ERROR_MSG${CoR}\n"
exit 1
fi
}
################################
# Delete a dead host by ID
dead_host_delete() {
local host_id="$1"
if [ -z "$host_id" ]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: The --dead-host-delete option requires a host 🆔.${CoR}"
echo -e " Usage : ${COLOR_ORANGE}$0 --dead-host-delete <id>${CoR}"
echo -e " Example: ${COLOR_GREEN}$0 --dead-host-delete 5${CoR}"
exit 1
fi
check_token_notverbose
CHECK_RESPONSE=$(curl -s -w "HTTPSTATUS:%{http_code}" \
"$BASE_URL/nginx/dead-hosts/$host_id" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" 2>/dev/null)
CHECK_BODY=${CHECK_RESPONSE//HTTPSTATUS:*/}
CHECK_STATUS=${CHECK_RESPONSE##*HTTPSTATUS:}
if [ "$CHECK_STATUS" -eq 404 ]; then
echo -e " ⛔ ${COLOR_RED}ERROR: Dead host ID ${COLOR_YELLOW}$host_id${COLOR_RED} not found!${CoR}"
exit 1
elif [ "$CHECK_STATUS" -ne 200 ]; then
echo -e " ⛔ ${COLOR_RED}ERROR: Failed to check dead host. Status: $CHECK_STATUS${CoR}"
exit 1
fi
DOMAIN_NAME=$(echo "$CHECK_BODY" | jq -r '.domain_names[0] // "unknown"')
if [ "$AUTO_YES" != true ]; then
echo -e " ┌───────────────────────────────────────────"
echo -e " │ ID: ${COLOR_YELLOW}$host_id${CoR}"
echo -e " │ Domain: ${COLOR_GREEN}$DOMAIN_NAME${CoR}"
echo -e " └───────────────────────────────────────────"
echo -e " ⚠️ ${COLOR_RED}WARNING: This action cannot be undone!${CoR}"
read -n 1 -r -p " 🔔 Confirm deletion? (y/n): " CONFIRM
echo
if [[ ! $CONFIRM =~ ^[Yy]$ ]]; then
echo -e " ❌ ${COLOR_RED}Operation cancelled${CoR}"
exit 1
fi
else
echo -e "\n ${COLOR_YELLOW}🔔 -y Auto-confirming deletion${CoR}"
fi
echo -e " 🗑️ Deleting dead host ${COLOR_GREEN}$DOMAIN_NAME${CoR} (ID: ${COLOR_GREEN}$host_id${CoR})..."
RESPONSE=$(curl -s -w "HTTPSTATUS:%{http_code}" -X DELETE \
"$BASE_URL/nginx/dead-hosts/$host_id" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" 2>/dev/null)
HTTP_STATUS=${RESPONSE##*HTTPSTATUS:}
if [ "${HTTP_STATUS:-0}" -eq 200 ]; then
echo -e " ✅ ${COLOR_GREEN}Dead host ${COLOR_YELLOW}$DOMAIN_NAME${CoR} (ID: $host_id) deleted successfully!${CoR}\n"
exit 0
else
echo -e " ⛔ ${COLOR_RED}Failed to delete dead host. Status: $HTTP_STATUS${CoR}"
exit 1
fi
}
################################
# Enable a dead host by ID
dead_host_enable() {
local host_id="$1"
if [ -z "$host_id" ]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: The --dead-host-enable option requires a host 🆔.${CoR}"
echo -e " Usage : ${COLOR_ORANGE}$0 --dead-host-enable <id>${CoR}"
echo -e " Example: ${COLOR_GREEN}$0 --dead-host-enable 5${CoR}"
return 1
fi
check_token_notverbose
CHECK_RESPONSE=$(curl -s -X GET "$BASE_URL/nginx/dead-hosts/$host_id" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
if echo "$CHECK_RESPONSE" | jq -e '.error' >/dev/null 2>&1; then
echo -e " ⛔ ${COLOR_RED}Dead host with ID $host_id does not exist${CoR}"
return 1
fi
DOMAIN_NAME=$(echo "$CHECK_RESPONSE" | jq -r '.domain_names[0]')
echo -e "\n ${COLOR_YELLOW}🔄 Enabling dead host ${CoR} 🆔${COLOR_CYAN}$host_id${CoR} 🌐Domain: ${COLOR_CYAN}$DOMAIN_NAME${CoR}"
RESPONSE=$(curl -s -X POST "$BASE_URL/nginx/dead-hosts/$host_id/enable" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" \
-H "Content-Type: application/json; charset=UTF-8")
if ! echo "$RESPONSE" | jq -e '.error' >/dev/null 2>&1; then
echo -e " ✅ ${COLOR_GREEN}Successfully enabled ${CoR}🆔${COLOR_CYAN}$host_id${CoR} 🌐Domain: ${COLOR_CYAN}$DOMAIN_NAME${CoR}"
else
ERROR_MSG=$(echo "$RESPONSE" | jq -r '.error.message // "Unknown error"')
echo -e " ⛔ ${COLOR_RED}Failed to enable dead host: $ERROR_MSG${CoR}"
return 1
fi
}
################################
# Disable a dead host by ID
dead_host_disable() {
local host_id="$1"
if [ -z "$host_id" ]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: The --dead-host-disable option requires a host 🆔.${CoR}"
echo -e " Usage : ${COLOR_ORANGE}$0 --dead-host-disable <id>${CoR}"
echo -e " Example: ${COLOR_GREEN}$0 --dead-host-disable 5${CoR}"
return 1
fi
check_token_notverbose
CHECK_RESPONSE=$(curl -s -X GET "$BASE_URL/nginx/dead-hosts/$host_id" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
if echo "$CHECK_RESPONSE" | jq -e '.error' >/dev/null 2>&1; then
echo -e " ⛔ ${COLOR_RED}Dead host with ID $host_id does not exist${CoR}"
return 1
fi
DOMAIN_NAME=$(echo "$CHECK_RESPONSE" | jq -r '.domain_names[0]')
echo -e "\n ${COLOR_YELLOW}🔄 Disabling dead host ${CoR} 🆔${COLOR_CYAN}$host_id${CoR} 🌐Domain: ${COLOR_CYAN}$DOMAIN_NAME${CoR}"
RESPONSE=$(curl -s -X POST "$BASE_URL/nginx/dead-hosts/$host_id/disable" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" \
-H "Content-Type: application/json; charset=UTF-8")
if ! echo "$RESPONSE" | jq -e '.error' >/dev/null 2>&1; then
echo -e " ✅ ${COLOR_GREEN}Successfully disabled ${CoR}🆔${COLOR_CYAN}$host_id${CoR} 🌐Domain: ${COLOR_CYAN}$DOMAIN_NAME${CoR}"
else
ERROR_MSG=$(echo "$RESPONSE" | jq -r '.error.message // "Unknown error"')
echo -e " ⛔ ${COLOR_RED}Failed to disable dead host: $ERROR_MSG${CoR}"
return 1
fi
}
################################
# Update an existing proxy host
update_proxy_host() {
check_token_notverbose
HOST_ID=$1
echo -e "\n 🔄 Updating proxy host for $DOMAIN_NAMES..."
# 🔥 check if the required parameters are set
if [ -z "$DOMAIN_NAMES" ] || [ -z "$FORWARD_HOST" ] || [ -z "$FORWARD_PORT" ] || [ -z "$FORWARD_SCHEME" ]; then
echo -e " ⛔${COLOR_RED} ERROR: Missing required parameters (domain, forward host, forward port, forward scheme).${CoR}"
exit 1
fi
# 🔥 check if the FORWARD_PORT is a number
if ! [[ "$FORWARD_PORT" =~ ^[0-9]+$ ]]; then
echo -e " ⛔${COLOR_RED} ERROR: FORWARD_PORT is not a number! Value: '$FORWARD_PORT'${CoR}"
exit 1
fi
# 🔥 Correct the CUSTOM_LOCATIONS
if [[ -z "$CUSTOM_LOCATIONS" || "$CUSTOM_LOCATIONS" == "null" ]]; then
CUSTOM_LOCATIONS_ESCAPED="[]"
else
CUSTOM_LOCATIONS_ESCAPED=$(echo "$CUSTOM_LOCATIONS" | jq -c . 2>/dev/null || echo '[]')
fi
# correct the boolean (true / false in JSON)
CACHING_ENABLED_JSON=$([ "$CACHING_ENABLED" == "true" ] && echo true || echo false)
BLOCK_EXPLOITS_JSON=$([ "$BLOCK_EXPLOITS" == "true" ] && echo true || echo false)
ALLOW_WEBSOCKET_UPGRADE_JSON=$([ "$ALLOW_WEBSOCKET_UPGRADE" == "true" ] && echo true || echo false)
HTTP2_SUPPORT_JSON=$([ "$HTTP2_SUPPORT" == "true" ] && echo true || echo false)
# 🔍 Debugging variables before JSON update:
debug_var
# 🔥 generate the JSON properly
DATA=$(
jq -n \
--arg domain "$DOMAIN_NAMES" \
--arg host "$FORWARD_HOST" \
--arg port "$FORWARD_PORT" \
--arg scheme "$FORWARD_SCHEME" \
--argjson caching "$CACHING_ENABLED_JSON" \
--argjson block_exploits "$BLOCK_EXPLOITS_JSON" \
--arg advanced_config "$ADVANCED_CONFIG" \
--argjson websocket_upgrade "$ALLOW_WEBSOCKET_UPGRADE_JSON" \
--argjson http2_support "$HTTP2_SUPPORT_JSON" \
--argjson enabled true \
--argjson locations "$CUSTOM_LOCATIONS_ESCAPED" \
'{
domain_names: [$domain],
forward_host: $host,
forward_port: ($port | tonumber),
access_list_id: null,
certificate_id: null,
ssl_forced: false,
caching_enabled: $caching,
block_exploits: $block_exploits,
advanced_config: $advanced_config,
meta: { dns_challenge: null },
allow_websocket_upgrade: $websocket_upgrade,
http2_support: $http2_support,
forward_scheme: $scheme,
enabled: $enabled,
locations: $locations
}'
)
# 🔍 check if the JSON is valid
if ! echo "$DATA" | jq empty >/dev/null 2>&1; then
echo -e " ⛔${COLOR_RED} ERROR: Invalid JSON generated:\n$DATA ${CoR}"
exit 1
fi
# 🚀 send the API request for update
RESPONSE=$(curl -s -X PUT "$BASE_URL/nginx/proxy-hosts/$HOST_ID" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" \
-H "Content-Type: application/json; charset=UTF-8" \
--data-raw "$DATA")
# 📢 check if the response is valid
ERROR_MSG=$(echo "$RESPONSE" | jq -r '.error.message // empty')
if [ -z "$ERROR_MSG" ]; then
PROXY_ID=$(echo "$RESPONSE" | jq -r '.id // "unknown"')
echo -e "\n ✅ ${COLOR_GREEN}SUCCESS: Proxy host 🔗$DOMAIN_NAMES (ID: ${COLOR_YELLOW}$PROXY_ID${COLOR_GREEN}) was created successfully! 🎉${CoR}\n"
else
echo -e " ⛔ ${COLOR_RED}Failed to create proxy host. Error: $ERROR_MSG ${CoR}"
exit 1
fi
}
################################
# Update field of existing proxy host
host_update() {
check_token_notverbose
HOST_ID="$1"
FIELD="$2"
NEW_VALUE="$3"
#echo -e "\n 🔄 DEBUG: host-update() function has started"
# echo -e " 🆔 HOST_ID: $HOST_ID"
# echo -e " 🏷 FIELD: $FIELD"
# echo -e " ✏️ VALUE: $NEW_VALUE"
# 1) Check that all required parameters are provided
if [ -z "$HOST_ID" ] || [ -z "$FIELD" ] || [ -z "$NEW_VALUE" ]; then
echo -e "\n ⛔ ${COLOR_RED}INVALID command: Missing required parameters.${CoR}"
echo -e " Usage: ${COLOR_ORANGE}$0 --host-update <host_id> <field=value>${CoR}"
exit 1
fi
# 2) Fetch current configuration
CURRENT_DATA=$(curl -s -X GET "$BASE_URL/nginx/proxy-hosts/$HOST_ID" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
#echo -e "\n 🔄 DEBUG: API response (CURRENT_DATA):\n$CURRENT_DATA\n"
if ! echo "$CURRENT_DATA" | jq empty >/dev/null 2>&1; then
echo -e " ⛔ ${COLOR_RED}ERROR:${CoR} Failed to fetch current proxy configuration."
exit 1
fi
# 3) Check if the requested field is editable
FILTERED_DATA=$(echo "$CURRENT_DATA" | jq '{
domain_names,
forward_host,
forward_port,
access_list_id,
certificate_id,
ssl_forced,
caching_enabled,
block_exploits,
advanced_config,
meta,
allow_websocket_upgrade,
http2_support,
forward_scheme,
enabled,
locations,
hsts_enabled,
hsts_subdomains
}')
#echo -e "\n 🔄 DEBUG: Filtered JSON before update:\n$FILTERED_DATA\n"
if ! echo "$FILTERED_DATA" | jq -e --arg field "$FIELD" 'has($field)' >/dev/null; then
echo -e " ⛔ ${COLOR_RED}ERROR:${CoR} The field '$FIELD' is not a valid field for update."
exit 1
fi
# 4) Modifier la configuration
if [ "$FIELD" = "forward_port" ]; then
UPDATED_DATA=$(echo "$FILTERED_DATA" |
jq --argjson newVal "$(echo "$NEW_VALUE" | jq -R 'tonumber? // 0')" \
'.forward_port = $newVal')
else
UPDATED_DATA=$(echo "$FILTERED_DATA" |
jq --arg newVal "$NEW_VALUE" \
".$FIELD = \$newVal")
fi
#echo -e "\n 🔄 DEBUG: JSON Sent to API:\n$UPDATED_DATA\n"
if ! echo "$UPDATED_DATA" | jq empty >/dev/null 2>&1; then
echo -e " ⛔ ${COLOR_RED}ERROR: Invalid JSON generated.${CoR}\n$UPDATED_DATA"
exit 1
fi
# Sending update request to API..."
RESPONSE=$(curl -s -X PUT "$BASE_URL/nginx/proxy-hosts/$HOST_ID" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" \
-H "Content-Type: application/json; charset=UTF-8" \
--data-raw "$UPDATED_DATA")
#echo -e "\n 🔄 DEBUG: API Response:\n$RESPONSE\n"
ERROR_MSG=$(echo "$RESPONSE" | jq -r '.error.message // empty')
if [ -z "$ERROR_MSG" ]; then
echo -e "\n ✅ ${COLOR_GREEN}SUCCESS:${CoR} Proxy host 🆔 $HOST_ID updated successfully! 🎉"
SUMMARY=$(curl -s -X GET "$BASE_URL/nginx/proxy-hosts/$HOST_ID" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
if echo "$SUMMARY" | jq empty >/dev/null 2>&1; then
echo -e " New Value Proxy host 🆔 $HOST_ID $FIELD : $(echo "$SUMMARY" | jq -r --arg field "$FIELD" '.[$field]')\n"
echo -e " 🔄 ${COLOR_CYAN}Summary Proxy Host:${CoR}"
echo -e " Domaine(s) : $(echo "$SUMMARY" | jq -r '.domain_names | join(", ")')"
echo -e " Forward Host : $(echo "$SUMMARY" | jq -r '.forward_host')"
echo -e " Forward Port : $(echo "$SUMMARY" | jq -r '.forward_port')"
echo -e " Forward Scheme : $(echo "$SUMMARY" | jq -r '.forward_scheme')"
echo -e " SSL Forced : $(echo "$SUMMARY" | jq -r '.ssl_forced')\n"
else
echo -e "\n ⚠️ ${COLOR_YELLOW}WARNING:${CoR} Error to get proxy host data."
fi
else
echo -e "\n ⛔ ${COLOR_RED}Failed to update proxy host. Error:${CoR} $ERROR_MSG"
exit 1
fi
}
################################
# Search for a proxy host by domain name
host_search() {
#check_token false
check_token_notverbose
if [ -z "$HOST_SEARCHNAME" ]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: The --host-search option requires a <host domain>.${CoR}"
echo -e " Usage : ${COLOR_ORANGE}$0 --host-search domain_name${CoR}"
echo -e " Example: ${COLOR_GREEN}$0 --host-search example.com${CoR}\n"
exit 1
fi
echo -e "\n ${COLOR_CYAN}🔍${CoR} Searching proxy hosts for: ${COLOR_YELLOW}$HOST_SEARCHNAME${CoR}"
RESPONSE=$(curl -s -X GET "$BASE_URL/nginx/proxy-hosts" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
if [ -z "$RESPONSE" ] || [ "$RESPONSE" == "null" ]; then
echo -e " ⛔ ${COLOR_RED}Error: Unable to retrieve proxy host data.${CoR}"
exit 1
fi
MATCHES=$(echo "$RESPONSE" | jq -c --arg search "$HOST_SEARCHNAME" '.[] | select(.domain_names[] | contains($search))')
if [ -z "$MATCHES" ]; then
echo -e " ❌ No proxy host found for: ${COLOR_YELLOW}$HOST_SEARCHNAME${CoR}"
else
echo "$MATCHES" | while IFS= read -r line; do
id=$(echo "$line" | jq -r '.id')
domain_names=$(echo "$line" | jq -r '.domain_names[]')
printf " ${COLOR_GREY}🆔${COLOR_YELLOW}%4s${CoR} ${COLOR_GREEN}%s${CoR}\n" "$id" "$domain_names"
done
fi
echo ""
exit 0
}
################################
################################
# Enable a proxy host by ID
#
# 💡 NPM-specific endpoint discovery (2025-05-30):
# ================================================
# Like host_disable(), this function uses NPM's dedicated endpoint:
#
# ✅ CORRECT approach:
# POST /api/nginx/proxy-hosts/{id}/enable with empty body
# → Actually enables the site and makes it accessible
#
# ❌ The generic PUT approach with JSON modification doesn't work reliably.
# Reference: Same discovery as host_disable() function above.
################################
# Enable a proxy host by ID
host_enable() {
local host_id="$1"
if [ -z "$host_id" ]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: The --host-enable option requires a host 🆔.${CoR}"
echo -e " Usage : ${COLOR_ORANGE}$0 --host-enable <host_id>${CoR}"
echo -e " Example: ${COLOR_GREEN}$0 --host-enable 42${CoR}"
return 1
fi
check_token_notverbose
# Check if the proxy host exists first
CHECK_RESPONSE=$(curl -s -X GET "$BASE_URL/nginx/proxy-hosts/$host_id" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
if [ $? -eq 0 ] && [ -n "$CHECK_RESPONSE" ]; then
# Check if the host was found (not a 404 error)
if echo "$CHECK_RESPONSE" | jq -e '.error' >/dev/null 2>&1; then
echo -e " ⛔ ${COLOR_RED}Proxy host with ID $host_id does not exist${CoR}"
return 1
fi
# Get domain name for display
DOMAIN_NAME=$(echo "$CHECK_RESPONSE" | jq -r '.domain_names[0]')
echo -e "\n ${COLOR_YELLOW}🔄 Enabling proxy host ${CoR} 🆔${COLOR_CYAN}$host_id${CoR} 🌐Domain: ${COLOR_CYAN}$DOMAIN_NAME${CoR}"
# Use the CORRECT NPM endpoint: POST to /enable (like the web interface does)
RESPONSE=$(curl -s -X POST "$BASE_URL/nginx/proxy-hosts/$host_id/enable" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" \
-H "Content-Type: application/json; charset=UTF-8")
if [ $? -eq 0 ] && ! echo "$RESPONSE" | jq -e '.error' >/dev/null 2>&1; then
echo -e " ✅ ${COLOR_GREEN}Successfully enabled ${CoR}🆔${COLOR_CYAN}$host_id${CoR} 🌐Domain: ${COLOR_CYAN}$DOMAIN_NAME${CoR}"
# echo -e " 🎯 ${COLOR_GREEN}Using NPM's native enable endpoint${CoR}\n"
else
ERROR_MSG=$(echo "$RESPONSE" | jq -r '.error.message // "Unknown error"')
echo -e " ⛔ ${COLOR_RED}Failed to enable proxy host: $ERROR_MSG${CoR}"
return 1
fi
else
echo -e " ⛔ ${COLOR_RED}Proxy host with ID $host_id does not exist${CoR}"
return 1
fi
}
################################
# Disable a proxy host by ID
#
# 🐛 IMPORTANT BUG DISCOVERY (2025-05-30):
# ==========================================
# The NPM web interface uses a DIFFERENT endpoint than expected!
#
# ❌ WRONG approach (what we tried before):
# PUT /api/nginx/proxy-hosts/{id} with modified JSON (enabled: false)
# → Shows "disabled" in interface but site remains ACCESSIBLE!
#
# ✅ CORRECT approach (what NPM web interface actually uses):
# POST /api/nginx/proxy-hosts/{id}/disable with empty body
# → Actually disables the site and makes it INACCESSIBLE!
#
# This was discovered by inspecting NPM web interface network traffic.
# NPM has dedicated /enable and /disable endpoints that work properly.
# Reference: GitHub issue #3 (nginx-proxy-manager-Bash-API)
################################
host_disable() {
local host_id="$1"
if [ -z "$host_id" ]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: The --host-disable option requires a host 🆔.${CoR}"
echo -e " Usage : ${COLOR_ORANGE}$0 --host-disable <host_id>${CoR}"
echo -e " Example: ${COLOR_GREEN}$0 --host-disable 42${CoR}"
return 1
fi
check_token_notverbose
# Check if the proxy host exists first
CHECK_RESPONSE=$(curl -s -X GET "$BASE_URL/nginx/proxy-hosts/$host_id" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
if [ $? -eq 0 ] && [ -n "$CHECK_RESPONSE" ]; then
# Check if the host was found (not a 404 error)
if echo "$CHECK_RESPONSE" | jq -e '.error' >/dev/null 2>&1; then
echo -e " ⛔ ${COLOR_RED}Proxy host with ID $host_id does not exist${CoR}"
return 1
fi
# Get domain name for display
DOMAIN_NAME=$(echo "$CHECK_RESPONSE" | jq -r '.domain_names[0]')
echo -e "\n ${COLOR_YELLOW}🔄 Disabling proxy host ${CoR} 🆔${COLOR_CYAN}$host_id${CoR} 🌐Domain: ${COLOR_CYAN}$DOMAIN_NAME${CoR}"
# Use the CORRECT NPM endpoint: POST to /disable (like the web interface does)
RESPONSE=$(curl -s -X POST "$BASE_URL/nginx/proxy-hosts/$host_id/disable" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" \
-H "Content-Type: application/json; charset=UTF-8")
if [ $? -eq 0 ] && ! echo "$RESPONSE" | jq -e '.error' >/dev/null 2>&1; then
echo -e " ✅ ${COLOR_GREEN}Successfully disabled ${CoR}🆔${COLOR_CYAN}$host_id${CoR} 🌐Domain: ${COLOR_CYAN}$DOMAIN_NAME${CoR}"
# echo -e " 🎯 ${COLOR_GREEN}Using NPM's native disable endpoint${CoR}\n"
else
ERROR_MSG=$(echo "$RESPONSE" | jq -r '.error.message // "Unknown error"')
echo -e " ⛔ ${COLOR_RED}Failed to disable proxy host: $ERROR_MSG${CoR}"
return 1
fi
else
echo -e " ⛔ ${COLOR_RED}Proxy host with ID $host_id does not exist${CoR}"
return 1
fi
}
################################
# Delete a proxy host
# $1: host_id - ID of the host to delete
host_delete() {
local host_id="$1"
# Check if ID is provided
if [ -z "$HOST_ID" ]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: The --host-delete option requires a host 🆔${CoR}"
echo -e " Usage : ${COLOR_ORANGE}$0 --host-delete <host_id>${CoR}"
echo -e " Example: ${COLOR_GREEN}$0 --host-delete 42${CoR}"
exit 1
fi
check_token_notverbose
# Check if host exists before attempting deletion
#echo -e "\n 🔎 Checking if host ID ${COLOR_GREEN}$HOST_ID${CoR} exists..."
CHECK_RESPONSE=$(curl -s -w "HTTPSTATUS:%{http_code}" \
"$BASE_URL/nginx/proxy-hosts/$HOST_ID" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" 2>/dev/null)
CHECK_BODY=${CHECK_RESPONSE//HTTPSTATUS:*/}
CHECK_STATUS=${CHECK_RESPONSE##*HTTPSTATUS:}
# Verify host existence
if [ "$CHECK_STATUS" -eq 404 ]; then
echo -e " ⛔ ${COLOR_RED}ERROR: Host ID ${COLOR_GREEN}$HOST_ID${COLOR_RED} not found!${CoR}"
echo -e "\n ${COLOR_YELLOW}💡 Tip: Use --host-list to see all available hosts and their IDs${CoR}"
exit 1
elif [ "$CHECK_STATUS" -ne 200 ]; then
echo -e " ⛔ ${COLOR_RED}ERROR: Failed to check host. Status: $CHECK_STATUS${CoR}"
if [ -n "$CHECK_BODY" ]; then
echo -e " 📝 Error details: $CHECK_BODY"
fi
exit 1
fi
# Extract domain name for confirmation
DOMAIN_NAME=$(echo "$CHECK_BODY" | jq -r '.domain_names[0] // "unknown"')
# Show confirmation prompt unless AUTO_YES is set
if [ "$AUTO_YES" != true ]; then
echo -e " ┌───────────────────────────────────────────"
echo -e " │ ID: ${COLOR_YELLOW}$HOST_ID${CoR}"
echo -e " │ Domain: ${COLOR_GREEN}$DOMAIN_NAME${CoR}"
echo -e " └───────────────────────────────────────────"
echo -e " ⚠️ ${COLOR_RED}WARNING: This action cannot be undone!${CoR}"
read -n 1 -r -p " 🔔 Confirm deletion? (y/n): " CONFIRM
echo
if [[ ! $CONFIRM =~ ^[Yy]$ ]]; then
echo -e " ❌ ${COLOR_RED}Operation cancelled${CoR}"
exit 1
fi
echo -e " 🗑️ Deleting proxy host ${COLOR_GREEN}$DOMAIN_NAME${CoR} (ID: ${COLOR_GREEN}$HOST_ID${CoR})"
else
echo -e "\n ${COLOR_YELLOW}🔔 -y Auto-confirming deletion${CoR}"
echo -e " 🗑️ Deleting proxy host ${COLOR_GREEN}$DOMAIN_NAME${CoR} (ID: ${COLOR_GREEN}$HOST_ID${CoR})..."
fi
# Perform deletion
RESPONSE=$(curl -s -w "HTTPSTATUS:%{http_code}" -X DELETE \
"$BASE_URL/nginx/proxy-hosts/$HOST_ID" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" 2>/dev/null)
HTTP_BODY=${RESPONSE//HTTPSTATUS:*/}
HTTP_STATUS=${RESPONSE##*HTTPSTATUS:}
if [ "${HTTP_STATUS:-0}" -eq 200 ]; then
echo -e " ✅ ${COLOR_GREEN}Proxy host ${COLOR_YELLOW}$DOMAIN_NAME${CoR} (ID: $HOST_ID) deleted successfully!${CoR}\n"
exit 0
else
echo -e " ⛔ ${COLOR_RED}Failed to delete proxy host. Status: $HTTP_STATUS${CoR}"
if [ -n "$HTTP_BODY" ]; then
echo -e " 📝 Error details: $HTTP_BODY"
fi
exit 1
fi
}
################################
# ACL proxy host
host_acl_enable() {
# Check that both arguments are provided
if [ -z "$HOST_ID" ] || [ -z "$ACCESS_LIST_ID" ]; then
echo -e "\n ⛔ ${COLOR_RED}Error: HOST_ID and ACCESS_LIST_ID are required to enable the ACL.${CoR}"
echo -e " Usage: ${COLOR_ORANGE}$0 --host-acl-enable <host_id> <access_list_id>${CoR}"
echo -e " Example: ${COLOR_GREEN}$0 --host-acl-enable 42 5${CoR}"
exit 1
fi
check_token_notverbose
echo -e "\n ${COLOR_YELLOW}🔓 Enabling ACL${CoR} host 🆔${COLOR_CYAN}$HOST_ID${CoR} with access list 🆔${COLOR_CYAN}$ACCESS_LIST_ID${CoR}"
DATA=$(jq -n \
--argjson access_list_id "$ACCESS_LIST_ID" \
--argjson enabled true \
'{
access_list_id: $access_list_id,
enabled: $enabled
}')
RESPONSE=$(curl -s -w "HTTPSTATUS:%{http_code}" -X PUT "$BASE_URL/nginx/proxy-hosts/$HOST_ID" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" \
-H "Content-Type: application/json; charset=UTF-8" \
--data-raw "$DATA")
HTTP_BODY=${RESPONSE//HTTPSTATUS:*/}
HTTP_STATUS=${RESPONSE##*HTTPSTATUS:}
if [ "$HTTP_STATUS" = "200" ]; then
echo -e " ✅ ${COLOR_GREEN}ACL successfully enabled access list ${CoR}🆔${COLOR_CYAN}$ACCESS_LIST_ID${CoR} for host🆔${COLOR_CYAN}$HOST_ID${CoR}\n"
else
echo -e " ⛔ ${COLOR_RED}Failed to enable ACL (HTTP $HTTP_STATUS). Error: $(echo "$HTTP_BODY" | jq -r '.error.message // "Unknown error"')${CoR}\n"
exit 1
fi
}
################################
# Disable ACL for a given proxy host
host_acl_disable() {
if [ -z "$HOST_ID" ]; then
echo -e "\n ⛔ ${COLOR_RED}Error: HOST_ID is required to disable the ACL.${CoR}"
echo -e " Usage: $0 --host-acl-disable <host_id>\n"
exit 1
fi
check_token_notverbose
DATA=$(jq -n \
--argjson access_list_id null \
--argjson enabled false \
'{
access_list_id: $access_list_id,
enabled: $enabled
}')
RESPONSE=$(curl -s -w "HTTPSTATUS:%{http_code}" -X PUT "$BASE_URL/nginx/proxy-hosts/$HOST_ID" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" \
-H "Content-Type: application/json; charset=UTF-8" \
--data-raw "$DATA")
HTTP_BODY=${RESPONSE//HTTPSTATUS:*/}
HTTP_STATUS=${RESPONSE##*HTTPSTATUS:}
if [ "$HTTP_STATUS" = "200" ]; then
echo -e "\n 🔒 ${COLOR_ORANGE}Disabling ACL for host${CoR}🆔${COLOR_CYAN} $HOST_ID${CoR}"
echo -e " ✅ ${COLOR_GREEN}ACL successfully disabled ACL for host ${CoR}🆔${COLOR_CYAN}$HOST_ID${CoR}\n"
else
echo -e "\n ⛔ ${COLOR_RED}Failed to disable ACL (HTTP $HTTP_STATUS). Error: $(echo "$HTTP_BODY" | jq -r '.error.message // "Unknown error"')${CoR}\n"
exit 1
fi
}
################################
# Show details of a specific proxy host
host_show() {
#local host_id="$1"
if [ -z "$host_id" ]; then
echo -e "\n ⛔ ${COLOR_RED}The --host-show option requires a host ID.${CoR}"
echo -e " Usage: ${COLOR_ORANGE}$0 --host-show <ID>${CoR}"
echo -e " To find ID Check with ${COLOR_ORANGE}$0 --host-list${CoR}\n"
return 1
fi
check_token_notverbose
[ "$JSON_OUTPUT" = true ] || echo -e "\n 🔍 Fetching details for proxy host ID: ${COLOR_YELLOW}$host_id${CoR}..."
# get host details
local response=$(curl -s -X GET "$BASE_URL/nginx/proxy-hosts/$host_id" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
# Check if the response contains an error
if echo "$response" | jq -e '.error' >/dev/null; then
if [ "$JSON_OUTPUT" = true ]; then
echo "$response" | jq '{error: (.error.message // "Unknown error")}'
exit 1
fi
echo -e " ⛔ ${COLOR_RED}Error: $(echo "$response" | jq -r '.error.message')${CoR}\n"
return 1
fi
if [ "$JSON_OUTPUT" = true ]; then
echo "$response" | jq .
exit 0
fi
# Format and display details
echo -e "\n📋 ${COLOR_YELLOW}Host Details:${CoR}"
echo -e "┌───────────────────────────────────"
echo -e "│ 🆔 ID: ${COLOR_GREEN_b}$(echo "$response" | jq -r '.id')${CoR}"
echo -e "│ 🌐 Domains: ${COLOR_GREEN_b}$(echo "$response" | jq -r '.domain_names[]' | tr '\n' ' ')${CoR}"
echo -e "│ 🔄 Forward Configuration:"
echo -e "│ • Host: ${COLOR_GREEN_b}$(echo "$response" | jq -r '.forward_host')${CoR}"
echo -e "│ • Port: ${COLOR_GREEN_b}$(echo "$response" | jq -r '.forward_port')${CoR}"
echo -e "│ • Scheme: ${COLOR_GREEN_b}$(echo "$response" | jq -r '.forward_scheme')${CoR}"
echo -e "│ ✅ Status: $(colorize_boolean $(echo "$response" | jq -r '.enabled | if . then "Enabled" else "Disabled" end'))"
echo -e "│ 🔒 SSL Configuration:"
echo -e "│ • Certificate ID: ${COLOR_ORANGE}$(echo "$response" | jq -r '.certificate_id')${CoR}"
echo -e "│ • SSL Forced: $(colorize_boolean $(echo "$response" | jq -r '.ssl_forced | if . then "true" else "false" end'))"
echo -e "│ • HTTP/2: $(colorize_boolean $(echo "$response" | jq -r '.http2_support | if . then "true" else "false" end'))"
echo -e "│ • HSTS: $(colorize_boolean $(echo "$response" | jq -r '.hsts_enabled | if . then "true" else "false" end'))"
echo -e "│ 🛠️ Features:"
echo -e "│ • Block Exploits: $(colorize_boolean $(echo "$response" | jq -r '.block_exploits | if . then "true" else "false" end'))"
echo -e "│ • Caching: $(colorize_boolean $(echo "$response" | jq -r '.caching_enabled | if . then "true" else "false" end'))"
echo -e "│ • Websocket Upgrade: $(colorize_boolean $(echo "$response" | jq -r '.allow_websocket_upgrade | if . then "true" else "false" end'))"
echo -e "│ 🔑 Access List ID: ${COLOR_ORANGE}$(echo "$response" | jq -r '.access_list_id')${CoR}"
# Check and display advanced configuration
if [ "$(echo "$response" | jq -r '.advanced_config')" != "null" ]; then
echo -e "│ ⚙️ Advanced Config: ${COLOR_GREEN}Yes${CoR}"
echo -e "│"
echo "$response" | jq -r '.advanced_config' | while IFS= read -r line; do
if [ -n "$line" ]; then
echo -e "│ ${COLOR_GRAY}$line${CoR}"
else
echo -e "│"
fi
done
else
echo -e "│ ⚙️ Advanced Config: ${COLOR_RED}No${CoR}"
fi
echo -e "└────────────────────────────────────"
return 0
}
################################
# Delete a certificate in NPM
# NOTE: NPM's DELETE /nginx/certificates/:id is a SOFT delete (sets is_deleted=1):
# the cert leaves the list/UI but its on-disk files remain. The optional --purge
# flag (see cert_purge_files) removes those leftover files. The DB row is left
# untouched on purpose to avoid any referential-integrity risk.
################################
# Remove the on-disk certificate files left behind by NPM's soft-delete.
# Requires NGINX_PATH_DOCKER (set in npm-api.conf) to point at the NPM data
# directory that holds custom_ssl/ and letsencrypt/. No-op (with a clear
# message) when that path is not configured/accessible on this host.
cert_purge_files() {
local id="$1"
local base="${NGINX_PATH_DOCKER:-}"
if [ -z "$base" ] || [ "$base" = "$DEFAULT_NGINX_PATH_DOCKER" ]; then
echo -e " ⚠️ ${COLOR_YELLOW}--purge skipped:${CoR} set ${COLOR_CYAN}NGINX_PATH_DOCKER${CoR} in npm-api.conf to the NPM data dir (holding ${COLOR_GREY}custom_ssl/${CoR} and ${COLOR_GREY}letsencrypt/${CoR}).\n"
return 0
fi
if [ ! -d "$base" ]; then
echo -e " ⚠️ ${COLOR_YELLOW}--purge skipped:${CoR} NGINX_PATH_DOCKER (${COLOR_GREY}$base${CoR}) is not an accessible directory on this host.\n"
return 0
fi
local targets=(
"$base/custom_ssl/npm-$id"
"$base/letsencrypt/live/npm-$id"
"$base/letsencrypt/archive/npm-$id"
"$base/letsencrypt/renewal/npm-$id.conf"
)
local removed=0 t
for t in "${targets[@]}"; do
if [ -e "$t" ]; then
if rm -rf "$t" 2>/dev/null; then
echo -e " 🧹 Removed ${COLOR_GREY}$t${CoR}"
removed=$((removed + 1))
else
echo -e " ⛔ ${COLOR_RED}Could not remove $t${CoR} ${COLOR_GREY}(permissions? try as root or inside the NPM container)${CoR}"
fi
fi
done
if [ "$removed" -eq 0 ]; then
echo -e " ℹ️ ${COLOR_GREY}--purge: no on-disk files found for npm-$id under $base${CoR}\n"
else
echo -e " ✅ ${COLOR_GREEN}Purged $removed on-disk item(s) for cert npm-$id${CoR}\n"
fi
}
################################
cert_delete() {
local CERT_IDENTIFIER="$1"
if [ -z "$CERT_IDENTIFIER" ]; then
echo -e "\n ⛔ ${COLOR_RED}Error: Please specify a domain or certificate ID${CoR}"
echo -e "Usage: --cert-delete <domain.com or ID>"
exit 1
fi
check_token_notverbose
# Get certificates list from API
CERTIFICATES=$(curl -s -X GET "$BASE_URL/nginx/certificates" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
# Check if input is a number (ID) or domain
if [[ "$CERT_IDENTIFIER" =~ ^[0-9]+$ ]]; then
# It's an ID
CERT_ID="$CERT_IDENTIFIER"
CERT_EXISTS=$(echo "$CERTIFICATES" | jq -r --arg id "$CERT_ID" '.[] | select(.id == ($id|tonumber)) | .id')
# Add verification for CERT_EXISTS
if [ -z "$CERT_EXISTS" ]; then
echo -e "\n ⛔ ${COLOR_RED}No certificate found with ID: $CERT_ID${CoR}"
exit 1
fi
#echo -e " ✅ Certificate ID $CERT_ID found"
else
# It's a domain - Get all matching certificates
MATCHING_CERTS=$(echo "$CERTIFICATES" | jq -r --arg domain "$CERT_IDENTIFIER" \
'[.[] | select(.domain_names[] == $domain or .nice_name == $domain)]')
CERT_COUNT=$(echo "$MATCHING_CERTS" | jq 'length')
if [ "$CERT_COUNT" -eq 0 ]; then
echo -e " ⛔ ${COLOR_RED}No certificates found for domain: $CERT_IDENTIFIER${CoR}"
exit 1
elif [ "$CERT_COUNT" -eq 1 ]; then
CERT_ID=$(echo "$MATCHING_CERTS" | jq -r '.[0].id')
else
# Multiple certificates found, let user choose
echo -e " 📜 Multiple certificates found for $CERT_IDENTIFIER:"
echo "$MATCHING_CERTS" | jq -r '.[] | "ID: \(.id) - Provider: \(.provider) - Expires: \(.expires_on) - Domains: \(.domain_names|join(", "))"' |
awk '{print NR ") " $0}'
if [ "$AUTO_YES" = true ]; then
echo -e " ⚠️ Multiple certificates found with -y option. Please specify certificate ID instead."
exit 1
fi
read -r -p "Enter the number of the certificate to delete (1-$CERT_COUNT): " CHOICE
if ! [[ "$CHOICE" =~ ^[0-9]+$ ]] || [ "$CHOICE" -lt 1 ] || [ "$CHOICE" -gt "$CERT_COUNT" ]; then
echo -e " ⛔ ${COLOR_RED}Invalid selection${CoR}"
exit 1
fi
CERT_ID=$(echo "$MATCHING_CERTS" | jq -r --arg idx "$((CHOICE - 1))" '.[$idx|tonumber].id')
fi
fi
if [ -z "$CERT_ID" ]; then
echo -e " ⛔ ${COLOR_RED}No valid certificate found${CoR}"
exit 1
fi
# Warn if the certificate is still referenced by a proxy/redirection host.
# NPM's delete is a soft-delete, so those hosts keep a dangling certificate_id
# until their SSL is reconfigured; and --purge would remove files still in use.
REFERENCING=$(
{
curl -s -X GET "$BASE_URL/nginx/proxy-hosts" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" |
jq -r --argjson id "$CERT_ID" '.[]? | select(.certificate_id == $id) | "proxy host #\(.id) (\(.domain_names | join(", ")))"' 2>/dev/null
curl -s -X GET "$BASE_URL/nginx/redirection-hosts" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" |
jq -r --argjson id "$CERT_ID" '.[]? | select(.certificate_id == $id) | "redirection host #\(.id) (\(.domain_names | join(", ")))"' 2>/dev/null
curl -s -X GET "$BASE_URL/nginx/streams" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" |
jq -r --argjson id "$CERT_ID" '.[]? | select(.certificate_id == $id) | "stream #\(.id) (port \(.incoming_port))"' 2>/dev/null
curl -s -X GET "$BASE_URL/nginx/dead-hosts" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" |
jq -r --argjson id "$CERT_ID" '.[]? | select(.certificate_id == $id) | "dead host #\(.id) (\(.domain_names | join(", ")))"' 2>/dev/null
} | grep -v '^$' || true
)
if [ -n "$REFERENCING" ]; then
echo -e "\n ⚠️ ${COLOR_YELLOW}Certificate ID $CERT_ID is still used by:${CoR}"
echo "$REFERENCING" | while IFS= read -r ref; do
echo -e " • ${COLOR_GREY}$ref${CoR}"
done
echo -e " ${COLOR_GREY}These hosts will keep a dangling certificate_id until you reconfigure their SSL.${CoR}"
if [ "$CERT_PURGE" = true ]; then
echo -e " ⛔ ${COLOR_RED}--purge refused:${CoR} the on-disk files are still in use. Disable/re-assign SSL on those hosts first.\n"
exit 1
fi
fi
# Ask for confirmation unless AUTO_YES is set
if [ "$AUTO_YES" = true ]; then
#echo -e " 🔔 The -y option was provided. Skipping confirmation prompt..."
CONFIRM="y"
else
echo -e "${COLOR_YELLOW}"
read -n 1 -r -p " ⚠️ Are you sure you want to delete certificate ID: $CERT_ID ? (y/n): " CONFIRM
echo -e "${CoR}"
fi
if [[ "$CONFIRM" != "y" ]]; then
echo -e "${COLOR_RED} ❌ Certificate deletion aborted.${CoR}\n"
exit 0
fi
echo -e "\n 🗑️ Deleting certificate ID: $CERT_ID..."
# Delete certificate through API
HTTP_RESPONSE=$(curl -s -w "HTTPSTATUS:%{http_code}" -X DELETE \
"$BASE_URL/nginx/certificates/$CERT_ID" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
HTTP_BODY=${HTTP_RESPONSE//HTTPSTATUS:*/}
HTTP_STATUS=${HTTP_RESPONSE##*HTTPSTATUS:}
if [ "${HTTP_STATUS:-0}" -eq 200 ]; then
echo -e " ✅ ${COLOR_GREEN}Certificate successfully deleted!${CoR}\n"
if [ "$CERT_PURGE" = true ]; then
cert_purge_files "$CERT_ID"
fi
else
echo -e " ⛔ ${COLOR_RED}Deletion failed. Status: $HTTP_STATUS. Response: $HTTP_BODY${CoR}\n"
fi
}
################################
# Generate Let's Encrypt certificate if not exists
cert_generate() {
# 1. Local variables
DOMAIN_NAMES="${1:-}"
DOMAIN=$DOMAIN_NAMES
EMAIL="${2:-}"
DNS_PROVIDER="${3:-}"
DNS_CREDENTIALS_JSON="${4:-}"
EXISTING_CERT=""
IS_WILDCARD=false
# 2. Basic validation
if [ -z "$DOMAIN_NAMES" ]; then
echo -e "\n ${COLOR_RED}❌${CoR} Error: Domain is required $DOMAIN"
echo -e " Usage: $0 --cert-generate <domain> [email] [--cert-email <email>] [--dns-provider <provider>] [--dns-credentials <json>]\n"
exit 1
fi
if [ -z "$EMAIL" ]; then
EMAIL="$DEFAULT_EMAIL"
echo -e "\n 📧 Using default email: ${COLOR_YELLOW}$EMAIL${CoR}"
fi
check_token_notverbose
# 3. Check if wildcard and validate requirements
if [[ "$DOMAIN" == \** ]]; then
IS_WILDCARD=true
if [ -z "$DNS_PROVIDER" ] || [ -z "$DNS_CREDENTIALS_JSON" ]; then
echo -e " ⛔ ${COLOR_RED}DNS provider and credentials are required for wildcard certificates${CoR}\n"
exit 1
fi
# Credentials: JSON object or raw certbot INI text ("key = value" lines)
if [[ "$DNS_CREDENTIALS_JSON" == \{* ]] && ! echo "$DNS_CREDENTIALS_JSON" | jq -e 'type == "object"' >/dev/null 2>&1; then
echo -e " ⛔ ${COLOR_RED}Invalid JSON format for DNS credentials${CoR}\n"
exit 1
fi
fi
# Proxy host using this domain (needed by --host-ssl-enable)
PROXY_RESPONSE=$(curl -s -X GET "$BASE_URL/nginx/proxy-hosts" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
DOMAIN_EXISTS=$(echo "$PROXY_RESPONSE" | jq -r --arg DOMAIN "$DOMAIN" \
'first(.[] | select(.domain_names | index($DOMAIN)) | .id) // empty')
# Only require the host to exist in NPM if it is not a wildcard
if [ "$IS_WILDCARD" = false ]; then
if [ -z "$DOMAIN_EXISTS" ]; then
echo -e "\n ${COLOR_RED}❌${CoR} Domain ${COLOR_YELLOW}$DOMAIN${CoR} is not configured in NPM."
echo -e " ${COLOR_YELLOW}💡${CoR} First create a proxy host with:"
echo -e " ${COLOR_CYAN}$0 --host-create $DOMAIN -i <forward_host> -p <forward_port>${CoR}\n"
exit 1
fi
fi
# Then check existing certificates
RESPONSE=$(curl -s -X GET "$BASE_URL/nginx/certificates" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
EXISTING_CERT=$(echo "$RESPONSE" | jq -c --arg domain "$DOMAIN" \
"$CERT_JQ_EXPIRED"' first(.[] | select(.domain_names | index($domain)) | select(is_expired | not)) // empty')
if [ -n "$EXISTING_CERT" ]; then
CERT_ID=$(echo "$EXISTING_CERT" | jq -r '.id')
CERT_EXPIRES=$(echo "$EXISTING_CERT" | jq -r '.expires_on')
echo -e " ${COLOR_GREEN}🔔${CoR} Valid certificate found for ${COLOR_YELLOW}$DOMAIN${CoR} (Certificate ID: ${COLOR_ORANGE}$CERT_ID${CoR}, expires in ${COLOR_YELLOW}$((($(date -d "$CERT_EXPIRES" +%s) - $(date +%s)) / 86400))${CoR} days)"
# Reuse it for --host-ssl-enable
GENERATED_CERT_ID="$CERT_ID"
return 0
fi
# 5. Display parameters and confirmation
echo -e "\n ${COLOR_CYAN}📝${CoR} Certificate generation parameters:"
echo -e " • Domain: ${COLOR_YELLOW}$DOMAIN${CoR}"
echo -e " • Email: ${COLOR_YELLOW}$EMAIL${CoR}"
if [ -n "$DNS_PROVIDER" ]; then
echo -e " • DNS Provider: ${COLOR_YELLOW}$DNS_PROVIDER${CoR}"
fi
# 6. Confirmation
if [ "$AUTO_YES" = true ]; then
echo -e "\n ${COLOR_YELLOW}🔔 The -y option was provided.${CoR} AUTO Yes activate.${CoR}"
CONFIRM="y"
else
echo -en " ${COLOR_RED}❓${CoR} No existing certificate found for ${COLOR_YELLOW}$DOMAIN${CoR}. Create new Let's Encrypt certificate? (y/n): "
read -n 1 -r CONFIRM
echo # New line after response
CONFIRM=${CONFIRM:-y} # Default to 'y' if empty
CONFIRM=$(echo "$CONFIRM" | tr '[:upper:]' '[:lower:]') # Convert to lowercase
fi
if [[ "$CONFIRM" != "y" ]]; then
echo -e "${COLOR_RED} ❌ Certificate creation aborted.${CoR}\n"
exit 0
fi
# 7. Prepare request data
echo -e " ${COLOR_YELLOW}🔔 Initiating certificate generation ${COLOR_GREEN}$DOMAIN${CoR}"
echo -e " ${COLOR_CYAN}🚀 Sending certificate generation request${CoR}"
echo -e " ${COLOR_ORANGE}⏳ This process may take a few minutes...${CoR}"
# NPM >= 2.13 takes the Let's Encrypt email from the NPM user account and
# rejects meta.letsencrypt_email / meta.letsencrypt_agree
local npm_ver legacy_le=false
npm_ver=$(curl -s --max-redirs 0 "$BASE_URL/" | jq -r '"\(.version.major // 0).\(.version.minor // 0)"' 2>/dev/null || echo "0.0")
# Unknown version (0.0) is treated as current NPM
if [ "$npm_ver" != "0.0" ] && [ $((${npm_ver%%.*} * 1000 + ${npm_ver#*.})) -lt 2013 ]; then
legacy_le=true
else
echo -e " 📧 ${COLOR_GREY}NPM $npm_ver uses the email of the NPM user account for Let's Encrypt${CoR}"
fi
if [ "$IS_WILDCARD" = true ]; then
echo -e " 🔑 Using DNS challenge with provider: $DNS_PROVIDER"
# NPM writes the credentials as-is into a certbot INI file: convert JSON to "key = value" lines
local dns_credentials_ini="$DNS_CREDENTIALS_JSON"
if echo "$DNS_CREDENTIALS_JSON" | jq -e 'type == "object"' >/dev/null 2>&1; then
# Cloudflare Global API Keys are 37 hex chars; anything else given as api_key
# (e.g. "cfut_..." tokens) is an API token, which certbot expects as api_token
if [[ "${DNS_PROVIDER,,}" == "cloudflare" ]] && echo "$DNS_CREDENTIALS_JSON" | jq -e \
'(.dns_cloudflare_api_token == null) and ((.dns_cloudflare_api_key // "") | test("^[0-9a-fA-F]{37}$") | not) and (.dns_cloudflare_api_key != null)' >/dev/null 2>&1; then
echo -e " ⚠️ ${COLOR_YELLOW}dns_cloudflare_api_key is not a Global API Key (37 hex chars): sending it as dns_cloudflare_api_token${CoR}"
DNS_CREDENTIALS_JSON=$(echo "$DNS_CREDENTIALS_JSON" | jq -c '{dns_cloudflare_api_token: .dns_cloudflare_api_key}')
fi
dns_credentials_ini=$(echo "$DNS_CREDENTIALS_JSON" | jq -r 'to_entries[] | "\(.key) = \(.value)"')
fi
REQUEST_DATA=$(jq -n \
--arg domain "$DOMAIN" \
--arg dns_provider "$DNS_PROVIDER" \
--arg credentials "$dns_credentials_ini" \
'{
provider: "letsencrypt",
domain_names: [$domain],
meta: {
dns_challenge: true,
dns_provider: $dns_provider,
dns_provider_credentials: $credentials,
propagation_seconds: 60
}
}')
else
REQUEST_DATA=$(jq -n \
--arg domain "$DOMAIN" \
'{
provider: "letsencrypt",
domain_names: [$domain],
meta: {}
}')
fi
if [ "$legacy_le" = true ]; then
REQUEST_DATA=$(echo "$REQUEST_DATA" | jq --arg email "$EMAIL" \
'.meta += {letsencrypt_agree: true, letsencrypt_email: $email}')
fi
# 8. Send request and handle response
HTTP_RESPONSE=$(curl -s -w "HTTPSTATUS:%{http_code}" -X POST "$BASE_URL/nginx/certificates" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" \
-H "Content-Type: application/json" \
--data "$REQUEST_DATA")
HTTP_BODY=${HTTP_RESPONSE//HTTPSTATUS:*/}
HTTP_STATUS=${HTTP_RESPONSE##*HTTPSTATUS:}
if [ "${HTTP_STATUS:-0}" -eq 201 ]; then
CERT_ID=$(echo "$HTTP_BODY" | jq -r '.id')
echo -e "\n ✅ ${COLOR_GREEN}Certificate generation initiated successfully!${CoR}"
echo -e " 📋 Certificate Details:"
echo -e " • Certificate ID: ${COLOR_YELLOW}$CERT_ID${CoR}"
echo -e " • Status: ${COLOR_GREEN}Created${CoR}"
echo -e " • Domain: ${COLOR_YELLOW}$DOMAIN${CoR}"
echo -e " • Provider: ${COLOR_YELLOW}Let's Encrypt${CoR}\n"
# Export variables for host_ssl_enable
export GENERATED_CERT_ID="$CERT_ID"
export DOMAIN_EXISTS
if [ "$HOST_SSL_ENABLE" = true ]; then
echo -e "\n ⏳ ${COLOR_YELLOW}Waiting for certificate propagation (30 seconds)...${CoR}"
sleep 30
if ! check_certificate_exists "$DOMAIN"; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: Certificate not found after generation${CoR}"
exit 1
fi
fi
#return 0
else
echo -e "\n ❌ ${COLOR_RED}Certificate generation failed!${CoR}"
ERROR_MSG=$(echo "$HTTP_BODY" | jq -r '.error.message // "Unknown error"')
echo -e " ⛔ Error: ${COLOR_RED}$ERROR_MSG${CoR}"
DEBUG_STACK=$(echo "$HTTP_BODY" | jq -r '.debug.stack[]? // empty')
if [ -n "$DEBUG_STACK" ]; then
echo -e "\n 🔍 Debug Stack:"
echo "$DEBUG_STACK" | while read -r line; do
echo -e " • ${COLOR_YELLOW}$line${CoR}"
done
fi
echo -e "\n ${COLOR_CYAN}🔍${CoR} Troubleshooting suggestions:"
echo -e " • Verify domain DNS records are properly configured"
echo -e " • Ensure domain is accessible via HTTP/HTTPS"
echo -e " • Check if Let's Encrypt rate limits are not exceeded"
echo -e " • Verify Nginx Proxy Manager is properly configured"
echo -e " • Check if port 80 is open and accessible"
echo -e " • Ensure no firewall is blocking access"
echo -e " • Check Nginx Proxy Manager logs for more details"
echo -e "\n ${COLOR_CYAN}💡${CoR} You can try:"
echo -e " • Wait a few minutes and try again (DNS propagation)"
echo -e " • Check Nginx Proxy Manager logs:"
echo -e " ${COLOR_GREEN}docker logs nginx-proxy-manager${CoR}"
echo -e " • Check Let's Encrypt logs:"
echo -e " ${COLOR_GREEN}docker exec nginx-proxy-manager cat /data/logs/letsencrypt.log${CoR}"
if [ "$IS_WILDCARD" = true ]; then
echo -e " • DNS challenge: check the --dns-credentials keys expected by the provider plugin"
echo -e " (Cloudflare: dns_cloudflare_api_token, or dns_cloudflare_email + dns_cloudflare_api_key for the Global API Key)"
fi
echo -e "\n 📋 Debug Information:"
echo -e " • HTTP Status: $HTTP_STATUS"
echo -e " • Response: $HTTP_BODY"
# Never print DNS provider secrets
echo -e " • Request Data: $(echo "$REQUEST_DATA" | jq -c 'if .meta.dns_provider_credentials then .meta.dns_provider_credentials = "***" else . end')"
return 1
fi
}
################################
# Enable SSL for a proxy host
host_ssl_enable() {
local HOST_ID="${1:-}"
local CERT_ID="${2:-}"
if [ "$CERT_GENERATE" = true ]; then
if [ -z "$DOMAIN_EXISTS" ]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: No proxy host found for ${COLOR_YELLOW}$CERT_DOMAIN${CoR}"
echo -e " Enable SSL manually: ${COLOR_ORANGE}$0 --host-ssl-enable <host_id> ${GENERATED_CERT_ID:-<cert_id>}${CoR}\n"
exit 1
fi
if [ -z "$GENERATED_CERT_ID" ]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: No certificate ID available${CoR}"
exit 1
fi
HOST_ID="$DOMAIN_EXISTS"
CERT_ID="$GENERATED_CERT_ID"
else
if [ -z "$HOST_ID" ]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR : The --host-ssl-enable option requires a host 🆔${CoR}"
echo -e " Usage : ${COLOR_ORANGE}$0 --host-ssl-enable <host_id> <cert_id>${CoR}"
echo -e " Example: ${COLOR_GREEN}$0 --host-ssl-enable 42 240${CoR}\n"
exit 1
fi
if [ -z "$CERT_ID" ]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: Certificate ID is required${CoR}"
echo -e " Usage : ${COLOR_ORANGE}$0 --host-ssl-enable <host_id> <cert_id>${CoR}"
echo -e " Example: ${COLOR_GREEN}$0 --host-ssl-enable 42 240${CoR}\n"
exit 1
fi
fi
# Quick check if host exists
check_token_notverbose
local CHECK_RESPONSE=$(curl -s -X GET "$BASE_URL/nginx/proxy-hosts/$HOST_ID" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
if [ "$(echo "$CHECK_RESPONSE" | jq -r .id)" = "null" ]; then
echo -e " ${COLOR_RED}❌${CoR} Host ID $HOST_ID not found"
return 1
fi
# Get host domain and current certificate ID
local HOST_DOMAIN=$(echo "$CHECK_RESPONSE" | jq -r '.domain_names[0]')
# Update SSL configuration
local DATA=$(jq -n \
--arg cert_id "$CERT_ID" \
'{
"certificate_id": ($cert_id|tonumber),
"ssl_forced": true,
"http2_support": true
}')
local HTTP_RESPONSE=$(curl -s -w "HTTPSTATUS:%{http_code}" -X PUT "$BASE_URL/nginx/proxy-hosts/$HOST_ID" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" \
-H "Content-Type: application/json; charset=UTF-8" \
--data-raw "$DATA")
local HTTP_BODY=${HTTP_RESPONSE//HTTPSTATUS:*/}
local HTTP_STATUS=${HTTP_RESPONSE##*HTTPSTATUS:}
if [ "${HTTP_STATUS:-0}" -eq 200 ]; then
echo -e "\n ✅ ${COLOR_GREEN}SSL enabled successfully for${CoR} ${COLOR_YELLOW}$HOST_DOMAIN${CoR} (ID: ${COLOR_CYAN}$HOST_ID${CoR}) (Cert ID: ${COLOR_CYAN}$CERT_ID${CoR})\n"
return 0
#exit 0
else
echo -e "\n ⛔ ${COLOR_RED}Failed to enable SSL. HTTP status: $HTTP_STATUS${CoR}\n"
echo -e " 📋 Error details: $HTTP_BODY \n"
return 1
fi
}
################################
# disable_ssl
host_ssl_disable() {
if [ -z "$HOST_ID" ]; then
echo -e "\n ⛔ ${COLOR_RED}INVALID command: Missing argument${CoR}"
echo -e " Usage : ${COLOR_ORANGE}$0 --host-ssl-disable <host_id>${CoR}"
echo -e " Find ID: ${COLOR_ORANGE}$0 --host-list${CoR}\n"
exit 1
fi
check_token_notverbose
CHECK_RESPONSE=$(curl -s -X GET "$BASE_URL/nginx/proxy-hosts/$HOST_ID" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
if echo "$CHECK_RESPONSE" | jq -e '.id' >/dev/null 2>&1; then
DATA=$(jq -n --argjson cert_id null '{
certificate_id: $cert_id,
ssl_forced: false,
http2_support: false,
hsts_enabled: false,
hsts_subdomains: false
}')
HTTP_RESPONSE=$(curl -s -w "HTTPSTATUS:%{http_code}" -X PUT "$BASE_URL/nginx/proxy-hosts/$HOST_ID" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" \
-H "Content-Type: application/json; charset=UTF-8" \
--data-raw "$DATA")
HTTP_BODY=${HTTP_RESPONSE//HTTPSTATUS:*/}
HTTP_STATUS=${HTTP_RESPONSE##*HTTPSTATUS:}
if [ "${HTTP_STATUS:-0}" -eq 200 ]; then
echo -e "\n 🚫 Disabling 🔓 SSL for proxy 🆔${COLOR_YELLOW}$HOST_ID${CoR}"
echo -e " ✅ ${COLOR_GREEN}SSL disabled successfully!${CoR} 🆔${COLOR_CYAN}$HOST_ID${CoR}\n"
else
echo " Data sent: $DATA" # Log the data sent
echo -e " ⛔ ${COLOR_RED}Failed to disable SSL. HTTP status: $HTTP_STATUS. Response: $HTTP_BODY${CoR}\n"
fi
else
echo -e " ⛔ ${COLOR_RED}Proxy host with ID $HOST_ID does not exist.${CoR}\n"
fi
}
################################
# Create a new access list with various options
# Usage:
# access_list_create <name> [options]
# Options:
# --auth <username> <password> Add basic authentication
# --access allow|deny <ip> Add IP access rule (legacy syntax)
# --allow "ip1,ip2" Add allowed IPs/ranges (comma-separated)
# --deny "ip1,ip2" Add denied IPs/ranges (comma-separated)
# --users "user1,user2" Add users with password prompts (comma-separated)
# --satisfy any|all Set satisfy condition (default: all)
# --pass-auth Enable pass auth
access_list_create() {
check_token_notverbose
if [ $# -lt 2 ]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: Insufficient arguments${CoR}"
echo -e " ${COLOR_CYAN}Usage:${CoR}"
echo -e " ${COLOR_ORANGE}$0 --access-list-create <name> [options]${CoR}"
echo -e "\n ${COLOR_CYAN}Available options:${CoR}"
echo -e " ${COLOR_GREEN}--satisfy [any|all]${CoR} Set access list satisfaction mode"
echo -e " ${COLOR_GREEN}--pass-auth [true|false]${CoR} Enable/disable password authentication"
echo -e " ${COLOR_GREEN}--users \"user1,user2\"${CoR} List of users (comma-separated)"
echo -e " ${COLOR_GREEN}--allow \"ip1,ip2\"${CoR} List of allowed IPs/ranges"
echo -e " ${COLOR_GREEN}--deny \"ip1,ip2\"${CoR} List of denied IPs/ranges"
echo -e " ${COLOR_GREEN}--auth <username> <password>${CoR} Add authentication user"
echo -e " ${COLOR_GREEN}--access allow|deny <ip>${CoR} Add single IP rule"
echo -e "\n ${COLOR_CYAN}Examples:${CoR}"
echo -e " ${COLOR_GREEN}$0 --access-list-create secure_area --auth admin secret123${CoR}"
echo -e " ${COLOR_GREEN}$0 --access-list-create office --allow \"192.168.1.0/24\"${CoR}"
echo -e " ${COLOR_GREEN}$0 --access-list-create full_options --users \"user1,user2\" --allow \"10.0.0.0/8\" --satisfy any --pass-auth true${CoR}\n"
return 1
fi
local NAME="$1"
shift
# Initialize variables
local SATISFY_ANY="false"
local PASS_AUTH="false"
local AUTH_ITEMS="[]"
local IP_CLIENTS="[]"
echo -e "\n🔑 Creating access list: ${COLOR_GREEN}$NAME${CoR}"
# Process all arguments
while [ $# -gt 0 ]; do
case "$1" in
--auth)
if [ $# -lt 3 ]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: --auth requires username and password${CoR}"
return 1
fi
AUTH_ITEMS=$(echo "$AUTH_ITEMS" | jq --arg user "$2" --arg pass "$3" '. + [{
username: $user,
password: $pass
}]')
shift 3
;;
--access)
if [ $# -lt 3 ]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: --access requires allow/deny and IP${CoR}"
return 1
fi
if [[ ! "$2" =~ ^(allow|deny)$ ]]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: Invalid access type. Must be 'allow' or 'deny'${CoR}"
return 1
fi
IP_CLIENTS=$(echo "$IP_CLIENTS" | jq --arg ip "$3" --arg dir "$2" '. + [{
address: $ip,
directive: $dir
}]')
shift 3
;;
--allow)
if [ $# -lt 2 ]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: --allow requires IP addresses/ranges${CoR}"
return 1
fi
# Split comma-separated IPs and add each one
IFS=',' read -ra IPS <<<"$2"
for ip in "${IPS[@]}"; do
ip=$(echo "$ip" | xargs) # trim whitespace
IP_CLIENTS=$(echo "$IP_CLIENTS" | jq --arg ip "$ip" '. + [{
address: $ip,
directive: "allow"
}]')
done
shift 2
;;
--deny)
if [ $# -lt 2 ]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: --deny requires IP addresses/ranges${CoR}"
return 1
fi
# Split comma-separated IPs and add each one
IFS=',' read -ra IPS <<<"$2"
for ip in "${IPS[@]}"; do
ip=$(echo "$ip" | xargs) # trim whitespace
IP_CLIENTS=$(echo "$IP_CLIENTS" | jq --arg ip "$ip" '. + [{
address: $ip,
directive: "deny"
}]')
done
shift 2
;;
--users)
if [ $# -lt 2 ]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: --users requires user list${CoR}"
return 1
fi
# Split comma-separated users and add each one (will need password prompt or default)
IFS=',' read -ra USERS <<<"$2"
for user in "${USERS[@]}"; do
user=$(echo "$user" | xargs) # trim whitespace
echo -e " 🔐 Enter password for user ${COLOR_CYAN}$user${CoR}:"
read -s password
echo
AUTH_ITEMS=$(echo "$AUTH_ITEMS" | jq --arg user "$user" --arg pass "$password" '. + [{
username: $user,
password: $pass
}]')
done
shift 2
;;
--satisfy)
if [ $# -lt 2 ]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: --satisfy requires any/all${CoR}"
return 1
fi
if [ "$2" = "any" ]; then
SATISFY_ANY="true"
elif [ "$2" = "all" ]; then
SATISFY_ANY="false"
else
echo -e "\n ⛔ ${COLOR_RED}ERROR: Invalid satisfy value. Must be 'any' or 'all'${CoR}"
return 1
fi
shift 2
;;
--pass-auth)
if [ $# -lt 2 ] || { [ "$2" != "true" ] && [ "$2" != "false" ]; }; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: --pass-auth requires true/false${CoR}"
return 1
fi
PASS_AUTH="$2"
shift 2
;;
*)
echo -e "\n ⛔ ${COLOR_RED}ERROR: Unknown option $1${CoR}"
return 1
;;
esac
done
# Check that at least one rule is defined
if [ "$AUTH_ITEMS" = "[]" ] && [ "$IP_CLIENTS" = "[]" ]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: At least one --auth or --access rule is required${CoR}"
return 1
fi
# Build payload
local PAYLOAD=$(jq -n \
--arg name "$NAME" \
--argjson satisfy_any "$SATISFY_ANY" \
--argjson pass_auth "$PASS_AUTH" \
--argjson items "$AUTH_ITEMS" \
--argjson clients "$IP_CLIENTS" \
'{
name: $name,
satisfy_any: $satisfy_any,
pass_auth: $pass_auth,
items: $items,
clients: $clients
}')
# Create access list via API
RESPONSE=$(curl -s -X POST "$BASE_URL/nginx/access-lists" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" \
-H "Content-Type: application/json" \
-d "$PAYLOAD")
# Check for errors
if [ "$(echo "$RESPONSE" | jq -r '.error.message // empty')" != "" ]; then
echo -e " ⛔ ${COLOR_RED}Failed to create access list${CoR}"
echo -e " Error: $(echo "$RESPONSE" | jq -r '.error.message')"
return 1
fi
# Display results
local NEW_ID=$(echo "$RESPONSE" | jq -r '.id')
echo -e " ✅ ${COLOR_GREEN}Access list created successfully!${CoR}"
echo -e " ┌───────────────────────────────────────────"
echo -e " │ ID: ${COLOR_YELLOW}$NEW_ID${CoR}"
echo -e " │ Name: ${COLOR_GREEN}$NAME${CoR}"
echo -e " │ Satisfy: ${COLOR_CYAN}$([ "$SATISFY_ANY" = "true" ] && echo "any" || echo "all")${CoR}"
echo -e " │ Pass Auth: ${COLOR_CYAN}$([ "$PASS_AUTH" = "true" ] && echo "yes" || echo "no")${CoR}"
if [ "$AUTH_ITEMS" != "[]" ]; then
echo -e " │ 👤 Authentication Rules:"
echo "$AUTH_ITEMS" | jq -r '.[] | " │ • User: \(.username)"'
fi
if [ "$IP_CLIENTS" != "[]" ]; then
echo -e " │ 🌐 Access Rules:"
echo "$IP_CLIENTS" | jq -r '.[] | " │ • \(.directive) \(.address)"'
fi
echo -e " └───────────────────────────────────────────"
}
# Update an existing access list with various options
# Usage:
# access_list_update <access_list_id> [options]
# Options:
# --name "new_name" Update access list name
# --satisfy any|all Update satisfaction mode
# --pass-auth true|false Update password authentication
# --allow "ip1,ip2" Add allowed IPs/ranges (comma-separated)
# --deny "ip1,ip2" Add denied IPs/ranges (comma-separated)
# --users "user1,user2" Add users with password prompts (comma-separated)
access_list_update() {
check_token_notverbose
if [ -z "$ACCESS_LIST_ID" ]; then
echo -e "\n ⛔ ${COLOR_RED}Error: ACCESS_LIST_ID is required.${CoR}"
echo -e " Usage: $0 --access-list-update <access_list_id> [options]"
return 1
fi
local access_list_id="$ACCESS_LIST_ID"
# Get the current access list details with expanded items and clients
# Note: The expand parameter is required to get full details of items and clients
local current_list=$(curl -s -X GET "$BASE_URL/nginx/access-lists/$access_list_id?expand=items%2Cclients" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
if [ "$(echo "$current_list" | jq -r '.error | length')" -ne 0 ]; then
echo -e " ⛔ ${COLOR_RED}Failed to fetch access list details. Error: $(echo "$current_list" | jq -r '.error')${CoR}"
return 1
fi
# Extract current values with proper null handling
local current_name=$(echo "$current_list" | jq -r '.name // "unnamed"')
local current_satisfy_any=$(echo "$current_list" | jq -r '.satisfy_any // false')
local current_pass_auth=$(echo "$current_list" | jq -r '.pass_auth // false')
# Keep only the fields accepted by PUT (GET adds id, hint, created_on...);
# an empty password tells NPM to keep the existing one
local current_items=$(echo "$current_list" | jq '[(.items // [])[] | {username, password: ""}]')
local current_clients=$(echo "$current_list" | jq '[(.clients // [])[] | {address, directive}]')
echo -e "\n🔑 ${COLOR_CYAN}Updating access list ID: ${COLOR_YELLOW}$access_list_id${CoR}"
echo -e "\n${COLOR_CYAN}Current Access List Details:${CoR}"
echo -e "Name: $current_name"
echo -e "Satisfy Any: $current_satisfy_any"
echo -e "Pass Auth: $current_pass_auth"
# Initialize variables with current values
local new_name="$current_name"
local satisfy_any="$current_satisfy_any"
local pass_auth="$current_pass_auth"
local items="$current_items"
local clients="$current_clients"
# Process command line arguments if provided
if [ $# -gt 0 ]; then
echo -e "\n${COLOR_CYAN}Processing command line arguments...${CoR}"
while [ $# -gt 0 ]; do
case "$1" in
--name)
if [ $# -lt 2 ]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: --name requires a value${CoR}"
return 1
fi
new_name="$2"
echo -e " 📝 Setting name to: ${COLOR_GREEN}$new_name${CoR}"
shift 2
;;
--satisfy)
if [ $# -lt 2 ]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: --satisfy requires any/all${CoR}"
return 1
fi
if [ "$2" = "any" ]; then
satisfy_any="true"
echo -e " 🔄 Setting satisfy to: ${COLOR_GREEN}any${CoR}"
elif [ "$2" = "all" ]; then
satisfy_any="false"
echo -e " 🔄 Setting satisfy to: ${COLOR_GREEN}all${CoR}"
else
echo -e "\n ⛔ ${COLOR_RED}ERROR: Invalid satisfy value. Must be 'any' or 'all'${CoR}"
return 1
fi
shift 2
;;
--pass-auth)
if [ $# -lt 2 ]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: --pass-auth requires true/false${CoR}"
return 1
fi
if [ "$2" = "true" ] || [ "$2" = "false" ]; then
pass_auth="$2"
echo -e " 🔓 Setting pass auth to: ${COLOR_GREEN}$pass_auth${CoR}"
else
echo -e "\n ⛔ ${COLOR_RED}ERROR: Invalid pass auth value. Must be 'true' or 'false'${CoR}"
return 1
fi
shift 2
;;
--allow)
if [ $# -lt 2 ]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: --allow requires IP addresses/ranges${CoR}"
return 1
fi
# Split comma-separated IPs and add each one
IFS=',' read -ra IPS <<<"$2"
for ip in "${IPS[@]}"; do
ip=$(echo "$ip" | xargs) # trim whitespace
clients=$(echo "$clients" | jq --arg ip "$ip" '. + [{
address: $ip,
directive: "allow"
}]')
done
echo -e " ✅ Added allow rules for: ${COLOR_GREEN}$2${CoR}"
shift 2
;;
--deny)
if [ $# -lt 2 ]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: --deny requires IP addresses/ranges${CoR}"
return 1
fi
# Split comma-separated IPs and add each one
IFS=',' read -ra IPS <<<"$2"
for ip in "${IPS[@]}"; do
ip=$(echo "$ip" | xargs) # trim whitespace
clients=$(echo "$clients" | jq --arg ip "$ip" '. + [{
address: $ip,
directive: "deny"
}]')
done
echo -e " ✅ Added deny rules for: ${COLOR_GREEN}$2${CoR}"
shift 2
;;
--users)
if [ $# -lt 2 ]; then
echo -e "\n ⛔ ${COLOR_RED}ERROR: --users requires user list${CoR}"
return 1
fi
# Split comma-separated users and add each one (will need password prompt or default)
IFS=',' read -ra USERS <<<"$2"
for user in "${USERS[@]}"; do
user=$(echo "$user" | xargs) # trim whitespace
echo -e " 🔐 Enter password for user ${COLOR_CYAN}$user${CoR}:"
read -s password
echo
items=$(echo "$items" | jq --arg user "$user" --arg pass "$password" '. + [{
username: $user,
password: $pass
}]')
done
echo -e " ✅ Added users: ${COLOR_GREEN}$2${CoR}"
shift 2
;;
*)
echo -e "\n ⛔ ${COLOR_RED}ERROR: Unknown option $1${CoR}"
echo -e " ${COLOR_CYAN}Available options:${CoR}"
echo -e " ${COLOR_GREEN}--name <new_name>${CoR} Update access list name"
echo -e " ${COLOR_GREEN}--satisfy any|all${CoR} Update satisfaction mode"
echo -e " ${COLOR_GREEN}--pass-auth true|false${CoR} Update pass auth setting"
echo -e " ${COLOR_GREEN}--allow \"ip1,ip2\"${CoR} Update allowed IPs/ranges"
echo -e " ${COLOR_GREEN}--deny \"ip1,ip2\"${CoR} Update denied IPs/ranges"
echo -e " ${COLOR_GREEN}--users \"user1,user2\"${CoR} Update list of users"
return 1
;;
esac
done
else
# Interactive mode if no arguments provided
echo -e "\n${COLOR_CYAN}No arguments provided, using current values.${CoR}"
echo -e "💡 ${COLOR_YELLOW}Tip: Use --name, --satisfy, --auth-type, or --pass-auth options for non-interactive updates${CoR}"
fi
# Prepare the JSON payload using jq for proper formatting (same structure as create)
local payload=$(jq -n \
--arg name "$new_name" \
--argjson satisfy_any "$satisfy_any" \
--argjson pass_auth "$pass_auth" \
--argjson items "$items" \
--argjson clients "$clients" \
'{
name: $name,
satisfy_any: $satisfy_any,
pass_auth: $pass_auth,
items: $items,
clients: $clients
}')
# Update the access list
local response=$(curl -s -X PUT "$BASE_URL/nginx/access-lists/$access_list_id" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" \
-H "Content-Type: application/json; charset=UTF-8" \
-d "$payload")
if [ "$(echo "$response" | jq -r '.error | length')" -eq 0 ]; then
echo -e "\n ✅ ${COLOR_GREEN}Access list updated successfully!${CoR}"
echo -e "\n${COLOR_CYAN}Updated Access List Details:${CoR}"
echo -e "ID: $access_list_id"
echo -e "Name: $new_name"
echo -e "Satisfy: $([ "$satisfy_any" = "true" ] && echo "any" || echo "all")"
echo -e "Pass Auth: $pass_auth"
else
echo -e "\n ⛔ ${COLOR_RED}Failed to update access list. Error: $(echo "$response" | jq -r '.error')${CoR}"
return 1
fi
}
################################
# Delete an access list
access_list_delete() {
if [ -z "$ACCESS_LIST_ID" ]; then
echo -e "\n ⛔ ${COLOR_RED}Error: ACCESS_LIST_ID is required.${CoR}"
echo -e " Usage: $0 --access-list-delete <access_list_id> [-y]"
exit 1
fi
check_token_notverbose
echo -e " 🔍 Checking access list ID: ${COLOR_YELLOW}$ACCESS_LIST_ID${CoR}"
# Check if access list exists
RESPONSE=$(curl -s -X GET "$BASE_URL/nginx/access-lists/$ACCESS_LIST_ID" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
if [ "$(echo "$RESPONSE" | jq -r '.error.code')" = "404" ]; then
echo -e " ⛔ ${COLOR_RED}Access list ID $ACCESS_LIST_ID not found${CoR}"
exit 1
fi
# Show details before deletion
echo -e " ┌───────────────────────────────────────────"
echo -e " │ ID: ${COLOR_YELLOW}$ACCESS_LIST_ID${CoR}"
echo -e " │ Name: ${COLOR_GREEN}$(echo "$RESPONSE" | jq -r '.name')${CoR}"
echo -e " └───────────────────────────────────────────"
# Confirm unless AUTO_YES
if [ "$AUTO_YES" != true ]; then
echo -e " ⚠️ ${COLOR_RED}WARNING: This action cannot be undone!${CoR}"
read -n 1 -r -p " 🔔 Confirm deletion? (y/n): " CONFIRM
echo
if [[ ! $CONFIRM =~ ^[Yy]$ ]]; then
echo -e " ❌ ${COLOR_RED}Operation cancelled${CoR}"
exit 1
fi
fi
# Delete access list
RESPONSE=$(curl -s -X DELETE "$BASE_URL/nginx/access-lists/$ACCESS_LIST_ID" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
if [ "$RESPONSE" = "true" ]; then
echo -e " ✅ ${COLOR_GREEN}Access list successfully deleted! ${CoR}🗑️ "
else
echo -e " ⛔ ${COLOR_RED}Failed to delete access list.${CoR}"
if [ -n "$RESPONSE" ]; then
echo -e " Error: $RESPONSE"
fi
exit 1
fi
}
################################
# Show all access lists with detailed information
################################
# Show all access lists with detailed information
access_list() {
check_token_notverbose
# Get all access lists
RESPONSE=$(curl -s -X GET "$BASE_URL/nginx/access-lists" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
# Check for API errors
if [ "$(echo "$RESPONSE" | jq -r 'if type=="object" then .error.message else empty end')" != "" ]; then
if [ "$JSON_OUTPUT" = true ]; then
echo "$RESPONSE" | jq '{error: (.error.message // "Failed to fetch access lists")}'
exit 1
fi
echo -e " ⛔ ${COLOR_RED}Failed to fetch access lists${CoR}"
echo -e " Error: $(echo "$RESPONSE" | jq -r '.error.message')"
return 1
fi
if [ "$JSON_OUTPUT" = true ]; then
echo "$RESPONSE" | jq .
exit 0
fi
echo -e "\n📋 ${COLOR_CYAN}Access Lists Management${CoR}\n"
# Display table header
echo "┌════════════════════════════════════════════════════════════════════════════════════┐"
echo -e "│ ${COLOR_CYAN}ID │ Name │ Authorization │ Access │ Satisfy │ Proxy Hosts ${CoR}│"
echo "├════════════════════════════════════════════════════════════════════════════════════┤"
# Process each access list and format output
echo "$RESPONSE" | jq -r '.[] | "\(.id)|\(.name)|\(if .items then .items|length else 0 end) User\(if (.items|length//0) != 1 then "s" else "" end)|\(if .clients then .clients|length else 0 end) Rule\(if (.clients|length//0) != 1 then "s" else "" end)|\(.satisfy_any)|\(.proxy_host_count // 0)"' |
while IFS="|" read -r id name users rules satisfy proxy_hosts; do
# Format satisfy display (Any/All)
satisfy_display=$([ "$satisfy" = "true" ] && echo "Any" || echo "All")
# Print formatted line (name truncated to column width; count guarded above)
printf "│ %-3s │ %s │ %-13s │ %-8s │ %-7s │ %-15s│\n" \
"$id" \
"$(truncate_pad "$name" 22)" \
"$users" \
"$rules" \
"$satisfy_display" \
"$proxy_hosts Proxy Hosts"
done
# Close table
echo "└════════════════════════════════════════════════════════════════════════════════════┘"
# Display creation date info
echo -e "\n${COLOR_CYAN}Note:${CoR} Use --access-list-show <id> to see detailed information about a specific access list\n"
}
################################
# Show detailed information for a specific access list
access_list_show() {
local id="$1"
check_token_notverbose
if [ -z "$id" ]; then
echo -e "\n⛔ ${COLOR_RED}Error: Access List ID is required${CoR}"
echo -e "Usage: $0 --access-list-show <id>"
return 1
fi
[ "$JSON_OUTPUT" = true ] || echo -e "\n📋 ${COLOR_CYAN}Access List Details${CoR}"
# Get specific access list with expanded items and clients
# Note: The expand parameter is required to get full details of items and clients
local response=$(curl -s -X GET "$BASE_URL/nginx/access-lists/$id?expand=items%2Cclients" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
# Check if response is valid JSON
if ! echo "$response" | jq empty 2>/dev/null; then
if [ "$JSON_OUTPUT" = true ]; then
echo '{"error": "Invalid response from API"}'
exit 1
fi
echo -e "\n⛔ ${COLOR_RED}Invalid response from API${CoR}"
return 1
fi
# Check if access list exists
if [ "$(echo "$response" | jq 'has("error")')" = "true" ]; then
if [ "$JSON_OUTPUT" = true ]; then
echo '{"error": "Access List not found"}'
exit 1
fi
echo -e "\n⛔ ${COLOR_RED}Access List not found${CoR}"
return 1
fi
if [ "$JSON_OUTPUT" = true ]; then
echo "$response" | jq .
exit 0
fi
# Create horizontal border
local h_border=$(printf '%*s' "80" '' | tr ' ' "=")
# Display basic information
echo -e "\n${COLOR_GREY}┌$h_border┐${CoR}"
echo -e "${COLOR_GREY}│${CoR} ${COLOR_CYAN}Basic Information${CoR}"
echo -e "${COLOR_GREY}├$h_border┤${CoR}"
echo -e "${COLOR_GREY}│${CoR} ID: ${COLOR_GREEN}$(echo "$response" | jq -r '.id')${CoR}"
echo -e "${COLOR_GREY}│${CoR} Name: ${COLOR_YELLOW}$(echo "$response" | jq -r '.name')${CoR}"
echo -e "${COLOR_GREY}│${CoR} Auth Type: ${COLOR_ORANGE}$(echo "$response" | jq -r '.auth_type // "none"')${CoR}"
# Display authentication settings
local pass_auth=$(echo "$response" | jq -r '.pass_auth')
local satisfy=$(echo "$response" | jq -r '.satisfy_any')
echo -e "${COLOR_GREY}│${CoR} Pass Auth: $([ "$pass_auth" = "true" ] && echo "${COLOR_GREEN}✓${CoR}" || echo "${COLOR_RED}✗${CoR}")"
echo -e "${COLOR_GREY}│${CoR} Satisfy: ${COLOR_YELLOW}$([ "$satisfy" = "true" ] && echo "Any" || echo "All")${CoR}"
# Display users
echo -e "${COLOR_GREY}├$h_border┤${CoR}"
echo -e "${COLOR_GREY}│${CoR} ${COLOR_CYAN}Authorized Users${CoR}"
echo -e "${COLOR_GREY}├$h_border┤${CoR}"
if [ "$(echo "$response" | jq 'has("items")')" = "true" ] && [ "$(echo "$response" | jq '.items != null')" = "true" ]; then
local users_count=$(echo "$response" | jq '.items | length')
if [ "$users_count" -gt 0 ]; then
echo "$response" | jq -r '.items[] | "│ • \(.username)"'
else
echo -e "${COLOR_GREY}│${CoR} No users configured"
fi
else
echo -e "${COLOR_GREY}│${CoR} No users configured"
fi
# Display IP whitelist
echo -e "${COLOR_GREY}├$h_border┤${CoR}"
echo -e "${COLOR_GREY}│${CoR} ${COLOR_CYAN}IP Whitelist${CoR}"
echo -e "${COLOR_GREY}├$h_border┤${CoR}"
if [ "$(echo "$response" | jq 'has("clients")')" = "true" ] && [ "$(echo "$response" | jq '.clients != null')" = "true" ]; then
local ips_count=$(echo "$response" | jq '.clients | length')
if [ "$ips_count" -gt 0 ]; then
echo "$response" | jq -r '.clients[] | "│ • \(.address) (\(.directive // "allow"))"' |
while IFS= read -r line; do
if [[ $line == *"allow"* ]]; then
echo -e "${COLOR_GREY}│${CoR} • ${COLOR_GREEN}${line#* • }${CoR}"
else
echo -e "${COLOR_GREY}│${CoR} • ${COLOR_RED}${line#* • }${CoR}"
fi
done
else
echo -e "${COLOR_GREY}│${CoR} No IPs whitelisted"
fi
else
echo -e "${COLOR_GREY}│${CoR} No IPs whitelisted"
fi
# Close the box
echo -e "${COLOR_GREY}└$h_border┘${CoR}"
# Display legend
echo -e "\n${COLOR_YELLOW}Legend:${CoR}"
echo -e " • Pass Auth: ${COLOR_GREEN}✓${CoR} = Enabled, ${COLOR_RED}✗${CoR} = Disabled"
echo -e " • IP Rules: ${COLOR_GREEN}allow${CoR} = Allowed, ${COLOR_RED}deny${CoR} = Denied\n"
}
################################
## backup
# Function to make a full backup
full_backup() {
check_dependencies
check_nginx_access
check_token_notverbose
DATE=$(date +"_%Y_%m_%d__%H_%M_%S")
echo -e "\n📦 ${COLOR_YELLOW}Starting full backup...${CoR}"
# Initialize counters
local users_count=0
local certs_count=0
local hosts_count=0
local redirect_hosts_count=0
local stream_hosts_count=0
local dead_hosts_count=0
local custom_certs_count=0
local letsencrypt_certs_count=0
local access_lists_count=0
local success_count=0
local error_count=0
# Create main backup directory with timestamp
BACKUP_PATH="$BACKUP_DIR"
mkdir -p "$BACKUP_PATH"
# Create required subdirectories
echo -e "\n📂 ${COLOR_CYAN}Creating backup directories...${CoR}"
for dir in ".user" ".settings" ".access_lists" ".Proxy_Hosts" ".Redirection_Hosts" ".Stream_Hosts" ".Dead_Hosts" ".ssl"; do
mkdir -p "$BACKUP_PATH/$dir" || {
echo -e " ⛔ ${COLOR_RED}Failed to create $dir directory${CoR}"
return 1
}
echo -e " ✓ Created: ${COLOR_GREY}$BACKUP_PATH/$dir${CoR}"
done
# Initialize empty JSON for full configuration
echo "{}" >"$BACKUP_PATH/full_config${DATE}.json"
echo -e "\n🔄 ${COLOR_CYAN}Starting configuration backup...${CoR}"
# 1. Backup Users
# trap 'echo "Error on line $LINENO"' ERR
# set -x # Debug mode ON
echo -e "\n👥 ${COLOR_CYAN}Backing up users...${CoR}"
USERS_RESPONSE=$(curl -s -X GET "$BASE_URL/users" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
if [ -n "$USERS_RESPONSE" ] && echo "$USERS_RESPONSE" | jq empty 2>/dev/null; then
users_count=$(echo "$USERS_RESPONSE" | jq '. | length')
# Save users to dedicated file
echo "$USERS_RESPONSE" | jq '.' >"$BACKUP_PATH/.user/users_${NGINX_IP//./_}$DATE.json"
# Add users to full configuration
jq --argjson users "$USERS_RESPONSE" '. + {users: $users}' \
"$BACKUP_PATH/full_config${DATE}.json" >"$BACKUP_PATH/full_config${DATE}.json.tmp"
mv "$BACKUP_PATH/full_config${DATE}.json.tmp" "$BACKUP_PATH/full_config${DATE}.json"
echo -e " ✅ ${COLOR_GREEN}Backed up $users_count users${CoR}"
success_count=$((success_count + 1))
else
echo -e " ⚠️ ${COLOR_YELLOW}No users found or invalid response${CoR}"
error_count=$((error_count + 1))
fi
# trap - ERR # Reset trap
# set +x # Debug mode OFF
# 2. Backup settings
echo -e "\n⚙️ ${COLOR_CYAN}Backing up settings...${CoR}"
SETTINGS_RESPONSE=$(curl -s -X GET "$BASE_URL/settings" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
#echo -e "\n🔍 DEBUG Settings Response: $SETTINGS_RESPONSE" # Debug line
if [ -n "$SETTINGS_RESPONSE" ] && echo "$SETTINGS_RESPONSE" | jq empty 2>/dev/null; then
# Save settings to dedicated file
echo "$SETTINGS_RESPONSE" | jq '.' >"$BACKUP_PATH/.settings/settings_${NGINX_IP//./_}$DATE.json"
# Add settings to full configuration
jq --argjson settings "$SETTINGS_RESPONSE" '. + {settings: $settings}' \
"$BACKUP_PATH/full_config${DATE}.json" >"$BACKUP_PATH/full_config${DATE}.json.tmp"
mv "$BACKUP_PATH/full_config${DATE}.json.tmp" "$BACKUP_PATH/full_config${DATE}.json"
echo -e " ✅ ${COLOR_GREEN}Settings backed up successfully${CoR}"
success_count=$((success_count + 1))
else
echo -e " ⚠️ ${COLOR_YELLOW}Invalid settings response${CoR}"
error_count=$((error_count + 1))
fi
# 3. Backup access lists
echo -e "\n🔑 ${COLOR_CYAN}Backing up access lists...${CoR}"
# Get list of access list IDs first
ACCESS_LISTS_IDS=$(curl -s -X GET "$BASE_URL/nginx/access-lists" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" | jq -r '.[].id' 2>/dev/null)
if [ -n "$ACCESS_LISTS_IDS" ]; then
access_lists_count=$(echo "$ACCESS_LISTS_IDS" | wc -l | tr -d ' ')
# Initialize JSON array for complete access lists
echo "[" >"$BACKUP_PATH/.access_lists/access_lists_${NGINX_IP//./_}$DATE.json"
local first=true
# Get each access list with expanded items and clients for complete backup
# Note: The expand parameter is required to get full details of items and clients
for list_id in $ACCESS_LISTS_IDS; do
if [ "$first" = true ]; then
first=false
else
echo "," >>"$BACKUP_PATH/.access_lists/access_lists_${NGINX_IP//./_}$DATE.json"
fi
curl -s -X GET "$BASE_URL/nginx/access-lists/$list_id?expand=items%2Cclients" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" >>"$BACKUP_PATH/.access_lists/access_lists_${NGINX_IP//./_}$DATE.json"
done
echo "]" >>"$BACKUP_PATH/.access_lists/access_lists_${NGINX_IP//./_}$DATE.json"
# Verify JSON is valid
if jq empty "$BACKUP_PATH/.access_lists/access_lists_${NGINX_IP//./_}$DATE.json" 2>/dev/null; then
# Add access lists to full configuration
ACCESS_LISTS_RESPONSE=$(cat "$BACKUP_PATH/.access_lists/access_lists_${NGINX_IP//./_}$DATE.json")
jq --argjson lists "$ACCESS_LISTS_RESPONSE" '. + {access_lists: $lists}' \
"$BACKUP_PATH/full_config${DATE}.json" >"$BACKUP_PATH/full_config${DATE}.json.tmp"
mv "$BACKUP_PATH/full_config${DATE}.json.tmp" "$BACKUP_PATH/full_config${DATE}.json"
echo -e " ✅ ${COLOR_GREEN}Backed up $access_lists_count access lists with complete details${CoR}"
success_count=$((success_count + 1))
else
echo -e " ⚠️ ${COLOR_YELLOW}Failed to create valid access lists backup${CoR}"
error_count=$((error_count + 1))
fi
else
echo -e " ⚠️ ${COLOR_YELLOW}No access lists found${CoR}"
error_count=$((error_count + 1))
fi
# 4. Backup proxy hosts
echo -e "\n🌐 ${COLOR_CYAN}Backing up proxy hosts...${CoR}"
ALL_HOSTS_RESPONSE=$(curl -s -X GET "$BASE_URL/nginx/proxy-hosts" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
if [ -n "$ALL_HOSTS_RESPONSE" ] && echo "$ALL_HOSTS_RESPONSE" | jq empty 2>/dev/null; then
hosts_count=$(echo "$ALL_HOSTS_RESPONSE" | jq '. | length')
# Save all hosts metadata
echo "$ALL_HOSTS_RESPONSE" | jq '.' >"$BACKUP_PATH/.Proxy_Hosts/all_hosts_${NGINX_IP//./_}$DATE.json"
# Process each proxy host - use while read with pipe to preserve variables
while IFS= read -r host; do
local host_id=$(echo "$host" | jq -r '.id')
local domain_name=$(echo "$host" | jq -r '.domain_names[0]' | sed 's/[^a-zA-Z0-9.]/_/g')
local cert_id=$(echo "$host" | jq -r '.certificate_id')
echo -e "\n 📥 Processing host: ${COLOR_GREEN}$domain_name${CoR} (ID: ${COLOR_YELLOW}$host_id${CoR})"
# Create directory for this proxy host
local PROXY_DIR="$BACKUP_PATH/.Proxy_Hosts/$domain_name"
mkdir -p "$PROXY_DIR/ssl" "$PROXY_DIR/logs"
# Save proxy host configuration
echo "$host" | jq '.' >"$PROXY_DIR/proxy_config.json"
# Get and save nginx configuration
local NGINX_CONFIG
NGINX_CONFIG=$(curl -s -X GET "$BASE_URL/nginx/proxy-hosts/$host_id/nginx" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
if [ -n "$NGINX_CONFIG" ]; then
echo "$NGINX_CONFIG" >"$PROXY_DIR/nginx.conf"
fi
# Get and save logs if they exist
curl -s -X GET "$BASE_URL/nginx/proxy-hosts/$host_id/access.log" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" >"$PROXY_DIR/logs/access.log"
curl -s -X GET "$BASE_URL/nginx/proxy-hosts/$host_id/error.log" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" >"$PROXY_DIR/logs/error.log"
# Process SSL certificate if exists
if [ -n "$cert_id" ] && [ "$cert_id" != "null" ] && [ "$cert_id" != "0" ]; then
echo -e " 🔒 Downloading SSL certificate (ID: $cert_id)"
# Get certificate metadata first
local CERT_META
CERT_META=$(curl -s -X GET "$BASE_URL/nginx/certificates/$cert_id" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
if [ -n "$CERT_META" ] && echo "$CERT_META" | jq empty 2>/dev/null; then
echo "$CERT_META" | jq '.' >"$PROXY_DIR/ssl/certificate_meta.json"
# Get certificate content
local CERT_CONTENT
CERT_CONTENT=$(curl -s -X GET "$BASE_URL/nginx/certificates/$cert_id/certificates" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" \
-H "Accept: application/json")
if [ -n "$CERT_CONTENT" ] && echo "$CERT_CONTENT" | jq empty 2>/dev/null; then
# Save certificate files in proxy host directory
echo "$CERT_CONTENT" | jq -r '.certificate' >"$PROXY_DIR/ssl/certificate.pem"
echo "$CERT_CONTENT" | jq -r '.private' >"$PROXY_DIR/ssl/private.key"
chmod 600 "$PROXY_DIR/ssl/private.key"
if echo "$CERT_CONTENT" | jq -r '.intermediate' >/dev/null 2>&1; then
echo "$CERT_CONTENT" | jq -r '.intermediate' >"$PROXY_DIR/ssl/chain.pem"
fi
# Create centralized SSL directory structure
local CERT_NAME
CERT_NAME=$(echo "$CERT_META" | jq -r '.nice_name // .domain_names[0]' | sed 's/[^a-zA-Z0-9.]/_/g')
local CENTRAL_SSL_DIR="$BACKUP_PATH/.ssl/$CERT_NAME"
mkdir -p "$CENTRAL_SSL_DIR"
# Save metadata and create symbolic links
echo "$CERT_META" | jq '.' >"$CENTRAL_SSL_DIR/certificate_meta.json"
ln -sf "$PROXY_DIR/ssl/certificate.pem" "$CENTRAL_SSL_DIR/certificate.pem"
ln -sf "$PROXY_DIR/ssl/private.key" "$CENTRAL_SSL_DIR/private.key"
[ -f "$PROXY_DIR/ssl/chain.pem" ] && ln -sf "$PROXY_DIR/ssl/chain.pem" "$CENTRAL_SSL_DIR/chain.pem"
# Add symlink to latest version
ln -sf "$CENTRAL_SSL_DIR" "$BACKUP_PATH/.ssl/${CERT_NAME}_latest"
echo -e " ✅ ${COLOR_GREEN}SSL certificate backed up successfully${CoR}"
# Count certificate type - maintenant les compteurs fonctionneront
if echo "$CERT_META" | jq -e '.provider // empty | contains("letsencrypt")' >/dev/null 2>&1; then
letsencrypt_certs_count=$((letsencrypt_certs_count + 1))
else
custom_certs_count=$((custom_certs_count + 1))
fi
certs_count=$((certs_count + 1))
success_count=$((success_count + 1))
else
echo -e " ⚠️ ${COLOR_YELLOW}Failed to download certificate content${CoR}"
error_count=$((error_count + 1))
fi
else
echo -e " ⚠️ ${COLOR_YELLOW}Failed to get certificate metadata${CoR}"
error_count=$((error_count + 1))
fi
fi
echo -e " ✅ ${COLOR_GREEN}Host $domain_name backed up successfully${CoR}"
done < <(echo "$ALL_HOSTS_RESPONSE" | jq -c '.[]')
# Create latest symlink for hosts
ln -sf "$BACKUP_PATH/.Proxy_Hosts/all_hosts_${NGINX_IP//./_}$DATE.json" \
"$BACKUP_PATH/.Proxy_Hosts/all_hosts_latest.json"
echo -e "\n ✅ ${COLOR_GREEN}Backed up $hosts_count proxy hosts${CoR}"
else
echo -e " ⚠️ ${COLOR_YELLOW}No proxy hosts found or invalid response${CoR}"
error_count=$((error_count + 1))
fi
# 5. Backup redirection hosts
echo -e "\n🔄 ${COLOR_CYAN}Backing up redirection hosts...${CoR}"
REDIRECTION_HOSTS_RESPONSE=$(curl -s -X GET "$BASE_URL/nginx/redirection-hosts" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
if [ -n "$REDIRECTION_HOSTS_RESPONSE" ] && echo "$REDIRECTION_HOSTS_RESPONSE" | jq empty 2>/dev/null; then
redirect_hosts_count=$(echo "$REDIRECTION_HOSTS_RESPONSE" | jq '. | length')
echo "$REDIRECTION_HOSTS_RESPONSE" | jq '.' >"$BACKUP_PATH/.Redirection_Hosts/redirection_hosts_${NGINX_IP//./_}$DATE.json"
ln -sf "$BACKUP_PATH/.Redirection_Hosts/redirection_hosts_${NGINX_IP//./_}$DATE.json" \
"$BACKUP_PATH/.Redirection_Hosts/redirection_hosts_latest.json"
jq --argjson hosts "$REDIRECTION_HOSTS_RESPONSE" '. + {redirection_hosts: $hosts}' \
"$BACKUP_PATH/full_config${DATE}.json" >"$BACKUP_PATH/full_config${DATE}.json.tmp"
mv "$BACKUP_PATH/full_config${DATE}.json.tmp" "$BACKUP_PATH/full_config${DATE}.json"
echo -e " ✅ ${COLOR_GREEN}Backed up $redirect_hosts_count redirection hosts${CoR}"
success_count=$((success_count + 1))
else
echo -e " ⚠️ ${COLOR_YELLOW}No redirection hosts found or invalid response${CoR}"
error_count=$((error_count + 1))
fi
# 6. Backup stream hosts
echo -e "\n🔌 ${COLOR_CYAN}Backing up stream hosts...${CoR}"
STREAM_HOSTS_RESPONSE=$(curl -s -X GET "$BASE_URL/nginx/streams" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
if [ -n "$STREAM_HOSTS_RESPONSE" ] && echo "$STREAM_HOSTS_RESPONSE" | jq empty 2>/dev/null; then
stream_hosts_count=$(echo "$STREAM_HOSTS_RESPONSE" | jq '. | length')
echo "$STREAM_HOSTS_RESPONSE" | jq '.' >"$BACKUP_PATH/.Stream_Hosts/stream_hosts_${NGINX_IP//./_}$DATE.json"
ln -sf "$BACKUP_PATH/.Stream_Hosts/stream_hosts_${NGINX_IP//./_}$DATE.json" \
"$BACKUP_PATH/.Stream_Hosts/stream_hosts_latest.json"
jq --argjson hosts "$STREAM_HOSTS_RESPONSE" '. + {streams: $hosts}' \
"$BACKUP_PATH/full_config${DATE}.json" >"$BACKUP_PATH/full_config${DATE}.json.tmp"
mv "$BACKUP_PATH/full_config${DATE}.json.tmp" "$BACKUP_PATH/full_config${DATE}.json"
echo -e " ✅ ${COLOR_GREEN}Backed up $stream_hosts_count stream hosts${CoR}"
success_count=$((success_count + 1))
else
echo -e " ⚠️ ${COLOR_YELLOW}No stream hosts found or invalid response${CoR}"
error_count=$((error_count + 1))
fi
# 7. Backup 404 hosts (dead hosts)
echo -e "\n🚫 ${COLOR_CYAN}Backing up 404 hosts...${CoR}"
DEAD_HOSTS_RESPONSE=$(curl -s -X GET "$BASE_URL/nginx/dead-hosts" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
if [ -n "$DEAD_HOSTS_RESPONSE" ] && echo "$DEAD_HOSTS_RESPONSE" | jq empty 2>/dev/null; then
dead_hosts_count=$(echo "$DEAD_HOSTS_RESPONSE" | jq '. | length')
echo "$DEAD_HOSTS_RESPONSE" | jq '.' >"$BACKUP_PATH/.Dead_Hosts/dead_hosts_${NGINX_IP//./_}$DATE.json"
ln -sf "$BACKUP_PATH/.Dead_Hosts/dead_hosts_${NGINX_IP//./_}$DATE.json" \
"$BACKUP_PATH/.Dead_Hosts/dead_hosts_latest.json"
jq --argjson hosts "$DEAD_HOSTS_RESPONSE" '. + {dead_hosts: $hosts}' \
"$BACKUP_PATH/full_config${DATE}.json" >"$BACKUP_PATH/full_config${DATE}.json.tmp"
mv "$BACKUP_PATH/full_config${DATE}.json.tmp" "$BACKUP_PATH/full_config${DATE}.json"
echo -e " ✅ ${COLOR_GREEN}Backed up $dead_hosts_count 404 hosts${CoR}"
success_count=$((success_count + 1))
else
echo -e " ⚠️ ${COLOR_YELLOW}No 404 hosts found or invalid response${CoR}"
error_count=$((error_count + 1))
fi
# Generate backup report and statistics
echo -e "\n📊 ${COLOR_YELLOW}Backup Summary:${CoR}"
echo -e " • ${COLOR_CYAN}Users:${CoR} $users_count"
echo -e " • ${COLOR_CYAN}Proxy Hosts:${CoR} $hosts_count"
echo -e " • ${COLOR_CYAN}Redirection Hosts:${CoR} $redirect_hosts_count"
echo -e " • ${COLOR_CYAN}Stream Hosts:${CoR} $stream_hosts_count"
echo -e " • ${COLOR_CYAN}404 Hosts:${CoR} $dead_hosts_count"
echo -e " • ${COLOR_CYAN}SSL Certificates:${CoR} $certs_count"
echo -e " ├─ Custom: $custom_certs_count"
echo -e " └─ Let's Encrypt: $letsencrypt_certs_count"
echo -e " • ${COLOR_CYAN}Access Lists:${CoR} $access_lists_count"
echo -e " • ${COLOR_CYAN}Success/Error:${CoR} $success_count/$error_count"
# Count backup files by type
local total_files=$(find "$BACKUP_PATH" -type f \( \
-name "*.json" -o \
-name "*.conf" -o \
-name "*.log" -o \
-name "*.pem" -o \
-name "*.key" \
\) | wc -l)
local config_files=$(find "$BACKUP_PATH" -maxdepth 1 -type f -name "full_config*.json" | wc -l)
local proxy_files=$(find "$BACKUP_PATH/.Proxy_Hosts" -type f -name "proxy_config.json" | wc -l)
local redirect_files=$(find "$BACKUP_PATH/.Redirection_Hosts" -type f -name "*.json" | wc -l)
local stream_files=$(find "$BACKUP_PATH/.Stream_Hosts" -type f -name "*.json" | wc -l)
local dead_files=$(find "$BACKUP_PATH/.Dead_Hosts" -type f -name "*.json" | wc -l)
local ssl_files=$(find "$BACKUP_PATH" -type f \( \
-name "certificate*.json" -o \
-name "*.pem" -o \
-name "*.key" \
\) | wc -l)
local access_files=$(find "$BACKUP_PATH/.access_lists" -type f -name "*.json" | wc -l)
local settings_files=$(find "$BACKUP_PATH/.settings" -type f -name "*.json" | wc -l)
local user_files=$(find "$BACKUP_PATH/.user" -type f -name "*.json" | wc -l)
echo -e "\n📁 ${COLOR_YELLOW}Backup Files Count:${CoR}"
echo -e " • Full Configurations: ${COLOR_GREY}$config_files files${CoR}"
echo -e " • Proxy Host Files: ${COLOR_GREY}$proxy_files files${CoR}"
echo -e " • Redirection Host Files: ${COLOR_GREY}$redirect_files files${CoR}"
echo -e " • Stream Host Files: ${COLOR_GREY}$stream_files files${CoR}"
echo -e " • 404 Host Files: ${COLOR_GREY}$dead_files files${CoR}"
echo -e " • SSL Certificate Files: ${COLOR_GREY}$ssl_files files${CoR}"
echo -e " • Access List Files: ${COLOR_GREY}$access_files files${CoR}"
echo -e " • Settings Files: ${COLOR_GREY}$settings_files files${CoR}"
echo -e " • User Files: ${COLOR_GREY}$user_files files${CoR}"
echo -e " • ${COLOR_GREEN}Total Backup Files: ${COLOR_GREY}$total_files files${CoR}"
# Display backup locations
echo -e "\n📂 ${COLOR_YELLOW}Backup Locations:${CoR}"
echo -e " • Full config: ${COLOR_GREY}$BACKUP_PATH/full_config${DATE}.json${CoR}"
echo -e " • Latest symlink: ${COLOR_GREY}$BACKUP_PATH/full_config_latest.json${CoR}"
echo -e " • Proxy configs: ${COLOR_GREY}$BACKUP_PATH/.Proxy_Hosts/${CoR}"
echo -e " • Host list full: ${COLOR_GREY}$BACKUP_PATH/.Proxy_Hosts/all_hosts_latest.json${CoR}"
# Calculate and display total backup size
local backup_size=$(du -sh "$BACKUP_PATH" | cut -f1)
echo -e "\n💾 ${COLOR_YELLOW}Backup Size:${CoR} ${COLOR_GREY}$backup_size${CoR}"
# Create latest symlink for full configuration
ln -sf "$BACKUP_PATH/full_config${DATE}.json" "$BACKUP_PATH/full_config_latest.json"
# Check for any errors during backup
if [ $error_count -gt 0 ]; then
echo -e "\n⚠️ ${COLOR_YELLOW}Backup completed with $error_count errors${CoR}"
echo -e " Please check the logs above for details."
else
echo -e "\n✅ ${COLOR_GREEN}Backup completed successfully!${CoR}"
fi
echo -e "\n📝 ${COLOR_GREY}Backup completed at: $(date '+%Y-%m-%d %H:%M:%S')${CoR}\n"
return $error_count
}
# Backup a single proxy host (via $HOST_ID) or all proxy hosts (if $HOST_ID is empty),
# including its SSL certificate, using the same on-disk layout as full_backup().
backup_host() {
check_dependencies
check_nginx_access
check_token_notverbose
BACKUP_PATH="$BACKUP_DIR"
mkdir -p "$BACKUP_PATH/.Proxy_Hosts"
local hosts_json
if [ -n "$HOST_ID" ]; then
echo -e "\n📦 ${COLOR_YELLOW}Backing up proxy host ID $HOST_ID...${CoR}"
local single_host
single_host=$(curl -s -X GET "$BASE_URL/nginx/proxy-hosts/$HOST_ID" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
if ! echo "$single_host" | jq -e '.id' >/dev/null 2>&1; then
echo -e " ⛔ ${COLOR_RED}Proxy host ID $HOST_ID not found.${CoR}\n"
return 1
fi
hosts_json="[$single_host]"
else
echo -e "\n📦 ${COLOR_YELLOW}Backing up all proxy hosts...${CoR}"
hosts_json=$(curl -s -X GET "$BASE_URL/nginx/proxy-hosts" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
fi
if [ -z "$hosts_json" ] || ! echo "$hosts_json" | jq empty 2>/dev/null; then
echo -e " ⚠️ ${COLOR_YELLOW}No proxy hosts found or invalid response${CoR}\n"
return 1
fi
local count=0
while IFS= read -r host; do
local host_id=$(echo "$host" | jq -r '.id')
local domain_name=$(echo "$host" | jq -r '.domain_names[0]' | sed 's/[^a-zA-Z0-9.]/_/g')
local cert_id=$(echo "$host" | jq -r '.certificate_id')
echo -e "\n 📥 Processing host: ${COLOR_GREEN}$domain_name${CoR} (ID: ${COLOR_YELLOW}$host_id${CoR})"
local PROXY_DIR="$BACKUP_PATH/.Proxy_Hosts/$domain_name"
mkdir -p "$PROXY_DIR/ssl" "$PROXY_DIR/logs"
echo "$host" | jq '.' >"$PROXY_DIR/proxy_config.json"
local NGINX_CONFIG
NGINX_CONFIG=$(curl -s -X GET "$BASE_URL/nginx/proxy-hosts/$host_id/nginx" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
[ -n "$NGINX_CONFIG" ] && echo "$NGINX_CONFIG" >"$PROXY_DIR/nginx.conf"
curl -s -X GET "$BASE_URL/nginx/proxy-hosts/$host_id/access.log" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" >"$PROXY_DIR/logs/access.log"
curl -s -X GET "$BASE_URL/nginx/proxy-hosts/$host_id/error.log" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" >"$PROXY_DIR/logs/error.log"
if [ -n "$cert_id" ] && [ "$cert_id" != "null" ] && [ "$cert_id" != "0" ]; then
echo -e " 🔒 Downloading SSL certificate (ID: $cert_id)"
local CERT_META
CERT_META=$(curl -s -X GET "$BASE_URL/nginx/certificates/$cert_id" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}")
if [ -n "$CERT_META" ] && echo "$CERT_META" | jq empty 2>/dev/null; then
echo "$CERT_META" | jq '.' >"$PROXY_DIR/ssl/certificate_meta.json"
local CERT_CONTENT
CERT_CONTENT=$(curl -s -X GET "$BASE_URL/nginx/certificates/$cert_id/certificates" \
-H "Authorization: Bearer ${TOKEN:-$(cat "$TOKEN_FILE")}" \
-H "Accept: application/json")
if [ -n "$CERT_CONTENT" ] && echo "$CERT_CONTENT" | jq empty 2>/dev/null; then
echo "$CERT_CONTENT" | jq -r '.certificate' >"$PROXY_DIR/ssl/certificate.pem"
echo "$CERT_CONTENT" | jq -r '.private' >"$PROXY_DIR/ssl/private.key"
chmod 600 "$PROXY_DIR/ssl/private.key"
echo "$CERT_CONTENT" | jq -r '.intermediate // empty' >"$PROXY_DIR/ssl/chain.pem"
echo -e " ✅ ${COLOR_GREEN}SSL certificate backed up${CoR}"
else
echo -e " ⚠️ ${COLOR_YELLOW}Failed to download certificate content${CoR}"
fi
else
echo -e " ⚠️ ${COLOR_YELLOW}Failed to get certificate metadata${CoR}"
fi
fi
echo -e " ✅ ${COLOR_GREEN}Host $domain_name backed up successfully${CoR}"
count=$((count + 1))
done < <(echo "$hosts_json" | jq -c '.[]')
echo -e "\n📝 ${COLOR_GREY}Backup completed at: $(date '+%Y-%m-%d %H:%M:%S')${CoR}"
echo -e " ${COLOR_CYAN}$count${CoR} proxy host(s) backed up in ${COLOR_GREY}$BACKUP_PATH/.Proxy_Hosts/${CoR}\n"
}
# List available per-host proxy backups (written by full_backup() or backup_host()).
# Purely local (no API call), so it works even without a valid/fresh token.
list_backups() {
check_dependencies
local ip_port_dir="${NGINX_IP//[.:]/_}_${NGINX_PORT}"
BACKUP_DIR="${BACKUP_DIR:-$DATA_DIR/$ip_port_dir/backups}"
BACKUP_PATH="$BACKUP_DIR"
local dir="$BACKUP_PATH/.Proxy_Hosts"
if [ ! -d "$dir" ] || [ -z "$(find "$dir" -mindepth 2 -maxdepth 2 -name "proxy_config.json" -print -quit 2>/dev/null)" ]; then
echo -e "\n ⚠️ ${COLOR_YELLOW}No proxy host backups found in ${COLOR_GREY}$dir${CoR}"
echo -e " Run ${COLOR_CYAN}--backup-host${CoR} or ${COLOR_CYAN}--backup${CoR} first.\n"
return 1
fi
echo -e "\n 📋 ${COLOR_YELLOW}Available proxy host backups:${CoR}"
echo -e " ${COLOR_GREY}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${CoR}"
printf " %-4s %-35s %s\n" "ID" "Domain" "Last backup"
while IFS= read -r file; do
local id domain mtime
id=$(jq -r '.id // "?"' "$file" 2>/dev/null)
domain=$(jq -r '.domain_names[0] // "?"' "$file" 2>/dev/null)
mtime=$(date -r "$file" '+%Y-%m-%d %H:%M:%S' 2>/dev/null)
printf " ${COLOR_CYAN}%-4s${CoR} %-35s ${COLOR_GREY}%s${CoR}\n" "$id" "$domain" "$mtime"
done < <(find "$dir" -mindepth 2 -maxdepth 2 -name "proxy_config.json" | sort)
echo ""
}
######################################
# Main menu logic
######################################
for arg in "$@"; do
if [ "$arg" = "-y" ]; then
AUTO_YES=true
#echo "Debug global: Setting AUTO_YES=true"
break
fi
done
# Pre-scan for --purge so its position on the command line does not matter
# (e.g. "--cert-delete 240 --purge" or "--cert-delete --purge 240").
for arg in "$@"; do
if [ "$arg" = "--purge" ]; then
CERT_PURGE=true
break
fi
done
while [[ "$#" -gt 0 ]]; do
case "$1" in
-y)
AUTO_YES=true
shift
;;
--json)
JSON_OUTPUT=true
shift
;;
--list)
LIST_JSON_COMMANDS=true
shift
;;
--purge)
# Handled via pre-scan (CERT_PURGE); only used together with --cert-delete.
CERT_PURGE=true
shift
;;
--help)
SHOW_HELP=true
shift
;;
--examples)
EXAMPLES=true
shift
;;
--info)
INFO=true
shift
;;
--show-default)
SHOW_DEFAULT=true
shift
;;
--check-token)
CHECK_TOKEN=true
shift
;;
--check-update)
CHECK_UPDATE=true
shift
;;
--backup)
BACKUP=true
shift
;;
--backup-host)
shift
if [[ -n "$1" && "$1" != -* ]]; then
HOST_ID="$1"
shift
fi
BACKUP_HOST=true
;;
--backup-host-list)
BACKUP_LIST=true
shift
;;
--clean-hosts)
exit 1 # not use !
CLEAN_HOSTS=true
shift
if [[ -n "$1" && "$1" != -* ]]; then
BACKUP_FILE="$1"
shift
else
BACKUP_FILE="$DEFAULT_BACKUP_FILE"
fi
;;
--user-list)
USER_LIST=true
shift
;;
--user-create)
shift
if [[ $# -lt 3 ]]; then
echo -e "\n 👤 ${COLOR_RED}The --user-create option requires username, password, and email.${CoR}"
echo -e " Usage: ${COLOR_ORANGE}$0 --user-create <username> <password> <email>${CoR}"
echo -e " Example:"
echo -e " ${COLOR_GREEN}$0 --user-create john secretpass john@domain.com${CoR}\n"
exit 1
fi
USERNAME="$1"
PASSWORD="$2"
EMAIL="$3"
USER_CREATE=true
shift 3
# Optional: grant admin role (default is a standard, non-admin user)
if [ $# -gt 0 ] && [ "$1" = "--admin" ]; then
USER_IS_ADMIN=true
shift
fi
;;
--user-delete)
shift
if [[ $# -eq 0 ]]; then
echo -e "\n ⛔ ${COLOR_RED}INVALID: The --user-delete option requires a user 🆔.${CoR}"
echo -e " Usage: ${COLOR_ORANGE}$0 --user-delete <user_id>${CoR}"
exit 1
fi
USER_ID="$1"
USER_DELETE=true
shift
;;
--host-show)
shift
if [[ $# -eq 0 ]]; then
echo -e "\n ⛔ ${COLOR_RED}INVALID: The --host-show option requires a host ${CoR}🆔"
echo -e " Usage : ${COLOR_ORANGE}$0 --host-show <ID>${CoR}"
echo -e " Example: $0 --host-show 42"
echo -e " Find ID: $0 --host-list\n"
exit 1
fi
host_id="$1"
HOST_SHOW=true
shift
;;
--host-list)
HOST_LIST=true
shift
;;
--host-list-full)
HOST_LIST_FULL=true
shift
;;
--host-search)
shift
HOST_SEARCHNAME="${1:-}"
if [ -z "$HOST_SEARCHNAME" ]; then
echo -e "\n ⛔ ${COLOR_RED}INVALID: The --host-search option requires a host name.${CoR}"
echo -e " Usage : ${COLOR_ORANGE}$0 --host-search hostname${CoR}"
echo -e " Example: $0 --host-search domain.com ${COLOR_YELLOW}or${CoR} dom ${COLOR_YELLOW}or${CoR} .com"
echo -e " Find ID: $0 --host-list\n"
exit 1
fi
HOST_SEARCH=true
shift
;;
--host-enable)
shift
if [ $# -eq 0 ] || [[ "$1" == -* ]]; then
echo -e "\n ⛔ ${COLOR_RED}INVALID: The --host-enable option requires a host${CoR} 🆔"
echo -e " Usage : ${COLOR_ORANGE}$0 --host-enable <host_id>${CoR}"
echo -e " Example: $0 --host-enable 42"
echo -e " Find ID: $0 --host-list\n"
exit 1
fi
HOST_ID="$1"
HOST_ENABLE=true
shift
;;
--host-disable)
shift
if [[ $# -eq 0 ]]; then
echo -e "\n ⛔ ${COLOR_RED}INVALID: The --host-disable option requires host${CoR} 🆔"
echo -e " Usage : ${COLOR_GREEN}$0 --host-disable <host_id>${CoR}"
echo -e " Example: $0 --host-disable 42"
echo -e " Find ID: $0 --host-list\n"
exit 1
fi
HOST_ID="$1"
HOST_DISABLE=true
shift
;;
--host-delete)
shift
if [ $# -eq 0 ] || [[ "$1" == -* ]]; then
echo -e "\n ⛔ ${COLOR_RED}INVALID: The --host-delete option requires a host${CoR} 🆔"
echo -e " ${COLOR_CYAN}Usage:${CoR}"
echo -e " ${COLOR_ORANGE}$0 --host-delete <host_id> [-y]${CoR}"
echo -e " ${COLOR_CYAN}Examples:${CoR}"
echo -e " ${COLOR_GREEN}$0 --host-delete 42${CoR}"
echo -e " ${COLOR_GREEN}$0 --host-delete 42 -y${CoR} ${COLOR_GREY}# Skip confirmation${CoR}"
echo -e "\n ${COLOR_YELLOW}💡 Tip: Use --host-list to see all available hosts and their IDs${CoR}"
exit 1
fi
if [[ "$1" =~ ^[0-9]+$ ]]; then
HOST_ID="$1"
HOST_DELETE=true
shift
else
echo -e "\n ⛔ ${COLOR_RED}INVALID: Invalid host ID '$1' - must be a number${CoR}"
echo -e "\n${COLOR_CYAN}Example:${CoR}"
echo -e " ${COLOR_GREEN}$0 --host-delete 42${CoR}"
echo -e "\n${COLOR_YELLOW}💡 Tip: Use --host-list to see all available hosts and their IDs${CoR}"
exit 1
fi
;;
--host-update)
if [[ "$#" -lt 3 ]]; then
echo -e "\n ⛔ ${COLOR_RED}INVALID: The --host-update option requires a host 🆔 and a field=value pair.${CoR}"
echo -e " Usage : ${COLOR_GREEN}$0 --host-update <host_id> <field=value>${CoR}"
echo -e " Find ID: $0 --host-list${CoR}\n"
exit 1
fi
# Check if $2 is a number
if [[ "$2" =~ ^[0-9]+$ ]]; then
HOST_ID="$2"
FIELD_VALUE="$3"
# Split FIELD and VALUE
if [[ "$FIELD_VALUE" == *"="* ]]; then
FIELD=$(echo "$FIELD_VALUE" | cut -d '=' -f1)
VALUE=$(echo "$FIELD_VALUE" | cut -d '=' -f2-)
HOST_UPDATE=true
else
echo -e "\n ⛔ ${COLOR_RED}INVALID: The field=value pair is malformed.${CoR}"
echo -e " Example: $0 --host-update 42 forward_host=new.backend.local"
exit 1
fi
shift 3
#host_update "$HOST_ID" "$FIELD" "$VALUE"
#HOST_UPDATE=true
else
echo -e "\n ⛔ ${COLOR_RED}INVALID: The --host-update option requires a valid (numeric) host 🆔.${CoR}"
exit 1
fi
;;
--host-create)
HOST_CREATE=true
shift
# Check if there are any remaining arguments after shift
if [ $# -eq 0 ]; then
echo -e "\n ⛔ ${COLOR_RED}INVALID: The --host-create option requires arguments${CoR}"
echo -e "\n Required options:"
echo -e " • Domain name ${COLOR_GREY}(positional argument)${CoR}"
echo -e " • -i, --forward-host ${COLOR_GREY}Forward host (e.g., 127.0.0.1)${CoR}"
echo -e " • -p, --forward-port ${COLOR_GREY}Forward port (e.g., 8080)${CoR}"
echo -e "\n Optional:"
echo -e " • -f, --forward-scheme ${COLOR_GREY}Protocol (http/https, default: http)${CoR}"
echo -e " • -b, --block-exploits ${COLOR_GREY}Block common exploits (true/false)${CoR}"
echo -e " • -c, --cache ${COLOR_GREY}Enable caching (true/false)${CoR}"
echo -e " • -w, --websocket ${COLOR_GREY}Allow websocket upgrade (true/false)${CoR}"
echo -e " • -h, --http2 ${COLOR_GREY}Enable HTTP/2 support (true/false)${CoR}"
echo -e " • -s, --ssl-force ${COLOR_GREY}Force SSL (true/false)${CoR}"
echo -e " • -l, --custom-locations ${COLOR_GREY}Custom location rules (JSON array)${CoR}"
echo -e " • -a, --advanced-config ${COLOR_GREY}Advanced Nginx configuration (string)${CoR}"
echo -e "\n Can be combined with:"
echo -e " • --cert-generate ${COLOR_GREY}Generate SSL certificate${CoR}"
echo -e " • --host-ssl-enable ${COLOR_GREY}Enable SSL after creation${CoR}"
echo -e " • -y ${COLOR_GREY}Skip all confirmations${CoR}"
echo -e "\n Examples:"
echo -e " ${COLOR_GREEN}$0 --host-create example.com -i 127.0.0.1 -p 8080${CoR}"
echo -e " ${COLOR_GREEN}$0 --host-create example.com -i 127.0.0.1 -p 8080 --cert-generate --host-ssl-enable -y${CoR}\n"
exit 1
fi
# Check if first argument is a valid domain (not starting with -)
if [[ "$1" == -* ]]; then
echo -e "\n ⛔ ${COLOR_RED}INVALID: First argument after --host-create must be a domain name${CoR}"
exit 1
fi
DOMAIN_NAMES="$1"
shift
# Process remaining options
while [[ $# -gt 0 ]]; do
case "$1" in
-i | --forward-host)
if [[ -n "$2" && "$2" != -* ]]; then
FORWARD_HOST="$2"
shift 2
else
echo -e "\n ⛔ ${COLOR_RED}INVALID: The --forward-host option requires a valid value${CoR}"
echo -e "\n Required options:"
echo -e " • Domain name ${COLOR_GREY}(positional argument)${CoR}"
echo -e " • -i, --forward-host ${COLOR_GREY}Forward host (e.g., 127.0.0.1)${CoR}"
echo -e " • -p, --forward-port ${COLOR_GREY}Forward port (e.g., 8080)${CoR}"
echo -e "\n Optional:"
echo -e " • -f, --forward-scheme ${COLOR_GREY}Protocol (http/https, default: http)${CoR}"
echo -e " • -b, --block-exploits ${COLOR_GREY}Block common exploits (true/false, default: false)${CoR}"
echo -e " • -c, --cache ${COLOR_GREY}Enable caching (true/false, default: false)${CoR}"
echo -e " • -w, --websocket ${COLOR_GREY}Allow websocket upgrade (true/false, default: false)${CoR}"
echo -e " • -h, --http2 ${COLOR_GREY}Enable HTTP/2 support (true/false, default: false)${CoR}"
echo -e " • -s, --ssl-force ${COLOR_GREY}Force SSL (true/false, default: false)${CoR}"
echo -e " • -l, --custom-locations ${COLOR_GREY}Custom location rules (JSON array)${CoR}"
echo -e " • -a, --advanced-config ${COLOR_GREY}Advanced Nginx configuration (string)${CoR}"
exit 1
fi
;;
-p | --forward-port)
if [[ -n "$2" && "$2" != -* && "$2" =~ ^[0-9]+$ ]]; then
FORWARD_PORT="$2"
shift 2
else
echo -e "\n ⛔ ${COLOR_RED}INVALID: The --forward-port option requires a valid number${CoR}"
echo -e "\n Required options:"
echo -e " • Domain name ${COLOR_GREY}(positional argument)${CoR}"
echo -e " • -i, --forward-host ${COLOR_GREY}Forward host (e.g., 127.0.0.1)${CoR}"
echo -e " • -p, --forward-port ${COLOR_GREY}Forward port (e.g., 8080)${CoR}"
exit 1
fi
;;
-f | --forward-scheme)
if [[ -n "$2" && "$2" != -* && "$2" =~ ^(http|https)$ ]]; then
FORWARD_SCHEME="$2"
shift 2
else
echo -e "\n ⛔ ${COLOR_RED}INVALID: The --forward-scheme option must be 'http' or 'https'${CoR}"
exit 1
fi
;;
-b | --block-exploits | -c | --cache | -w | --websocket | -h | --http2 | -s | --ssl-force)
# Process boolean options
# Note: Variable names must match those used in create_or_update_proxy_host() function
opt_name=${1#-?}
opt_name=${opt_name#--}
if [[ "$2" =~ ^(true|false)$ ]]; then
case "$opt_name" in
block-exploits) BLOCK_EXPLOITS="$2" ;;
cache) CACHING_ENABLED="$2" ;; # Fixed: was CACHE_ENABLED (Issue #23)
websocket) ALLOW_WEBSOCKET_UPGRADE="$2" ;; # Fixed: was WEBSOCKET_SUPPORT (Issue #23)
http2) HTTP2_SUPPORT="$2" ;;
ssl-force) SSL_FORCED="$2" ;;
esac
shift 2
else
echo -e "\n ⛔ ${COLOR_RED}INVALID: The $1 option must be 'true' or 'false'${CoR}"
exit 1
fi
;;
-l | --custom-locations)
# Fixed: Added support for custom locations option (Issue #22)
if [[ -n "$2" && "$2" != -* ]]; then
CUSTOM_LOCATIONS="$2"
shift 2
else
echo -e "\n ⛔ ${COLOR_RED}INVALID: The --custom-locations option requires a JSON value${CoR}"
echo -e " ${COLOR_CYAN}Example:${CoR}"
echo -e " ${COLOR_GREEN}$0 --host-create example.com -i 192.168.1.10 -p 8080 -l '[{\"path\":\"/api\",\"forward_host\":\"192.168.1.11\",\"forward_port\":8081}]'${CoR}"
exit 1
fi
;;
-a | --advanced-config)
# Fixed: Added support for advanced config option (Issue #22)
if [[ -n "$2" && "$2" != -* ]]; then
ADVANCED_CONFIG="$2"
shift 2
else
echo -e "\n ⛔ ${COLOR_RED}INVALID: The --advanced-config option requires a configuration string${CoR}"
echo -e " ${COLOR_CYAN}Example:${CoR}"
echo -e " ${COLOR_GREEN}$0 --host-create example.com -i 192.168.1.10 -p 8080 -a 'proxy_set_header X-Real-IP \\\$remote_addr;'${CoR}"
exit 1
fi
;;
# Support for chaining commands
--cert-generate | --host-ssl-enable | -y | --cert-email | --dns-provider | --dns-credentials)
# Just store these flags, they'll be processed later
case "$1" in
--cert-generate)
CERT_GENERATE=true
shift
if [ $# -gt 0 ] && [[ "$1" != --* ]]; then
CERT_DOMAIN="$1"
shift
else
# If no specific argument for --cert-generate, use the host domain
CERT_DOMAIN="$DOMAIN_NAMES"
fi
;;
--cert-email)
shift
CERT_EMAIL="$1"
shift
;;
--dns-provider)
shift
CERT_DNS_PROVIDER="$1"
shift
;;
--dns-credentials)
shift
CERT_DNS_CREDENTIALS="$1"
shift
;;
--host-ssl-enable)
HOST_SSL_ENABLE=true
shift
;;
-y)
AUTO_YES=true
shift
;;
esac
;;
*)
if [[ "$1" == -* ]]; then
echo -e "\n ⚠️ ${COLOR_YELLOW}WARNING: Unknown option ignored -> $1${CoR}"
fi
shift
;;
esac
done
# check settings
if [ -z "$FORWARD_HOST" ] || [ -z "$FORWARD_PORT" ]; then
echo -e "\n ⛔ ${COLOR_RED}INVALID: Missing required parameters${CoR}"
echo -e " Required options:"
echo -e " • Domain name: ${COLOR_GREEN}$DOMAIN_NAMES${CoR} ${COLOR_GREY}(provided)${CoR}"
[ -z "$FORWARD_HOST" ] && echo -e " • -i, --forward-host ${COLOR_RED}Missing${CoR}"
[ -z "$FORWARD_PORT" ] && echo -e " • -p, --forward-port ${COLOR_RED}Missing${CoR}"
echo -e "\n Example:"
echo -e " ${COLOR_GREEN}$0 --host-create example.com -i 127.0.0.1 -p 8080${CoR}\n"
exit 1
fi
# Create proxy host
create_or_update_proxy_host "$DOMAIN_NAMES" "$FORWARD_HOST" "$FORWARD_PORT" \
"${FORWARD_SCHEME:-http}" "${BLOCK_EXPLOITS:-false}" "${CACHING_ENABLED:-false}" \
"${ALLOW_WEBSOCKET_UPGRADE:-false}" "${HTTP2_SUPPORT:-false}" "${SSL_FORCED:-false}"
;;
--host-ssl-enable)
shift
if [ $# -eq 0 ] || [[ "$1" == -* ]]; then
echo -e "\n ⛔ ${COLOR_RED}The --host-ssl-enable option requires a host ID and certificate ID.${CoR}"
echo -e "\n ${COLOR_CYAN}Usage:${CoR}"
echo -e " ${COLOR_ORANGE}$0 --host-ssl-enable <host_id> <cert_id>${CoR}"
echo -e " ${COLOR_CYAN}Examples:${CoR}"
echo -e " ${COLOR_GREEN}$0 --host-ssl-enable 42 240${CoR}"
echo -e " ${COLOR_GREEN}$0 --host-ssl-enable 42 240 -y${CoR} ${COLOR_GREY}# Skip confirmation${CoR}"
echo -e " ${COLOR_YELLOW}💡 Tips:${CoR}"
echo -e " • Use ${COLOR_GREEN}--host-list${CoR} to see all available hosts"
echo -e " • Use ${COLOR_GREEN}--cert-list${CoR} to see all available certificates\n"
exit 1
fi
if [[ "$1" =~ ^[0-9]+$ ]]; then
HOST_ID="$1"
shift
# Check for mandatory cert_id
if [ $# -gt 0 ] && [[ "$1" =~ ^[0-9]+$ ]]; then
CERT_ID="$1"
shift
else
echo -e "\n ⛔ ${COLOR_RED}ERROR: Certificate ID is required${CoR}"
echo -e " ${COLOR_CYAN}Usage:${CoR} $0 --host-ssl-enable <host_id> <cert_id>"
echo -e " ${COLOR_YELLOW}💡 Use --cert-list to see all available certificates${CoR}\n"
exit 1
fi
HOST_SSL_ENABLE=true
else
echo -e "\n ⛔ ${COLOR_RED}ERROR: Invalid host ID '$1' - must be a number${CoR}"
echo -e " ${COLOR_CYAN}Usage:${CoR} $0 --host-ssl-enable <host_id> <cert_id>"
echo -e " ${COLOR_YELLOW}💡 Use --host-list to see all available hosts${CoR}\n"
exit 1
fi
;;
--host-ssl-disable)
HOST_SSL_DISABLE=true
shift
if [ $# -gt 0 ]; then
HOST_ID="$1"
shift
else
echo -e "\n ⛔ ${COLOR_RED}INVALID command: Missing argument${CoR}"
echo -e " Usage : ${COLOR_ORANGE}$0 --host-ssl-disable <host_id>${CoR}"
echo -e " Find HostID: ${COLOR_GREEN}$0 --host-list${CoR}\n"
exit 1
fi
;;
--host-acl-enable)
HOST_ACL_ENABLE=true
shift
if [ $# -lt 2 ]; then
echo -e "\n ⛔ ${COLOR_RED}INVALID: The --host-acl-enable option requires two arguments: host_id and access_list_id${CoR}"
echo -e " Usage : ${COLOR_ORANGE}$0 --host-acl-enable <host_id> <access_list_id>${CoR}"
echo -e " Example: ${COLOR_GREEN}$0 --host-acl-enable 42 5${CoR}"
echo -e " Find ID: ${COLOR_ORANGE}$0 --host-list${CoR} AND ${COLOR_ORANGE}--access-list${CoR}\n"
exit 1
fi
HOST_ID="$1"
ACCESS_LIST_ID="$2"
shift 2
;;
--host-acl-disable)
HOST_ACL_DISABLE=true
shift
if [ $# -gt 0 ]; then
HOST_ID="$1"
shift
else
echo -e "\n ⛔ ${COLOR_RED}The --host-acl-disable option requires a host ID.${CoR}"
echo -e " Usage: $0 --host-acl-disable <host_id>\n"
exit 1
fi
#host_acl_disable "$HOST_ID"
;;
--cert-generate)
shift
if [ $# -eq 0 ] || [[ "$1" == -* ]]; then
echo -e "\n 🛡️ ${COLOR_RED}The --cert-generate option requires a domain.${CoR}"
echo -e "\n ${COLOR_ORANGE}Usage: $0 --cert-generate domain [email] [--cert-email <email>] [--dns-provider <provider>] [--dns-credentials <json>]${CoR}"
echo -e "\n ${COLOR_YELLOW}Options${CoR}:"
echo -e " • --cert-email <email> : Email for Let's Encrypt (alternative to positional email)"
echo -e " • --dns-provider <provider> : DNS provider for wildcard certificates"
echo -e " • --dns-credentials <json> : DNS credentials in JSON format"
echo -e " • --host-ssl-enable : Enable SSL after certificate generation"
echo -e " • -y : Skip confirmations"
echo -e " ${COLOR_YELLOW}Note${CoR}:\n • If email is not provided, default email ${COLOR_YELLOW}$DEFAULT_EMAIL${CoR} will be used"
echo -e " • For wildcard certificates (${COLOR_YELLOW}*.domain.com${CoR}), DNS challenge is required\n"
echo -e " Examples:"
echo -e " ${COLOR_GREEN}$0 --cert-generate example.com${CoR}"
echo -e " ${COLOR_GREEN}$0 --cert-generate example.com admin@example.com${CoR}"
echo -e " ${COLOR_GREEN}$0 --cert-generate example.com --cert-email admin@example.com${CoR}"
echo -e " ${COLOR_GREEN}$0 --cert-generate \"*.example.com\" --dns-provider cloudflare --dns-credentials '{\"dns_cloudflare_api_token\":\"your_token\"}'${CoR}\n"
exit 1
fi
# Store domain
CERT_DOMAIN="$1"
CERT_DNS_PROVIDER=""
CERT_DNS_CREDENTIALS=""
HOST_SSL_ENABLE=false
shift
# Check and store email
if [ $# -gt 0 ] && [[ "$1" != -* ]]; then
CERT_EMAIL="$1"
shift
else
CERT_EMAIL="$DEFAULT_EMAIL"
fi
# Parse DNS options
while [ $# -gt 0 ] && [[ "$1" == --* ]]; do
case "$1" in
--cert-email)
shift
if [ $# -gt 0 ] && [[ "$1" != --* ]]; then
CERT_EMAIL="$1"
shift
else
echo -e "\n ⛔ ${COLOR_RED}Missing certificate email${CoR}"
exit 1
fi
;;
--dns-provider)
shift
if [ $# -gt 0 ] && [[ "$1" != --* ]]; then
CERT_DNS_PROVIDER="$1"
shift
else
echo -e "\n ⛔ ${COLOR_RED}Missing DNS provider value${CoR}"
exit 1
fi
;;
--dns-credentials)
shift
if [ $# -gt 0 ] && [[ "$1" != --* ]]; then
CERT_DNS_CREDENTIALS="$1"
shift
else
echo -e "\n ⛔ ${COLOR_RED}Missing DNS credentials${CoR}"
exit 1
fi
;;
--host-ssl-enable)
HOST_SSL_ENABLE=true
shift
;;
*)
echo -e "\n ⚠️ ${COLOR_YELLOW}Unknown option: $1${CoR}"
shift
;;
esac
done
# Validate wildcard certificate requirements
if [[ "$CERT_DOMAIN" == \** ]]; then
if [ -z "$CERT_DNS_PROVIDER" ] || [ -z "$CERT_DNS_CREDENTIALS" ]; then
echo -e "\n ⛔ ${COLOR_RED}Wildcard certificates require DNS challenge. Please provide --dns-provider and --dns-credentials.${CoR}"
echo -e " Example: ${COLOR_GREEN}$0 --cert-generate \"*.example.com\" --dns-provider cloudflare --dns-credentials '{\"dns_cloudflare_api_token\":\"your_token\"}'${CoR}\n"
exit 1
fi
fi
# Set flag after validating all arguments
CERT_GENERATE=true
;;
--cert-delete)
shift
if [ $# -eq 0 ] || [[ "$1" == -* ]]; then
echo -e "\n ⛔ ${COLOR_RED}The --cert-delete option requires a certificate ID.${CoR}"
echo -e "\n ${COLOR_CYAN}Usage:${CoR}"
echo -e " ${COLOR_ORANGE}$0 --cert-delete <cert_id> [-y] [--purge]${CoR}"
echo -e "\n ${COLOR_CYAN}Examples:${CoR}"
echo -e " ${COLOR_GREEN}$0 --cert-delete 240${CoR}"
echo -e " ${COLOR_GREEN}$0 --cert-delete 240 -y${CoR} ${COLOR_GREY}# Skip confirmation${CoR}"
echo -e " ${COLOR_GREEN}$0 --cert-delete 240 --purge${CoR} ${COLOR_GREY}# Also remove on-disk cert files (requires NGINX_PATH_DOCKER)${CoR}"
echo -e "\n ${COLOR_YELLOW}💡 Tips:${CoR}"
echo -e " • Use ${COLOR_GREEN}--cert-list${CoR} to see all certificates and their IDs"
echo -e " • ${COLOR_GREY}Without --purge the API does a soft-delete only (the cert leaves the UI but its files remain)${CoR}\n"
exit 1
fi
CERT_DELETE=true
CERT_ID="$1"
shift
;;
--cert-show)
shift
search_term="${1:-}"
CERT_SHOW=true
if [ -n "$search_term" ]; then
shift
fi
;;
--cert-list | --cert-show-all)
LIST_CERT_ALL=true
shift
;;
--cert-download)
shift
if [ $# -eq 0 ] || [[ "$1" == -* ]]; then
echo -e "\n ⛔ ${COLOR_RED}The --cert-download option requires a certificate ID.${CoR}"
echo -e "\n ${COLOR_CYAN}Usage:${CoR}"
echo -e " ${COLOR_ORANGE}$0 --cert-download <cert_id> [output_dir] [cert_name]${CoR}"
echo -e "\n ${COLOR_CYAN}Examples:${CoR}"
echo -e " ${COLOR_GREEN}$0 --cert-download 240${CoR}"
echo -e " ${COLOR_GREEN}$0 --cert-download 240 ./certs${CoR}"
echo -e " ${COLOR_GREEN}$0 --cert-download 240 ./certs mydomain${CoR}"
echo -e "\n ${COLOR_YELLOW}💡 Tips:${CoR}"
echo -e " • Use ${COLOR_GREEN}--cert-list${CoR} to see all certificates and their IDs"
echo -e " • Supports both new API (JSON) and legacy API (ZIP) with automatic fallback"
echo -e " • Creates standardized certificate files (.crt, .key, .chain.crt) and ZIP archive\n"
exit 1
fi
CERT_DOWNLOAD=true
CERT_ID="$1"
shift
# Optional output directory
if [ $# -gt 0 ] && [[ "$1" != -* ]]; then
CERT_OUTPUT_DIR="$1"
shift
fi
# Optional certificate name
if [ $# -gt 0 ] && [[ "$1" != -* ]]; then
CERT_NAME="$1"
shift
fi
;;
--access-list)
ACCESS_LIST=true
shift
;;
--access-list-show)
ACCESS_LIST_SHOW=true
shift
# Check if access list ID is provided
if [ $# -eq 0 ] || [[ "$1" == -* ]]; then
echo -e "\n⛔ ${COLOR_RED}Error: The access list ID is required.${CoR}"
echo -e "\n ${COLOR_CYAN}Usage:${CoR}"
echo -e " ${COLOR_ORANGE}$0 --access-list-show <id>${CoR}"
echo -e "\n ${COLOR_CYAN}Examples:${CoR}"
echo -e " ${COLOR_GREEN}$0 --access-list-show 5${CoR}"
echo -e " ${COLOR_GREEN}$0 --access-list-show 123${CoR}"
echo -e "\n ${COLOR_YELLOW}💡 Tip:${CoR}"
echo -e " • Use ${COLOR_GREEN}--access-list${CoR} to see all access lists and their IDs\n"
exit 1
fi
ACCESS_LIST_ID="$1"
shift
;;
--access-list-create)
shift
# Stash remaining args and dispatch at the bottom (like --access-list-update),
# instead of running mid-parse which left the bottom dispatch unreachable.
ACCESS_LIST_CREATE=true
ACCESS_LIST_CREATE_ARGS=()
while [ $# -gt 0 ]; do
ACCESS_LIST_CREATE_ARGS+=("$1")
shift
done
;;
--access-list-update)
shift
if [ $# -eq 0 ] || [[ "$1" == -* ]]; then
echo -e "\n ⛔ ${COLOR_RED}The --access-list-update option requires an access list ID.${CoR}"
echo -e " ${COLOR_CYAN}Usage:${CoR}"
echo -e " ${COLOR_ORANGE}$0 --access-list-update <access_list_id> [options]${CoR}"
echo -e " ${COLOR_CYAN}Examples:${CoR}"
echo -e " ${COLOR_GREEN}$0 --access-list-update 123 --name \"new_name\"${CoR}"
echo -e " ${COLOR_GREEN}$0 --access-list-update 123 --name \"new_name\" --satisfy any${CoR}"
echo -e " ${COLOR_YELLOW}💡 Tip: Use --access-list to see all available access lists${CoR}"
exit 1
fi
if [[ "$1" =~ ^[0-9]+$ ]]; then
ACCESS_LIST_ID="$1"
ACCESS_LIST_UPDATE=true
shift
else
echo -e "\n ⛔ ${COLOR_RED}INVALID: Invalid access list ID '$1' - must be a number${CoR}"
echo -e " ${COLOR_CYAN}Examples:${CoR}"
echo -e " ${COLOR_GREEN}$0 --access-list-update 123 --name \"new_name\"${CoR}"
echo -e " ${COLOR_GREEN}$0 --access-list-update 123 --name \"new_name\" --satisfy any${CoR}"
echo -e " ${COLOR_YELLOW}💡 Tip: Use --access-list to see all available access lists${CoR}"
exit 1
fi
# Process remaining arguments for access list update
# Store them to pass to the function later
ACCESS_LIST_UPDATE_ARGS=()
while [ $# -gt 0 ]; do
ACCESS_LIST_UPDATE_ARGS+=("$1")
shift
done
;;
--access-list-delete)
shift
if [ $# -eq 0 ] || [[ "$1" == -* ]]; then
echo -e "\n ⛔ ${COLOR_RED}The --access-list-delete option requires an access list ID.${CoR}"
echo -e " ${COLOR_CYAN}Usage:${CoR}"
echo -e " ${COLOR_ORANGE}$0 --access-list-delete <access_list_id> [-y]${CoR}"
echo -e " ${COLOR_CYAN}Examples:${CoR}"
echo -e " ${COLOR_GREEN}$0 --access-list-delete 42${CoR}"
echo -e " ${COLOR_GREEN}$0 --access-list-delete 42 -y${CoR} ${COLOR_GREY}# Skip confirmation${CoR}"
echo -e " ${COLOR_YELLOW}💡 Tip: Use --access-list to see all available access lists${CoR}"
exit 1
fi
if [[ "$1" =~ ^[0-9]+$ ]]; then
ACCESS_LIST_ID="$1"
ACCESS_LIST_DELETE=true
shift
else
echo -e "\n ⛔ ${COLOR_RED}INVALID: Invalid access list ID '$1' - must be a number${CoR}"
echo -e " ${COLOR_CYAN}Examples:${CoR}"
echo -e " ${COLOR_GREEN}$0 --access-list-delete 42${CoR}"
echo -e " ${COLOR_GREEN}$0 --access-list-delete 42 -y${CoR} ${COLOR_GREY}# Skip confirmation${CoR}"
echo -e " ${COLOR_YELLOW}💡 Tip: Use --access-list to see all available access lists${CoR}"
exit 1
fi
;;
--redirect-host-list)
REDIRECT_HOST_LIST=true
shift
;;
--redirect-host-create)
shift
if [ $# -eq 0 ] || [[ "$1" == -* ]]; then
echo -e "\n ⛔ ${COLOR_RED}INVALID: --redirect-host-create requires a domain name.${CoR}"
echo -e " Usage : ${COLOR_ORANGE}$0 --redirect-host-create domain.com --forward-domain target.com [options]${CoR}"
echo -e " Options:"
echo -e " --forward-domain domain Target domain (${COLOR_RED}required${CoR})"
echo -e " --forward-scheme http|https (default: http)"
echo -e " --http-code 301|302|303|307|308 (default: 301)"
echo -e " --preserve-path true|false (default: false)"
exit 1
fi
DOMAIN_NAMES="$1"
shift
while [[ $# -gt 0 ]]; do
case "$1" in
--forward-domain)
if [[ -n "$2" && "$2" != -* ]]; then
FORWARD_DOMAIN="$2"
shift 2
else
echo -e "\n ⛔ ${COLOR_RED}--forward-domain requires a value${CoR}"
exit 1
fi
;;
--forward-scheme | -f)
if [[ -n "$2" && "$2" =~ ^(http|https)$ ]]; then
FORWARD_SCHEME="$2"
shift 2
else
echo -e "\n ⛔ ${COLOR_RED}--forward-scheme must be 'http' or 'https'${CoR}"
exit 1
fi
;;
--http-code)
if [[ -n "$2" && "$2" =~ ^(301|302|303|307|308)$ ]]; then
FORWARD_HTTP_CODE="$2"
shift 2
else
echo -e "\n ⛔ ${COLOR_RED}--http-code must be 301, 302, 303, 307, or 308${CoR}"
exit 1
fi
;;
--preserve-path)
if [[ -n "$2" && "$2" =~ ^(true|false)$ ]]; then
PRESERVE_PATH="$2"
shift 2
else
echo -e "\n ⛔ ${COLOR_RED}--preserve-path must be 'true' or 'false'${CoR}"
exit 1
fi
;;
-b | --block-exploits)
if [[ -n "$2" && "$2" =~ ^(true|false)$ ]]; then
BLOCK_EXPLOITS="$2"
shift 2
else
echo -e "\n ⛔ ${COLOR_RED}--block-exploits must be 'true' or 'false'${CoR}"
exit 1
fi
;;
-a | --advanced-config)
if [[ -n "$2" && "$2" != -* ]]; then
ADVANCED_CONFIG="$2"
shift 2
else
echo -e "\n ⛔ ${COLOR_RED}--advanced-config requires a value${CoR}"
exit 1
fi
;;
-y)
AUTO_YES=true
shift
;;
*)
if [[ "$1" == -* ]]; then echo -e "\n ⚠️ ${COLOR_YELLOW}WARNING: Unknown option ignored -> $1${CoR}"; fi
shift
;;
esac
done
if [ -z "$FORWARD_DOMAIN" ]; then
echo -e "\n ⛔ ${COLOR_RED}INVALID: --forward-domain is required${CoR}"
echo -e " Example: ${COLOR_GREEN}$0 --redirect-host-create example.com --forward-domain target.com${CoR}"
exit 1
fi
REDIRECT_HOST_CREATE=true
;;
--redirect-host-delete)
shift
if [ $# -eq 0 ] || [[ "$1" == -* ]]; then
echo -e "\n ⛔ ${COLOR_RED}INVALID: --redirect-host-delete requires a host 🆔.${CoR}"
echo -e " Usage : ${COLOR_ORANGE}$0 --redirect-host-delete <id> [-y]${CoR}"
echo -e " Example: ${COLOR_GREEN}$0 --redirect-host-delete 5${CoR}"
exit 1
fi
if [[ "$1" =~ ^[0-9]+$ ]]; then
REDIRECT_HOST_ID="$1"
REDIRECT_HOST_DELETE=true
shift
else
echo -e "\n ⛔ ${COLOR_RED}INVALID: ID must be a number.${CoR}"
exit 1
fi
;;
--redirect-host-enable)
shift
if [ $# -eq 0 ] || [[ "$1" == -* ]]; then
echo -e "\n ⛔ ${COLOR_RED}INVALID: --redirect-host-enable requires a host 🆔.${CoR}"
echo -e " Usage : ${COLOR_ORANGE}$0 --redirect-host-enable <id>${CoR}"
exit 1
fi
REDIRECT_HOST_ID="$1"
REDIRECT_HOST_ENABLE=true
shift
;;
--redirect-host-disable)
shift
if [ $# -eq 0 ] || [[ "$1" == -* ]]; then
echo -e "\n ⛔ ${COLOR_RED}INVALID: --redirect-host-disable requires a host 🆔.${CoR}"
echo -e " Usage : ${COLOR_ORANGE}$0 --redirect-host-disable <id>${CoR}"
exit 1
fi
REDIRECT_HOST_ID="$1"
REDIRECT_HOST_DISABLE=true
shift
;;
--stream-host-list)
STREAM_HOST_LIST=true
shift
;;
--stream-host-create)
shift
if [ $# -eq 0 ] || [[ "$1" == -* ]] || ! [[ "$1" =~ ^[0-9]+$ ]]; then
echo -e "\n ⛔ ${COLOR_RED}INVALID: --stream-host-create requires an incoming port (number).${CoR}"
echo -e " Usage : ${COLOR_ORANGE}$0 --stream-host-create <incoming_port> --forward-host <host> --forward-port <port> [options]${CoR}"
echo -e " Options:"
echo -e " --forward-host host|ip Target host (${COLOR_RED}required${CoR})"
echo -e " --forward-port port Target port (${COLOR_RED}required${CoR})"
echo -e " --tcp Enable TCP forwarding"
echo -e " --udp Enable UDP forwarding"
echo -e " --cert-id id Certificate ID (default: 0)"
exit 1
fi
INCOMING_PORT="$1"
shift
while [[ $# -gt 0 ]]; do
case "$1" in
--forward-host)
if [[ -n "$2" && "$2" != -* ]]; then
FORWARD_HOST="$2"
shift 2
else
echo -e "\n ⛔ ${COLOR_RED}--forward-host requires a value${CoR}"
exit 1
fi
;;
--forward-port)
if [[ -n "$2" && "$2" =~ ^[0-9]+$ ]]; then
FORWARD_PORT="$2"
shift 2
else
echo -e "\n ⛔ ${COLOR_RED}--forward-port must be a number${CoR}"
exit 1
fi
;;
--tcp)
TCP_FORWARDING=true
shift
;;
--udp)
UDP_FORWARDING=true
shift
;;
--cert-id)
if [[ -n "$2" && "$2" =~ ^[0-9]+$ ]]; then
CERT_ID="$2"
shift 2
else
echo -e "\n ⛔ ${COLOR_RED}--cert-id must be a number${CoR}"
exit 1
fi
;;
-y)
AUTO_YES=true
shift
;;
*)
if [[ "$1" == -* ]]; then echo -e "\n ⚠️ ${COLOR_YELLOW}WARNING: Unknown option ignored -> $1${CoR}"; fi
shift
;;
esac
done
if [ -z "$FORWARD_HOST" ] || [ -z "$FORWARD_PORT" ]; then
echo -e "\n ⛔ ${COLOR_RED}INVALID: --forward-host and --forward-port are required${CoR}"
echo -e " Example: ${COLOR_GREEN}$0 --stream-host-create 53 --forward-host pihole --forward-port 53 --tcp --udp${CoR}"
exit 1
fi
STREAM_HOST_CREATE=true
;;
--stream-host-delete)
shift
if [ $# -eq 0 ] || [[ "$1" == -* ]]; then
echo -e "\n ⛔ ${COLOR_RED}INVALID: --stream-host-delete requires a host 🆔.${CoR}"
echo -e " Usage : ${COLOR_ORANGE}$0 --stream-host-delete <id> [-y]${CoR}"
echo -e " Example: ${COLOR_GREEN}$0 --stream-host-delete 5${CoR}"
exit 1
fi
if [[ "$1" =~ ^[0-9]+$ ]]; then
STREAM_HOST_ID="$1"
STREAM_HOST_DELETE=true
shift
else
echo -e "\n ⛔ ${COLOR_RED}INVALID: ID must be a number.${CoR}"
exit 1
fi
;;
--stream-host-enable)
shift
if [ $# -eq 0 ] || [[ "$1" == -* ]]; then
echo -e "\n ⛔ ${COLOR_RED}INVALID: --stream-host-enable requires a host 🆔.${CoR}"
echo -e " Usage : ${COLOR_ORANGE}$0 --stream-host-enable <id>${CoR}"
exit 1
fi
STREAM_HOST_ID="$1"
STREAM_HOST_ENABLE=true
shift
;;
--stream-host-disable)
shift
if [ $# -eq 0 ] || [[ "$1" == -* ]]; then
echo -e "\n ⛔ ${COLOR_RED}INVALID: --stream-host-disable requires a host 🆔.${CoR}"
echo -e " Usage : ${COLOR_ORANGE}$0 --stream-host-disable <id>${CoR}"
exit 1
fi
STREAM_HOST_ID="$1"
STREAM_HOST_DISABLE=true
shift
;;
--dead-host-list)
DEAD_HOST_LIST=true
shift
;;
--dead-host-create)
shift
if [ $# -eq 0 ] || [[ "$1" == -* ]]; then
echo -e "\n ⛔ ${COLOR_RED}INVALID: --dead-host-create requires a domain name.${CoR}"
echo -e " Usage : ${COLOR_ORANGE}$0 --dead-host-create domain.com [options]${CoR}"
echo -e " Options:"
echo -e " --cert-id id Certificate ID (default: 0)"
echo -e " --ssl-forced true|false (default: false)"
echo -e " --http2 true|false (default: false)"
echo -e " --hsts true|false (default: false)"
echo -e " --hsts-subdomains true|false (default: false)"
echo -e " --advanced-config string Custom nginx config"
exit 1
fi
DOMAIN_NAMES="$1"
shift
while [[ $# -gt 0 ]]; do
case "$1" in
--cert-id)
if [[ -n "$2" && "$2" =~ ^[0-9]+$ ]]; then
CERT_ID="$2"
shift 2
else
echo -e "\n ⛔ ${COLOR_RED}--cert-id must be a number${CoR}"
exit 1
fi
;;
--ssl-forced)
if [[ -n "$2" && "$2" =~ ^(true|false)$ ]]; then
SSL_FORCED="$2"
shift 2
else
echo -e "\n ⛔ ${COLOR_RED}--ssl-forced must be 'true' or 'false'${CoR}"
exit 1
fi
;;
--http2)
if [[ -n "$2" && "$2" =~ ^(true|false)$ ]]; then
HTTP2_SUPPORT="$2"
shift 2
else
echo -e "\n ⛔ ${COLOR_RED}--http2 must be 'true' or 'false'${CoR}"
exit 1
fi
;;
--hsts)
if [[ -n "$2" && "$2" =~ ^(true|false)$ ]]; then
HSTS_ENABLED="$2"
shift 2
else
echo -e "\n ⛔ ${COLOR_RED}--hsts must be 'true' or 'false'${CoR}"
exit 1
fi
;;
--hsts-subdomains)
if [[ -n "$2" && "$2" =~ ^(true|false)$ ]]; then
HSTS_SUBDOMAINS="$2"
shift 2
else
echo -e "\n ⛔ ${COLOR_RED}--hsts-subdomains must be 'true' or 'false'${CoR}"
exit 1
fi
;;
-a | --advanced-config)
if [[ -n "$2" && "$2" != -* ]]; then
ADVANCED_CONFIG="$2"
shift 2
else
echo -e "\n ⛔ ${COLOR_RED}--advanced-config requires a value${CoR}"
exit 1
fi
;;
-y)
AUTO_YES=true
shift
;;
*)
if [[ "$1" == -* ]]; then echo -e "\n ⚠️ ${COLOR_YELLOW}WARNING: Unknown option ignored -> $1${CoR}"; fi
shift
;;
esac
done
DEAD_HOST_CREATE=true
;;
--dead-host-delete)
shift
if [ $# -eq 0 ] || [[ "$1" == -* ]]; then
echo -e "\n ⛔ ${COLOR_RED}INVALID: --dead-host-delete requires a host 🆔.${CoR}"
echo -e " Usage : ${COLOR_ORANGE}$0 --dead-host-delete <id> [-y]${CoR}"
echo -e " Example: ${COLOR_GREEN}$0 --dead-host-delete 5${CoR}"
exit 1
fi
if [[ "$1" =~ ^[0-9]+$ ]]; then
DEAD_HOST_ID="$1"
DEAD_HOST_DELETE=true
shift
else
echo -e "\n ⛔ ${COLOR_RED}INVALID: ID must be a number.${CoR}"
exit 1
fi
;;
--dead-host-enable)
shift
if [ $# -eq 0 ] || [[ "$1" == -* ]]; then
echo -e "\n ⛔ ${COLOR_RED}INVALID: --dead-host-enable requires a host 🆔.${CoR}"
echo -e " Usage : ${COLOR_ORANGE}$0 --dead-host-enable <id>${CoR}"
exit 1
fi
DEAD_HOST_ID="$1"
DEAD_HOST_ENABLE=true
shift
;;
--dead-host-disable)
shift
if [ $# -eq 0 ] || [[ "$1" == -* ]]; then
echo -e "\n ⛔ ${COLOR_RED}INVALID: --dead-host-disable requires a host 🆔.${CoR}"
echo -e " Usage : ${COLOR_ORANGE}$0 --dead-host-disable <id>${CoR}"
exit 1
fi
DEAD_HOST_ID="$1"
DEAD_HOST_DISABLE=true
shift
;;
*)
echo -e "\n ${COLOR_RED}⛔ Unknown option:${CoR} $1"
echo -e " ${COLOR_GREY}Use --help to see available commands.${CoR}\n"
exit 1
;;
esac
#shift
done
##############################################################
# logic
##############################################################
#echo "Debug after case: ACCESS_LIST_DELETE=$ACCESS_LIST_DELETE ACCESS_LIST_ID=$ACCESS_LIST_ID AUTO_YES=$AUTO_YES"
if [ "$LIST_JSON_COMMANDS" = true ]; then
list_json_commands
elif [ "$SHOW_HELP" = true ]; then
show_help
elif [ "$SHOW_DEFAULT" = true ]; then
show_default
elif [ "$EXAMPLES" = true ]; then
examples_cli
elif [ "$CHECK_TOKEN" = true ]; then
check_token true
elif [ "$CHECK_UPDATE" = true ]; then
check_update
elif [ "$INFO" = true ]; then
display_info
# Actions users
elif [ "$USER_CREATE" = true ]; then
user_create "$USERNAME" "$PASSWORD" "$EMAIL"
elif [ "$USER_DELETE" = true ]; then
user_delete "$USER_ID"
elif [ "$USER_LIST" = true ]; then
user_list
elif [ "$CERT_DELETE" = true ]; then
cert_delete "$CERT_ID"
elif [ "$CERT_SHOW" = true ]; then
cert_show "$search_term"
elif [ "$LIST_CERT_ALL" = true ]; then
list_cert_all
elif [ "$CERT_DOWNLOAD" = true ]; then
cert_download "$CERT_ID" "${CERT_OUTPUT_DIR:-./certificates}" "${CERT_NAME:-certificate_$CERT_ID}"
elif [ "$ACCESS_LIST" = true ]; then
access_list
elif [ "$ACCESS_LIST_CREATE" = true ]; then
access_list_create "${ACCESS_LIST_CREATE_ARGS[@]}"
elif [ "$ACCESS_LIST_UPDATE" = true ]; then
access_list_update "${ACCESS_LIST_UPDATE_ARGS[@]}"
elif [ "$ACCESS_LIST_DELETE" = true ]; then
access_list_delete
elif [ "$ACCESS_LIST_SHOW" = true ]; then
access_list_show "$ACCESS_LIST_ID"
# Actions hotes
elif [ "$HOST_LIST" = true ]; then
host_list
elif [ "$HOST_LIST_FULL" = true ]; then
host_list_full
elif [ "$HOST_SEARCH" = true ]; then
host_search
elif [ "$HOST_SHOW" = true ]; then
host_show "$HOST_ID"
elif [ "$HOST_CREATE" = true ]; then
# Certificate generation (--cert-generate) is handled inside create_or_update_proxy_host
create_or_update_proxy_host "$DOMAIN_NAMES" "$FORWARD_HOST" "$FORWARD_PORT"
exit 0
# Actions SSL
elif [ "$CERT_GENERATE" = true ] && [ "$HOST_CREATE" != true ]; then # ✅ Ajout de la condition
cert_generate "$CERT_DOMAIN" "$CERT_EMAIL" "$CERT_DNS_PROVIDER" "$CERT_DNS_CREDENTIALS"
if [ "$HOST_SSL_ENABLE" = true ]; then
host_ssl_enable "$HOST_ID"
exit 0
fi
#elif [ "$CERT_GENERATE" = true ]; then
# cert_generate "$CERT_DOMAIN" "$CERT_EMAIL" "$CERT_DNS_PROVIDER" "$CERT_DNS_CREDENTIALS"
# If --host-ssl-enable
# if [ "$HOST_SSL_ENABLE" = true ]; then
# host_ssl_enable "$HOST_ID"
# exit 0
# fi
elif [ "$HOST_DELETE" = true ]; then
host_delete "$HOST_ID"
elif [ "$HOST_ENABLE" = true ]; then
host_enable "$HOST_ID"
elif [ "$HOST_DISABLE" = true ]; then
host_disable "$HOST_ID"
elif [ "$HOST_UPDATE" = true ]; then
host_update "$HOST_ID" "$FIELD" "$VALUE"
exit 0
# Actions ACL
elif [ "$HOST_ACL_ENABLE" = true ]; then
host_acl_enable
elif [ "$HOST_ACL_DISABLE" = true ]; then
host_acl_disable
# Redirection Host Management
elif [ "$REDIRECT_HOST_LIST" = true ]; then
redirect_host_list
elif [ "$REDIRECT_HOST_CREATE" = true ]; then
create_or_update_redirect_host
elif [ "$REDIRECT_HOST_DELETE" = true ]; then
redirect_host_delete "$REDIRECT_HOST_ID"
elif [ "$REDIRECT_HOST_ENABLE" = true ]; then
redirect_host_enable "$REDIRECT_HOST_ID"
elif [ "$REDIRECT_HOST_DISABLE" = true ]; then
redirect_host_disable "$REDIRECT_HOST_ID"
# Stream Host Management
elif [ "$STREAM_HOST_LIST" = true ]; then
stream_host_list
elif [ "$STREAM_HOST_CREATE" = true ]; then
create_or_update_stream_host
elif [ "$STREAM_HOST_DELETE" = true ]; then
stream_host_delete "$STREAM_HOST_ID"
elif [ "$STREAM_HOST_ENABLE" = true ]; then
stream_host_enable "$STREAM_HOST_ID"
elif [ "$STREAM_HOST_DISABLE" = true ]; then
stream_host_disable "$STREAM_HOST_ID"
# Dead Host (404) Management
elif [ "$DEAD_HOST_LIST" = true ]; then
dead_host_list
elif [ "$DEAD_HOST_CREATE" = true ]; then
create_or_update_dead_host
elif [ "$DEAD_HOST_DELETE" = true ]; then
dead_host_delete "$DEAD_HOST_ID"
elif [ "$DEAD_HOST_ENABLE" = true ]; then
dead_host_enable "$DEAD_HOST_ID"
elif [ "$DEAD_HOST_DISABLE" = true ]; then
dead_host_disable "$DEAD_HOST_ID"
elif [ "$HOST_SSL_ENABLE" = true ]; then
host_ssl_enable "$HOST_ID" "$CERT_ID"
elif [ "$HOST_SSL_DISABLE" = true ]; then
host_ssl_disable
elif [ "$SSL_RESTORE" = true ]; then
restore_ssl_certificates
# Actions backup/restore
elif [ "$BACKUP" = true ]; then
full_backup
elif [ "$BACKUP_HOST" = true ]; then
backup_host
elif [ "$BACKUP_LIST" = true ]; then
list_backups
elif [ "$CLEAN_HOSTS" = true ]; then
clean-hosts
#reimport_hosts "$@"
#reimport_hosts "$BACKUP_FILE"
#exit $?
#check_validity_of_backup_file "$BACKUP_FILE"
else
display_info
exit 0
#
# echo -e "\n ⛔ ${COLOR_RED}No valid option provided${CoR}"
# echo -e " Use --help to see available commands."
# exit 1
fi