From f000322a06f78273f48fb354768418c6fd8c3290 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=BE=D1=80=D0=B8=D1=81=D0=BE=D0=B2=20=D0=A1=D0=B5?= =?UTF-8?q?=D0=BC=D1=91=D0=BD?= Date: Fri, 12 Dec 2025 18:29:27 +0500 Subject: [PATCH 1/4] fix: handle CPU threshold error to prevent false notifications (#3603) Previously, when GetTgCpu() failed, the error was ignored and threshold defaulted to 0, causing notifications to be sent for any CPU usage. Now the job properly checks for errors and skips notifications if: - The threshold cannot be retrieved (error) - The threshold is not set or is 0 This ensures notifications are only sent when CPU usage exceeds the configured threshold value from settings. --- web/job/check_cpu_usage.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/web/job/check_cpu_usage.go b/web/job/check_cpu_usage.go index 2ea87747..b2208f31 100644 --- a/web/job/check_cpu_usage.go +++ b/web/job/check_cpu_usage.go @@ -22,7 +22,11 @@ func NewCheckCpuJob() *CheckCpuJob { // Run checks CPU usage over the last minute and sends a Telegram alert if it exceeds the threshold. func (j *CheckCpuJob) Run() { - threshold, _ := j.settingService.GetTgCpu() + threshold, err := j.settingService.GetTgCpu() + if err != nil || threshold <= 0 { + // If threshold cannot be retrieved or is not set, skip sending notifications + return + } // get latest status of server percent, err := cpu.Percent(1*time.Minute, false) From 4c797dc154b2b50f7d2d2bdfd28b7c5e30a78cb4 Mon Sep 17 00:00:00 2001 From: zd <67220210+0zd0@users.noreply.github.com> Date: Tue, 23 Dec 2025 17:43:25 +0300 Subject: [PATCH 2/4] fix: display of outbound traffic (#3604) shows the direction of traffic --- .env.example | 4 ++++ CONTRIBUTING.md | 5 +++++ web/html/xray.html | 4 ++-- 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 .env.example create mode 100644 CONTRIBUTING.md diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..62aa8c0b --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +XUI_DEBUG=true +XUI_DB_FOLDER=x-ui +XUI_LOG_FOLDER=x-ui +XUI_BIN_FOLDER=x-ui \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..5455e8b5 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,5 @@ +## Local Development Setup + +- Create a directory named `x-ui` in the project root +- Rename `.env.example` to `.env ` +- Run `main.go` \ No newline at end of file diff --git a/web/html/xray.html b/web/html/xray.html index 4dacd021..f4d89c84 100644 --- a/web/html/xray.html +++ b/web/html/xray.html @@ -527,10 +527,10 @@ findOutboundTraffic(o) { for (const otraffic of this.outboundsTraffic) { if (otraffic.tag == o.tag) { - return SizeFormatter.sizeFormat(otraffic.up) + ' / ' + SizeFormatter.sizeFormat(otraffic.down); + return `↑ ${SizeFormatter.sizeFormat(otraffic.up)} / ${SizeFormatter.sizeFormat(otraffic.down)} ↓` } } - return SizeFormatter.sizeFormat(0) + ' / ' + SizeFormatter.sizeFormat(0); + return `${SizeFormatter.sizeFormat(0)} / ${SizeFormatter.sizeFormat(0)}` }, findOutboundAddress(o) { serverObj = null; From 69ccdba734226ad6c1b8853d545bf8c8b462bdd6 Mon Sep 17 00:00:00 2001 From: Sanaei Date: Sun, 28 Dec 2025 00:03:33 +0100 Subject: [PATCH 3/4] Self-signed SSL (#3611) --- install.sh | 459 +++++++++++++++++++++++++++++++++++++++++++++++++--- update.sh | 467 ++++++++++++++++++++++++++++++++++++++++++++++++++++- x-ui.sh | 253 +++++++++++++++++++++++++++-- 3 files changed, 1136 insertions(+), 43 deletions(-) diff --git a/install.sh b/install.sh index fd730bca..0cf91b44 100644 --- a/install.sh +++ b/install.sh @@ -39,32 +39,46 @@ arch() { echo "Arch: $(arch)" +# Simple helpers +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 +} + install_base() { case "${release}" in ubuntu | debian | armbian) - apt-get update && apt-get install -y -q wget curl tar tzdata + apt-get update && apt-get install -y -q wget curl tar tzdata openssl socat ;; fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) - dnf -y update && dnf install -y -q wget curl tar tzdata + dnf -y update && dnf install -y -q wget curl tar tzdata openssl socat ;; centos) if [[ "${VERSION_ID}" =~ ^7 ]]; then - yum -y update && yum install -y wget curl tar tzdata + yum -y update && yum install -y wget curl tar tzdata openssl socat else - dnf -y update && dnf install -y -q wget curl tar tzdata + dnf -y update && dnf install -y -q wget curl tar tzdata openssl socat fi ;; arch | manjaro | parch) - pacman -Syu && pacman -Syu --noconfirm wget curl tar tzdata + pacman -Syu && pacman -Syu --noconfirm wget curl tar tzdata openssl socat ;; opensuse-tumbleweed | opensuse-leap) - zypper refresh && zypper -q install -y wget curl tar timezone + zypper refresh && zypper -q install -y wget curl tar timezone openssl socat ;; alpine) - apk update && apk add wget curl tar tzdata + apk update && apk add wget curl tar tzdata openssl socat ;; *) - apt-get update && apt-get install -y -q wget curl tar tzdata + apt-get update && apt-get install -y -q wget curl tar tzdata openssl socat ;; esac } @@ -75,10 +89,373 @@ gen_random_string() { echo "$random_string" } +install_acme() { + echo -e "${green}Installing acme.sh for SSL certificate management...${plain}" + cd ~ || return 1 + curl -s https://get.acme.sh | sh >/dev/null 2>&1 + if [ $? -ne 0 ]; then + echo -e "${red}Failed to install acme.sh${plain}" + return 1 + else + echo -e "${green}acme.sh installed successfully${plain}" + fi + return 0 +} + +setup_ssl_certificate() { + local domain="$1" + local server_ip="$2" + local existing_port="$3" + local existing_webBasePath="$4" + + echo -e "${green}Setting up SSL certificate...${plain}" + + # Check if acme.sh is installed + if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then + install_acme + if [ $? -ne 0 ]; then + echo -e "${yellow}Failed to install acme.sh, skipping SSL setup${plain}" + return 1 + fi + fi + + # Create certificate directory + local certPath="/root/cert/${domain}" + mkdir -p "$certPath" + + # Issue certificate + echo -e "${green}Issuing SSL certificate for ${domain}...${plain}" + echo -e "${yellow}Note: Port 80 must be open and accessible from the internet${plain}" + + ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt >/dev/null 2>&1 + ~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport 80 --force + + if [ $? -ne 0 ]; then + echo -e "${yellow}Failed to issue certificate for ${domain}${plain}" + echo -e "${yellow}Please ensure port 80 is open and try again later with: x-ui${plain}" + rm -rf ~/.acme.sh/${domain} 2>/dev/null + rm -rf "$certPath" 2>/dev/null + return 1 + fi + + # Install certificate + ~/.acme.sh/acme.sh --installcert -d ${domain} \ + --key-file /root/cert/${domain}/privkey.pem \ + --fullchain-file /root/cert/${domain}/fullchain.pem \ + --reloadcmd "systemctl restart x-ui" >/dev/null 2>&1 + + if [ $? -ne 0 ]; then + echo -e "${yellow}Failed to install certificate${plain}" + return 1 + fi + + # Enable auto-renew + ~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1 + chmod 755 $certPath/* 2>/dev/null + + # Set certificate for panel + local webCertFile="/root/cert/${domain}/fullchain.pem" + local webKeyFile="/root/cert/${domain}/privkey.pem" + + if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then + /usr/local/x-ui/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" >/dev/null 2>&1 + echo -e "${green}SSL certificate installed and configured successfully!${plain}" + return 0 + else + echo -e "${yellow}Certificate files not found${plain}" + return 1 + fi +} + +# Fallback: generate a self-signed certificate (not publicly trusted) +setup_self_signed_certificate() { + local name="$1" # domain or IP to place in SAN + local certDir="/root/cert/selfsigned" + + echo -e "${yellow}Generating a self-signed certificate (not publicly trusted)...${plain}" + + mkdir -p "$certDir" + + local sanExt="" + if is_ip "$name"; then + sanExt="IP:${name}" + else + sanExt="DNS:${name}" + fi + + # Use -addext if supported; fallback to config file if needed + openssl req -x509 -nodes -newkey rsa:2048 -days 365 \ + -keyout "${certDir}/privkey.pem" \ + -out "${certDir}/fullchain.pem" \ + -subj "/CN=${name}" \ + -addext "subjectAltName=${sanExt}" >/dev/null 2>&1 + + if [[ $? -ne 0 ]]; then + # Fallback via temporary config file (for older OpenSSL versions) + local tmpCfg="${certDir}/openssl.cnf" + cat > "$tmpCfg" </dev/null 2>&1 + rm -f "$tmpCfg" + fi + + if [[ ! -f "${certDir}/fullchain.pem" || ! -f "${certDir}/privkey.pem" ]]; then + echo -e "${red}Failed to generate self-signed certificate${plain}" + return 1 + fi + + chmod 755 ${certDir}/* 2>/dev/null + /usr/local/x-ui/x-ui cert -webCert "${certDir}/fullchain.pem" -webCertKey "${certDir}/privkey.pem" >/dev/null 2>&1 + echo -e "${yellow}Self-signed certificate configured. Browsers will show a warning.${plain}" + return 0 +} + +# Comprehensive manual SSL certificate issuance via acme.sh +ssl_cert_issue() { + local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep 'webBasePath:' | awk -F': ' '{print $2}' | tr -d '[:space:]' | sed 's#^/##') + local existing_port=$(/usr/local/x-ui/x-ui setting -show true | grep 'port:' | awk -F': ' '{print $2}' | tr -d '[:space:]') + + # check for acme.sh first + if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then + echo "acme.sh could not be found. Installing now..." + cd ~ || return 1 + curl -s https://get.acme.sh | sh + if [ $? -ne 0 ]; then + echo -e "${red}Failed to install acme.sh${plain}" + return 1 + else + echo -e "${green}acme.sh installed successfully${plain}" + fi + 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 + echo -e "${red}Domain name cannot be empty. Please try again.${plain}" + continue + fi + + if ! is_domain "$domain"; then + echo -e "${red}Invalid domain format: ${domain}. Please enter a valid domain name.${plain}" + continue + fi + + break + done + echo -e "${green}Your domain is: ${domain}, checking it...${plain}" + + # 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) + echo -e "${red}System already has certificates for this domain. Cannot issue again.${plain}" + echo -e "${yellow}Current certificate details:${plain}" + echo "$certInfo" + return 1 + else + echo -e "${green}Your domain is ready for issuing certificates now...${plain}" + 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 + echo -e "${yellow}Your input ${WebPort} is invalid, will use default port 80.${plain}" + WebPort=80 + fi + echo -e "${green}Will use port: ${WebPort} to issue certificates. Please make sure this port is open.${plain}" + + # Stop panel temporarily + echo -e "${yellow}Stopping panel temporarily...${plain}" + systemctl stop x-ui 2>/dev/null || rc-service x-ui stop 2>/dev/null + + # 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 + echo -e "${red}Issuing certificate failed, please check logs.${plain}" + rm -rf ~/.acme.sh/${domain} + systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null + return 1 + else + echo -e "${green}Issuing certificate succeeded, installing certificates...${plain}" + fi + + # Setup reload command + reloadCmd="systemctl restart x-ui || rc-service x-ui restart" + echo -e "${green}Default --reloadcmd for ACME is: ${yellow}systemctl restart x-ui || rc-service x-ui restart${plain}" + echo -e "${green}This command will run on every certificate issue and renew.${plain}" + 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 ; systemctl restart x-ui" + 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) + echo -e "${green}Reloadcmd is: systemctl reload nginx ; systemctl restart x-ui${plain}" + reloadCmd="systemctl reload nginx ; systemctl restart x-ui" + ;; + 2) + echo -e "${yellow}It's recommended to put x-ui restart at the end${plain}" + read -rp "Please enter your custom reloadcmd: " reloadCmd + echo -e "${green}Reloadcmd is: ${reloadCmd}${plain}" + ;; + *) + echo -e "${green}Keeping default reloadcmd${plain}" + ;; + 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 + echo -e "${red}Installing certificate failed, exiting.${plain}" + rm -rf ~/.acme.sh/${domain} + systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null + return 1 + else + echo -e "${green}Installing certificate succeeded, enabling auto renew...${plain}" + fi + + # enable auto-renew + ~/.acme.sh/acme.sh --upgrade --auto-upgrade + if [ $? -ne 0 ]; then + echo -e "${yellow}Auto renew setup had issues, certificate details:${plain}" + ls -lah /root/cert/${domain}/ + chmod 755 $certPath/* + else + echo -e "${green}Auto renew succeeded, certificate details:${plain}" + ls -lah /root/cert/${domain}/ + chmod 755 $certPath/* + fi + + # Restart panel + systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null + + # 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 + /usr/local/x-ui/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" + echo -e "${green}Certificate paths set for the panel${plain}" + echo -e "${green}Certificate File: $webCertFile${plain}" + echo -e "${green}Private Key File: $webKeyFile${plain}" + echo "" + echo -e "${green}Access URL: https://${domain}:${existing_port}/${existing_webBasePath}${plain}" + echo -e "${yellow}Panel will restart to apply SSL certificate...${plain}" + systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null + else + echo -e "${red}Error: Certificate or private key file not found for domain: $domain.${plain}" + fi + else + echo -e "${yellow}Skipping panel path setting.${plain}" + fi + + return 0 +} + +# Reusable interactive SSL setup (domain or self-signed) +# Sets global `SSL_HOST` to the chosen domain/IP for Access URL usage +prompt_and_setup_ssl() { + local panel_port="$1" + local web_base_path="$2" # expected without leading slash + local server_ip="$3" + + local ssl_choice="" + + echo -e "${yellow}Choose SSL certificate setup method:${plain}" + echo -e "${green}1.${plain} Let's Encrypt (domain required, recommended)" + echo -e "${green}2.${plain} Self-signed certificate (not publicly trusted)" + read -rp "Choose an option (default 2): " ssl_choice + ssl_choice="${ssl_choice// /}" # Trim whitespace + + # Default to 2 (self-signed) if not 1 + if [[ "$ssl_choice" != "1" ]]; then + ssl_choice="2" + fi + + case "$ssl_choice" in + 1) + # User chose Let's Encrypt domain option + echo -e "${green}Using ssl_cert_issue() for comprehensive domain setup...${plain}" + ssl_cert_issue + # Extract the domain that was used from the certificate + local cert_domain=$(~/.acme.sh/acme.sh --list 2>/dev/null | tail -1 | awk '{print $1}') + if [[ -n "${cert_domain}" ]]; then + SSL_HOST="${cert_domain}" + echo -e "${green}✓ SSL certificate configured successfully with domain: ${cert_domain}${plain}" + else + echo -e "${yellow}SSL setup may have completed, but domain extraction failed${plain}" + SSL_HOST="${server_ip}" + fi + ;; + 2) + # User chose self-signed option + # Stop panel if running + if [[ $release == "alpine" ]]; then + rc-service x-ui stop >/dev/null 2>&1 + else + systemctl stop x-ui >/dev/null 2>&1 + fi + echo -e "${yellow}Using server IP for self-signed certificate: ${server_ip}${plain}" + setup_self_signed_certificate "${server_ip}" + if [ $? -eq 0 ]; then + SSL_HOST="${server_ip}" + echo -e "${green}✓ Self-signed SSL configured successfully${plain}" + else + echo -e "${red}✗ Self-signed SSL setup failed${plain}" + SSL_HOST="${server_ip}" + fi + # Start panel after SSL is configured + if [[ $release == "alpine" ]]; then + rc-service x-ui start >/dev/null 2>&1 + else + systemctl start x-ui >/dev/null 2>&1 + fi + ;; + *) + echo -e "${red}Invalid option. Skipping SSL setup.${plain}" + SSL_HOST="${server_ip}" + ;; + esac +} + config_after_install() { local existing_hasDefaultCredential=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'hasDefaultCredential: .+' | awk '{print $2}') - local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}') + local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}' | sed 's#^/##') local existing_port=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') + # Properly detect empty cert by checking if cert: line exists and has content after it + local existing_cert=$(/usr/local/x-ui/x-ui setting -getCert true | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]') local URL_lists=( "https://api4.ipify.org" "https://ipv4.icanhazip.com" @@ -111,20 +488,50 @@ config_after_install() { fi /usr/local/x-ui/x-ui setting -username "${config_username}" -password "${config_password}" -port "${config_port}" -webBasePath "${config_webBasePath}" - echo -e "This is a fresh installation, generating random login info for security concerns:" - echo -e "###############################################" - echo -e "${green}Username: ${config_username}${plain}" - echo -e "${green}Password: ${config_password}${plain}" - echo -e "${green}Port: ${config_port}${plain}" + + echo "" + echo -e "${green}═══════════════════════════════════════════${plain}" + echo -e "${green} SSL Certificate Setup (MANDATORY) ${plain}" + echo -e "${green}═══════════════════════════════════════════${plain}" + echo -e "${yellow}For security, SSL certificate is required for all panels.${plain}" + echo -e "${yellow}Let's Encrypt requires a domain name (IP certificates are not issued).${plain}" + echo "" + + prompt_and_setup_ssl "${config_port}" "${config_webBasePath}" "${server_ip}" + + # Display final credentials and access information + echo "" + echo -e "${green}═══════════════════════════════════════════${plain}" + echo -e "${green} Panel Installation Complete! ${plain}" + echo -e "${green}═══════════════════════════════════════════${plain}" + echo -e "${green}Username: ${config_username}${plain}" + echo -e "${green}Password: ${config_password}${plain}" + echo -e "${green}Port: ${config_port}${plain}" echo -e "${green}WebBasePath: ${config_webBasePath}${plain}" - echo -e "${green}Access URL: http://${server_ip}:${config_port}/${config_webBasePath}${plain}" - echo -e "###############################################" + echo -e "${green}Access URL: https://${SSL_HOST}:${config_port}/${config_webBasePath}${plain}" + echo -e "${green}═══════════════════════════════════════════${plain}" + echo -e "${yellow}⚠ IMPORTANT: Save these credentials securely!${plain}" + echo -e "${yellow}⚠ SSL Certificate: Enabled and configured${plain}" else local config_webBasePath=$(gen_random_string 18) echo -e "${yellow}WebBasePath is missing or too short. Generating a new one...${plain}" /usr/local/x-ui/x-ui setting -webBasePath "${config_webBasePath}" echo -e "${green}New WebBasePath: ${config_webBasePath}${plain}" - echo -e "${green}Access URL: http://${server_ip}:${existing_port}/${config_webBasePath}${plain}" + + # If the panel is already installed but no certificate is configured, prompt for SSL now + if [[ -z "${existing_cert}" ]]; then + echo "" + echo -e "${green}═══════════════════════════════════════════${plain}" + echo -e "${green} SSL Certificate Setup (RECOMMENDED) ${plain}" + echo -e "${green}═══════════════════════════════════════════${plain}" + echo -e "${yellow}Let's Encrypt requires a domain name (IP certificates are not issued).${plain}" + echo "" + prompt_and_setup_ssl "${existing_port}" "${config_webBasePath}" "${server_ip}" + echo -e "${green}Access URL: https://${SSL_HOST}:${existing_port}/${config_webBasePath}${plain}" + else + # If a cert already exists, just show the access URL + echo -e "${green}Access URL: https://${server_ip}:${existing_port}/${config_webBasePath}${plain}" + fi fi else if [[ "$existing_hasDefaultCredential" == "true" ]]; then @@ -139,7 +546,23 @@ config_after_install() { echo -e "${green}Password: ${config_password}${plain}" echo -e "###############################################" else - echo -e "${green}Username, Password, and WebBasePath are properly set. Exiting...${plain}" + echo -e "${green}Username, Password, and WebBasePath are properly set.${plain}" + fi + + # Existing install: if no cert configured, prompt user to set domain or self-signed + # Properly detect empty cert by checking if cert: line exists and has content after it + existing_cert=$(/usr/local/x-ui/x-ui setting -getCert true | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]') + if [[ -z "$existing_cert" ]]; then + echo "" + echo -e "${green}═══════════════════════════════════════════${plain}" + echo -e "${green} SSL Certificate Setup (RECOMMENDED) ${plain}" + echo -e "${green}═══════════════════════════════════════════${plain}" + echo -e "${yellow}Let's Encrypt requires a domain name (IP certificates are not issued).${plain}" + echo "" + prompt_and_setup_ssl "${existing_port}" "${existing_webBasePath}" "${server_ip}" + echo -e "${green}Access URL: https://${SSL_HOST}:${existing_port}/${existing_webBasePath}${plain}" + else + echo -e "${green}SSL certificate already configured. No action needed.${plain}" fi fi diff --git a/update.sh b/update.sh index 66c0566c..ff0cea41 100755 --- a/update.sh +++ b/update.sh @@ -70,33 +70,415 @@ arch() { echo "Arch: $(arch)" +# Simple helpers +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 +} + +gen_random_string() { + local length="$1" + local random_string=$(LC_ALL=C tr -dc 'a-zA-Z0-9' /dev/null 2>&1 && apt-get install -y -q wget curl tar tzdata >/dev/null 2>&1 + apt-get update >/dev/null 2>&1 && apt-get install -y -q wget curl tar tzdata openssl socat >/dev/null 2>&1 ;; fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) - dnf -y update >/dev/null 2>&1 && dnf install -y -q wget curl tar tzdata >/dev/null 2>&1 + dnf -y update >/dev/null 2>&1 && dnf install -y -q wget curl tar tzdata openssl socat >/dev/null 2>&1 ;; centos) if [[ "${VERSION_ID}" =~ ^7 ]]; then - yum -y update >/dev/null 2>&1 && yum install -y -q wget curl tar tzdata >/dev/null 2>&1 + yum -y update >/dev/null 2>&1 && yum install -y -q wget curl tar tzdata openssl socat >/dev/null 2>&1 else - dnf -y update >/dev/null 2>&1 && dnf install -y -q wget curl tar tzdata >/dev/null 2>&1 + dnf -y update >/dev/null 2>&1 && dnf install -y -q wget curl tar tzdata openssl socat >/dev/null 2>&1 fi ;; arch | manjaro | parch) - pacman -Syu >/dev/null 2>&1 && pacman -Syu --noconfirm wget curl tar tzdata >/dev/null 2>&1 + pacman -Syu >/dev/null 2>&1 && pacman -Syu --noconfirm wget curl tar tzdata openssl socat >/dev/null 2>&1 ;; opensuse-tumbleweed | opensuse-leap) - zypper refresh >/dev/null 2>&1 && zypper -q install -y wget curl tar timezone >/dev/null 2>&1 + zypper refresh >/dev/null 2>&1 && zypper -q install -y wget curl tar timezone openssl socat >/dev/null 2>&1 ;; alpine) - apk update >/dev/null 2>&1 && apk add wget curl tar tzdata >/dev/null 2>&1 + apk update >/dev/null 2>&1 && apk add wget curl tar tzdata openssl socat >/dev/null 2>&1 ;; *) - apt-get update >/dev/null 2>&1 && apt install -y -q wget curl tar tzdata >/dev/null 2>&1 + apt-get update >/dev/null 2>&1 && apt install -y -q wget curl tar tzdata openssl socat >/dev/null 2>&1 + ;; + esac +} + +install_acme() { + echo -e "${green}Installing acme.sh for SSL certificate management...${plain}" + cd ~ || return 1 + curl -s https://get.acme.sh | sh >/dev/null 2>&1 + if [ $? -ne 0 ]; then + echo -e "${red}Failed to install acme.sh${plain}" + return 1 + else + echo -e "${green}acme.sh installed successfully${plain}" + fi + return 0 +} + +setup_ssl_certificate() { + local domain="$1" + local server_ip="$2" + local existing_port="$3" + local existing_webBasePath="$4" + + echo -e "${green}Setting up SSL certificate...${plain}" + + # Check if acme.sh is installed + if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then + install_acme + if [ $? -ne 0 ]; then + echo -e "${yellow}Failed to install acme.sh, skipping SSL setup${plain}" + return 1 + fi + fi + + # Create certificate directory + local certPath="/root/cert/${domain}" + mkdir -p "$certPath" + + # Issue certificate + echo -e "${green}Issuing SSL certificate for ${domain}...${plain}" + echo -e "${yellow}Note: Port 80 must be open and accessible from the internet${plain}" + + ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt >/dev/null 2>&1 + ~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport 80 --force + + if [ $? -ne 0 ]; then + echo -e "${yellow}Failed to issue certificate for ${domain}${plain}" + echo -e "${yellow}Please ensure port 80 is open and try again later with: x-ui${plain}" + rm -rf ~/.acme.sh/${domain} 2>/dev/null + rm -rf "$certPath" 2>/dev/null + return 1 + fi + + # Install certificate + ~/.acme.sh/acme.sh --installcert -d ${domain} \ + --key-file /root/cert/${domain}/privkey.pem \ + --fullchain-file /root/cert/${domain}/fullchain.pem \ + --reloadcmd "systemctl restart x-ui" >/dev/null 2>&1 + + if [ $? -ne 0 ]; then + echo -e "${yellow}Failed to install certificate${plain}" + return 1 + fi + + # Enable auto-renew + ~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1 + chmod 755 $certPath/* 2>/dev/null + + # Set certificate for panel + local webCertFile="/root/cert/${domain}/fullchain.pem" + local webKeyFile="/root/cert/${domain}/privkey.pem" + + if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then + /usr/local/x-ui/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" >/dev/null 2>&1 + echo -e "${green}SSL certificate installed and configured successfully!${plain}" + return 0 + else + echo -e "${yellow}Certificate files not found${plain}" + return 1 + fi +} + +# Fallback: generate a self-signed certificate (not publicly trusted) +setup_self_signed_certificate() { + local name="$1" # domain or IP to place in SAN + local certDir="/root/cert/selfsigned" + + echo -e "${yellow}Generating a self-signed certificate (not publicly trusted)...${plain}" + + mkdir -p "$certDir" + + local sanExt="" + if is_ip "$name"; then + sanExt="IP:${name}" + else + sanExt="DNS:${name}" + fi + + # Try -addext; fallback to config if not supported + openssl req -x509 -nodes -newkey rsa:2048 -days 365 \ + -keyout "${certDir}/privkey.pem" \ + -out "${certDir}/fullchain.pem" \ + -subj "/CN=${name}" \ + -addext "subjectAltName=${sanExt}" >/dev/null 2>&1 + + if [[ $? -ne 0 ]]; then + local tmpCfg="${certDir}/openssl.cnf" + cat > "$tmpCfg" </dev/null 2>&1 + rm -f "$tmpCfg" + fi + + if [[ ! -f "${certDir}/fullchain.pem" || ! -f "${certDir}/privkey.pem" ]]; then + echo -e "${red}Failed to generate self-signed certificate${plain}" + return 1 + fi + + chmod 755 ${certDir}/* 2>/dev/null + /usr/local/x-ui/x-ui cert -webCert "${certDir}/fullchain.pem" -webCertKey "${certDir}/privkey.pem" >/dev/null 2>&1 + echo -e "${yellow}Self-signed certificate configured. Browsers will show a warning.${plain}" + return 0 +} +# Comprehensive manual SSL certificate issuance via acme.sh +ssl_cert_issue() { + local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep 'webBasePath:' | awk -F': ' '{print $2}' | tr -d '[:space:]' | sed 's#^/##') + local existing_port=$(/usr/local/x-ui/x-ui setting -show true | grep 'port:' | awk -F': ' '{print $2}' | tr -d '[:space:]') + + # check for acme.sh first + if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then + echo "acme.sh could not be found. Installing now..." + cd ~ || return 1 + curl -s https://get.acme.sh | sh + if [ $? -ne 0 ]; then + echo -e "${red}Failed to install acme.sh${plain}" + return 1 + else + echo -e "${green}acme.sh installed successfully${plain}" + fi + 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 + echo -e "${red}Domain name cannot be empty. Please try again.${plain}" + continue + fi + + if ! is_domain "$domain"; then + echo -e "${red}Invalid domain format: ${domain}. Please enter a valid domain name.${plain}" + continue + fi + + break + done + echo -e "${green}Your domain is: ${domain}, checking it...${plain}" + + # 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) + echo -e "${red}System already has certificates for this domain. Cannot issue again.${plain}" + echo -e "${yellow}Current certificate details:${plain}" + echo "$certInfo" + return 1 + else + echo -e "${green}Your domain is ready for issuing certificates now...${plain}" + 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 + echo -e "${yellow}Your input ${WebPort} is invalid, will use default port 80.${plain}" + WebPort=80 + fi + echo -e "${green}Will use port: ${WebPort} to issue certificates. Please make sure this port is open.${plain}" + + # Stop panel temporarily + echo -e "${yellow}Stopping panel temporarily...${plain}" + systemctl stop x-ui 2>/dev/null || rc-service x-ui stop 2>/dev/null + + # 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 + echo -e "${red}Issuing certificate failed, please check logs.${plain}" + rm -rf ~/.acme.sh/${domain} + systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null + return 1 + else + echo -e "${green}Issuing certificate succeeded, installing certificates...${plain}" + fi + + # Setup reload command + reloadCmd="systemctl restart x-ui || rc-service x-ui restart" + echo -e "${green}Default --reloadcmd for ACME is: ${yellow}systemctl restart x-ui || rc-service x-ui restart${plain}" + echo -e "${green}This command will run on every certificate issue and renew.${plain}" + 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 ; systemctl restart x-ui" + 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) + echo -e "${green}Reloadcmd is: systemctl reload nginx ; systemctl restart x-ui${plain}" + reloadCmd="systemctl reload nginx ; systemctl restart x-ui" + ;; + 2) + echo -e "${yellow}It's recommended to put x-ui restart at the end${plain}" + read -rp "Please enter your custom reloadcmd: " reloadCmd + echo -e "${green}Reloadcmd is: ${reloadCmd}${plain}" + ;; + *) + echo -e "${green}Keeping default reloadcmd${plain}" + ;; + 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 + echo -e "${red}Installing certificate failed, exiting.${plain}" + rm -rf ~/.acme.sh/${domain} + systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null + return 1 + else + echo -e "${green}Installing certificate succeeded, enabling auto renew...${plain}" + fi + + # enable auto-renew + ~/.acme.sh/acme.sh --upgrade --auto-upgrade + if [ $? -ne 0 ]; then + echo -e "${yellow}Auto renew setup had issues, certificate details:${plain}" + ls -lah /root/cert/${domain}/ + chmod 755 $certPath/* + else + echo -e "${green}Auto renew succeeded, certificate details:${plain}" + ls -lah /root/cert/${domain}/ + chmod 755 $certPath/* + fi + + # Restart panel + systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null + + # 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 + /usr/local/x-ui/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" + echo -e "${green}Certificate paths set for the panel${plain}" + echo -e "${green}Certificate File: $webCertFile${plain}" + echo -e "${green}Private Key File: $webKeyFile${plain}" + echo "" + echo -e "${green}Access URL: https://${domain}:${existing_port}/${existing_webBasePath}${plain}" + echo -e "${yellow}Panel will restart to apply SSL certificate...${plain}" + systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null + else + echo -e "${red}Error: Certificate or private key file not found for domain: $domain.${plain}" + fi + else + echo -e "${yellow}Skipping panel path setting.${plain}" + fi + + return 0 +} +# Unified interactive SSL setup (domain or self-signed) +# Sets global `SSL_HOST` to the chosen domain/IP +prompt_and_setup_ssl() { + local panel_port="$1" + local web_base_path="$2" # expected without leading slash + local server_ip="$3" + + local ssl_choice="" + + echo -e "${yellow}Choose SSL certificate setup method:${plain}" + echo -e "${green}1.${plain} Let's Encrypt (domain required, recommended)" + echo -e "${green}2.${plain} Self-signed certificate (for testing/local use)" + read -rp "Choose an option (default 2): " ssl_choice + ssl_choice="${ssl_choice// /}" # Trim whitespace + + # Default to 2 (self-signed) if not 1 + if [[ "$ssl_choice" != "1" ]]; then + ssl_choice="2" + fi + + case "$ssl_choice" in + 1) + # User chose Let's Encrypt domain option + echo -e "${green}Using ssl_cert_issue() for comprehensive domain setup...${plain}" + ssl_cert_issue + # Extract the domain that was used from the certificate + local cert_domain=$(~/.acme.sh/acme.sh --list 2>/dev/null | tail -1 | awk '{print $1}') + if [[ -n "${cert_domain}" ]]; then + SSL_HOST="${cert_domain}" + echo -e "${green}✓ SSL certificate configured successfully with domain: ${cert_domain}${plain}" + else + echo -e "${yellow}SSL setup may have completed, but domain extraction failed${plain}" + SSL_HOST="${server_ip}" + fi + ;; + 2) + # User chose self-signed option + # Stop panel if running + if [[ $release == "alpine" ]]; then + rc-service x-ui stop >/dev/null 2>&1 + else + systemctl stop x-ui >/dev/null 2>&1 + fi + echo -e "${yellow}Using server IP for self-signed certificate: ${server_ip}${plain}" + setup_self_signed_certificate "${server_ip}" + if [ $? -eq 0 ]; then + SSL_HOST="${server_ip}" + echo -e "${green}✓ Self-signed SSL configured successfully${plain}" + else + echo -e "${red}✗ Self-signed SSL setup failed${plain}" + SSL_HOST="${server_ip}" + fi + # Start panel after SSL is configured + if [[ $release == "alpine" ]]; then + rc-service x-ui start >/dev/null 2>&1 + else + systemctl start x-ui >/dev/null 2>&1 + fi + ;; + 0) + echo -e "${yellow}Skipping SSL setup${plain}" + SSL_HOST="${server_ip}" + ;; + *) + echo -e "${red}Invalid option. Skipping SSL setup.${plain}" + SSL_HOST="${server_ip}" ;; esac } @@ -105,6 +487,75 @@ config_after_update() { echo -e "${yellow}x-ui settings:${plain}" /usr/local/x-ui/x-ui setting -show true /usr/local/x-ui/x-ui migrate + + # Properly detect empty cert by checking if cert: line exists and has content after it + local existing_cert=$(/usr/local/x-ui/x-ui setting -getCert true 2>/dev/null | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]') + local existing_port=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') + local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}' | sed 's#^/##') + + # Get server IP + 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_bin} -s --max-time 3 "${ip_address}" 2>/dev/null | tr -d '[:space:]') + if [[ -n "${server_ip}" ]]; then + break + fi + done + + # Handle missing/short webBasePath + if [[ ${#existing_webBasePath} -lt 4 ]]; then + echo -e "${yellow}WebBasePath is missing or too short. Generating a new one...${plain}" + local config_webBasePath=$(gen_random_string 18) + /usr/local/x-ui/x-ui setting -webBasePath "${config_webBasePath}" + existing_webBasePath="${config_webBasePath}" + echo -e "${green}New WebBasePath: ${config_webBasePath}${plain}" + fi + + # Check and prompt for SSL if missing + if [[ -z "$existing_cert" ]]; then + echo "" + echo -e "${red}═══════════════════════════════════════════${plain}" + echo -e "${red} ⚠ NO SSL CERTIFICATE DETECTED ⚠ ${plain}" + echo -e "${red}═══════════════════════════════════════════${plain}" + echo -e "${yellow}For security, SSL certificate is MANDATORY for all panels.${plain}" + echo -e "${yellow}Let's Encrypt requires a domain name; IP certs are not issued. Use self-signed for IP.${plain}" + echo "" + + if [[ -z "${server_ip}" ]]; then + echo -e "${red}Failed to detect server IP${plain}" + echo -e "${yellow}Please configure SSL manually using: x-ui${plain}" + return + fi + + # Prompt and setup SSL (domain or self-signed) + prompt_and_setup_ssl "${existing_port}" "${existing_webBasePath}" "${server_ip}" + + echo "" + echo -e "${green}═══════════════════════════════════════════${plain}" + echo -e "${green} Panel Access Information ${plain}" + echo -e "${green}═══════════════════════════════════════════${plain}" + echo -e "${green}Access URL: https://${SSL_HOST}:${existing_port}/${existing_webBasePath}${plain}" + echo -e "${green}═══════════════════════════════════════════${plain}" + echo -e "${yellow}⚠ SSL Certificate: Enabled and configured${plain}" + else + echo -e "${green}SSL certificate is already configured${plain}" + # Show access URL with existing certificate + local cert_domain=$(basename "$(dirname "$existing_cert")") + echo "" + echo -e "${green}═══════════════════════════════════════════${plain}" + echo -e "${green} Panel Access Information ${plain}" + echo -e "${green}═══════════════════════════════════════════${plain}" + echo -e "${green}Access URL: https://${cert_domain}:${existing_port}/${existing_webBasePath}${plain}" + echo -e "${green}═══════════════════════════════════════════${plain}" + fi } update_x-ui() { diff --git a/x-ui.sh b/x-ui.sh index f8ead9d6..8e62b72a 100644 --- a/x-ui.sh +++ b/x-ui.sh @@ -213,6 +213,57 @@ gen_random_string() { echo "$random_string" } +# Generate and configure a self-signed SSL certificate +setup_self_signed_certificate() { + local name="$1" # domain or IP to place in SAN + local certDir="/root/cert/selfsigned" + + LOGI "Generating a self-signed certificate (not publicly trusted)..." + + mkdir -p "$certDir" + + local sanExt="" + if [[ "$name" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ || "$name" =~ : ]]; then + sanExt="IP:${name}" + else + sanExt="DNS:${name}" + fi + + openssl req -x509 -nodes -newkey rsa:2048 -days 365 \ + -keyout "${certDir}/privkey.pem" \ + -out "${certDir}/fullchain.pem" \ + -subj "/CN=${name}" \ + -addext "subjectAltName=${sanExt}" >/dev/null 2>&1 + + if [[ $? -ne 0 ]]; then + local tmpCfg="${certDir}/openssl.cnf" + cat > "$tmpCfg" </dev/null 2>&1 + rm -f "$tmpCfg" + fi + + if [[ ! -f "${certDir}/fullchain.pem" || ! -f "${certDir}/privkey.pem" ]]; then + LOGE "Failed to generate self-signed certificate" + return 1 + fi + + chmod 755 ${certDir}/* >/dev/null 2>&1 + /usr/local/x-ui/x-ui cert -webCert "${certDir}/fullchain.pem" -webCertKey "${certDir}/privkey.pem" >/dev/null 2>&1 + LOGI "Self-signed certificate configured. Browsers will show a warning." + return 0 +} + reset_webbasepath() { echo -e "${yellow}Resetting Web Base Path${plain}" @@ -256,7 +307,7 @@ check_config() { 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=$(/usr/local/x-ui/x-ui setting -getCert true | grep -Eo 'cert: .+' | awk '{print $2}') + local existing_cert=$(/usr/local/x-ui/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) @@ -271,7 +322,22 @@ check_config() { echo -e "${green}Access URL: https://${server_ip}:${existing_port}${existing_webBasePath}${plain}" fi else - echo -e "${green}Access URL: http://${server_ip}:${existing_port}${existing_webBasePath}${plain}" + echo -e "${red}⚠ WARNING: No SSL certificate configured!${plain}" + read -rp "Generate a self-signed SSL certificate now? [y/N]: " gen_self + if [[ "$gen_self" == "y" || "$gen_self" == "Y" ]]; then + stop >/dev/null 2>&1 + setup_self_signed_certificate "${server_ip}" + if [[ $? -eq 0 ]]; then + restart >/dev/null 2>&1 + echo -e "${green}Access URL: https://${server_ip}:${existing_port}${existing_webBasePath}${plain}" + else + LOGE "Self-signed SSL setup failed." + echo -e "${yellow}You can try again via option 18 (SSL Certificate Management).${plain}" + 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 } @@ -958,6 +1024,7 @@ ssl_cert_issue_main() { 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} Auto SSL for Server IP" echo -e "${green}\t0.${plain} Back to Main Menu" read -rp "Choose an option: " choice @@ -1051,6 +1118,16 @@ ssl_cert_issue_main() { fi ssl_cert_issue_main ;; + 6) + echo -e "${yellow}Automatic SSL Certificate for Server IP${plain}" + echo -e "This will automatically obtain and configure an SSL certificate for your server's IP address." + echo -e "${yellow}Note: Let's Encrypt supports IP certificates. Make sure port 80 is open.${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" @@ -1059,6 +1136,134 @@ ssl_cert_issue_main() { esac } +ssl_cert_issue_for_ip() { + LOGI "Starting automatic SSL certificate generation for server IP..." + + local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}') + local existing_port=$(/usr/local/x-ui/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}" + + # 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 + + # check if certificate already exists for this IP + local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}') + if [ "${currentCert}" == "${server_ip}" ]; then + LOGI "Certificate already exists for IP: ${server_ip}" + certPath="/root/cert/${server_ip}" + else + # create directory for certificate + certPath="/root/cert/${server_ip}" + if [ ! -d "$certPath" ]; then + mkdir -p "$certPath" + else + rm -rf "$certPath" + mkdir -p "$certPath" + 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..." + + # issue the certificate for IP + ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt + ~/.acme.sh/acme.sh --issue -d ${server_ip} --listen-v6 --standalone --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" + rm -rf ~/.acme.sh/${server_ip} + return 1 + else + LOGI "Certificate issued successfully for IP: ${server_ip}" + fi + + # install the certificate + ~/.acme.sh/acme.sh --installcert -d ${server_ip} \ + --key-file /root/cert/${server_ip}/privkey.pem \ + --fullchain-file /root/cert/${server_ip}/fullchain.pem \ + --reloadcmd "x-ui restart" + + if [ $? -ne 0 ]; then + LOGE "Failed to install certificate" + rm -rf ~/.acme.sh/${server_ip} + return 1 + else + LOGI "Certificate installed successfully" + fi + + # enable auto-renew + ~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1 + chmod 755 $certPath/* + fi + + # Set certificate paths for the panel + local webCertFile="/root/cert/${server_ip}/fullchain.pem" + local webKeyFile="/root/cert/${server_ip}/privkey.pem" + + if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then + /usr/local/x-ui/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" + LOGI "Certificate configured for panel" + LOGI " - Certificate File: $webCertFile" + LOGI " - Private Key File: $webKeyFile" + 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=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}') local existing_port=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') @@ -1072,33 +1277,32 @@ ssl_cert_issue() { fi fi - # install socat second + # install socat case "${release}" in ubuntu | debian | armbian) - apt-get update && apt-get install socat -y + 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 && dnf -y install socat + dnf -y update >/dev/null 2>&1 && dnf -y install socat >/dev/null 2>&1 ;; centos) - if [[ "${VERSION_ID}" =~ ^7 ]]; then - yum -y update && yum -y install socat - else - dnf -y update && dnf -y install socat - fi + 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 + pacman -Sy --noconfirm socat >/dev/null 2>&1 ;; - opensuse-tumbleweed | opensuse-leap) - zypper refresh && zypper -q install -y socat + 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 + apk add socat curl openssl >/dev/null 2>&1 ;; *) - echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n" - exit 1 + LOGW "Unsupported OS for automatic socat installation" ;; esac if [ $? -ne 0 ]; then @@ -1110,7 +1314,22 @@ ssl_cert_issue() { # get the domain here, and we need to verify it local domain="" - read -rp "Please enter your domain name: " 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 From 260eedf8c417458b22bb17224035985a213bf3cc Mon Sep 17 00:00:00 2001 From: Wyatt Date: Sun, 28 Dec 2025 23:38:26 +0800 Subject: [PATCH 4/4] fix: add missing is_domain helper function to x-ui.sh (#3612) The is_domain function was being called in ssl_cert_issue() but was never defined in x-ui.sh, causing 'Invalid domain format' errors for valid domains. Added is_ipv4, is_ipv6, is_ip, and is_domain helper functions to match the definitions in install.sh and update.sh. Co-authored-by: wyatt --- x-ui.sh | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/x-ui.sh b/x-ui.sh index 8e62b72a..32ddd8e0 100644 --- a/x-ui.sh +++ b/x-ui.sh @@ -19,6 +19,20 @@ 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