#!/bin/bash # lib/ssl.sh - SSL certificate management (acme.sh, Let's Encrypt, Cloudflare) # Include guard [[ -n "${__X_UI_SSL_INCLUDED:-}" ]] && return 0 __X_UI_SSL_INCLUDED=1 # Source dependencies source "${LIB_DIR}/common.sh" source "${LIB_DIR}/service.sh" install_acme() { # Check if acme.sh is already installed if command -v ~/.acme.sh/acme.sh &>/dev/null; then LOGI "acme.sh is already installed." return 0 fi LOGI "Installing acme.sh..." cd ~ || return 1 # Ensure you can change to the home directory curl -s https://get.acme.sh | sh if [ $? -ne 0 ]; then LOGE "Installation of acme.sh failed." return 1 else LOGI "Installation of acme.sh succeeded." fi return 0 } is_port_in_use() { local port="$1" if command -v ss >/dev/null 2>&1; then ss -ltn 2>/dev/null | awk -v p=":${port}$" '$4 ~ p {exit 0} END {exit 1}' return fi if command -v netstat >/dev/null 2>&1; then netstat -lnt 2>/dev/null | awk -v p=":${port} " '$4 ~ p {exit 0} END {exit 1}' return fi if command -v lsof >/dev/null 2>&1; then lsof -nP -iTCP:${port} -sTCP:LISTEN >/dev/null 2>&1 && return 0 fi return 1 } ssl_cert_issue_main() { echo -e "${green}\t1.${plain} Get SSL (Domain)" echo -e "${green}\t2.${plain} Revoke" echo -e "${green}\t3.${plain} Force Renew" echo -e "${green}\t4.${plain} Show Existing Domains" echo -e "${green}\t5.${plain} Set Cert paths for the panel" echo -e "${green}\t6.${plain} Get SSL for IP Address (6-day cert, auto-renews)" echo -e "${green}\t0.${plain} Back to Main Menu" read -rp "Choose an option: " choice case "$choice" in 0) show_menu ;; 1) ssl_cert_issue ssl_cert_issue_main ;; 2) local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \;) if [ -z "$domains" ]; then echo "No certificates found to revoke." else echo "Existing domains:" echo "$domains" read -rp "Please enter a domain from the list to revoke the certificate: " domain if echo "$domains" | grep -qw "$domain"; then ~/.acme.sh/acme.sh --revoke -d ${domain} LOGI "Certificate revoked for domain: $domain" else echo "Invalid domain entered." fi fi ssl_cert_issue_main ;; 3) local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \;) if [ -z "$domains" ]; then echo "No certificates found to renew." else echo "Existing domains:" echo "$domains" read -rp "Please enter a domain from the list to renew the SSL certificate: " domain if echo "$domains" | grep -qw "$domain"; then ~/.acme.sh/acme.sh --renew -d ${domain} --force LOGI "Certificate forcefully renewed for domain: $domain" else echo "Invalid domain entered." fi fi ssl_cert_issue_main ;; 4) local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \;) if [ -z "$domains" ]; then echo "No certificates found." else echo "Existing domains and their paths:" for domain in $domains; do local cert_path="/root/cert/${domain}/fullchain.pem" local key_path="/root/cert/${domain}/privkey.pem" if [[ -f "${cert_path}" && -f "${key_path}" ]]; then echo -e "Domain: ${domain}" echo -e "\tCertificate Path: ${cert_path}" echo -e "\tPrivate Key Path: ${key_path}" else echo -e "Domain: ${domain} - Certificate or Key missing." fi done fi ssl_cert_issue_main ;; 5) local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \;) if [ -z "$domains" ]; then echo "No certificates found." else echo "Available domains:" echo "$domains" read -rp "Please choose a domain to set the panel paths: " domain if echo "$domains" | grep -qw "$domain"; then local webCertFile="/root/cert/${domain}/fullchain.pem" local webKeyFile="/root/cert/${domain}/privkey.pem" if [[ -f "${webCertFile}" && -f "${webKeyFile}" ]]; then ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" echo "Panel paths set for domain: $domain" echo " - Certificate File: $webCertFile" echo " - Private Key File: $webKeyFile" restart else echo "Certificate or private key not found for domain: $domain." fi else echo "Invalid domain entered." fi fi ssl_cert_issue_main ;; 6) echo -e "${yellow}Let's Encrypt SSL Certificate for IP Address${plain}" echo -e "This will obtain a certificate for your server's IP using the shortlived profile." echo -e "${yellow}Certificate valid for ~6 days, auto-renews via acme.sh cron job.${plain}" echo -e "${yellow}Port 80 must be open and accessible from the internet.${plain}" confirm "Do you want to proceed?" "y" if [[ $? == 0 ]]; then ssl_cert_issue_for_ip fi ssl_cert_issue_main ;; *) echo -e "${red}Invalid option. Please select a valid number.${plain}\n" ssl_cert_issue_main ;; esac } ssl_cert_issue_for_ip() { LOGI "Starting automatic SSL certificate generation for server IP..." LOGI "Using Let's Encrypt shortlived profile (~6 days validity, auto-renews)" local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}') local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') # Get server IP local server_ip=$(curl -s --max-time 3 https://api.ipify.org) if [ -z "$server_ip" ]; then server_ip=$(curl -s --max-time 3 https://4.ident.me) fi if [ -z "$server_ip" ]; then LOGE "Failed to get server IP address" return 1 fi LOGI "Server IP detected: ${server_ip}" # Ask for optional IPv6 local ipv6_addr="" read -rp "Do you have an IPv6 address to include? (leave empty to skip): " ipv6_addr ipv6_addr="${ipv6_addr// /}" # Trim whitespace # check for acme.sh first if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then LOGI "acme.sh not found, installing..." install_acme if [ $? -ne 0 ]; then LOGE "Failed to install acme.sh" return 1 fi fi # install socat case "${release}" in ubuntu | debian | armbian) apt-get update >/dev/null 2>&1 && apt-get install socat -y >/dev/null 2>&1 ;; fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) dnf -y update >/dev/null 2>&1 && dnf -y install socat >/dev/null 2>&1 ;; centos) if [[ "${VERSION_ID}" =~ ^7 ]]; then yum -y update >/dev/null 2>&1 && yum -y install socat >/dev/null 2>&1 else dnf -y update >/dev/null 2>&1 && dnf -y install socat >/dev/null 2>&1 fi ;; arch | manjaro | parch) pacman -Sy --noconfirm socat >/dev/null 2>&1 ;; opensuse-tumbleweed | opensuse-leap) zypper refresh >/dev/null 2>&1 && zypper -q install -y socat >/dev/null 2>&1 ;; alpine) apk add socat curl openssl >/dev/null 2>&1 ;; *) LOGW "Unsupported OS for automatic socat installation" ;; esac # Create certificate directory certPath="/root/cert/ip" mkdir -p "$certPath" # Build domain arguments local domain_args="-d ${server_ip}" if [[ -n "$ipv6_addr" ]] && is_ipv6 "$ipv6_addr"; then domain_args="${domain_args} -d ${ipv6_addr}" LOGI "Including IPv6 address: ${ipv6_addr}" fi # Choose port for HTTP-01 listener (default 80, allow override) local WebPort="" read -rp "Port to use for ACME HTTP-01 listener (default 80): " WebPort WebPort="${WebPort:-80}" if ! [[ "${WebPort}" =~ ^[0-9]+$ ]] || ((WebPort < 1 || WebPort > 65535)); then LOGE "Invalid port provided. Falling back to 80." WebPort=80 fi LOGI "Using port ${WebPort} to issue certificate for IP: ${server_ip}" if [[ "${WebPort}" -ne 80 ]]; then LOGI "Reminder: Let's Encrypt still reaches port 80; forward external port 80 to ${WebPort} for validation." fi while true; do if is_port_in_use "${WebPort}"; then LOGI "Port ${WebPort} is currently in use." local alt_port="" read -rp "Enter another port for acme.sh standalone listener (leave empty to abort): " alt_port alt_port="${alt_port// /}" if [[ -z "${alt_port}" ]]; then LOGE "Port ${WebPort} is busy; cannot proceed with issuance." return 1 fi if ! [[ "${alt_port}" =~ ^[0-9]+$ ]] || ((alt_port < 1 || alt_port > 65535)); then LOGE "Invalid port provided." return 1 fi WebPort="${alt_port}" continue else LOGI "Port ${WebPort} is free and ready for standalone validation." break fi done # Reload command - restarts panel after renewal local reloadCmd="systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null" # issue the certificate for IP with shortlived profile ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt ~/.acme.sh/acme.sh --issue \ ${domain_args} \ --standalone \ --server letsencrypt \ --certificate-profile shortlived \ --days 6 \ --httpport ${WebPort} \ --force if [ $? -ne 0 ]; then LOGE "Failed to issue certificate for IP: ${server_ip}" LOGE "Make sure port ${WebPort} is open and the server is accessible from the internet" # Cleanup acme.sh data for both IPv4 and IPv6 if specified rm -rf ~/.acme.sh/${server_ip} 2>/dev/null [[ -n "$ipv6_addr" ]] && rm -rf ~/.acme.sh/${ipv6_addr} 2>/dev/null rm -rf ${certPath} 2>/dev/null return 1 else LOGI "Certificate issued successfully for IP: ${server_ip}" fi # Install the certificate # Note: acme.sh may report "Reload error" and exit non-zero if reloadcmd fails, # but the cert files are still installed. We check for files instead of exit code. ~/.acme.sh/acme.sh --installcert -d ${server_ip} \ --key-file "${certPath}/privkey.pem" \ --fullchain-file "${certPath}/fullchain.pem" \ --reloadcmd "${reloadCmd}" 2>&1 || true # Verify certificate files exist (don't rely on exit code - reloadcmd failure causes non-zero) if [[ ! -f "${certPath}/fullchain.pem" || ! -f "${certPath}/privkey.pem" ]]; then LOGE "Certificate files not found after installation" # Cleanup acme.sh data for both IPv4 and IPv6 if specified rm -rf ~/.acme.sh/${server_ip} 2>/dev/null [[ -n "$ipv6_addr" ]] && rm -rf ~/.acme.sh/${ipv6_addr} 2>/dev/null rm -rf ${certPath} 2>/dev/null return 1 fi LOGI "Certificate files installed successfully" # enable auto-renew ~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1 chmod 600 $certPath/privkey.pem 2>/dev/null chmod 644 $certPath/fullchain.pem 2>/dev/null # Set certificate paths for the panel local webCertFile="${certPath}/fullchain.pem" local webKeyFile="${certPath}/privkey.pem" if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" LOGI "Certificate configured for panel" LOGI " - Certificate File: $webCertFile" LOGI " - Private Key File: $webKeyFile" LOGI " - Validity: ~6 days (auto-renews via acme.sh cron)" echo -e "${green}Access URL: https://${server_ip}:${existing_port}${existing_webBasePath}${plain}" LOGI "Panel will restart to apply SSL certificate..." restart return 0 else LOGE "Certificate files not found after installation" return 1 fi } ssl_cert_issue() { local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}') local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') # check for acme.sh first if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then echo "acme.sh could not be found. we will install it" install_acme if [ $? -ne 0 ]; then LOGE "install acme failed, please check logs" exit 1 fi fi # install socat case "${release}" in ubuntu | debian | armbian) apt-get update >/dev/null 2>&1 && apt-get install socat -y >/dev/null 2>&1 ;; fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) dnf -y update >/dev/null 2>&1 && dnf -y install socat >/dev/null 2>&1 ;; centos) if [[ "${VERSION_ID}" =~ ^7 ]]; then yum -y update >/dev/null 2>&1 && yum -y install socat >/dev/null 2>&1 else dnf -y update >/dev/null 2>&1 && dnf -y install socat >/dev/null 2>&1 fi ;; arch | manjaro | parch) pacman -Sy --noconfirm socat >/dev/null 2>&1 ;; opensuse-tumbleweed | opensuse-leap) zypper refresh >/dev/null 2>&1 && zypper -q install -y socat >/dev/null 2>&1 ;; alpine) apk add socat curl openssl >/dev/null 2>&1 ;; *) LOGW "Unsupported OS for automatic socat installation" ;; esac if [ $? -ne 0 ]; then LOGE "install socat failed, please check logs" exit 1 else LOGI "install socat succeed..." fi # get the domain here, and we need to verify it local domain="" while true; do read -rp "Please enter your domain name: " domain domain="${domain// /}" # Trim whitespace if [[ -z "$domain" ]]; then LOGE "Domain name cannot be empty. Please try again." continue fi if ! is_domain "$domain"; then LOGE "Invalid domain format: ${domain}. Please enter a valid domain name." continue fi break done LOGD "Your domain is: ${domain}, checking it..." # check if there already exists a certificate local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}') if [ "${currentCert}" == "${domain}" ]; then local certInfo=$(~/.acme.sh/acme.sh --list) LOGE "System already has certificates for this domain. Cannot issue again. Current certificate details:" LOGI "$certInfo" exit 1 else LOGI "Your domain is ready for issuing certificates now..." fi # create a directory for the certificate certPath="/root/cert/${domain}" if [ ! -d "$certPath" ]; then mkdir -p "$certPath" else rm -rf "$certPath" mkdir -p "$certPath" fi # get the port number for the standalone server local WebPort=80 read -rp "Please choose which port to use (default is 80): " WebPort if [[ ${WebPort} -gt 65535 || ${WebPort} -lt 1 ]]; then LOGE "Your input ${WebPort} is invalid, will use default port 80." WebPort=80 fi LOGI "Will use port: ${WebPort} to issue certificates. Please make sure this port is open." # issue the certificate ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt ~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport ${WebPort} --force if [ $? -ne 0 ]; then LOGE "Issuing certificate failed, please check logs." rm -rf ~/.acme.sh/${domain} exit 1 else LOGE "Issuing certificate succeeded, installing certificates..." fi reloadCmd="x-ui restart" LOGI "Default --reloadcmd for ACME is: ${yellow}x-ui restart" LOGI "This command will run on every certificate issue and renew." read -rp "Would you like to modify --reloadcmd for ACME? (y/n): " setReloadcmd if [[ "$setReloadcmd" == "y" || "$setReloadcmd" == "Y" ]]; then echo -e "\n${green}\t1.${plain} Preset: systemctl reload nginx ; x-ui restart" echo -e "${green}\t2.${plain} Input your own command" echo -e "${green}\t0.${plain} Keep default reloadcmd" read -rp "Choose an option: " choice case "$choice" in 1) LOGI "Reloadcmd is: systemctl reload nginx ; x-ui restart" reloadCmd="systemctl reload nginx ; x-ui restart" ;; 2) LOGD "It's recommended to put x-ui restart at the end, so it won't raise an error if other services fails" read -rp "Please enter your reloadcmd (example: systemctl reload nginx ; x-ui restart): " reloadCmd LOGI "Your reloadcmd is: ${reloadCmd}" ;; *) LOGI "Keep default reloadcmd" ;; esac fi # install the certificate ~/.acme.sh/acme.sh --installcert -d ${domain} \ --key-file /root/cert/${domain}/privkey.pem \ --fullchain-file /root/cert/${domain}/fullchain.pem --reloadcmd "${reloadCmd}" if [ $? -ne 0 ]; then LOGE "Installing certificate failed, exiting." rm -rf ~/.acme.sh/${domain} exit 1 else LOGI "Installing certificate succeeded, enabling auto renew..." fi # enable auto-renew ~/.acme.sh/acme.sh --upgrade --auto-upgrade if [ $? -ne 0 ]; then LOGE "Auto renew failed, certificate details:" ls -lah cert/* chmod 600 $certPath/privkey.pem chmod 644 $certPath/fullchain.pem exit 1 else LOGI "Auto renew succeeded, certificate details:" ls -lah cert/* chmod 600 $certPath/privkey.pem chmod 644 $certPath/fullchain.pem fi # Prompt user to set panel paths after successful certificate installation read -rp "Would you like to set this certificate for the panel? (y/n): " setPanel if [[ "$setPanel" == "y" || "$setPanel" == "Y" ]]; then local webCertFile="/root/cert/${domain}/fullchain.pem" local webKeyFile="/root/cert/${domain}/privkey.pem" if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" LOGI "Panel paths set for domain: $domain" LOGI " - Certificate File: $webCertFile" LOGI " - Private Key File: $webKeyFile" echo -e "${green}Access URL: https://${domain}:${existing_port}${existing_webBasePath}${plain}" restart else LOGE "Error: Certificate or private key file not found for domain: $domain." fi else LOGI "Skipping panel path setting." fi } ssl_cert_issue_CF() { local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}') local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') LOGI "****** Instructions for Use ******" LOGI "Follow the steps below to complete the process:" LOGI "1. Cloudflare Registered E-mail." LOGI "2. Cloudflare Global API Key." LOGI "3. The Domain Name." LOGI "4. Once the certificate is issued, you will be prompted to set the certificate for the panel (optional)." LOGI "5. The script also supports automatic renewal of the SSL certificate after installation." confirm "Do you confirm the information and wish to proceed? [y/n]" "y" if [ $? -eq 0 ]; then # Check for acme.sh first if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then echo "acme.sh could not be found. We will install it." install_acme if [ $? -ne 0 ]; then LOGE "Install acme failed, please check logs." exit 1 fi fi CF_Domain="" LOGD "Please set a domain name:" read -rp "Input your domain here: " CF_Domain LOGD "Your domain name is set to: ${CF_Domain}" # Set up Cloudflare API details CF_GlobalKey="" CF_AccountEmail="" LOGD "Please set the API key:" read -rp "Input your key here: " CF_GlobalKey LOGD "Your API key is: ${CF_GlobalKey}" LOGD "Please set up registered email:" read -rp "Input your email here: " CF_AccountEmail LOGD "Your registered email address is: ${CF_AccountEmail}" # Set the default CA to Let's Encrypt ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt if [ $? -ne 0 ]; then LOGE "Default CA, Let'sEncrypt fail, script exiting..." exit 1 fi export CF_Key="${CF_GlobalKey}" export CF_Email="${CF_AccountEmail}" # Issue the certificate using Cloudflare DNS ~/.acme.sh/acme.sh --issue --dns dns_cf -d ${CF_Domain} -d *.${CF_Domain} --log --force if [ $? -ne 0 ]; then LOGE "Certificate issuance failed, script exiting..." exit 1 else LOGI "Certificate issued successfully, Installing..." fi # Install the certificate certPath="/root/cert/${CF_Domain}" if [ -d "$certPath" ]; then rm -rf ${certPath} fi mkdir -p ${certPath} if [ $? -ne 0 ]; then LOGE "Failed to create directory: ${certPath}" exit 1 fi reloadCmd="x-ui restart" LOGI "Default --reloadcmd for ACME is: ${yellow}x-ui restart" LOGI "This command will run on every certificate issue and renew." read -rp "Would you like to modify --reloadcmd for ACME? (y/n): " setReloadcmd if [[ "$setReloadcmd" == "y" || "$setReloadcmd" == "Y" ]]; then echo -e "\n${green}\t1.${plain} Preset: systemctl reload nginx ; x-ui restart" echo -e "${green}\t2.${plain} Input your own command" echo -e "${green}\t0.${plain} Keep default reloadcmd" read -rp "Choose an option: " choice case "$choice" in 1) LOGI "Reloadcmd is: systemctl reload nginx ; x-ui restart" reloadCmd="systemctl reload nginx ; x-ui restart" ;; 2) LOGD "It's recommended to put x-ui restart at the end, so it won't raise an error if other services fails" read -rp "Please enter your reloadcmd (example: systemctl reload nginx ; x-ui restart): " reloadCmd LOGI "Your reloadcmd is: ${reloadCmd}" ;; *) LOGI "Keep default reloadcmd" ;; esac fi ~/.acme.sh/acme.sh --installcert -d ${CF_Domain} -d *.${CF_Domain} \ --key-file ${certPath}/privkey.pem \ --fullchain-file ${certPath}/fullchain.pem --reloadcmd "${reloadCmd}" if [ $? -ne 0 ]; then LOGE "Certificate installation failed, script exiting..." exit 1 else LOGI "Certificate installed successfully, Turning on automatic updates..." fi # Enable auto-update ~/.acme.sh/acme.sh --upgrade --auto-upgrade if [ $? -ne 0 ]; then LOGE "Auto update setup failed, script exiting..." exit 1 else LOGI "The certificate is installed and auto-renewal is turned on. Specific information is as follows:" ls -lah ${certPath}/* chmod 600 ${certPath}/privkey.pem chmod 644 ${certPath}/fullchain.pem fi # Prompt user to set panel paths after successful certificate installation read -rp "Would you like to set this certificate for the panel? (y/n): " setPanel if [[ "$setPanel" == "y" || "$setPanel" == "Y" ]]; then local webCertFile="${certPath}/fullchain.pem" local webKeyFile="${certPath}/privkey.pem" if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" LOGI "Panel paths set for domain: $CF_Domain" LOGI " - Certificate File: $webCertFile" LOGI " - Private Key File: $webKeyFile" echo -e "${green}Access URL: https://${CF_Domain}:${existing_port}${existing_webBasePath}${plain}" restart else LOGE "Error: Certificate or private key file not found for domain: $CF_Domain." fi else LOGI "Skipping panel path setting." fi else show_menu fi }