From 69ccdba734226ad6c1b8853d545bf8c8b462bdd6 Mon Sep 17 00:00:00 2001 From: Sanaei Date: Sun, 28 Dec 2025 00:03:33 +0100 Subject: [PATCH] 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