From 244d6b85441ffb766375b29779bc1819a321b70f Mon Sep 17 00:00:00 2001 From: Michael S2pac Date: Thu, 15 Jan 2026 21:26:44 +0300 Subject: [PATCH 1/2] Redesigned project structure: x-ui.sh -> lib --- lib/bbr.sh | 76 ++ lib/common.sh | 93 +++ lib/extras.sh | 142 ++++ lib/firewall.sh | 200 +++++ lib/geo.sh | 69 ++ lib/install.sh | 129 +++ lib/iplimit.sh | 398 +++++++++ lib/service.sh | 287 +++++++ lib/settings.sh | 132 +++ lib/ssl.sh | 628 ++++++++++++++ x-ui.sh | 2084 +---------------------------------------------- 11 files changed, 2186 insertions(+), 2052 deletions(-) create mode 100644 lib/bbr.sh create mode 100644 lib/common.sh create mode 100644 lib/extras.sh create mode 100644 lib/firewall.sh create mode 100644 lib/geo.sh create mode 100644 lib/install.sh create mode 100644 lib/iplimit.sh create mode 100644 lib/service.sh create mode 100644 lib/settings.sh create mode 100644 lib/ssl.sh diff --git a/lib/bbr.sh b/lib/bbr.sh new file mode 100644 index 00000000..d9ee9236 --- /dev/null +++ b/lib/bbr.sh @@ -0,0 +1,76 @@ +#!/bin/bash +# lib/bbr.sh - BBR TCP congestion control management + +# Include guard +[[ -n "${__X_UI_BBR_INCLUDED:-}" ]] && return 0 +__X_UI_BBR_INCLUDED=1 + +# Source dependencies +source "${LIB_DIR}/common.sh" + +bbr_menu() { + echo -e "${green}\t1.${plain} Enable BBR" + echo -e "${green}\t2.${plain} Disable BBR" + echo -e "${green}\t0.${plain} Back to Main Menu" + read -rp "Choose an option: " choice + case "$choice" in + 0) + show_menu + ;; + 1) + enable_bbr + bbr_menu + ;; + 2) + disable_bbr + bbr_menu + ;; + *) + echo -e "${red}Invalid option. Please select a valid number.${plain}\n" + bbr_menu + ;; + esac +} + +disable_bbr() { + + if ! grep -q "net.core.default_qdisc=fq" /etc/sysctl.conf || ! grep -q "net.ipv4.tcp_congestion_control=bbr" /etc/sysctl.conf; then + echo -e "${yellow}BBR is not currently enabled.${plain}" + before_show_menu + fi + + # Replace BBR with CUBIC configurations + sed -i 's/net.core.default_qdisc=fq/net.core.default_qdisc=pfifo_fast/' /etc/sysctl.conf + sed -i 's/net.ipv4.tcp_congestion_control=bbr/net.ipv4.tcp_congestion_control=cubic/' /etc/sysctl.conf + + # Apply changes + sysctl -p + + # Verify that BBR is replaced with CUBIC + if [[ $(sysctl net.ipv4.tcp_congestion_control | awk '{print $3}') == "cubic" ]]; then + echo -e "${green}BBR has been replaced with CUBIC successfully.${plain}" + else + echo -e "${red}Failed to replace BBR with CUBIC. Please check your system configuration.${plain}" + fi +} + +enable_bbr() { + if grep -q "net.core.default_qdisc=fq" /etc/sysctl.conf && grep -q "net.ipv4.tcp_congestion_control=bbr" /etc/sysctl.conf; then + echo -e "${green}BBR is already enabled!${plain}" + before_show_menu + fi + + # Enable BBR + echo "net.core.default_qdisc=fq" | tee -a /etc/sysctl.conf + echo "net.ipv4.tcp_congestion_control=bbr" | tee -a /etc/sysctl.conf + + # Apply changes + sysctl -p + + # Verify that BBR is enabled + if [[ $(sysctl net.ipv4.tcp_congestion_control | awk '{print $3}') == "bbr" ]]; then + echo -e "${green}BBR has been enabled successfully.${plain}" + else + echo -e "${red}Failed to enable BBR. Please check your system configuration.${plain}" + fi +} diff --git a/lib/common.sh b/lib/common.sh new file mode 100644 index 00000000..ca1cc80d --- /dev/null +++ b/lib/common.sh @@ -0,0 +1,93 @@ +#!/bin/bash +# lib/common.sh - Base library with colors, logging, helpers, and global variables + +# Include guard +[[ -n "${__X_UI_COMMON_INCLUDED:-}" ]] && return 0 +__X_UI_COMMON_INCLUDED=1 + +# Colors +red='\033[0;31m' +green='\033[0;32m' +blue='\033[0;34m' +yellow='\033[0;33m' +plain='\033[0m' + +# Logging functions +LOGD() { + echo -e "${yellow}[DEG] $* ${plain}" +} + +LOGE() { + echo -e "${red}[ERR] $* ${plain}" +} + +LOGI() { + echo -e "${green}[INF] $* ${plain}" +} + +# Simple helpers for domain/IP validation +is_ipv4() { + [[ "$1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] && return 0 || return 1 +} + +is_ipv6() { + [[ "$1" =~ : ]] && return 0 || return 1 +} + +is_ip() { + is_ipv4 "$1" || is_ipv6 "$1" +} + +is_domain() { + [[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+[A-Za-z]{2,}$ ]] && return 0 || return 1 +} + +# Generate random string +gen_random_string() { + local length="$1" + local random_string=$(LC_ALL=C tr -dc 'a-zA-Z0-9' &2 + exit 1 +fi + +os_version="" +os_version=$(grep "^VERSION_ID" /etc/os-release | cut -d '=' -f2 | tr -d '"' | tr -d '.') + +# Declare global variables +xui_folder="${XUI_MAIN_FOLDER:=/usr/local/x-ui}" +xui_service="${XUI_SERVICE:=/etc/systemd/system}" +log_folder="${XUI_LOG_FOLDER:=/var/log/x-ui}" +mkdir -p "${log_folder}" +iplimit_log_path="${log_folder}/3xipl.log" +iplimit_banned_log_path="${log_folder}/3xipl-banned.log" diff --git a/lib/extras.sh b/lib/extras.sh new file mode 100644 index 00000000..b246f6aa --- /dev/null +++ b/lib/extras.sh @@ -0,0 +1,142 @@ +#!/bin/bash +# lib/extras.sh - Extra utilities (speedtest, SSH port forwarding) + +# Include guard +[[ -n "${__X_UI_EXTRAS_INCLUDED:-}" ]] && return 0 +__X_UI_EXTRAS_INCLUDED=1 + +# Source dependencies +source "${LIB_DIR}/common.sh" +source "${LIB_DIR}/service.sh" + +run_speedtest() { + # Check if Speedtest is already installed + if ! command -v speedtest &>/dev/null; then + # If not installed, determine installation method + if command -v snap &>/dev/null; then + # Use snap to install Speedtest + echo "Installing Speedtest using snap..." + snap install speedtest + else + # Fallback to using package managers + local pkg_manager="" + local speedtest_install_script="" + + if command -v dnf &>/dev/null; then + pkg_manager="dnf" + speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh" + elif command -v yum &>/dev/null; then + pkg_manager="yum" + speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh" + elif command -v apt-get &>/dev/null; then + pkg_manager="apt-get" + speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh" + elif command -v apt &>/dev/null; then + pkg_manager="apt" + speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh" + fi + + if [[ -z $pkg_manager ]]; then + echo "Error: Package manager not found. You may need to install Speedtest manually." + return 1 + else + echo "Installing Speedtest using $pkg_manager..." + curl -s $speedtest_install_script | bash + $pkg_manager install -y speedtest + fi + fi + fi + + speedtest +} + +SSH_port_forwarding() { + local URL_lists=( + "https://api4.ipify.org" + "https://ipv4.icanhazip.com" + "https://v4.api.ipinfo.io/ip" + "https://ipv4.myexternalip.com/raw" + "https://4.ident.me" + "https://check-host.net/ip" + ) + local server_ip="" + for ip_address in "${URL_lists[@]}"; do + server_ip=$(curl -s --max-time 3 "${ip_address}" 2>/dev/null | tr -d '[:space:]') + if [[ -n "${server_ip}" ]]; then + break + fi + done + local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}') + local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') + local existing_listenIP=$(${xui_folder}/x-ui setting -getListen true | grep -Eo 'listenIP: .+' | awk '{print $2}') + local existing_cert=$(${xui_folder}/x-ui setting -getCert true | grep -Eo 'cert: .+' | awk '{print $2}') + local existing_key=$(${xui_folder}/x-ui setting -getCert true | grep -Eo 'key: .+' | awk '{print $2}') + + local config_listenIP="" + local listen_choice="" + + if [[ -n "$existing_cert" && -n "$existing_key" ]]; then + echo -e "${green}Panel is secure with SSL.${plain}" + before_show_menu + fi + if [[ -z "$existing_cert" && -z "$existing_key" && (-z "$existing_listenIP" || "$existing_listenIP" == "0.0.0.0") ]]; then + echo -e "\n${red}Warning: No Cert and Key found! The panel is not secure.${plain}" + echo "Please obtain a certificate or set up SSH port forwarding." + fi + + if [[ -n "$existing_listenIP" && "$existing_listenIP" != "0.0.0.0" && (-z "$existing_cert" && -z "$existing_key") ]]; then + echo -e "\n${green}Current SSH Port Forwarding Configuration:${plain}" + echo -e "Standard SSH command:" + echo -e "${yellow}ssh -L 2222:${existing_listenIP}:${existing_port} root@${server_ip}${plain}" + echo -e "\nIf using SSH key:" + echo -e "${yellow}ssh -i -L 2222:${existing_listenIP}:${existing_port} root@${server_ip}${plain}" + echo -e "\nAfter connecting, access the panel at:" + echo -e "${yellow}http://localhost:2222${existing_webBasePath}${plain}" + fi + + echo -e "\nChoose an option:" + echo -e "${green}1.${plain} Set listen IP" + echo -e "${green}2.${plain} Clear listen IP" + echo -e "${green}0.${plain} Back to Main Menu" + read -rp "Choose an option: " num + + case "$num" in + 1) + if [[ -z "$existing_listenIP" || "$existing_listenIP" == "0.0.0.0" ]]; then + echo -e "\nNo listenIP configured. Choose an option:" + echo -e "1. Use default IP (127.0.0.1)" + echo -e "2. Set a custom IP" + read -rp "Select an option (1 or 2): " listen_choice + + config_listenIP="127.0.0.1" + [[ "$listen_choice" == "2" ]] && read -rp "Enter custom IP to listen on: " config_listenIP + + ${xui_folder}/x-ui setting -listenIP "${config_listenIP}" >/dev/null 2>&1 + echo -e "${green}listen IP has been set to ${config_listenIP}.${plain}" + echo -e "\n${green}SSH Port Forwarding Configuration:${plain}" + echo -e "Standard SSH command:" + echo -e "${yellow}ssh -L 2222:${config_listenIP}:${existing_port} root@${server_ip}${plain}" + echo -e "\nIf using SSH key:" + echo -e "${yellow}ssh -i -L 2222:${config_listenIP}:${existing_port} root@${server_ip}${plain}" + echo -e "\nAfter connecting, access the panel at:" + echo -e "${yellow}http://localhost:2222${existing_webBasePath}${plain}" + restart + else + config_listenIP="${existing_listenIP}" + echo -e "${green}Current listen IP is already set to ${config_listenIP}.${plain}" + fi + ;; + 2) + ${xui_folder}/x-ui setting -listenIP 0.0.0.0 >/dev/null 2>&1 + echo -e "${green}Listen IP has been cleared.${plain}" + restart + ;; + 0) + show_menu + ;; + *) + echo -e "${red}Invalid option. Please select a valid number.${plain}\n" + SSH_port_forwarding + ;; + esac +} diff --git a/lib/firewall.sh b/lib/firewall.sh new file mode 100644 index 00000000..c1994b4b --- /dev/null +++ b/lib/firewall.sh @@ -0,0 +1,200 @@ +#!/bin/bash +# lib/firewall.sh - UFW firewall management + +# Include guard +[[ -n "${__X_UI_FIREWALL_INCLUDED:-}" ]] && return 0 +__X_UI_FIREWALL_INCLUDED=1 + +# Source dependencies +source "${LIB_DIR}/common.sh" + +firewall_menu() { + echo -e "${green}\t1.${plain} ${green}Install${plain} Firewall" + echo -e "${green}\t2.${plain} Port List [numbered]" + echo -e "${green}\t3.${plain} ${green}Open${plain} Ports" + echo -e "${green}\t4.${plain} ${red}Delete${plain} Ports from List" + echo -e "${green}\t5.${plain} ${green}Enable${plain} Firewall" + echo -e "${green}\t6.${plain} ${red}Disable${plain} Firewall" + echo -e "${green}\t7.${plain} Firewall Status" + echo -e "${green}\t0.${plain} Back to Main Menu" + read -rp "Choose an option: " choice + case "$choice" in + 0) + show_menu + ;; + 1) + install_firewall + firewall_menu + ;; + 2) + ufw status numbered + firewall_menu + ;; + 3) + open_ports + firewall_menu + ;; + 4) + delete_ports + firewall_menu + ;; + 5) + ufw enable + firewall_menu + ;; + 6) + ufw disable + firewall_menu + ;; + 7) + ufw status verbose + firewall_menu + ;; + *) + echo -e "${red}Invalid option. Please select a valid number.${plain}\n" + firewall_menu + ;; + esac +} + +install_firewall() { + if ! command -v ufw &>/dev/null; then + echo "ufw firewall is not installed. Installing now..." + apt-get update + apt-get install -y ufw + else + echo "ufw firewall is already installed" + fi + + # Check if the firewall is inactive + if ufw status | grep -q "Status: active"; then + echo "Firewall is already active" + else + echo "Activating firewall..." + # Open the necessary ports + ufw allow ssh + ufw allow http + ufw allow https + ufw allow 2053/tcp #webPort + ufw allow 2096/tcp #subport + + # Enable the firewall + ufw --force enable + fi +} + +open_ports() { + # Prompt the user to enter the ports they want to open + read -rp "Enter the ports you want to open (e.g. 80,443,2053 or range 400-500): " ports + + # Check if the input is valid + if ! [[ $ports =~ ^([0-9]+|[0-9]+-[0-9]+)(,([0-9]+|[0-9]+-[0-9]+))*$ ]]; then + echo "Error: Invalid input. Please enter a comma-separated list of ports or a range of ports (e.g. 80,443,2053 or 400-500)." >&2 + exit 1 + fi + + # Open the specified ports using ufw + IFS=',' read -ra PORT_LIST <<<"$ports" + for port in "${PORT_LIST[@]}"; do + if [[ $port == *-* ]]; then + # Split the range into start and end ports + start_port=$(echo $port | cut -d'-' -f1) + end_port=$(echo $port | cut -d'-' -f2) + # Open the port range + ufw allow $start_port:$end_port/tcp + ufw allow $start_port:$end_port/udp + else + # Open the single port + ufw allow "$port" + fi + done + + # Confirm that the ports are opened + echo "Opened the specified ports:" + for port in "${PORT_LIST[@]}"; do + if [[ $port == *-* ]]; then + start_port=$(echo $port | cut -d'-' -f1) + end_port=$(echo $port | cut -d'-' -f2) + # Check if the port range has been successfully opened + (ufw status | grep -q "$start_port:$end_port") && echo "$start_port-$end_port" + else + # Check if the individual port has been successfully opened + (ufw status | grep -q "$port") && echo "$port" + fi + done +} + +delete_ports() { + # Display current rules with numbers + echo "Current UFW rules:" + ufw status numbered + + # Ask the user how they want to delete rules + echo "Do you want to delete rules by:" + echo "1) Rule numbers" + echo "2) Ports" + read -rp "Enter your choice (1 or 2): " choice + + if [[ $choice -eq 1 ]]; then + # Deleting by rule numbers + read -rp "Enter the rule numbers you want to delete (1, 2, etc.): " rule_numbers + + # Validate the input + if ! [[ $rule_numbers =~ ^([0-9]+)(,[0-9]+)*$ ]]; then + echo "Error: Invalid input. Please enter a comma-separated list of rule numbers." >&2 + exit 1 + fi + + # Split numbers into an array + IFS=',' read -ra RULE_NUMBERS <<<"$rule_numbers" + for rule_number in "${RULE_NUMBERS[@]}"; do + # Delete the rule by number + ufw delete "$rule_number" || echo "Failed to delete rule number $rule_number" + done + + echo "Selected rules have been deleted." + + elif [[ $choice -eq 2 ]]; then + # Deleting by ports + read -rp "Enter the ports you want to delete (e.g. 80,443,2053 or range 400-500): " ports + + # Validate the input + if ! [[ $ports =~ ^([0-9]+|[0-9]+-[0-9]+)(,([0-9]+|[0-9]+-[0-9]+))*$ ]]; then + echo "Error: Invalid input. Please enter a comma-separated list of ports or a range of ports (e.g. 80,443,2053 or 400-500)." >&2 + exit 1 + fi + + # Split ports into an array + IFS=',' read -ra PORT_LIST <<<"$ports" + for port in "${PORT_LIST[@]}"; do + if [[ $port == *-* ]]; then + # Split the port range + start_port=$(echo $port | cut -d'-' -f1) + end_port=$(echo $port | cut -d'-' -f2) + # Delete the port range + ufw delete allow $start_port:$end_port/tcp + ufw delete allow $start_port:$end_port/udp + else + # Delete a single port + ufw delete allow "$port" + fi + done + + # Confirmation of deletion + echo "Deleted the specified ports:" + for port in "${PORT_LIST[@]}"; do + if [[ $port == *-* ]]; then + start_port=$(echo $port | cut -d'-' -f1) + end_port=$(echo $port | cut -d'-' -f2) + # Check if the port range has been deleted + (ufw status | grep -q "$start_port:$end_port") || echo "$start_port-$end_port" + else + # Check if the individual port has been deleted + (ufw status | grep -q "$port") || echo "$port" + fi + done + else + echo "${red}Error:${plain} Invalid choice. Please enter 1 or 2." >&2 + exit 1 + fi +} diff --git a/lib/geo.sh b/lib/geo.sh new file mode 100644 index 00000000..0280d691 --- /dev/null +++ b/lib/geo.sh @@ -0,0 +1,69 @@ +#!/bin/bash +# lib/geo.sh - Geo files management + +# Include guard +[[ -n "${__X_UI_GEO_INCLUDED:-}" ]] && return 0 +__X_UI_GEO_INCLUDED=1 + +# Source dependencies +source "${LIB_DIR}/common.sh" +source "${LIB_DIR}/service.sh" + +update_all_geofiles() { + update_geofiles "main" + update_geofiles "IR" + update_geofiles "RU" +} + +update_geofiles() { + case "${1}" in + "main") dat_files=(geoip geosite); dat_source="Loyalsoldier/v2ray-rules-dat";; + "IR") dat_files=(geoip_IR geosite_IR); dat_source="chocolate4u/Iran-v2ray-rules" ;; + "RU") dat_files=(geoip_RU geosite_RU); dat_source="runetfreedom/russia-v2ray-rules-dat";; + esac + for dat in "${dat_files[@]}"; do + curl -fLRo ${xui_folder}/bin/${dat}.dat -z ${xui_folder}/bin/${dat}.dat \ + https://github.com/${dat_source}/releases/latest/download/${dat%%_}.dat + done +} + +update_geo() { + echo -e "${green}\t1.${plain} Loyalsoldier (geoip.dat, geosite.dat)" + echo -e "${green}\t2.${plain} chocolate4u (geoip_IR.dat, geosite_IR.dat)" + echo -e "${green}\t3.${plain} runetfreedom (geoip_RU.dat, geosite_RU.dat)" + echo -e "${green}\t4.${plain} All" + echo -e "${green}\t0.${plain} Back to Main Menu" + read -rp "Choose an option: " choice + + case "$choice" in + 0) + show_menu + ;; + 1) + update_geofiles "main" + echo -e "${green}Loyalsoldier datasets have been updated successfully!${plain}" + restart + ;; + 2) + update_geofiles "IR" + echo -e "${green}chocolate4u datasets have been updated successfully!${plain}" + restart + ;; + 3) + update_geofiles "RU" + echo -e "${green}runetfreedom datasets have been updated successfully!${plain}" + restart + ;; + 4) + update_all_geofiles + echo -e "${green}All geo files have been updated successfully!${plain}" + restart + ;; + *) + echo -e "${red}Invalid option. Please select a valid number.${plain}\n" + update_geo + ;; + esac + + before_show_menu +} diff --git a/lib/install.sh b/lib/install.sh new file mode 100644 index 00000000..f7e5b2f9 --- /dev/null +++ b/lib/install.sh @@ -0,0 +1,129 @@ +#!/bin/bash +# lib/install.sh - Install, update, and uninstall functions + +# Include guard +[[ -n "${__X_UI_INSTALL_INCLUDED:-}" ]] && return 0 +__X_UI_INSTALL_INCLUDED=1 + +# Source dependencies +source "${LIB_DIR}/common.sh" +source "${LIB_DIR}/service.sh" + +install() { + bash <(curl -Ls https://raw.githubusercontent.com/MHSanaei/3x-ui/main/install.sh) + if [[ $? == 0 ]]; then + if [[ $# == 0 ]]; then + start + else + start 0 + fi + fi +} + +update() { + confirm "This function will update all x-ui components to the latest version, and the data will not be lost. Do you want to continue?" "y" + if [[ $? != 0 ]]; then + LOGE "Cancelled" + if [[ $# == 0 ]]; then + before_show_menu + fi + return 0 + fi + bash <(curl -Ls https://raw.githubusercontent.com/MHSanaei/3x-ui/main/update.sh) + if [[ $? == 0 ]]; then + LOGI "Update is complete, Panel has automatically restarted " + before_show_menu + fi +} + +update_menu() { + echo -e "${yellow}Updating Menu${plain}" + confirm "This function will update the menu to the latest changes." "y" + if [[ $? != 0 ]]; then + LOGE "Cancelled" + if [[ $# == 0 ]]; then + before_show_menu + fi + return 0 + fi + + curl -fLRo /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh + chmod +x ${xui_folder}/x-ui.sh + chmod +x /usr/bin/x-ui + + if [[ $? == 0 ]]; then + echo -e "${green}Update successful. The panel has automatically restarted.${plain}" + exit 0 + else + echo -e "${red}Failed to update the menu.${plain}" + return 1 + fi +} + +legacy_version() { + echo -n "Enter the panel version (like 2.4.0):" + read -r tag_version + + if [ -z "$tag_version" ]; then + echo "Panel version cannot be empty. Exiting." + exit 1 + fi + # Use the entered panel version in the download link + install_command="bash <(curl -Ls "https://raw.githubusercontent.com/mhsanaei/3x-ui/v$tag_version/install.sh") v$tag_version" + + echo "Downloading and installing panel version $tag_version..." + eval $install_command +} + +# Function to handle the deletion of the script file +delete_script() { + rm "$0" # Remove the script file itself + exit 1 +} + +uninstall() { + confirm "Are you sure you want to uninstall the panel? xray will also uninstalled!" "n" + if [[ $? != 0 ]]; then + if [[ $# == 0 ]]; then + show_menu + fi + return 0 + fi + + if [[ $release == "alpine" ]]; then + rc-service x-ui stop + rc-update del x-ui + rm /etc/init.d/x-ui -f + else + systemctl stop x-ui + systemctl disable x-ui + rm ${xui_service}/x-ui.service -f + systemctl daemon-reload + systemctl reset-failed + fi + + rm /etc/x-ui/ -rf + rm ${xui_folder}/ -rf + + echo "" + echo -e "Uninstalled Successfully.\n" + echo "If you need to install this panel again, you can use below command:" + echo -e "${green}bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)${plain}" + echo "" + # Trap the SIGTERM signal + trap delete_script SIGTERM + delete_script +} + +update_shell() { + curl -fLRo /usr/bin/x-ui -z /usr/bin/x-ui https://github.com/MHSanaei/3x-ui/raw/main/x-ui.sh + if [[ $? != 0 ]]; then + echo "" + LOGE "Failed to download script, Please check whether the machine can connect Github" + before_show_menu + else + chmod +x /usr/bin/x-ui + LOGI "Upgrade script succeeded, Please rerun the script" + before_show_menu + fi +} diff --git a/lib/iplimit.sh b/lib/iplimit.sh new file mode 100644 index 00000000..1fdd2d08 --- /dev/null +++ b/lib/iplimit.sh @@ -0,0 +1,398 @@ +#!/bin/bash +# lib/iplimit.sh - Fail2ban IP limiting management + +# Include guard +[[ -n "${__X_UI_IPLIMIT_INCLUDED:-}" ]] && return 0 +__X_UI_IPLIMIT_INCLUDED=1 + +# Source dependencies +source "${LIB_DIR}/common.sh" +source "${LIB_DIR}/service.sh" + +ip_validation() { + ipv6_regex="^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$" + ipv4_regex="^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)$" +} + +iplimit_main() { + echo -e "\n${green}\t1.${plain} Install Fail2ban and configure IP Limit" + echo -e "${green}\t2.${plain} Change Ban Duration" + echo -e "${green}\t3.${plain} Unban Everyone" + echo -e "${green}\t4.${plain} Ban Logs" + echo -e "${green}\t5.${plain} Ban an IP Address" + echo -e "${green}\t6.${plain} Unban an IP Address" + echo -e "${green}\t7.${plain} Real-Time Logs" + echo -e "${green}\t8.${plain} Service Status" + echo -e "${green}\t9.${plain} Service Restart" + echo -e "${green}\t10.${plain} Uninstall Fail2ban and IP Limit" + echo -e "${green}\t0.${plain} Back to Main Menu" + read -rp "Choose an option: " choice + case "$choice" in + 0) + show_menu + ;; + 1) + confirm "Proceed with installation of Fail2ban & IP Limit?" "y" + if [[ $? == 0 ]]; then + install_iplimit + else + iplimit_main + fi + ;; + 2) + read -rp "Please enter new Ban Duration in Minutes [default 30]: " NUM + if [[ $NUM =~ ^[0-9]+$ ]]; then + create_iplimit_jails ${NUM} + if [[ $release == "alpine" ]]; then + rc-service fail2ban restart + else + systemctl restart fail2ban + fi + else + echo -e "${red}${NUM} is not a number! Please, try again.${plain}" + fi + iplimit_main + ;; + 3) + confirm "Proceed with Unbanning everyone from IP Limit jail?" "y" + if [[ $? == 0 ]]; then + fail2ban-client reload --restart --unban 3x-ipl + truncate -s 0 "${iplimit_banned_log_path}" + echo -e "${green}All users Unbanned successfully.${plain}" + iplimit_main + else + echo -e "${yellow}Cancelled.${plain}" + fi + iplimit_main + ;; + 4) + show_banlog + iplimit_main + ;; + 5) + read -rp "Enter the IP address you want to ban: " ban_ip + ip_validation + if [[ $ban_ip =~ $ipv4_regex || $ban_ip =~ $ipv6_regex ]]; then + fail2ban-client set 3x-ipl banip "$ban_ip" + echo -e "${green}IP Address ${ban_ip} has been banned successfully.${plain}" + else + echo -e "${red}Invalid IP address format! Please try again.${plain}" + fi + iplimit_main + ;; + 6) + read -rp "Enter the IP address you want to unban: " unban_ip + ip_validation + if [[ $unban_ip =~ $ipv4_regex || $unban_ip =~ $ipv6_regex ]]; then + fail2ban-client set 3x-ipl unbanip "$unban_ip" + echo -e "${green}IP Address ${unban_ip} has been unbanned successfully.${plain}" + else + echo -e "${red}Invalid IP address format! Please try again.${plain}" + fi + iplimit_main + ;; + 7) + tail -f /var/log/fail2ban.log + iplimit_main + ;; + 8) + service fail2ban status + iplimit_main + ;; + 9) + if [[ $release == "alpine" ]]; then + rc-service fail2ban restart + else + systemctl restart fail2ban + fi + iplimit_main + ;; + 10) + remove_iplimit + iplimit_main + ;; + *) + echo -e "${red}Invalid option. Please select a valid number.${plain}\n" + iplimit_main + ;; + esac +} + +install_iplimit() { + if ! command -v fail2ban-client &>/dev/null; then + echo -e "${green}Fail2ban is not installed. Installing now...!${plain}\n" + + # Check the OS and install necessary packages + case "${release}" in + ubuntu) + apt-get update + if [[ "${os_version}" -ge 24 ]]; then + apt-get install python3-pip -y + python3 -m pip install pyasynchat --break-system-packages + fi + apt-get install fail2ban -y + ;; + debian) + apt-get update + if [ "$os_version" -ge 12 ]; then + apt-get install -y python3-systemd + fi + apt-get install -y fail2ban + ;; + armbian) + apt-get update && apt-get install fail2ban -y + ;; + fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) + dnf -y update && dnf -y install fail2ban + ;; + centos) + if [[ "${VERSION_ID}" =~ ^7 ]]; then + yum update -y && yum install epel-release -y + yum -y install fail2ban + else + dnf -y update && dnf -y install fail2ban + fi + ;; + arch | manjaro | parch) + pacman -Syu --noconfirm fail2ban + ;; + alpine) + apk add fail2ban + ;; + *) + echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n" + exit 1 + ;; + esac + + if ! command -v fail2ban-client &>/dev/null; then + echo -e "${red}Fail2ban installation failed.${plain}\n" + exit 1 + fi + + echo -e "${green}Fail2ban installed successfully!${plain}\n" + else + echo -e "${yellow}Fail2ban is already installed.${plain}\n" + fi + + echo -e "${green}Configuring IP Limit...${plain}\n" + + # make sure there's no conflict for jail files + iplimit_remove_conflicts + + # Check if log file exists + if ! test -f "${iplimit_banned_log_path}"; then + touch ${iplimit_banned_log_path} + fi + + # Check if service log file exists so fail2ban won't return error + if ! test -f "${iplimit_log_path}"; then + touch ${iplimit_log_path} + fi + + # Create the iplimit jail files + # we didn't pass the bantime here to use the default value + create_iplimit_jails + + # Launching fail2ban + if [[ $release == "alpine" ]]; then + if [[ $(rc-service fail2ban status | grep -F 'status: started' -c) == 0 ]]; then + rc-service fail2ban start + else + rc-service fail2ban restart + fi + rc-update add fail2ban + else + if ! systemctl is-active --quiet fail2ban; then + systemctl start fail2ban + else + systemctl restart fail2ban + fi + systemctl enable fail2ban + fi + + echo -e "${green}IP Limit installed and configured successfully!${plain}\n" + before_show_menu +} + +remove_iplimit() { + echo -e "${green}\t1.${plain} Only remove IP Limit configurations" + echo -e "${green}\t2.${plain} Uninstall Fail2ban and IP Limit" + echo -e "${green}\t0.${plain} Back to Main Menu" + read -rp "Choose an option: " num + case "$num" in + 1) + rm -f /etc/fail2ban/filter.d/3x-ipl.conf + rm -f /etc/fail2ban/action.d/3x-ipl.conf + rm -f /etc/fail2ban/jail.d/3x-ipl.conf + if [[ $release == "alpine" ]]; then + rc-service fail2ban restart + else + systemctl restart fail2ban + fi + echo -e "${green}IP Limit removed successfully!${plain}\n" + before_show_menu + ;; + 2) + rm -rf /etc/fail2ban + if [[ $release == "alpine" ]]; then + rc-service fail2ban stop + else + systemctl stop fail2ban + fi + case "${release}" in + ubuntu | debian | armbian) + apt-get remove -y fail2ban + apt-get purge -y fail2ban -y + apt-get autoremove -y + ;; + fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) + dnf remove fail2ban -y + dnf autoremove -y + ;; + centos) + if [[ "${VERSION_ID}" =~ ^7 ]]; then + yum remove fail2ban -y + yum autoremove -y + else + dnf remove fail2ban -y + dnf autoremove -y + fi + ;; + arch | manjaro | parch) + pacman -Rns --noconfirm fail2ban + ;; + alpine) + apk del fail2ban + ;; + *) + echo -e "${red}Unsupported operating system. Please uninstall Fail2ban manually.${plain}\n" + exit 1 + ;; + esac + echo -e "${green}Fail2ban and IP Limit removed successfully!${plain}\n" + before_show_menu + ;; + 0) + show_menu + ;; + *) + echo -e "${red}Invalid option. Please select a valid number.${plain}\n" + remove_iplimit + ;; + esac +} + +show_banlog() { + local system_log="/var/log/fail2ban.log" + + echo -e "${green}Checking ban logs...${plain}\n" + + if [[ $release == "alpine" ]]; then + if [[ $(rc-service fail2ban status | grep -F 'status: started' -c) == 0 ]]; then + echo -e "${red}Fail2ban service is not running!${plain}\n" + return 1 + fi + else + if ! systemctl is-active --quiet fail2ban; then + echo -e "${red}Fail2ban service is not running!${plain}\n" + return 1 + fi + fi + + if [[ -f "$system_log" ]]; then + echo -e "${green}Recent system ban activities from fail2ban.log:${plain}" + grep "3x-ipl" "$system_log" | grep -E "Ban|Unban" | tail -n 10 || echo -e "${yellow}No recent system ban activities found${plain}" + echo "" + fi + + if [[ -f "${iplimit_banned_log_path}" ]]; then + echo -e "${green}3X-IPL ban log entries:${plain}" + if [[ -s "${iplimit_banned_log_path}" ]]; then + grep -v "INIT" "${iplimit_banned_log_path}" | tail -n 10 || echo -e "${yellow}No ban entries found${plain}" + else + echo -e "${yellow}Ban log file is empty${plain}" + fi + else + echo -e "${red}Ban log file not found at: ${iplimit_banned_log_path}${plain}" + fi + + echo -e "\n${green}Current jail status:${plain}" + fail2ban-client status 3x-ipl || echo -e "${yellow}Unable to get jail status${plain}" +} + +create_iplimit_jails() { + # Use default bantime if not passed => 30 minutes + local bantime="${1:-30}" + + # Uncomment 'allowipv6 = auto' in fail2ban.conf + sed -i 's/#allowipv6 = auto/allowipv6 = auto/g' /etc/fail2ban/fail2ban.conf + + # On Debian 12+ fail2ban's default backend should be changed to systemd + if [[ "${release}" == "debian" && ${os_version} -ge 12 ]]; then + sed -i '0,/action =/s/backend = auto/backend = systemd/' /etc/fail2ban/jail.conf + fi + + cat << EOF > /etc/fail2ban/jail.d/3x-ipl.conf +[3x-ipl] +enabled=true +backend=auto +filter=3x-ipl +action=3x-ipl +logpath=${iplimit_log_path} +maxretry=2 +findtime=32 +bantime=${bantime}m +EOF + + cat << EOF > /etc/fail2ban/filter.d/3x-ipl.conf +[Definition] +datepattern = ^%%Y/%%m/%%d %%H:%%M:%%S +failregex = \[LIMIT_IP\]\s*Email\s*=\s*.+\s*\|\|\s*SRC\s*=\s* +ignoreregex = +EOF + + cat << EOF > /etc/fail2ban/action.d/3x-ipl.conf +[INCLUDES] +before = iptables-allports.conf + +[Definition] +actionstart = -N f2b- + -A f2b- -j + -I -p -j f2b- + +actionstop = -D -p -j f2b- + + -X f2b- + +actioncheck = -n -L | grep -q 'f2b-[ \t]' + +actionban = -I f2b- 1 -s -j + echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") BAN [Email] = [IP] = banned for seconds." >> ${iplimit_banned_log_path} + +actionunban = -D f2b- -s -j + echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") UNBAN [Email] = [IP] = unbanned." >> ${iplimit_banned_log_path} + +[Init] +name = default +protocol = tcp +chain = INPUT +EOF + + echo -e "${green}Ip Limit jail files created with a bantime of ${bantime} minutes.${plain}" +} + +iplimit_remove_conflicts() { + local jail_files=( + /etc/fail2ban/jail.conf + /etc/fail2ban/jail.local + ) + + for file in "${jail_files[@]}"; do + # Check for [3x-ipl] config in jail file then remove it + if test -f "${file}" && grep -qw '3x-ipl' ${file}; then + sed -i "/\[3x-ipl\]/,/^$/d" ${file} + echo -e "${yellow}Removing conflicts of [3x-ipl] in jail (${file})!${plain}\n" + fi + done +} diff --git a/lib/service.sh b/lib/service.sh new file mode 100644 index 00000000..c1488300 --- /dev/null +++ b/lib/service.sh @@ -0,0 +1,287 @@ +#!/bin/bash +# lib/service.sh - Service control functions (start, stop, restart, status, enable, disable) + +# Include guard +[[ -n "${__X_UI_SERVICE_INCLUDED:-}" ]] && return 0 +__X_UI_SERVICE_INCLUDED=1 + +# Source dependencies +source "${LIB_DIR}/common.sh" + +# 0: running, 1: not running, 2: not installed +check_status() { + if [[ $release == "alpine" ]]; then + if [[ ! -f /etc/init.d/x-ui ]]; then + return 2 + fi + if [[ $(rc-service x-ui status | grep -F 'status: started' -c) == 1 ]]; then + return 0 + else + return 1 + fi + else + if [[ ! -f ${xui_service}/x-ui.service ]]; then + return 2 + fi + temp=$(systemctl status x-ui | grep Active | awk '{print $3}' | cut -d "(" -f2 | cut -d ")" -f1) + if [[ "${temp}" == "running" ]]; then + return 0 + else + return 1 + fi + fi +} + +check_enabled() { + if [[ $release == "alpine" ]]; then + if [[ $(rc-update show | grep -F 'x-ui' | grep default -c) == 1 ]]; then + return 0 + else + return 1 + fi + else + temp=$(systemctl is-enabled x-ui) + if [[ "${temp}" == "enabled" ]]; then + return 0 + else + return 1 + fi + fi +} + +check_uninstall() { + check_status + if [[ $? != 2 ]]; then + echo "" + LOGE "Panel installed, Please do not reinstall" + if [[ $# == 0 ]]; then + before_show_menu + fi + return 1 + else + return 0 + fi +} + +check_install() { + check_status + if [[ $? == 2 ]]; then + echo "" + LOGE "Please install the panel first" + if [[ $# == 0 ]]; then + before_show_menu + fi + return 1 + else + return 0 + fi +} + +show_status() { + check_status + case $? in + 0) + echo -e "Panel state: ${green}Running${plain}" + show_enable_status + ;; + 1) + echo -e "Panel state: ${yellow}Not Running${plain}" + show_enable_status + ;; + 2) + echo -e "Panel state: ${red}Not Installed${plain}" + ;; + esac + show_xray_status +} + +show_enable_status() { + check_enabled + if [[ $? == 0 ]]; then + echo -e "Start automatically: ${green}Yes${plain}" + else + echo -e "Start automatically: ${red}No${plain}" + fi +} + +check_xray_status() { + count=$(ps -ef | grep "xray-linux" | grep -v "grep" | wc -l) + if [[ count -ne 0 ]]; then + return 0 + else + return 1 + fi +} + +show_xray_status() { + check_xray_status + if [[ $? == 0 ]]; then + echo -e "xray state: ${green}Running${plain}" + else + echo -e "xray state: ${red}Not Running${plain}" + fi +} + +start() { + check_status + if [[ $? == 0 ]]; then + echo "" + LOGI "Panel is running, No need to start again, If you need to restart, please select restart" + else + if [[ $release == "alpine" ]]; then + rc-service x-ui start + else + systemctl start x-ui + fi + sleep 2 + check_status + if [[ $? == 0 ]]; then + LOGI "x-ui Started Successfully" + else + LOGE "panel Failed to start, Probably because it takes longer than two seconds to start, Please check the log information later" + fi + fi + + if [[ $# == 0 ]]; then + before_show_menu + fi +} + +stop() { + check_status + if [[ $? == 1 ]]; then + echo "" + LOGI "Panel stopped, No need to stop again!" + else + if [[ $release == "alpine" ]]; then + rc-service x-ui stop + else + systemctl stop x-ui + fi + sleep 2 + check_status + if [[ $? == 1 ]]; then + LOGI "x-ui and xray stopped successfully" + else + LOGE "Panel stop failed, Probably because the stop time exceeds two seconds, Please check the log information later" + fi + fi + + if [[ $# == 0 ]]; then + before_show_menu + fi +} + +restart() { + if [[ $release == "alpine" ]]; then + rc-service x-ui restart + else + systemctl restart x-ui + fi + sleep 2 + check_status + if [[ $? == 0 ]]; then + LOGI "x-ui and xray Restarted successfully" + else + LOGE "Panel restart failed, Probably because it takes longer than two seconds to start, Please check the log information later" + fi + if [[ $# == 0 ]]; then + before_show_menu + fi +} + +status() { + if [[ $release == "alpine" ]]; then + rc-service x-ui status + else + systemctl status x-ui -l + fi + if [[ $# == 0 ]]; then + before_show_menu + fi +} + +enable() { + if [[ $release == "alpine" ]]; then + rc-update add x-ui + else + systemctl enable x-ui + fi + if [[ $? == 0 ]]; then + LOGI "x-ui Set to boot automatically on startup successfully" + else + LOGE "x-ui Failed to set Autostart" + fi + + if [[ $# == 0 ]]; then + before_show_menu + fi +} + +disable() { + if [[ $release == "alpine" ]]; then + rc-update del x-ui + else + systemctl disable x-ui + fi + if [[ $? == 0 ]]; then + LOGI "x-ui Autostart Cancelled successfully" + else + LOGE "x-ui Failed to cancel autostart" + fi + + if [[ $# == 0 ]]; then + before_show_menu + fi +} + +show_log() { + if [[ $release == "alpine" ]]; then + echo -e "${green}\t1.${plain} Debug Log" + echo -e "${green}\t0.${plain} Back to Main Menu" + read -rp "Choose an option: " choice + + case "$choice" in + 0) + show_menu + ;; + 1) + grep -F 'x-ui[' /var/log/messages + if [[ $# == 0 ]]; then + before_show_menu + fi + ;; + *) + echo -e "${red}Invalid option. Please select a valid number.${plain}\n" + show_log + ;; + esac + else + echo -e "${green}\t1.${plain} Debug Log" + echo -e "${green}\t2.${plain} Clear All logs" + echo -e "${green}\t0.${plain} Back to Main Menu" + read -rp "Choose an option: " choice + + case "$choice" in + 0) + show_menu + ;; + 1) + journalctl -u x-ui -e --no-pager -f -p debug + if [[ $# == 0 ]]; then + before_show_menu + fi + ;; + 2) + sudo journalctl --rotate + sudo journalctl --vacuum-time=1s + echo "All Logs cleared." + restart + ;; + *) + echo -e "${red}Invalid option. Please select a valid number.${plain}\n" + show_log + ;; + esac + fi +} diff --git a/lib/settings.sh b/lib/settings.sh new file mode 100644 index 00000000..38ef3da2 --- /dev/null +++ b/lib/settings.sh @@ -0,0 +1,132 @@ +#!/bin/bash +# lib/settings.sh - Panel settings management + +# Include guard +[[ -n "${__X_UI_SETTINGS_INCLUDED:-}" ]] && return 0 +__X_UI_SETTINGS_INCLUDED=1 + +# Source dependencies +source "${LIB_DIR}/common.sh" +source "${LIB_DIR}/service.sh" +source "${LIB_DIR}/ssl.sh" + +reset_user() { + confirm "Are you sure to reset the username and password of the panel?" "n" + if [[ $? != 0 ]]; then + if [[ $# == 0 ]]; then + show_menu + fi + return 0 + fi + + read -rp "Please set the login username [default is a random username]: " config_account + [[ -z $config_account ]] && config_account=$(gen_random_string 10) + read -rp "Please set the login password [default is a random password]: " config_password + [[ -z $config_password ]] && config_password=$(gen_random_string 18) + + read -rp "Do you want to disable currently configured two-factor authentication? (y/n): " twoFactorConfirm + if [[ $twoFactorConfirm != "y" && $twoFactorConfirm != "Y" ]]; then + ${xui_folder}/x-ui setting -username ${config_account} -password ${config_password} -resetTwoFactor false >/dev/null 2>&1 + else + ${xui_folder}/x-ui setting -username ${config_account} -password ${config_password} -resetTwoFactor true >/dev/null 2>&1 + echo -e "Two factor authentication has been disabled." + fi + + echo -e "Panel login username has been reset to: ${green} ${config_account} ${plain}" + echo -e "Panel login password has been reset to: ${green} ${config_password} ${plain}" + echo -e "${green} Please use the new login username and password to access the X-UI panel. Also remember them! ${plain}" + confirm_restart +} + +reset_webbasepath() { + echo -e "${yellow}Resetting Web Base Path${plain}" + + read -rp "Are you sure you want to reset the web base path? (y/n): " confirm + if [[ $confirm != "y" && $confirm != "Y" ]]; then + echo -e "${yellow}Operation canceled.${plain}" + return + fi + + config_webBasePath=$(gen_random_string 18) + + # Apply the new web base path setting + ${xui_folder}/x-ui setting -webBasePath "${config_webBasePath}" >/dev/null 2>&1 + + echo -e "Web base path has been reset to: ${green}${config_webBasePath}${plain}" + echo -e "${green}Please use the new web base path to access the panel.${plain}" + restart +} + +reset_config() { + confirm "Are you sure you want to reset all panel settings, Account data will not be lost, Username and password will not change" "n" + if [[ $? != 0 ]]; then + if [[ $# == 0 ]]; then + show_menu + fi + return 0 + fi + ${xui_folder}/x-ui setting -reset + echo -e "All panel settings have been reset to default." + restart +} + +check_config() { + local info=$(${xui_folder}/x-ui setting -show true) + if [[ $? != 0 ]]; then + LOGE "get current settings error, please check logs" + show_menu + return + fi + LOGI "${info}" + + local existing_webBasePath=$(echo "$info" | grep -Eo 'webBasePath: .+' | awk '{print $2}') + local existing_port=$(echo "$info" | grep -Eo 'port: .+' | awk '{print $2}') + local existing_cert=$(${xui_folder}/x-ui setting -getCert true | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]') + local server_ip=$(curl -s --max-time 3 https://api.ipify.org) + if [ -z "$server_ip" ]; then + server_ip=$(curl -s --max-time 3 https://4.ident.me) + fi + + if [[ -n "$existing_cert" ]]; then + local domain=$(basename "$(dirname "$existing_cert")") + + if [[ "$domain" =~ ^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then + echo -e "${green}Access URL: https://${domain}:${existing_port}${existing_webBasePath}${plain}" + else + echo -e "${green}Access URL: https://${server_ip}:${existing_port}${existing_webBasePath}${plain}" + fi + else + echo -e "${red}⚠ WARNING: No SSL certificate configured!${plain}" + echo -e "${yellow}You can get a Let's Encrypt certificate for your IP address (valid ~6 days, auto-renews).${plain}" + read -rp "Generate SSL certificate for IP now? [y/N]: " gen_ssl + if [[ "$gen_ssl" == "y" || "$gen_ssl" == "Y" ]]; then + stop >/dev/null 2>&1 + ssl_cert_issue_for_ip + if [[ $? -eq 0 ]]; then + echo -e "${green}Access URL: https://${server_ip}:${existing_port}${existing_webBasePath}${plain}" + # ssl_cert_issue_for_ip already restarts the panel, but ensure it's running + start >/dev/null 2>&1 + else + LOGE "IP certificate setup failed." + echo -e "${yellow}You can try again via option 18 (SSL Certificate Management).${plain}" + start >/dev/null 2>&1 + fi + else + echo -e "${yellow}Access URL: http://${server_ip}:${existing_port}${existing_webBasePath}${plain}" + echo -e "${yellow}For security, please configure SSL certificate using option 18 (SSL Certificate Management)${plain}" + fi + fi +} + +set_port() { + echo -n "Enter port number[1-65535]: " + read -r port + if [[ -z "${port}" ]]; then + LOGD "Cancelled" + before_show_menu + else + ${xui_folder}/x-ui setting -port ${port} + echo -e "The port is set, Please restart the panel now, and use the new port ${green}${port}${plain} to access web panel" + confirm_restart + fi +} diff --git a/lib/ssl.sh b/lib/ssl.sh new file mode 100644 index 00000000..5ba2bb19 --- /dev/null +++ b/lib/ssl.sh @@ -0,0 +1,628 @@ +#!/bin/bash +# lib/ssl.sh - SSL certificate management (acme.sh, Let's Encrypt, Cloudflare) + +# Include guard +[[ -n "${__X_UI_SSL_INCLUDED:-}" ]] && return 0 +__X_UI_SSL_INCLUDED=1 + +# Source dependencies +source "${LIB_DIR}/common.sh" +source "${LIB_DIR}/service.sh" + +install_acme() { + # Check if acme.sh is already installed + if command -v ~/.acme.sh/acme.sh &>/dev/null; then + LOGI "acme.sh is already installed." + return 0 + fi + + LOGI "Installing acme.sh..." + cd ~ || return 1 # Ensure you can change to the home directory + + curl -s https://get.acme.sh | sh + if [ $? -ne 0 ]; then + LOGE "Installation of acme.sh failed." + return 1 + else + LOGI "Installation of acme.sh succeeded." + fi + + return 0 +} + +ssl_cert_issue_main() { + echo -e "${green}\t1.${plain} Get SSL (Domain)" + echo -e "${green}\t2.${plain} Revoke" + echo -e "${green}\t3.${plain} Force Renew" + echo -e "${green}\t4.${plain} Show Existing Domains" + echo -e "${green}\t5.${plain} Set Cert paths for the panel" + echo -e "${green}\t6.${plain} Get SSL for IP Address (6-day cert, auto-renews)" + echo -e "${green}\t0.${plain} Back to Main Menu" + + read -rp "Choose an option: " choice + case "$choice" in + 0) + show_menu + ;; + 1) + ssl_cert_issue + ssl_cert_issue_main + ;; + 2) + local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \;) + if [ -z "$domains" ]; then + echo "No certificates found to revoke." + else + echo "Existing domains:" + echo "$domains" + read -rp "Please enter a domain from the list to revoke the certificate: " domain + if echo "$domains" | grep -qw "$domain"; then + ~/.acme.sh/acme.sh --revoke -d ${domain} + LOGI "Certificate revoked for domain: $domain" + else + echo "Invalid domain entered." + fi + fi + ssl_cert_issue_main + ;; + 3) + local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \;) + if [ -z "$domains" ]; then + echo "No certificates found to renew." + else + echo "Existing domains:" + echo "$domains" + read -rp "Please enter a domain from the list to renew the SSL certificate: " domain + if echo "$domains" | grep -qw "$domain"; then + ~/.acme.sh/acme.sh --renew -d ${domain} --force + LOGI "Certificate forcefully renewed for domain: $domain" + else + echo "Invalid domain entered." + fi + fi + ssl_cert_issue_main + ;; + 4) + local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \;) + if [ -z "$domains" ]; then + echo "No certificates found." + else + echo "Existing domains and their paths:" + for domain in $domains; do + local cert_path="/root/cert/${domain}/fullchain.pem" + local key_path="/root/cert/${domain}/privkey.pem" + if [[ -f "${cert_path}" && -f "${key_path}" ]]; then + echo -e "Domain: ${domain}" + echo -e "\tCertificate Path: ${cert_path}" + echo -e "\tPrivate Key Path: ${key_path}" + else + echo -e "Domain: ${domain} - Certificate or Key missing." + fi + done + fi + ssl_cert_issue_main + ;; + 5) + local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \;) + if [ -z "$domains" ]; then + echo "No certificates found." + else + echo "Available domains:" + echo "$domains" + read -rp "Please choose a domain to set the panel paths: " domain + + if echo "$domains" | grep -qw "$domain"; then + local webCertFile="/root/cert/${domain}/fullchain.pem" + local webKeyFile="/root/cert/${domain}/privkey.pem" + + if [[ -f "${webCertFile}" && -f "${webKeyFile}" ]]; then + ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" + echo "Panel paths set for domain: $domain" + echo " - Certificate File: $webCertFile" + echo " - Private Key File: $webKeyFile" + restart + else + echo "Certificate or private key not found for domain: $domain." + fi + else + echo "Invalid domain entered." + fi + fi + ssl_cert_issue_main + ;; + 6) + echo -e "${yellow}Let's Encrypt SSL Certificate for IP Address${plain}" + echo -e "This will obtain a certificate for your server's IP using the shortlived profile." + echo -e "${yellow}Certificate valid for ~6 days, auto-renews via acme.sh cron job.${plain}" + echo -e "${yellow}Port 80 must be open and accessible from the internet.${plain}" + confirm "Do you want to proceed?" "y" + if [[ $? == 0 ]]; then + ssl_cert_issue_for_ip + fi + ssl_cert_issue_main + ;; + + *) + echo -e "${red}Invalid option. Please select a valid number.${plain}\n" + ssl_cert_issue_main + ;; + esac +} + +ssl_cert_issue_for_ip() { + LOGI "Starting automatic SSL certificate generation for server IP..." + LOGI "Using Let's Encrypt shortlived profile (~6 days validity, auto-renews)" + + local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}') + local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') + + # Get server IP + local server_ip=$(curl -s --max-time 3 https://api.ipify.org) + if [ -z "$server_ip" ]; then + server_ip=$(curl -s --max-time 3 https://4.ident.me) + fi + + if [ -z "$server_ip" ]; then + LOGE "Failed to get server IP address" + return 1 + fi + + LOGI "Server IP detected: ${server_ip}" + + # Ask for optional IPv6 + local ipv6_addr="" + read -rp "Do you have an IPv6 address to include? (leave empty to skip): " ipv6_addr + ipv6_addr="${ipv6_addr// /}" # Trim whitespace + + # check for acme.sh first + if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then + LOGI "acme.sh not found, installing..." + install_acme + if [ $? -ne 0 ]; then + LOGE "Failed to install acme.sh" + return 1 + fi + fi + + # install socat + case "${release}" in + ubuntu | debian | armbian) + apt-get update >/dev/null 2>&1 && apt-get install socat -y >/dev/null 2>&1 + ;; + fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) + dnf -y update >/dev/null 2>&1 && dnf -y install socat >/dev/null 2>&1 + ;; + centos) + if [[ "${VERSION_ID}" =~ ^7 ]]; then + yum -y update >/dev/null 2>&1 && yum -y install socat >/dev/null 2>&1 + else + dnf -y update >/dev/null 2>&1 && dnf -y install socat >/dev/null 2>&1 + fi + ;; + arch | manjaro | parch) + pacman -Sy --noconfirm socat >/dev/null 2>&1 + ;; + opensuse-tumbleweed | opensuse-leap) + zypper refresh >/dev/null 2>&1 && zypper -q install -y socat >/dev/null 2>&1 + ;; + alpine) + apk add socat curl openssl >/dev/null 2>&1 + ;; + *) + LOGW "Unsupported OS for automatic socat installation" + ;; + esac + + # Create certificate directory + certPath="/root/cert/ip" + mkdir -p "$certPath" + + # Build domain arguments + local domain_args="-d ${server_ip}" + if [[ -n "$ipv6_addr" ]] && is_ipv6 "$ipv6_addr"; then + domain_args="${domain_args} -d ${ipv6_addr}" + LOGI "Including IPv6 address: ${ipv6_addr}" + fi + + # Use port 80 for certificate issuance + local WebPort=80 + LOGI "Using port ${WebPort} to issue certificate for IP: ${server_ip}" + LOGI "Make sure port ${WebPort} is open and not in use..." + + # Reload command - restarts panel after renewal + local reloadCmd="systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null" + + # issue the certificate for IP with shortlived profile + ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt + ~/.acme.sh/acme.sh --issue \ + ${domain_args} \ + --standalone \ + --server letsencrypt \ + --certificate-profile shortlived \ + --days 6 \ + --httpport ${WebPort} \ + --force + + if [ $? -ne 0 ]; then + LOGE "Failed to issue certificate for IP: ${server_ip}" + LOGE "Make sure port ${WebPort} is open and the server is accessible from the internet" + # Cleanup acme.sh data for both IPv4 and IPv6 if specified + rm -rf ~/.acme.sh/${server_ip} 2>/dev/null + [[ -n "$ipv6_addr" ]] && rm -rf ~/.acme.sh/${ipv6_addr} 2>/dev/null + rm -rf ${certPath} 2>/dev/null + return 1 + else + LOGI "Certificate issued successfully for IP: ${server_ip}" + fi + + # Install the certificate + # Note: acme.sh may report "Reload error" and exit non-zero if reloadcmd fails, + # but the cert files are still installed. We check for files instead of exit code. + ~/.acme.sh/acme.sh --installcert -d ${server_ip} \ + --key-file "${certPath}/privkey.pem" \ + --fullchain-file "${certPath}/fullchain.pem" \ + --reloadcmd "${reloadCmd}" 2>&1 || true + + # Verify certificate files exist (don't rely on exit code - reloadcmd failure causes non-zero) + if [[ ! -f "${certPath}/fullchain.pem" || ! -f "${certPath}/privkey.pem" ]]; then + LOGE "Certificate files not found after installation" + # Cleanup acme.sh data for both IPv4 and IPv6 if specified + rm -rf ~/.acme.sh/${server_ip} 2>/dev/null + [[ -n "$ipv6_addr" ]] && rm -rf ~/.acme.sh/${ipv6_addr} 2>/dev/null + rm -rf ${certPath} 2>/dev/null + return 1 + fi + + LOGI "Certificate files installed successfully" + + # enable auto-renew + ~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1 + chmod 600 $certPath/privkey.pem 2>/dev/null + chmod 644 $certPath/fullchain.pem 2>/dev/null + + # Set certificate paths for the panel + local webCertFile="${certPath}/fullchain.pem" + local webKeyFile="${certPath}/privkey.pem" + + if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then + ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" + LOGI "Certificate configured for panel" + LOGI " - Certificate File: $webCertFile" + LOGI " - Private Key File: $webKeyFile" + LOGI " - Validity: ~6 days (auto-renews via acme.sh cron)" + echo -e "${green}Access URL: https://${server_ip}:${existing_port}${existing_webBasePath}${plain}" + LOGI "Panel will restart to apply SSL certificate..." + restart + return 0 + else + LOGE "Certificate files not found after installation" + return 1 + fi +} + +ssl_cert_issue() { + local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}') + local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') + # check for acme.sh first + if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then + echo "acme.sh could not be found. we will install it" + install_acme + if [ $? -ne 0 ]; then + LOGE "install acme failed, please check logs" + exit 1 + fi + fi + + # install socat + case "${release}" in + ubuntu | debian | armbian) + apt-get update >/dev/null 2>&1 && apt-get install socat -y >/dev/null 2>&1 + ;; + fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) + dnf -y update >/dev/null 2>&1 && dnf -y install socat >/dev/null 2>&1 + ;; + centos) + if [[ "${VERSION_ID}" =~ ^7 ]]; then + yum -y update >/dev/null 2>&1 && yum -y install socat >/dev/null 2>&1 + else + dnf -y update >/dev/null 2>&1 && dnf -y install socat >/dev/null 2>&1 + fi + ;; + arch | manjaro | parch) + pacman -Sy --noconfirm socat >/dev/null 2>&1 + ;; + opensuse-tumbleweed | opensuse-leap) + zypper refresh >/dev/null 2>&1 && zypper -q install -y socat >/dev/null 2>&1 + ;; + alpine) + apk add socat curl openssl >/dev/null 2>&1 + ;; + *) + LOGW "Unsupported OS for automatic socat installation" + ;; + esac + if [ $? -ne 0 ]; then + LOGE "install socat failed, please check logs" + exit 1 + else + LOGI "install socat succeed..." + fi + + # get the domain here, and we need to verify it + local domain="" + while true; do + read -rp "Please enter your domain name: " domain + domain="${domain// /}" # Trim whitespace + + if [[ -z "$domain" ]]; then + LOGE "Domain name cannot be empty. Please try again." + continue + fi + + if ! is_domain "$domain"; then + LOGE "Invalid domain format: ${domain}. Please enter a valid domain name." + continue + fi + + break + done + LOGD "Your domain is: ${domain}, checking it..." + + # check if there already exists a certificate + local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}') + if [ "${currentCert}" == "${domain}" ]; then + local certInfo=$(~/.acme.sh/acme.sh --list) + LOGE "System already has certificates for this domain. Cannot issue again. Current certificate details:" + LOGI "$certInfo" + exit 1 + else + LOGI "Your domain is ready for issuing certificates now..." + fi + + # create a directory for the certificate + certPath="/root/cert/${domain}" + if [ ! -d "$certPath" ]; then + mkdir -p "$certPath" + else + rm -rf "$certPath" + mkdir -p "$certPath" + fi + + # get the port number for the standalone server + local WebPort=80 + read -rp "Please choose which port to use (default is 80): " WebPort + if [[ ${WebPort} -gt 65535 || ${WebPort} -lt 1 ]]; then + LOGE "Your input ${WebPort} is invalid, will use default port 80." + WebPort=80 + fi + LOGI "Will use port: ${WebPort} to issue certificates. Please make sure this port is open." + + # issue the certificate + ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt + ~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport ${WebPort} --force + if [ $? -ne 0 ]; then + LOGE "Issuing certificate failed, please check logs." + rm -rf ~/.acme.sh/${domain} + exit 1 + else + LOGE "Issuing certificate succeeded, installing certificates..." + fi + + reloadCmd="x-ui restart" + + LOGI "Default --reloadcmd for ACME is: ${yellow}x-ui restart" + LOGI "This command will run on every certificate issue and renew." + read -rp "Would you like to modify --reloadcmd for ACME? (y/n): " setReloadcmd + if [[ "$setReloadcmd" == "y" || "$setReloadcmd" == "Y" ]]; then + echo -e "\n${green}\t1.${plain} Preset: systemctl reload nginx ; x-ui restart" + echo -e "${green}\t2.${plain} Input your own command" + echo -e "${green}\t0.${plain} Keep default reloadcmd" + read -rp "Choose an option: " choice + case "$choice" in + 1) + LOGI "Reloadcmd is: systemctl reload nginx ; x-ui restart" + reloadCmd="systemctl reload nginx ; x-ui restart" + ;; + 2) + LOGD "It's recommended to put x-ui restart at the end, so it won't raise an error if other services fails" + read -rp "Please enter your reloadcmd (example: systemctl reload nginx ; x-ui restart): " reloadCmd + LOGI "Your reloadcmd is: ${reloadCmd}" + ;; + *) + LOGI "Keep default reloadcmd" + ;; + esac + fi + + # install the certificate + ~/.acme.sh/acme.sh --installcert -d ${domain} \ + --key-file /root/cert/${domain}/privkey.pem \ + --fullchain-file /root/cert/${domain}/fullchain.pem --reloadcmd "${reloadCmd}" + + if [ $? -ne 0 ]; then + LOGE "Installing certificate failed, exiting." + rm -rf ~/.acme.sh/${domain} + exit 1 + else + LOGI "Installing certificate succeeded, enabling auto renew..." + fi + + # enable auto-renew + ~/.acme.sh/acme.sh --upgrade --auto-upgrade + if [ $? -ne 0 ]; then + LOGE "Auto renew failed, certificate details:" + ls -lah cert/* + chmod 600 $certPath/privkey.pem + chmod 644 $certPath/fullchain.pem + exit 1 + else + LOGI "Auto renew succeeded, certificate details:" + ls -lah cert/* + chmod 600 $certPath/privkey.pem + chmod 644 $certPath/fullchain.pem + fi + + # Prompt user to set panel paths after successful certificate installation + read -rp "Would you like to set this certificate for the panel? (y/n): " setPanel + if [[ "$setPanel" == "y" || "$setPanel" == "Y" ]]; then + local webCertFile="/root/cert/${domain}/fullchain.pem" + local webKeyFile="/root/cert/${domain}/privkey.pem" + + if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then + ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" + LOGI "Panel paths set for domain: $domain" + LOGI " - Certificate File: $webCertFile" + LOGI " - Private Key File: $webKeyFile" + echo -e "${green}Access URL: https://${domain}:${existing_port}${existing_webBasePath}${plain}" + restart + else + LOGE "Error: Certificate or private key file not found for domain: $domain." + fi + else + LOGI "Skipping panel path setting." + fi +} + +ssl_cert_issue_CF() { + local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}') + local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') + LOGI "****** Instructions for Use ******" + LOGI "Follow the steps below to complete the process:" + LOGI "1. Cloudflare Registered E-mail." + LOGI "2. Cloudflare Global API Key." + LOGI "3. The Domain Name." + LOGI "4. Once the certificate is issued, you will be prompted to set the certificate for the panel (optional)." + LOGI "5. The script also supports automatic renewal of the SSL certificate after installation." + + confirm "Do you confirm the information and wish to proceed? [y/n]" "y" + + if [ $? -eq 0 ]; then + # Check for acme.sh first + if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then + echo "acme.sh could not be found. We will install it." + install_acme + if [ $? -ne 0 ]; then + LOGE "Install acme failed, please check logs." + exit 1 + fi + fi + + CF_Domain="" + + LOGD "Please set a domain name:" + read -rp "Input your domain here: " CF_Domain + LOGD "Your domain name is set to: ${CF_Domain}" + + # Set up Cloudflare API details + CF_GlobalKey="" + CF_AccountEmail="" + LOGD "Please set the API key:" + read -rp "Input your key here: " CF_GlobalKey + LOGD "Your API key is: ${CF_GlobalKey}" + + LOGD "Please set up registered email:" + read -rp "Input your email here: " CF_AccountEmail + LOGD "Your registered email address is: ${CF_AccountEmail}" + + # Set the default CA to Let's Encrypt + ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt + if [ $? -ne 0 ]; then + LOGE "Default CA, Let'sEncrypt fail, script exiting..." + exit 1 + fi + + export CF_Key="${CF_GlobalKey}" + export CF_Email="${CF_AccountEmail}" + + # Issue the certificate using Cloudflare DNS + ~/.acme.sh/acme.sh --issue --dns dns_cf -d ${CF_Domain} -d *.${CF_Domain} --log --force + if [ $? -ne 0 ]; then + LOGE "Certificate issuance failed, script exiting..." + exit 1 + else + LOGI "Certificate issued successfully, Installing..." + fi + + # Install the certificate + certPath="/root/cert/${CF_Domain}" + if [ -d "$certPath" ]; then + rm -rf ${certPath} + fi + + mkdir -p ${certPath} + if [ $? -ne 0 ]; then + LOGE "Failed to create directory: ${certPath}" + exit 1 + fi + + reloadCmd="x-ui restart" + + LOGI "Default --reloadcmd for ACME is: ${yellow}x-ui restart" + LOGI "This command will run on every certificate issue and renew." + read -rp "Would you like to modify --reloadcmd for ACME? (y/n): " setReloadcmd + if [[ "$setReloadcmd" == "y" || "$setReloadcmd" == "Y" ]]; then + echo -e "\n${green}\t1.${plain} Preset: systemctl reload nginx ; x-ui restart" + echo -e "${green}\t2.${plain} Input your own command" + echo -e "${green}\t0.${plain} Keep default reloadcmd" + read -rp "Choose an option: " choice + case "$choice" in + 1) + LOGI "Reloadcmd is: systemctl reload nginx ; x-ui restart" + reloadCmd="systemctl reload nginx ; x-ui restart" + ;; + 2) + LOGD "It's recommended to put x-ui restart at the end, so it won't raise an error if other services fails" + read -rp "Please enter your reloadcmd (example: systemctl reload nginx ; x-ui restart): " reloadCmd + LOGI "Your reloadcmd is: ${reloadCmd}" + ;; + *) + LOGI "Keep default reloadcmd" + ;; + esac + fi + ~/.acme.sh/acme.sh --installcert -d ${CF_Domain} -d *.${CF_Domain} \ + --key-file ${certPath}/privkey.pem \ + --fullchain-file ${certPath}/fullchain.pem --reloadcmd "${reloadCmd}" + + if [ $? -ne 0 ]; then + LOGE "Certificate installation failed, script exiting..." + exit 1 + else + LOGI "Certificate installed successfully, Turning on automatic updates..." + fi + + # Enable auto-update + ~/.acme.sh/acme.sh --upgrade --auto-upgrade + if [ $? -ne 0 ]; then + LOGE "Auto update setup failed, script exiting..." + exit 1 + else + LOGI "The certificate is installed and auto-renewal is turned on. Specific information is as follows:" + ls -lah ${certPath}/* + chmod 600 ${certPath}/privkey.pem + chmod 644 ${certPath}/fullchain.pem + fi + + # Prompt user to set panel paths after successful certificate installation + read -rp "Would you like to set this certificate for the panel? (y/n): " setPanel + if [[ "$setPanel" == "y" || "$setPanel" == "Y" ]]; then + local webCertFile="${certPath}/fullchain.pem" + local webKeyFile="${certPath}/privkey.pem" + + if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then + ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" + LOGI "Panel paths set for domain: $CF_Domain" + LOGI " - Certificate File: $webCertFile" + LOGI " - Private Key File: $webKeyFile" + echo -e "${green}Access URL: https://${CF_Domain}:${existing_port}${existing_webBasePath}${plain}" + restart + else + LOGE "Error: Certificate or private key file not found for domain: $CF_Domain." + fi + else + LOGI "Skipping panel path setting." + fi + else + show_menu + fi +} diff --git a/x-ui.sh b/x-ui.sh index bdb48817..340d456a 100644 --- a/x-ui.sh +++ b/x-ui.sh @@ -1,80 +1,37 @@ #!/bin/bash +# x-ui.sh - 3X-UI Panel Management Script (Entrypoint) +# This is the main entrypoint that sources modular library files -red='\033[0;31m' -green='\033[0;32m' -blue='\033[0;34m' -yellow='\033[0;33m' -plain='\033[0m' +# Resolve the actual script location (handles symlinks) +#SCRIPT_PATH="$(readlink -f "$0" 2>/dev/null || realpath "$0" 2>/dev/null || echo "$0")" +#SCRIPT_DIR="$(dirname "$SCRIPT_PATH")" +#LIB_DIR="${SCRIPT_DIR}/lib" +# +## Fallback for installed location +#[[ ! -d "$LIB_DIR" ]] && LIB_DIR="/usr/local/x-ui/lib" -#Add some basic function here -function LOGD() { - echo -e "${yellow}[DEG] $* ${plain}" -} +LIB_DIR="${LIB_DIR:=/usr/local/x-ui/lib}" +# Export LIB_DIR for use by library files +export LIB_DIR -function LOGE() { - echo -e "${red}[ERR] $* ${plain}" -} +# Source all library files +source "${LIB_DIR}/common.sh" +source "${LIB_DIR}/service.sh" +source "${LIB_DIR}/ssl.sh" +source "${LIB_DIR}/settings.sh" +source "${LIB_DIR}/firewall.sh" +source "${LIB_DIR}/iplimit.sh" +source "${LIB_DIR}/bbr.sh" +source "${LIB_DIR}/geo.sh" +source "${LIB_DIR}/install.sh" +source "${LIB_DIR}/extras.sh" -function LOGI() { - echo -e "${green}[INF] $* ${plain}" -} - -# Simple helpers for domain/IP validation -is_ipv4() { - [[ "$1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] && return 0 || return 1 -} -is_ipv6() { - [[ "$1" =~ : ]] && return 0 || return 1 -} -is_ip() { - is_ipv4 "$1" || is_ipv6 "$1" -} -is_domain() { - [[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+[A-Za-z]{2,}$ ]] && return 0 || return 1 -} - -# check root -[[ $EUID -ne 0 ]] && LOGE "ERROR: You must be root to run this script! \n" && exit 1 - -# Check OS and set release variable -if [[ -f /etc/os-release ]]; then - source /etc/os-release - release=$ID -elif [[ -f /usr/lib/os-release ]]; then - source /usr/lib/os-release - release=$ID -else - echo "Failed to check the system OS, please contact the author!" >&2 - exit 1 -fi +# Print OS info echo "The OS release is: $release" -os_version="" -os_version=$(grep "^VERSION_ID" /etc/os-release | cut -d '=' -f2 | tr -d '"' | tr -d '.') - -# Declare Variables -xui_folder="${XUI_MAIN_FOLDER:=/usr/local/x-ui}" -xui_service="${XUI_SERVICE:=/etc/systemd/system}" -log_folder="${XUI_LOG_FOLDER:=/var/log/x-ui}" -mkdir -p "${log_folder}" -iplimit_log_path="${log_folder}/3xipl.log" -iplimit_banned_log_path="${log_folder}/3xipl-banned.log" - -confirm() { - if [[ $# > 1 ]]; then - echo && read -rp "$1 [Default $2]: " temp - if [[ "${temp}" == "" ]]; then - temp=$2 - fi - else - read -rp "$1 [y/n]: " temp - fi - if [[ "${temp}" == "y" || "${temp}" == "Y" ]]; then - return 0 - else - return 1 - fi -} +#============================================================================= +# Menu Functions (kept in entrypoint to avoid circular dependencies) +#============================================================================= confirm_restart() { confirm "Restart the panel, Attention: Restarting the panel will also restart xray" "y" @@ -90,1987 +47,6 @@ before_show_menu() { show_menu } -install() { - bash <(curl -Ls https://raw.githubusercontent.com/MHSanaei/3x-ui/main/install.sh) - if [[ $? == 0 ]]; then - if [[ $# == 0 ]]; then - start - else - start 0 - fi - fi -} - -update() { - confirm "This function will update all x-ui components to the latest version, and the data will not be lost. Do you want to continue?" "y" - if [[ $? != 0 ]]; then - LOGE "Cancelled" - if [[ $# == 0 ]]; then - before_show_menu - fi - return 0 - fi - bash <(curl -Ls https://raw.githubusercontent.com/MHSanaei/3x-ui/main/update.sh) - if [[ $? == 0 ]]; then - LOGI "Update is complete, Panel has automatically restarted " - before_show_menu - fi -} - -update_menu() { - echo -e "${yellow}Updating Menu${plain}" - confirm "This function will update the menu to the latest changes." "y" - if [[ $? != 0 ]]; then - LOGE "Cancelled" - if [[ $# == 0 ]]; then - before_show_menu - fi - return 0 - fi - - curl -fLRo /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh - chmod +x ${xui_folder}/x-ui.sh - chmod +x /usr/bin/x-ui - - if [[ $? == 0 ]]; then - echo -e "${green}Update successful. The panel has automatically restarted.${plain}" - exit 0 - else - echo -e "${red}Failed to update the menu.${plain}" - return 1 - fi -} - -legacy_version() { - echo -n "Enter the panel version (like 2.4.0):" - read -r tag_version - - if [ -z "$tag_version" ]; then - echo "Panel version cannot be empty. Exiting." - exit 1 - fi - # Use the entered panel version in the download link - install_command="bash <(curl -Ls "https://raw.githubusercontent.com/mhsanaei/3x-ui/v$tag_version/install.sh") v$tag_version" - - echo "Downloading and installing panel version $tag_version..." - eval $install_command -} - -# Function to handle the deletion of the script file -delete_script() { - rm "$0" # Remove the script file itself - exit 1 -} - -uninstall() { - confirm "Are you sure you want to uninstall the panel? xray will also uninstalled!" "n" - if [[ $? != 0 ]]; then - if [[ $# == 0 ]]; then - show_menu - fi - return 0 - fi - - if [[ $release == "alpine" ]]; then - rc-service x-ui stop - rc-update del x-ui - rm /etc/init.d/x-ui -f - else - systemctl stop x-ui - systemctl disable x-ui - rm ${xui_service}/x-ui.service -f - systemctl daemon-reload - systemctl reset-failed - fi - - rm /etc/x-ui/ -rf - rm ${xui_folder}/ -rf - - echo "" - echo -e "Uninstalled Successfully.\n" - echo "If you need to install this panel again, you can use below command:" - echo -e "${green}bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)${plain}" - echo "" - # Trap the SIGTERM signal - trap delete_script SIGTERM - delete_script -} - -reset_user() { - confirm "Are you sure to reset the username and password of the panel?" "n" - if [[ $? != 0 ]]; then - if [[ $# == 0 ]]; then - show_menu - fi - return 0 - fi - - read -rp "Please set the login username [default is a random username]: " config_account - [[ -z $config_account ]] && config_account=$(gen_random_string 10) - read -rp "Please set the login password [default is a random password]: " config_password - [[ -z $config_password ]] && config_password=$(gen_random_string 18) - - read -rp "Do you want to disable currently configured two-factor authentication? (y/n): " twoFactorConfirm - if [[ $twoFactorConfirm != "y" && $twoFactorConfirm != "Y" ]]; then - ${xui_folder}/x-ui setting -username ${config_account} -password ${config_password} -resetTwoFactor false >/dev/null 2>&1 - else - ${xui_folder}/x-ui setting -username ${config_account} -password ${config_password} -resetTwoFactor true >/dev/null 2>&1 - echo -e "Two factor authentication has been disabled." - fi - - echo -e "Panel login username has been reset to: ${green} ${config_account} ${plain}" - echo -e "Panel login password has been reset to: ${green} ${config_password} ${plain}" - echo -e "${green} Please use the new login username and password to access the X-UI panel. Also remember them! ${plain}" - confirm_restart -} - -gen_random_string() { - local length="$1" - local random_string=$(LC_ALL=C tr -dc 'a-zA-Z0-9' /dev/null 2>&1 - - echo -e "Web base path has been reset to: ${green}${config_webBasePath}${plain}" - echo -e "${green}Please use the new web base path to access the panel.${plain}" - restart -} - -reset_config() { - confirm "Are you sure you want to reset all panel settings, Account data will not be lost, Username and password will not change" "n" - if [[ $? != 0 ]]; then - if [[ $# == 0 ]]; then - show_menu - fi - return 0 - fi - ${xui_folder}/x-ui setting -reset - echo -e "All panel settings have been reset to default." - restart -} - -check_config() { - local info=$(${xui_folder}/x-ui setting -show true) - if [[ $? != 0 ]]; then - LOGE "get current settings error, please check logs" - show_menu - return - fi - LOGI "${info}" - - local existing_webBasePath=$(echo "$info" | grep -Eo 'webBasePath: .+' | awk '{print $2}') - local existing_port=$(echo "$info" | grep -Eo 'port: .+' | awk '{print $2}') - local existing_cert=$(${xui_folder}/x-ui setting -getCert true | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]') - local server_ip=$(curl -s --max-time 3 https://api.ipify.org) - if [ -z "$server_ip" ]; then - server_ip=$(curl -s --max-time 3 https://4.ident.me) - fi - - if [[ -n "$existing_cert" ]]; then - local domain=$(basename "$(dirname "$existing_cert")") - - if [[ "$domain" =~ ^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then - echo -e "${green}Access URL: https://${domain}:${existing_port}${existing_webBasePath}${plain}" - else - echo -e "${green}Access URL: https://${server_ip}:${existing_port}${existing_webBasePath}${plain}" - fi - else - echo -e "${red}⚠ WARNING: No SSL certificate configured!${plain}" - echo -e "${yellow}You can get a Let's Encrypt certificate for your IP address (valid ~6 days, auto-renews).${plain}" - read -rp "Generate SSL certificate for IP now? [y/N]: " gen_ssl - if [[ "$gen_ssl" == "y" || "$gen_ssl" == "Y" ]]; then - stop >/dev/null 2>&1 - ssl_cert_issue_for_ip - if [[ $? -eq 0 ]]; then - echo -e "${green}Access URL: https://${server_ip}:${existing_port}${existing_webBasePath}${plain}" - # ssl_cert_issue_for_ip already restarts the panel, but ensure it's running - start >/dev/null 2>&1 - else - LOGE "IP certificate setup failed." - echo -e "${yellow}You can try again via option 18 (SSL Certificate Management).${plain}" - start >/dev/null 2>&1 - fi - else - echo -e "${yellow}Access URL: http://${server_ip}:${existing_port}${existing_webBasePath}${plain}" - echo -e "${yellow}For security, please configure SSL certificate using option 18 (SSL Certificate Management)${plain}" - fi - fi -} - -set_port() { - echo -n "Enter port number[1-65535]: " - read -r port - if [[ -z "${port}" ]]; then - LOGD "Cancelled" - before_show_menu - else - ${xui_folder}/x-ui setting -port ${port} - echo -e "The port is set, Please restart the panel now, and use the new port ${green}${port}${plain} to access web panel" - confirm_restart - fi -} - -start() { - check_status - if [[ $? == 0 ]]; then - echo "" - LOGI "Panel is running, No need to start again, If you need to restart, please select restart" - else - if [[ $release == "alpine" ]]; then - rc-service x-ui start - else - systemctl start x-ui - fi - sleep 2 - check_status - if [[ $? == 0 ]]; then - LOGI "x-ui Started Successfully" - else - LOGE "panel Failed to start, Probably because it takes longer than two seconds to start, Please check the log information later" - fi - fi - - if [[ $# == 0 ]]; then - before_show_menu - fi -} - -stop() { - check_status - if [[ $? == 1 ]]; then - echo "" - LOGI "Panel stopped, No need to stop again!" - else - if [[ $release == "alpine" ]]; then - rc-service x-ui stop - else - systemctl stop x-ui - fi - sleep 2 - check_status - if [[ $? == 1 ]]; then - LOGI "x-ui and xray stopped successfully" - else - LOGE "Panel stop failed, Probably because the stop time exceeds two seconds, Please check the log information later" - fi - fi - - if [[ $# == 0 ]]; then - before_show_menu - fi -} - -restart() { - if [[ $release == "alpine" ]]; then - rc-service x-ui restart - else - systemctl restart x-ui - fi - sleep 2 - check_status - if [[ $? == 0 ]]; then - LOGI "x-ui and xray Restarted successfully" - else - LOGE "Panel restart failed, Probably because it takes longer than two seconds to start, Please check the log information later" - fi - if [[ $# == 0 ]]; then - before_show_menu - fi -} - -status() { - if [[ $release == "alpine" ]]; then - rc-service x-ui status - else - systemctl status x-ui -l - fi - if [[ $# == 0 ]]; then - before_show_menu - fi -} - -enable() { - if [[ $release == "alpine" ]]; then - rc-update add x-ui - else - systemctl enable x-ui - fi - if [[ $? == 0 ]]; then - LOGI "x-ui Set to boot automatically on startup successfully" - else - LOGE "x-ui Failed to set Autostart" - fi - - if [[ $# == 0 ]]; then - before_show_menu - fi -} - -disable() { - if [[ $release == "alpine" ]]; then - rc-update del x-ui - else - systemctl disable x-ui - fi - if [[ $? == 0 ]]; then - LOGI "x-ui Autostart Cancelled successfully" - else - LOGE "x-ui Failed to cancel autostart" - fi - - if [[ $# == 0 ]]; then - before_show_menu - fi -} - -show_log() { - if [[ $release == "alpine" ]]; then - echo -e "${green}\t1.${plain} Debug Log" - echo -e "${green}\t0.${plain} Back to Main Menu" - read -rp "Choose an option: " choice - - case "$choice" in - 0) - show_menu - ;; - 1) - grep -F 'x-ui[' /var/log/messages - if [[ $# == 0 ]]; then - before_show_menu - fi - ;; - *) - echo -e "${red}Invalid option. Please select a valid number.${plain}\n" - show_log - ;; - esac - else - echo -e "${green}\t1.${plain} Debug Log" - echo -e "${green}\t2.${plain} Clear All logs" - echo -e "${green}\t0.${plain} Back to Main Menu" - read -rp "Choose an option: " choice - - case "$choice" in - 0) - show_menu - ;; - 1) - journalctl -u x-ui -e --no-pager -f -p debug - if [[ $# == 0 ]]; then - before_show_menu - fi - ;; - 2) - sudo journalctl --rotate - sudo journalctl --vacuum-time=1s - echo "All Logs cleared." - restart - ;; - *) - echo -e "${red}Invalid option. Please select a valid number.${plain}\n" - show_log - ;; - esac - fi -} - -bbr_menu() { - echo -e "${green}\t1.${plain} Enable BBR" - echo -e "${green}\t2.${plain} Disable BBR" - echo -e "${green}\t0.${plain} Back to Main Menu" - read -rp "Choose an option: " choice - case "$choice" in - 0) - show_menu - ;; - 1) - enable_bbr - bbr_menu - ;; - 2) - disable_bbr - bbr_menu - ;; - *) - echo -e "${red}Invalid option. Please select a valid number.${plain}\n" - bbr_menu - ;; - esac -} - -disable_bbr() { - - if ! grep -q "net.core.default_qdisc=fq" /etc/sysctl.conf || ! grep -q "net.ipv4.tcp_congestion_control=bbr" /etc/sysctl.conf; then - echo -e "${yellow}BBR is not currently enabled.${plain}" - before_show_menu - fi - - # Replace BBR with CUBIC configurations - sed -i 's/net.core.default_qdisc=fq/net.core.default_qdisc=pfifo_fast/' /etc/sysctl.conf - sed -i 's/net.ipv4.tcp_congestion_control=bbr/net.ipv4.tcp_congestion_control=cubic/' /etc/sysctl.conf - - # Apply changes - sysctl -p - - # Verify that BBR is replaced with CUBIC - if [[ $(sysctl net.ipv4.tcp_congestion_control | awk '{print $3}') == "cubic" ]]; then - echo -e "${green}BBR has been replaced with CUBIC successfully.${plain}" - else - echo -e "${red}Failed to replace BBR with CUBIC. Please check your system configuration.${plain}" - fi -} - -enable_bbr() { - if grep -q "net.core.default_qdisc=fq" /etc/sysctl.conf && grep -q "net.ipv4.tcp_congestion_control=bbr" /etc/sysctl.conf; then - echo -e "${green}BBR is already enabled!${plain}" - before_show_menu - fi - - # Enable BBR - echo "net.core.default_qdisc=fq" | tee -a /etc/sysctl.conf - echo "net.ipv4.tcp_congestion_control=bbr" | tee -a /etc/sysctl.conf - - # Apply changes - sysctl -p - - # Verify that BBR is enabled - if [[ $(sysctl net.ipv4.tcp_congestion_control | awk '{print $3}') == "bbr" ]]; then - echo -e "${green}BBR has been enabled successfully.${plain}" - else - echo -e "${red}Failed to enable BBR. Please check your system configuration.${plain}" - fi -} - -update_shell() { - curl -fLRo /usr/bin/x-ui -z /usr/bin/x-ui https://github.com/MHSanaei/3x-ui/raw/main/x-ui.sh - if [[ $? != 0 ]]; then - echo "" - LOGE "Failed to download script, Please check whether the machine can connect Github" - before_show_menu - else - chmod +x /usr/bin/x-ui - LOGI "Upgrade script succeeded, Please rerun the script" - before_show_menu - fi -} - -# 0: running, 1: not running, 2: not installed -check_status() { - if [[ $release == "alpine" ]]; then - if [[ ! -f /etc/init.d/x-ui ]]; then - return 2 - fi - if [[ $(rc-service x-ui status | grep -F 'status: started' -c) == 1 ]]; then - return 0 - else - return 1 - fi - else - if [[ ! -f ${xui_service}/x-ui.service ]]; then - return 2 - fi - temp=$(systemctl status x-ui | grep Active | awk '{print $3}' | cut -d "(" -f2 | cut -d ")" -f1) - if [[ "${temp}" == "running" ]]; then - return 0 - else - return 1 - fi - fi -} - -check_enabled() { - if [[ $release == "alpine" ]]; then - if [[ $(rc-update show | grep -F 'x-ui' | grep default -c) == 1 ]]; then - return 0 - else - return 1 - fi - else - temp=$(systemctl is-enabled x-ui) - if [[ "${temp}" == "enabled" ]]; then - return 0 - else - return 1 - fi - fi -} - -check_uninstall() { - check_status - if [[ $? != 2 ]]; then - echo "" - LOGE "Panel installed, Please do not reinstall" - if [[ $# == 0 ]]; then - before_show_menu - fi - return 1 - else - return 0 - fi -} - -check_install() { - check_status - if [[ $? == 2 ]]; then - echo "" - LOGE "Please install the panel first" - if [[ $# == 0 ]]; then - before_show_menu - fi - return 1 - else - return 0 - fi -} - -show_status() { - check_status - case $? in - 0) - echo -e "Panel state: ${green}Running${plain}" - show_enable_status - ;; - 1) - echo -e "Panel state: ${yellow}Not Running${plain}" - show_enable_status - ;; - 2) - echo -e "Panel state: ${red}Not Installed${plain}" - ;; - esac - show_xray_status -} - -show_enable_status() { - check_enabled - if [[ $? == 0 ]]; then - echo -e "Start automatically: ${green}Yes${plain}" - else - echo -e "Start automatically: ${red}No${plain}" - fi -} - -check_xray_status() { - count=$(ps -ef | grep "xray-linux" | grep -v "grep" | wc -l) - if [[ count -ne 0 ]]; then - return 0 - else - return 1 - fi -} - -show_xray_status() { - check_xray_status - if [[ $? == 0 ]]; then - echo -e "xray state: ${green}Running${plain}" - else - echo -e "xray state: ${red}Not Running${plain}" - fi -} - -firewall_menu() { - echo -e "${green}\t1.${plain} ${green}Install${plain} Firewall" - echo -e "${green}\t2.${plain} Port List [numbered]" - echo -e "${green}\t3.${plain} ${green}Open${plain} Ports" - echo -e "${green}\t4.${plain} ${red}Delete${plain} Ports from List" - echo -e "${green}\t5.${plain} ${green}Enable${plain} Firewall" - echo -e "${green}\t6.${plain} ${red}Disable${plain} Firewall" - echo -e "${green}\t7.${plain} Firewall Status" - echo -e "${green}\t0.${plain} Back to Main Menu" - read -rp "Choose an option: " choice - case "$choice" in - 0) - show_menu - ;; - 1) - install_firewall - firewall_menu - ;; - 2) - ufw status numbered - firewall_menu - ;; - 3) - open_ports - firewall_menu - ;; - 4) - delete_ports - firewall_menu - ;; - 5) - ufw enable - firewall_menu - ;; - 6) - ufw disable - firewall_menu - ;; - 7) - ufw status verbose - firewall_menu - ;; - *) - echo -e "${red}Invalid option. Please select a valid number.${plain}\n" - firewall_menu - ;; - esac -} - -install_firewall() { - if ! command -v ufw &>/dev/null; then - echo "ufw firewall is not installed. Installing now..." - apt-get update - apt-get install -y ufw - else - echo "ufw firewall is already installed" - fi - - # Check if the firewall is inactive - if ufw status | grep -q "Status: active"; then - echo "Firewall is already active" - else - echo "Activating firewall..." - # Open the necessary ports - ufw allow ssh - ufw allow http - ufw allow https - ufw allow 2053/tcp #webPort - ufw allow 2096/tcp #subport - - # Enable the firewall - ufw --force enable - fi -} - -open_ports() { - # Prompt the user to enter the ports they want to open - read -rp "Enter the ports you want to open (e.g. 80,443,2053 or range 400-500): " ports - - # Check if the input is valid - if ! [[ $ports =~ ^([0-9]+|[0-9]+-[0-9]+)(,([0-9]+|[0-9]+-[0-9]+))*$ ]]; then - echo "Error: Invalid input. Please enter a comma-separated list of ports or a range of ports (e.g. 80,443,2053 or 400-500)." >&2 - exit 1 - fi - - # Open the specified ports using ufw - IFS=',' read -ra PORT_LIST <<<"$ports" - for port in "${PORT_LIST[@]}"; do - if [[ $port == *-* ]]; then - # Split the range into start and end ports - start_port=$(echo $port | cut -d'-' -f1) - end_port=$(echo $port | cut -d'-' -f2) - # Open the port range - ufw allow $start_port:$end_port/tcp - ufw allow $start_port:$end_port/udp - else - # Open the single port - ufw allow "$port" - fi - done - - # Confirm that the ports are opened - echo "Opened the specified ports:" - for port in "${PORT_LIST[@]}"; do - if [[ $port == *-* ]]; then - start_port=$(echo $port | cut -d'-' -f1) - end_port=$(echo $port | cut -d'-' -f2) - # Check if the port range has been successfully opened - (ufw status | grep -q "$start_port:$end_port") && echo "$start_port-$end_port" - else - # Check if the individual port has been successfully opened - (ufw status | grep -q "$port") && echo "$port" - fi - done -} - -delete_ports() { - # Display current rules with numbers - echo "Current UFW rules:" - ufw status numbered - - # Ask the user how they want to delete rules - echo "Do you want to delete rules by:" - echo "1) Rule numbers" - echo "2) Ports" - read -rp "Enter your choice (1 or 2): " choice - - if [[ $choice -eq 1 ]]; then - # Deleting by rule numbers - read -rp "Enter the rule numbers you want to delete (1, 2, etc.): " rule_numbers - - # Validate the input - if ! [[ $rule_numbers =~ ^([0-9]+)(,[0-9]+)*$ ]]; then - echo "Error: Invalid input. Please enter a comma-separated list of rule numbers." >&2 - exit 1 - fi - - # Split numbers into an array - IFS=',' read -ra RULE_NUMBERS <<<"$rule_numbers" - for rule_number in "${RULE_NUMBERS[@]}"; do - # Delete the rule by number - ufw delete "$rule_number" || echo "Failed to delete rule number $rule_number" - done - - echo "Selected rules have been deleted." - - elif [[ $choice -eq 2 ]]; then - # Deleting by ports - read -rp "Enter the ports you want to delete (e.g. 80,443,2053 or range 400-500): " ports - - # Validate the input - if ! [[ $ports =~ ^([0-9]+|[0-9]+-[0-9]+)(,([0-9]+|[0-9]+-[0-9]+))*$ ]]; then - echo "Error: Invalid input. Please enter a comma-separated list of ports or a range of ports (e.g. 80,443,2053 or 400-500)." >&2 - exit 1 - fi - - # Split ports into an array - IFS=',' read -ra PORT_LIST <<<"$ports" - for port in "${PORT_LIST[@]}"; do - if [[ $port == *-* ]]; then - # Split the port range - start_port=$(echo $port | cut -d'-' -f1) - end_port=$(echo $port | cut -d'-' -f2) - # Delete the port range - ufw delete allow $start_port:$end_port/tcp - ufw delete allow $start_port:$end_port/udp - else - # Delete a single port - ufw delete allow "$port" - fi - done - - # Confirmation of deletion - echo "Deleted the specified ports:" - for port in "${PORT_LIST[@]}"; do - if [[ $port == *-* ]]; then - start_port=$(echo $port | cut -d'-' -f1) - end_port=$(echo $port | cut -d'-' -f2) - # Check if the port range has been deleted - (ufw status | grep -q "$start_port:$end_port") || echo "$start_port-$end_port" - else - # Check if the individual port has been deleted - (ufw status | grep -q "$port") || echo "$port" - fi - done - else - echo "${red}Error:${plain} Invalid choice. Please enter 1 or 2." >&2 - exit 1 - fi -} - -update_all_geofiles() { - update_geofiles "main" - update_geofiles "IR" - update_geofiles "RU" -} - -update_geofiles() { - case "${1}" in - "main") dat_files=(geoip geosite); dat_source="Loyalsoldier/v2ray-rules-dat";; - "IR") dat_files=(geoip_IR geosite_IR); dat_source="chocolate4u/Iran-v2ray-rules" ;; - "RU") dat_files=(geoip_RU geosite_RU); dat_source="runetfreedom/russia-v2ray-rules-dat";; - esac - for dat in "${dat_files[@]}"; do - curl -fLRo ${xui_folder}/bin/${dat}.dat -z ${xui_folder}/bin/${dat}.dat \ - https://github.com/${dat_source}/releases/latest/download/${dat%%_}.dat - done -} - -update_geo() { - echo -e "${green}\t1.${plain} Loyalsoldier (geoip.dat, geosite.dat)" - echo -e "${green}\t2.${plain} chocolate4u (geoip_IR.dat, geosite_IR.dat)" - echo -e "${green}\t3.${plain} runetfreedom (geoip_RU.dat, geosite_RU.dat)" - echo -e "${green}\t4.${plain} All" - echo -e "${green}\t0.${plain} Back to Main Menu" - read -rp "Choose an option: " choice - - case "$choice" in - 0) - show_menu - ;; - 1) - update_geofiles "main" - echo -e "${green}Loyalsoldier datasets have been updated successfully!${plain}" - restart - ;; - 2) - update_geofiles "IR" - echo -e "${green}chocolate4u datasets have been updated successfully!${plain}" - restart - ;; - 3) - update_geofiles "RU" - echo -e "${green}runetfreedom datasets have been updated successfully!${plain}" - restart - ;; - 4) - update_all_geofiles - echo -e "${green}All geo files have been updated successfully!${plain}" - restart - ;; - *) - echo -e "${red}Invalid option. Please select a valid number.${plain}\n" - update_geo - ;; - esac - - before_show_menu -} - -install_acme() { - # Check if acme.sh is already installed - if command -v ~/.acme.sh/acme.sh &>/dev/null; then - LOGI "acme.sh is already installed." - return 0 - fi - - LOGI "Installing acme.sh..." - cd ~ || return 1 # Ensure you can change to the home directory - - curl -s https://get.acme.sh | sh - if [ $? -ne 0 ]; then - LOGE "Installation of acme.sh failed." - return 1 - else - LOGI "Installation of acme.sh succeeded." - fi - - return 0 -} - -ssl_cert_issue_main() { - echo -e "${green}\t1.${plain} Get SSL (Domain)" - echo -e "${green}\t2.${plain} Revoke" - echo -e "${green}\t3.${plain} Force Renew" - echo -e "${green}\t4.${plain} Show Existing Domains" - echo -e "${green}\t5.${plain} Set Cert paths for the panel" - echo -e "${green}\t6.${plain} Get SSL for IP Address (6-day cert, auto-renews)" - echo -e "${green}\t0.${plain} Back to Main Menu" - - read -rp "Choose an option: " choice - case "$choice" in - 0) - show_menu - ;; - 1) - ssl_cert_issue - ssl_cert_issue_main - ;; - 2) - local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \;) - if [ -z "$domains" ]; then - echo "No certificates found to revoke." - else - echo "Existing domains:" - echo "$domains" - read -rp "Please enter a domain from the list to revoke the certificate: " domain - if echo "$domains" | grep -qw "$domain"; then - ~/.acme.sh/acme.sh --revoke -d ${domain} - LOGI "Certificate revoked for domain: $domain" - else - echo "Invalid domain entered." - fi - fi - ssl_cert_issue_main - ;; - 3) - local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \;) - if [ -z "$domains" ]; then - echo "No certificates found to renew." - else - echo "Existing domains:" - echo "$domains" - read -rp "Please enter a domain from the list to renew the SSL certificate: " domain - if echo "$domains" | grep -qw "$domain"; then - ~/.acme.sh/acme.sh --renew -d ${domain} --force - LOGI "Certificate forcefully renewed for domain: $domain" - else - echo "Invalid domain entered." - fi - fi - ssl_cert_issue_main - ;; - 4) - local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \;) - if [ -z "$domains" ]; then - echo "No certificates found." - else - echo "Existing domains and their paths:" - for domain in $domains; do - local cert_path="/root/cert/${domain}/fullchain.pem" - local key_path="/root/cert/${domain}/privkey.pem" - if [[ -f "${cert_path}" && -f "${key_path}" ]]; then - echo -e "Domain: ${domain}" - echo -e "\tCertificate Path: ${cert_path}" - echo -e "\tPrivate Key Path: ${key_path}" - else - echo -e "Domain: ${domain} - Certificate or Key missing." - fi - done - fi - ssl_cert_issue_main - ;; - 5) - local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \;) - if [ -z "$domains" ]; then - echo "No certificates found." - else - echo "Available domains:" - echo "$domains" - read -rp "Please choose a domain to set the panel paths: " domain - - if echo "$domains" | grep -qw "$domain"; then - local webCertFile="/root/cert/${domain}/fullchain.pem" - local webKeyFile="/root/cert/${domain}/privkey.pem" - - if [[ -f "${webCertFile}" && -f "${webKeyFile}" ]]; then - ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" - echo "Panel paths set for domain: $domain" - echo " - Certificate File: $webCertFile" - echo " - Private Key File: $webKeyFile" - restart - else - echo "Certificate or private key not found for domain: $domain." - fi - else - echo "Invalid domain entered." - fi - fi - ssl_cert_issue_main - ;; - 6) - echo -e "${yellow}Let's Encrypt SSL Certificate for IP Address${plain}" - echo -e "This will obtain a certificate for your server's IP using the shortlived profile." - echo -e "${yellow}Certificate valid for ~6 days, auto-renews via acme.sh cron job.${plain}" - echo -e "${yellow}Port 80 must be open and accessible from the internet.${plain}" - confirm "Do you want to proceed?" "y" - if [[ $? == 0 ]]; then - ssl_cert_issue_for_ip - fi - ssl_cert_issue_main - ;; - - *) - echo -e "${red}Invalid option. Please select a valid number.${plain}\n" - ssl_cert_issue_main - ;; - esac -} - -ssl_cert_issue_for_ip() { - LOGI "Starting automatic SSL certificate generation for server IP..." - LOGI "Using Let's Encrypt shortlived profile (~6 days validity, auto-renews)" - - local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}') - local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') - - # Get server IP - local server_ip=$(curl -s --max-time 3 https://api.ipify.org) - if [ -z "$server_ip" ]; then - server_ip=$(curl -s --max-time 3 https://4.ident.me) - fi - - if [ -z "$server_ip" ]; then - LOGE "Failed to get server IP address" - return 1 - fi - - LOGI "Server IP detected: ${server_ip}" - - # Ask for optional IPv6 - local ipv6_addr="" - read -rp "Do you have an IPv6 address to include? (leave empty to skip): " ipv6_addr - ipv6_addr="${ipv6_addr// /}" # Trim whitespace - - # check for acme.sh first - if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then - LOGI "acme.sh not found, installing..." - install_acme - if [ $? -ne 0 ]; then - LOGE "Failed to install acme.sh" - return 1 - fi - fi - - # install socat - case "${release}" in - ubuntu | debian | armbian) - apt-get update >/dev/null 2>&1 && apt-get install socat -y >/dev/null 2>&1 - ;; - fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) - dnf -y update >/dev/null 2>&1 && dnf -y install socat >/dev/null 2>&1 - ;; - centos) - if [[ "${VERSION_ID}" =~ ^7 ]]; then - yum -y update >/dev/null 2>&1 && yum -y install socat >/dev/null 2>&1 - else - dnf -y update >/dev/null 2>&1 && dnf -y install socat >/dev/null 2>&1 - fi - ;; - arch | manjaro | parch) - pacman -Sy --noconfirm socat >/dev/null 2>&1 - ;; - opensuse-tumbleweed | opensuse-leap) - zypper refresh >/dev/null 2>&1 && zypper -q install -y socat >/dev/null 2>&1 - ;; - alpine) - apk add socat curl openssl >/dev/null 2>&1 - ;; - *) - LOGW "Unsupported OS for automatic socat installation" - ;; - esac - - # Create certificate directory - certPath="/root/cert/ip" - mkdir -p "$certPath" - - # Build domain arguments - local domain_args="-d ${server_ip}" - if [[ -n "$ipv6_addr" ]] && is_ipv6 "$ipv6_addr"; then - domain_args="${domain_args} -d ${ipv6_addr}" - LOGI "Including IPv6 address: ${ipv6_addr}" - fi - - # Use port 80 for certificate issuance - local WebPort=80 - LOGI "Using port ${WebPort} to issue certificate for IP: ${server_ip}" - LOGI "Make sure port ${WebPort} is open and not in use..." - - # Reload command - restarts panel after renewal - local reloadCmd="systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null" - - # issue the certificate for IP with shortlived profile - ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt - ~/.acme.sh/acme.sh --issue \ - ${domain_args} \ - --standalone \ - --server letsencrypt \ - --certificate-profile shortlived \ - --days 6 \ - --httpport ${WebPort} \ - --force - - if [ $? -ne 0 ]; then - LOGE "Failed to issue certificate for IP: ${server_ip}" - LOGE "Make sure port ${WebPort} is open and the server is accessible from the internet" - # Cleanup acme.sh data for both IPv4 and IPv6 if specified - rm -rf ~/.acme.sh/${server_ip} 2>/dev/null - [[ -n "$ipv6_addr" ]] && rm -rf ~/.acme.sh/${ipv6_addr} 2>/dev/null - rm -rf ${certPath} 2>/dev/null - return 1 - else - LOGI "Certificate issued successfully for IP: ${server_ip}" - fi - - # Install the certificate - # Note: acme.sh may report "Reload error" and exit non-zero if reloadcmd fails, - # but the cert files are still installed. We check for files instead of exit code. - ~/.acme.sh/acme.sh --installcert -d ${server_ip} \ - --key-file "${certPath}/privkey.pem" \ - --fullchain-file "${certPath}/fullchain.pem" \ - --reloadcmd "${reloadCmd}" 2>&1 || true - - # Verify certificate files exist (don't rely on exit code - reloadcmd failure causes non-zero) - if [[ ! -f "${certPath}/fullchain.pem" || ! -f "${certPath}/privkey.pem" ]]; then - LOGE "Certificate files not found after installation" - # Cleanup acme.sh data for both IPv4 and IPv6 if specified - rm -rf ~/.acme.sh/${server_ip} 2>/dev/null - [[ -n "$ipv6_addr" ]] && rm -rf ~/.acme.sh/${ipv6_addr} 2>/dev/null - rm -rf ${certPath} 2>/dev/null - return 1 - fi - - LOGI "Certificate files installed successfully" - - # enable auto-renew - ~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1 - chmod 600 $certPath/privkey.pem 2>/dev/null - chmod 644 $certPath/fullchain.pem 2>/dev/null - - # Set certificate paths for the panel - local webCertFile="${certPath}/fullchain.pem" - local webKeyFile="${certPath}/privkey.pem" - - if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then - ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" - LOGI "Certificate configured for panel" - LOGI " - Certificate File: $webCertFile" - LOGI " - Private Key File: $webKeyFile" - LOGI " - Validity: ~6 days (auto-renews via acme.sh cron)" - echo -e "${green}Access URL: https://${server_ip}:${existing_port}${existing_webBasePath}${plain}" - LOGI "Panel will restart to apply SSL certificate..." - restart - return 0 - else - LOGE "Certificate files not found after installation" - return 1 - fi -} - -ssl_cert_issue() { - local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}') - local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') - # check for acme.sh first - if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then - echo "acme.sh could not be found. we will install it" - install_acme - if [ $? -ne 0 ]; then - LOGE "install acme failed, please check logs" - exit 1 - fi - fi - - # install socat - case "${release}" in - ubuntu | debian | armbian) - apt-get update >/dev/null 2>&1 && apt-get install socat -y >/dev/null 2>&1 - ;; - fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) - dnf -y update >/dev/null 2>&1 && dnf -y install socat >/dev/null 2>&1 - ;; - centos) - if [[ "${VERSION_ID}" =~ ^7 ]]; then - yum -y update >/dev/null 2>&1 && yum -y install socat >/dev/null 2>&1 - else - dnf -y update >/dev/null 2>&1 && dnf -y install socat >/dev/null 2>&1 - fi - ;; - arch | manjaro | parch) - pacman -Sy --noconfirm socat >/dev/null 2>&1 - ;; - opensuse-tumbleweed | opensuse-leap) - zypper refresh >/dev/null 2>&1 && zypper -q install -y socat >/dev/null 2>&1 - ;; - alpine) - apk add socat curl openssl >/dev/null 2>&1 - ;; - *) - LOGW "Unsupported OS for automatic socat installation" - ;; - esac - if [ $? -ne 0 ]; then - LOGE "install socat failed, please check logs" - exit 1 - else - LOGI "install socat succeed..." - fi - - # get the domain here, and we need to verify it - local domain="" - while true; do - read -rp "Please enter your domain name: " domain - domain="${domain// /}" # Trim whitespace - - if [[ -z "$domain" ]]; then - LOGE "Domain name cannot be empty. Please try again." - continue - fi - - if ! is_domain "$domain"; then - LOGE "Invalid domain format: ${domain}. Please enter a valid domain name." - continue - fi - - break - done - LOGD "Your domain is: ${domain}, checking it..." - - # check if there already exists a certificate - local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}') - if [ "${currentCert}" == "${domain}" ]; then - local certInfo=$(~/.acme.sh/acme.sh --list) - LOGE "System already has certificates for this domain. Cannot issue again. Current certificate details:" - LOGI "$certInfo" - exit 1 - else - LOGI "Your domain is ready for issuing certificates now..." - fi - - # create a directory for the certificate - certPath="/root/cert/${domain}" - if [ ! -d "$certPath" ]; then - mkdir -p "$certPath" - else - rm -rf "$certPath" - mkdir -p "$certPath" - fi - - # get the port number for the standalone server - local WebPort=80 - read -rp "Please choose which port to use (default is 80): " WebPort - if [[ ${WebPort} -gt 65535 || ${WebPort} -lt 1 ]]; then - LOGE "Your input ${WebPort} is invalid, will use default port 80." - WebPort=80 - fi - LOGI "Will use port: ${WebPort} to issue certificates. Please make sure this port is open." - - # issue the certificate - ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt - ~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport ${WebPort} --force - if [ $? -ne 0 ]; then - LOGE "Issuing certificate failed, please check logs." - rm -rf ~/.acme.sh/${domain} - exit 1 - else - LOGE "Issuing certificate succeeded, installing certificates..." - fi - - reloadCmd="x-ui restart" - - LOGI "Default --reloadcmd for ACME is: ${yellow}x-ui restart" - LOGI "This command will run on every certificate issue and renew." - read -rp "Would you like to modify --reloadcmd for ACME? (y/n): " setReloadcmd - if [[ "$setReloadcmd" == "y" || "$setReloadcmd" == "Y" ]]; then - echo -e "\n${green}\t1.${plain} Preset: systemctl reload nginx ; x-ui restart" - echo -e "${green}\t2.${plain} Input your own command" - echo -e "${green}\t0.${plain} Keep default reloadcmd" - read -rp "Choose an option: " choice - case "$choice" in - 1) - LOGI "Reloadcmd is: systemctl reload nginx ; x-ui restart" - reloadCmd="systemctl reload nginx ; x-ui restart" - ;; - 2) - LOGD "It's recommended to put x-ui restart at the end, so it won't raise an error if other services fails" - read -rp "Please enter your reloadcmd (example: systemctl reload nginx ; x-ui restart): " reloadCmd - LOGI "Your reloadcmd is: ${reloadCmd}" - ;; - *) - LOGI "Keep default reloadcmd" - ;; - esac - fi - - # install the certificate - ~/.acme.sh/acme.sh --installcert -d ${domain} \ - --key-file /root/cert/${domain}/privkey.pem \ - --fullchain-file /root/cert/${domain}/fullchain.pem --reloadcmd "${reloadCmd}" - - if [ $? -ne 0 ]; then - LOGE "Installing certificate failed, exiting." - rm -rf ~/.acme.sh/${domain} - exit 1 - else - LOGI "Installing certificate succeeded, enabling auto renew..." - fi - - # enable auto-renew - ~/.acme.sh/acme.sh --upgrade --auto-upgrade - if [ $? -ne 0 ]; then - LOGE "Auto renew failed, certificate details:" - ls -lah cert/* - chmod 600 $certPath/privkey.pem - chmod 644 $certPath/fullchain.pem - exit 1 - else - LOGI "Auto renew succeeded, certificate details:" - ls -lah cert/* - chmod 600 $certPath/privkey.pem - chmod 644 $certPath/fullchain.pem - fi - - # Prompt user to set panel paths after successful certificate installation - read -rp "Would you like to set this certificate for the panel? (y/n): " setPanel - if [[ "$setPanel" == "y" || "$setPanel" == "Y" ]]; then - local webCertFile="/root/cert/${domain}/fullchain.pem" - local webKeyFile="/root/cert/${domain}/privkey.pem" - - if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then - ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" - LOGI "Panel paths set for domain: $domain" - LOGI " - Certificate File: $webCertFile" - LOGI " - Private Key File: $webKeyFile" - echo -e "${green}Access URL: https://${domain}:${existing_port}${existing_webBasePath}${plain}" - restart - else - LOGE "Error: Certificate or private key file not found for domain: $domain." - fi - else - LOGI "Skipping panel path setting." - fi -} - -ssl_cert_issue_CF() { - local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}') - local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') - LOGI "****** Instructions for Use ******" - LOGI "Follow the steps below to complete the process:" - LOGI "1. Cloudflare Registered E-mail." - LOGI "2. Cloudflare Global API Key." - LOGI "3. The Domain Name." - LOGI "4. Once the certificate is issued, you will be prompted to set the certificate for the panel (optional)." - LOGI "5. The script also supports automatic renewal of the SSL certificate after installation." - - confirm "Do you confirm the information and wish to proceed? [y/n]" "y" - - if [ $? -eq 0 ]; then - # Check for acme.sh first - if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then - echo "acme.sh could not be found. We will install it." - install_acme - if [ $? -ne 0 ]; then - LOGE "Install acme failed, please check logs." - exit 1 - fi - fi - - CF_Domain="" - - LOGD "Please set a domain name:" - read -rp "Input your domain here: " CF_Domain - LOGD "Your domain name is set to: ${CF_Domain}" - - # Set up Cloudflare API details - CF_GlobalKey="" - CF_AccountEmail="" - LOGD "Please set the API key:" - read -rp "Input your key here: " CF_GlobalKey - LOGD "Your API key is: ${CF_GlobalKey}" - - LOGD "Please set up registered email:" - read -rp "Input your email here: " CF_AccountEmail - LOGD "Your registered email address is: ${CF_AccountEmail}" - - # Set the default CA to Let's Encrypt - ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt - if [ $? -ne 0 ]; then - LOGE "Default CA, Let'sEncrypt fail, script exiting..." - exit 1 - fi - - export CF_Key="${CF_GlobalKey}" - export CF_Email="${CF_AccountEmail}" - - # Issue the certificate using Cloudflare DNS - ~/.acme.sh/acme.sh --issue --dns dns_cf -d ${CF_Domain} -d *.${CF_Domain} --log --force - if [ $? -ne 0 ]; then - LOGE "Certificate issuance failed, script exiting..." - exit 1 - else - LOGI "Certificate issued successfully, Installing..." - fi - - # Install the certificate - certPath="/root/cert/${CF_Domain}" - if [ -d "$certPath" ]; then - rm -rf ${certPath} - fi - - mkdir -p ${certPath} - if [ $? -ne 0 ]; then - LOGE "Failed to create directory: ${certPath}" - exit 1 - fi - - reloadCmd="x-ui restart" - - LOGI "Default --reloadcmd for ACME is: ${yellow}x-ui restart" - LOGI "This command will run on every certificate issue and renew." - read -rp "Would you like to modify --reloadcmd for ACME? (y/n): " setReloadcmd - if [[ "$setReloadcmd" == "y" || "$setReloadcmd" == "Y" ]]; then - echo -e "\n${green}\t1.${plain} Preset: systemctl reload nginx ; x-ui restart" - echo -e "${green}\t2.${plain} Input your own command" - echo -e "${green}\t0.${plain} Keep default reloadcmd" - read -rp "Choose an option: " choice - case "$choice" in - 1) - LOGI "Reloadcmd is: systemctl reload nginx ; x-ui restart" - reloadCmd="systemctl reload nginx ; x-ui restart" - ;; - 2) - LOGD "It's recommended to put x-ui restart at the end, so it won't raise an error if other services fails" - read -rp "Please enter your reloadcmd (example: systemctl reload nginx ; x-ui restart): " reloadCmd - LOGI "Your reloadcmd is: ${reloadCmd}" - ;; - *) - LOGI "Keep default reloadcmd" - ;; - esac - fi - ~/.acme.sh/acme.sh --installcert -d ${CF_Domain} -d *.${CF_Domain} \ - --key-file ${certPath}/privkey.pem \ - --fullchain-file ${certPath}/fullchain.pem --reloadcmd "${reloadCmd}" - - if [ $? -ne 0 ]; then - LOGE "Certificate installation failed, script exiting..." - exit 1 - else - LOGI "Certificate installed successfully, Turning on automatic updates..." - fi - - # Enable auto-update - ~/.acme.sh/acme.sh --upgrade --auto-upgrade - if [ $? -ne 0 ]; then - LOGE "Auto update setup failed, script exiting..." - exit 1 - else - LOGI "The certificate is installed and auto-renewal is turned on. Specific information is as follows:" - ls -lah ${certPath}/* - chmod 600 ${certPath}/privkey.pem - chmod 644 ${certPath}/fullchain.pem - fi - - # Prompt user to set panel paths after successful certificate installation - read -rp "Would you like to set this certificate for the panel? (y/n): " setPanel - if [[ "$setPanel" == "y" || "$setPanel" == "Y" ]]; then - local webCertFile="${certPath}/fullchain.pem" - local webKeyFile="${certPath}/privkey.pem" - - if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then - ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" - LOGI "Panel paths set for domain: $CF_Domain" - LOGI " - Certificate File: $webCertFile" - LOGI " - Private Key File: $webKeyFile" - echo -e "${green}Access URL: https://${CF_Domain}:${existing_port}${existing_webBasePath}${plain}" - restart - else - LOGE "Error: Certificate or private key file not found for domain: $CF_Domain." - fi - else - LOGI "Skipping panel path setting." - fi - else - show_menu - fi -} - -run_speedtest() { - # Check if Speedtest is already installed - if ! command -v speedtest &>/dev/null; then - # If not installed, determine installation method - if command -v snap &>/dev/null; then - # Use snap to install Speedtest - echo "Installing Speedtest using snap..." - snap install speedtest - else - # Fallback to using package managers - local pkg_manager="" - local speedtest_install_script="" - - if command -v dnf &>/dev/null; then - pkg_manager="dnf" - speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh" - elif command -v yum &>/dev/null; then - pkg_manager="yum" - speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh" - elif command -v apt-get &>/dev/null; then - pkg_manager="apt-get" - speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh" - elif command -v apt &>/dev/null; then - pkg_manager="apt" - speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh" - fi - - if [[ -z $pkg_manager ]]; then - echo "Error: Package manager not found. You may need to install Speedtest manually." - return 1 - else - echo "Installing Speedtest using $pkg_manager..." - curl -s $speedtest_install_script | bash - $pkg_manager install -y speedtest - fi - fi - fi - - speedtest -} - - - -ip_validation() { - ipv6_regex="^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$" - ipv4_regex="^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)$" -} - -iplimit_main() { - echo -e "\n${green}\t1.${plain} Install Fail2ban and configure IP Limit" - echo -e "${green}\t2.${plain} Change Ban Duration" - echo -e "${green}\t3.${plain} Unban Everyone" - echo -e "${green}\t4.${plain} Ban Logs" - echo -e "${green}\t5.${plain} Ban an IP Address" - echo -e "${green}\t6.${plain} Unban an IP Address" - echo -e "${green}\t7.${plain} Real-Time Logs" - echo -e "${green}\t8.${plain} Service Status" - echo -e "${green}\t9.${plain} Service Restart" - echo -e "${green}\t10.${plain} Uninstall Fail2ban and IP Limit" - echo -e "${green}\t0.${plain} Back to Main Menu" - read -rp "Choose an option: " choice - case "$choice" in - 0) - show_menu - ;; - 1) - confirm "Proceed with installation of Fail2ban & IP Limit?" "y" - if [[ $? == 0 ]]; then - install_iplimit - else - iplimit_main - fi - ;; - 2) - read -rp "Please enter new Ban Duration in Minutes [default 30]: " NUM - if [[ $NUM =~ ^[0-9]+$ ]]; then - create_iplimit_jails ${NUM} - if [[ $release == "alpine" ]]; then - rc-service fail2ban restart - else - systemctl restart fail2ban - fi - else - echo -e "${red}${NUM} is not a number! Please, try again.${plain}" - fi - iplimit_main - ;; - 3) - confirm "Proceed with Unbanning everyone from IP Limit jail?" "y" - if [[ $? == 0 ]]; then - fail2ban-client reload --restart --unban 3x-ipl - truncate -s 0 "${iplimit_banned_log_path}" - echo -e "${green}All users Unbanned successfully.${plain}" - iplimit_main - else - echo -e "${yellow}Cancelled.${plain}" - fi - iplimit_main - ;; - 4) - show_banlog - iplimit_main - ;; - 5) - read -rp "Enter the IP address you want to ban: " ban_ip - ip_validation - if [[ $ban_ip =~ $ipv4_regex || $ban_ip =~ $ipv6_regex ]]; then - fail2ban-client set 3x-ipl banip "$ban_ip" - echo -e "${green}IP Address ${ban_ip} has been banned successfully.${plain}" - else - echo -e "${red}Invalid IP address format! Please try again.${plain}" - fi - iplimit_main - ;; - 6) - read -rp "Enter the IP address you want to unban: " unban_ip - ip_validation - if [[ $unban_ip =~ $ipv4_regex || $unban_ip =~ $ipv6_regex ]]; then - fail2ban-client set 3x-ipl unbanip "$unban_ip" - echo -e "${green}IP Address ${unban_ip} has been unbanned successfully.${plain}" - else - echo -e "${red}Invalid IP address format! Please try again.${plain}" - fi - iplimit_main - ;; - 7) - tail -f /var/log/fail2ban.log - iplimit_main - ;; - 8) - service fail2ban status - iplimit_main - ;; - 9) - if [[ $release == "alpine" ]]; then - rc-service fail2ban restart - else - systemctl restart fail2ban - fi - iplimit_main - ;; - 10) - remove_iplimit - iplimit_main - ;; - *) - echo -e "${red}Invalid option. Please select a valid number.${plain}\n" - iplimit_main - ;; - esac -} - -install_iplimit() { - if ! command -v fail2ban-client &>/dev/null; then - echo -e "${green}Fail2ban is not installed. Installing now...!${plain}\n" - - # Check the OS and install necessary packages - case "${release}" in - ubuntu) - apt-get update - if [[ "${os_version}" -ge 24 ]]; then - apt-get install python3-pip -y - python3 -m pip install pyasynchat --break-system-packages - fi - apt-get install fail2ban -y - ;; - debian) - apt-get update - if [ "$os_version" -ge 12 ]; then - apt-get install -y python3-systemd - fi - apt-get install -y fail2ban - ;; - armbian) - apt-get update && apt-get install fail2ban -y - ;; - fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) - dnf -y update && dnf -y install fail2ban - ;; - centos) - if [[ "${VERSION_ID}" =~ ^7 ]]; then - yum update -y && yum install epel-release -y - yum -y install fail2ban - else - dnf -y update && dnf -y install fail2ban - fi - ;; - arch | manjaro | parch) - pacman -Syu --noconfirm fail2ban - ;; - alpine) - apk add fail2ban - ;; - *) - echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n" - exit 1 - ;; - esac - - if ! command -v fail2ban-client &>/dev/null; then - echo -e "${red}Fail2ban installation failed.${plain}\n" - exit 1 - fi - - echo -e "${green}Fail2ban installed successfully!${plain}\n" - else - echo -e "${yellow}Fail2ban is already installed.${plain}\n" - fi - - echo -e "${green}Configuring IP Limit...${plain}\n" - - # make sure there's no conflict for jail files - iplimit_remove_conflicts - - # Check if log file exists - if ! test -f "${iplimit_banned_log_path}"; then - touch ${iplimit_banned_log_path} - fi - - # Check if service log file exists so fail2ban won't return error - if ! test -f "${iplimit_log_path}"; then - touch ${iplimit_log_path} - fi - - # Create the iplimit jail files - # we didn't pass the bantime here to use the default value - create_iplimit_jails - - # Launching fail2ban - if [[ $release == "alpine" ]]; then - if [[ $(rc-service fail2ban status | grep -F 'status: started' -c) == 0 ]]; then - rc-service fail2ban start - else - rc-service fail2ban restart - fi - rc-update add fail2ban - else - if ! systemctl is-active --quiet fail2ban; then - systemctl start fail2ban - else - systemctl restart fail2ban - fi - systemctl enable fail2ban - fi - - echo -e "${green}IP Limit installed and configured successfully!${plain}\n" - before_show_menu -} - -remove_iplimit() { - echo -e "${green}\t1.${plain} Only remove IP Limit configurations" - echo -e "${green}\t2.${plain} Uninstall Fail2ban and IP Limit" - echo -e "${green}\t0.${plain} Back to Main Menu" - read -rp "Choose an option: " num - case "$num" in - 1) - rm -f /etc/fail2ban/filter.d/3x-ipl.conf - rm -f /etc/fail2ban/action.d/3x-ipl.conf - rm -f /etc/fail2ban/jail.d/3x-ipl.conf - if [[ $release == "alpine" ]]; then - rc-service fail2ban restart - else - systemctl restart fail2ban - fi - echo -e "${green}IP Limit removed successfully!${plain}\n" - before_show_menu - ;; - 2) - rm -rf /etc/fail2ban - if [[ $release == "alpine" ]]; then - rc-service fail2ban stop - else - systemctl stop fail2ban - fi - case "${release}" in - ubuntu | debian | armbian) - apt-get remove -y fail2ban - apt-get purge -y fail2ban -y - apt-get autoremove -y - ;; - fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) - dnf remove fail2ban -y - dnf autoremove -y - ;; - centos) - if [[ "${VERSION_ID}" =~ ^7 ]]; then - yum remove fail2ban -y - yum autoremove -y - else - dnf remove fail2ban -y - dnf autoremove -y - fi - ;; - arch | manjaro | parch) - pacman -Rns --noconfirm fail2ban - ;; - alpine) - apk del fail2ban - ;; - *) - echo -e "${red}Unsupported operating system. Please uninstall Fail2ban manually.${plain}\n" - exit 1 - ;; - esac - echo -e "${green}Fail2ban and IP Limit removed successfully!${plain}\n" - before_show_menu - ;; - 0) - show_menu - ;; - *) - echo -e "${red}Invalid option. Please select a valid number.${plain}\n" - remove_iplimit - ;; - esac -} - -show_banlog() { - local system_log="/var/log/fail2ban.log" - - echo -e "${green}Checking ban logs...${plain}\n" - - if [[ $release == "alpine" ]]; then - if [[ $(rc-service fail2ban status | grep -F 'status: started' -c) == 0 ]]; then - echo -e "${red}Fail2ban service is not running!${plain}\n" - return 1 - fi - else - if ! systemctl is-active --quiet fail2ban; then - echo -e "${red}Fail2ban service is not running!${plain}\n" - return 1 - fi - fi - - if [[ -f "$system_log" ]]; then - echo -e "${green}Recent system ban activities from fail2ban.log:${plain}" - grep "3x-ipl" "$system_log" | grep -E "Ban|Unban" | tail -n 10 || echo -e "${yellow}No recent system ban activities found${plain}" - echo "" - fi - - if [[ -f "${iplimit_banned_log_path}" ]]; then - echo -e "${green}3X-IPL ban log entries:${plain}" - if [[ -s "${iplimit_banned_log_path}" ]]; then - grep -v "INIT" "${iplimit_banned_log_path}" | tail -n 10 || echo -e "${yellow}No ban entries found${plain}" - else - echo -e "${yellow}Ban log file is empty${plain}" - fi - else - echo -e "${red}Ban log file not found at: ${iplimit_banned_log_path}${plain}" - fi - - echo -e "\n${green}Current jail status:${plain}" - fail2ban-client status 3x-ipl || echo -e "${yellow}Unable to get jail status${plain}" -} - -create_iplimit_jails() { - # Use default bantime if not passed => 30 minutes - local bantime="${1:-30}" - - # Uncomment 'allowipv6 = auto' in fail2ban.conf - sed -i 's/#allowipv6 = auto/allowipv6 = auto/g' /etc/fail2ban/fail2ban.conf - - # On Debian 12+ fail2ban's default backend should be changed to systemd - if [[ "${release}" == "debian" && ${os_version} -ge 12 ]]; then - sed -i '0,/action =/s/backend = auto/backend = systemd/' /etc/fail2ban/jail.conf - fi - - cat << EOF > /etc/fail2ban/jail.d/3x-ipl.conf -[3x-ipl] -enabled=true -backend=auto -filter=3x-ipl -action=3x-ipl -logpath=${iplimit_log_path} -maxretry=2 -findtime=32 -bantime=${bantime}m -EOF - - cat << EOF > /etc/fail2ban/filter.d/3x-ipl.conf -[Definition] -datepattern = ^%%Y/%%m/%%d %%H:%%M:%%S -failregex = \[LIMIT_IP\]\s*Email\s*=\s*.+\s*\|\|\s*SRC\s*=\s* -ignoreregex = -EOF - - cat << EOF > /etc/fail2ban/action.d/3x-ipl.conf -[INCLUDES] -before = iptables-allports.conf - -[Definition] -actionstart = -N f2b- - -A f2b- -j - -I -p -j f2b- - -actionstop = -D -p -j f2b- - - -X f2b- - -actioncheck = -n -L | grep -q 'f2b-[ \t]' - -actionban = -I f2b- 1 -s -j - echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") BAN [Email] = [IP] = banned for seconds." >> ${iplimit_banned_log_path} - -actionunban = -D f2b- -s -j - echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") UNBAN [Email] = [IP] = unbanned." >> ${iplimit_banned_log_path} - -[Init] -name = default -protocol = tcp -chain = INPUT -EOF - - echo -e "${green}Ip Limit jail files created with a bantime of ${bantime} minutes.${plain}" -} - -iplimit_remove_conflicts() { - local jail_files=( - /etc/fail2ban/jail.conf - /etc/fail2ban/jail.local - ) - - for file in "${jail_files[@]}"; do - # Check for [3x-ipl] config in jail file then remove it - if test -f "${file}" && grep -qw '3x-ipl' ${file}; then - sed -i "/\[3x-ipl\]/,/^$/d" ${file} - echo -e "${yellow}Removing conflicts of [3x-ipl] in jail (${file})!${plain}\n" - fi - done -} - -SSH_port_forwarding() { - local URL_lists=( - "https://api4.ipify.org" - "https://ipv4.icanhazip.com" - "https://v4.api.ipinfo.io/ip" - "https://ipv4.myexternalip.com/raw" - "https://4.ident.me" - "https://check-host.net/ip" - ) - local server_ip="" - for ip_address in "${URL_lists[@]}"; do - server_ip=$(curl -s --max-time 3 "${ip_address}" 2>/dev/null | tr -d '[:space:]') - if [[ -n "${server_ip}" ]]; then - break - fi - done - local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}') - local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') - local existing_listenIP=$(${xui_folder}/x-ui setting -getListen true | grep -Eo 'listenIP: .+' | awk '{print $2}') - local existing_cert=$(${xui_folder}/x-ui setting -getCert true | grep -Eo 'cert: .+' | awk '{print $2}') - local existing_key=$(${xui_folder}/x-ui setting -getCert true | grep -Eo 'key: .+' | awk '{print $2}') - - local config_listenIP="" - local listen_choice="" - - if [[ -n "$existing_cert" && -n "$existing_key" ]]; then - echo -e "${green}Panel is secure with SSL.${plain}" - before_show_menu - fi - if [[ -z "$existing_cert" && -z "$existing_key" && (-z "$existing_listenIP" || "$existing_listenIP" == "0.0.0.0") ]]; then - echo -e "\n${red}Warning: No Cert and Key found! The panel is not secure.${plain}" - echo "Please obtain a certificate or set up SSH port forwarding." - fi - - if [[ -n "$existing_listenIP" && "$existing_listenIP" != "0.0.0.0" && (-z "$existing_cert" && -z "$existing_key") ]]; then - echo -e "\n${green}Current SSH Port Forwarding Configuration:${plain}" - echo -e "Standard SSH command:" - echo -e "${yellow}ssh -L 2222:${existing_listenIP}:${existing_port} root@${server_ip}${plain}" - echo -e "\nIf using SSH key:" - echo -e "${yellow}ssh -i -L 2222:${existing_listenIP}:${existing_port} root@${server_ip}${plain}" - echo -e "\nAfter connecting, access the panel at:" - echo -e "${yellow}http://localhost:2222${existing_webBasePath}${plain}" - fi - - echo -e "\nChoose an option:" - echo -e "${green}1.${plain} Set listen IP" - echo -e "${green}2.${plain} Clear listen IP" - echo -e "${green}0.${plain} Back to Main Menu" - read -rp "Choose an option: " num - - case "$num" in - 1) - if [[ -z "$existing_listenIP" || "$existing_listenIP" == "0.0.0.0" ]]; then - echo -e "\nNo listenIP configured. Choose an option:" - echo -e "1. Use default IP (127.0.0.1)" - echo -e "2. Set a custom IP" - read -rp "Select an option (1 or 2): " listen_choice - - config_listenIP="127.0.0.1" - [[ "$listen_choice" == "2" ]] && read -rp "Enter custom IP to listen on: " config_listenIP - - ${xui_folder}/x-ui setting -listenIP "${config_listenIP}" >/dev/null 2>&1 - echo -e "${green}listen IP has been set to ${config_listenIP}.${plain}" - echo -e "\n${green}SSH Port Forwarding Configuration:${plain}" - echo -e "Standard SSH command:" - echo -e "${yellow}ssh -L 2222:${config_listenIP}:${existing_port} root@${server_ip}${plain}" - echo -e "\nIf using SSH key:" - echo -e "${yellow}ssh -i -L 2222:${config_listenIP}:${existing_port} root@${server_ip}${plain}" - echo -e "\nAfter connecting, access the panel at:" - echo -e "${yellow}http://localhost:2222${existing_webBasePath}${plain}" - restart - else - config_listenIP="${existing_listenIP}" - echo -e "${green}Current listen IP is already set to ${config_listenIP}.${plain}" - fi - ;; - 2) - ${xui_folder}/x-ui setting -listenIP 0.0.0.0 >/dev/null 2>&1 - echo -e "${green}Listen IP has been cleared.${plain}" - restart - ;; - 0) - show_menu - ;; - *) - echo -e "${red}Invalid option. Please select a valid number.${plain}\n" - SSH_port_forwarding - ;; - esac -} - show_usage() { echo -e "┌────────────────────────────────────────────────────────────────┐ │ ${blue}x-ui control menu usages (subcommands):${plain} │ @@ -2219,7 +195,11 @@ show_menu() { esac } -if [[ $# > 0 ]]; then +#============================================================================= +# CLI Argument Handling +#============================================================================= + +if [[ $# -gt 0 ]]; then case $1 in "start") check_install 0 && start 0 From fc87e2117b3673abd7e86e1ec6c9516d5dd9e656 Mon Sep 17 00:00:00 2001 From: Michael S2pac Date: Fri, 16 Jan 2026 11:52:13 +0300 Subject: [PATCH 2/2] Updated github workflow --- .github/workflows/release.yml | 147 +++++++++++++++++----------------- 1 file changed, 74 insertions(+), 73 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d68ea808..821966f4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,7 +6,7 @@ on: types: [published] push: branches: - - main + - test-branch tags: - "v*.*.*" paths: @@ -82,6 +82,7 @@ jobs: cp x-ui.service.debian x-ui/ cp x-ui.service.rhel x-ui/ cp x-ui.sh x-ui/ + cp -r lib x-ui/ mv x-ui/xui-release x-ui/x-ui mkdir x-ui/bin cd x-ui/bin @@ -152,75 +153,75 @@ jobs: # ================================= # Windows Build # ================================= - build-windows: - name: Build for Windows - permissions: - contents: write - strategy: - matrix: - platform: - - amd64 - runs-on: windows-latest - steps: - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Setup Go - uses: actions/setup-go@v6 - with: - go-version-file: go.mod - check-latest: true - - - name: Build 3X-UI for Windows - shell: pwsh - run: | - $env:CGO_ENABLED="1" - $env:GOOS="windows" - $env:GOARCH="amd64" - go build -ldflags "-w -s" -o xui-release.exe -v main.go - - mkdir x-ui - Copy-Item xui-release.exe x-ui\ - mkdir x-ui\bin - cd x-ui\bin - - # Download Xray for Windows - $Xray_URL = "https://github.com/XTLS/Xray-core/releases/download/v25.12.8/" - Invoke-WebRequest -Uri "${Xray_URL}Xray-windows-64.zip" -OutFile "Xray-windows-64.zip" - Expand-Archive -Path "Xray-windows-64.zip" -DestinationPath . - Remove-Item "Xray-windows-64.zip" - Remove-Item geoip.dat, geosite.dat -ErrorAction SilentlyContinue - Invoke-WebRequest -Uri "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat" -OutFile "geoip.dat" - Invoke-WebRequest -Uri "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat" -OutFile "geosite.dat" - Invoke-WebRequest -Uri "https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat" -OutFile "geoip_IR.dat" - Invoke-WebRequest -Uri "https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat" -OutFile "geosite_IR.dat" - Invoke-WebRequest -Uri "https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat" -OutFile "geoip_RU.dat" - Invoke-WebRequest -Uri "https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat" -OutFile "geosite_RU.dat" - Rename-Item xray.exe xray-windows-amd64.exe - cd .. - Copy-Item -Path ..\windows_files\* -Destination . -Recurse - cd .. - - - name: Package to Zip - shell: pwsh - run: | - Compress-Archive -Path .\x-ui -DestinationPath "x-ui-windows-amd64.zip" - - - name: Upload files to Artifacts - uses: actions/upload-artifact@v4 - with: - name: x-ui-windows-amd64 - path: ./x-ui-windows-amd64.zip - - - name: Upload files to GH release - uses: svenstaro/upload-release-action@v2 - if: | - (github.event_name == 'release' && github.event.action == 'published') || - (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) - with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - tag: ${{ github.ref }} - file: x-ui-windows-amd64.zip - asset_name: x-ui-windows-amd64.zip - overwrite: true - prerelease: true \ No newline at end of file +# build-windows: +# name: Build for Windows +# permissions: +# contents: write +# strategy: +# matrix: +# platform: +# - amd64 +# runs-on: windows-latest +# steps: +# - name: Checkout repository +# uses: actions/checkout@v5 +# +# - name: Setup Go +# uses: actions/setup-go@v6 +# with: +# go-version-file: go.mod +# check-latest: true +# +# - name: Build 3X-UI for Windows +# shell: pwsh +# run: | +# $env:CGO_ENABLED="1" +# $env:GOOS="windows" +# $env:GOARCH="amd64" +# go build -ldflags "-w -s" -o xui-release.exe -v main.go +# +# mkdir x-ui +# Copy-Item xui-release.exe x-ui\ +# mkdir x-ui\bin +# cd x-ui\bin +# +# # Download Xray for Windows +# $Xray_URL = "https://github.com/XTLS/Xray-core/releases/download/v25.12.8/" +# Invoke-WebRequest -Uri "${Xray_URL}Xray-windows-64.zip" -OutFile "Xray-windows-64.zip" +# Expand-Archive -Path "Xray-windows-64.zip" -DestinationPath . +# Remove-Item "Xray-windows-64.zip" +# Remove-Item geoip.dat, geosite.dat -ErrorAction SilentlyContinue +# Invoke-WebRequest -Uri "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat" -OutFile "geoip.dat" +# Invoke-WebRequest -Uri "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat" -OutFile "geosite.dat" +# Invoke-WebRequest -Uri "https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat" -OutFile "geoip_IR.dat" +# Invoke-WebRequest -Uri "https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat" -OutFile "geosite_IR.dat" +# Invoke-WebRequest -Uri "https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat" -OutFile "geoip_RU.dat" +# Invoke-WebRequest -Uri "https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat" -OutFile "geosite_RU.dat" +# Rename-Item xray.exe xray-windows-amd64.exe +# cd .. +# Copy-Item -Path ..\windows_files\* -Destination . -Recurse +# cd .. +# +# - name: Package to Zip +# shell: pwsh +# run: | +# Compress-Archive -Path .\x-ui -DestinationPath "x-ui-windows-amd64.zip" +# +# - name: Upload files to Artifacts +# uses: actions/upload-artifact@v4 +# with: +# name: x-ui-windows-amd64 +# path: ./x-ui-windows-amd64.zip +# +# - name: Upload files to GH release +# uses: svenstaro/upload-release-action@v2 +# if: | +# (github.event_name == 'release' && github.event.action == 'published') || +# (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) +# with: +# repo_token: ${{ secrets.GITHUB_TOKEN }} +# tag: ${{ github.ref }} +# file: x-ui-windows-amd64.zip +# asset_name: x-ui-windows-amd64.zip +# overwrite: true +# prerelease: true \ No newline at end of file