mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-01-13 01:02:46 +00:00
Merge branch 'main' into dev-systemd-environmentfile
This commit is contained in:
commit
21827ed897
11 changed files with 1315 additions and 98 deletions
4
.env.example
Normal file
4
.env.example
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
XUI_DEBUG=true
|
||||||
|
XUI_DB_FOLDER=x-ui
|
||||||
|
XUI_LOG_FOLDER=x-ui
|
||||||
|
XUI_BIN_FOLDER=x-ui
|
||||||
5
CONTRIBUTING.md
Normal file
5
CONTRIBUTING.md
Normal file
|
|
@ -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`
|
||||||
|
|
@ -109,7 +109,7 @@ func GetLogFolder() string {
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
return filepath.Join(".", "log")
|
return filepath.Join(".", "log")
|
||||||
}
|
}
|
||||||
return "/var/log"
|
return "/var/log/x-ui"
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyFile(src, dst string) error {
|
func copyFile(src, dst string) error {
|
||||||
|
|
|
||||||
474
install.sh
474
install.sh
|
|
@ -39,32 +39,46 @@ arch() {
|
||||||
|
|
||||||
echo "Arch: $(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() {
|
install_base() {
|
||||||
case "${release}" in
|
case "${release}" in
|
||||||
ubuntu | debian | armbian)
|
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)
|
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)
|
centos)
|
||||||
if [[ "${VERSION_ID}" =~ ^7 ]]; then
|
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
|
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
|
fi
|
||||||
;;
|
;;
|
||||||
arch | manjaro | parch)
|
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)
|
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)
|
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
|
esac
|
||||||
}
|
}
|
||||||
|
|
@ -75,10 +89,373 @@ gen_random_string() {
|
||||||
echo "$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" <<EOF
|
||||||
|
[req]
|
||||||
|
distinguished_name=req_distinguished_name
|
||||||
|
req_extensions=v3_req
|
||||||
|
[req_distinguished_name]
|
||||||
|
[v3_req]
|
||||||
|
subjectAltName=${sanExt}
|
||||||
|
EOF
|
||||||
|
openssl req -x509 -nodes -newkey rsa:2048 -days 365 \
|
||||||
|
-keyout "${certDir}/privkey.pem" \
|
||||||
|
-out "${certDir}/fullchain.pem" \
|
||||||
|
-subj "/CN=${name}" \
|
||||||
|
-config "$tmpCfg" -extensions v3_req >/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() {
|
config_after_install() {
|
||||||
local existing_hasDefaultCredential=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'hasDefaultCredential: .+' | awk '{print $2}')
|
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}')
|
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=(
|
local URL_lists=(
|
||||||
"https://api4.ipify.org"
|
"https://api4.ipify.org"
|
||||||
"https://ipv4.icanhazip.com"
|
"https://ipv4.icanhazip.com"
|
||||||
|
|
@ -111,20 +488,50 @@ config_after_install() {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
/usr/local/x-ui/x-ui setting -username "${config_username}" -password "${config_password}" -port "${config_port}" -webBasePath "${config_webBasePath}"
|
/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 ""
|
||||||
echo -e "${green}Username: ${config_username}${plain}"
|
echo -e "${green}═══════════════════════════════════════════${plain}"
|
||||||
echo -e "${green}Password: ${config_password}${plain}"
|
echo -e "${green} SSL Certificate Setup (MANDATORY) ${plain}"
|
||||||
echo -e "${green}Port: ${config_port}${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}WebBasePath: ${config_webBasePath}${plain}"
|
||||||
echo -e "${green}Access URL: http://${server_ip}:${config_port}/${config_webBasePath}${plain}"
|
echo -e "${green}Access URL: https://${SSL_HOST}:${config_port}/${config_webBasePath}${plain}"
|
||||||
echo -e "###############################################"
|
echo -e "${green}═══════════════════════════════════════════${plain}"
|
||||||
|
echo -e "${yellow}⚠ IMPORTANT: Save these credentials securely!${plain}"
|
||||||
|
echo -e "${yellow}⚠ SSL Certificate: Enabled and configured${plain}"
|
||||||
else
|
else
|
||||||
local config_webBasePath=$(gen_random_string 18)
|
local config_webBasePath=$(gen_random_string 18)
|
||||||
echo -e "${yellow}WebBasePath is missing or too short. Generating a new one...${plain}"
|
echo -e "${yellow}WebBasePath is missing or too short. Generating a new one...${plain}"
|
||||||
/usr/local/x-ui/x-ui setting -webBasePath "${config_webBasePath}"
|
/usr/local/x-ui/x-ui setting -webBasePath "${config_webBasePath}"
|
||||||
echo -e "${green}New WebBasePath: ${config_webBasePath}${plain}"
|
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
|
fi
|
||||||
else
|
else
|
||||||
if [[ "$existing_hasDefaultCredential" == "true" ]]; then
|
if [[ "$existing_hasDefaultCredential" == "true" ]]; then
|
||||||
|
|
@ -139,7 +546,23 @@ config_after_install() {
|
||||||
echo -e "${green}Password: ${config_password}${plain}"
|
echo -e "${green}Password: ${config_password}${plain}"
|
||||||
echo -e "###############################################"
|
echo -e "###############################################"
|
||||||
else
|
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
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -218,7 +641,22 @@ install_x-ui() {
|
||||||
# Update x-ui cli and se set permission
|
# Update x-ui cli and se set permission
|
||||||
mv -f /usr/bin/x-ui-temp /usr/bin/x-ui
|
mv -f /usr/bin/x-ui-temp /usr/bin/x-ui
|
||||||
chmod +x /usr/bin/x-ui
|
chmod +x /usr/bin/x-ui
|
||||||
|
mkdir -p /var/log/x-ui
|
||||||
config_after_install
|
config_after_install
|
||||||
|
|
||||||
|
# Etckeeper compatibility
|
||||||
|
if [ -d "/etc/.git" ]; then
|
||||||
|
if [ -f "/etc/.gitignore" ]; then
|
||||||
|
if ! grep -q "x-ui/x-ui.db" "/etc/.gitignore"; then
|
||||||
|
echo "" >> "/etc/.gitignore"
|
||||||
|
echo "x-ui/x-ui.db" >> "/etc/.gitignore"
|
||||||
|
echo -e "${green}Added x-ui.db to /etc/.gitignore for etckeeper${plain}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "x-ui/x-ui.db" > "/etc/.gitignore"
|
||||||
|
echo -e "${green}Created /etc/.gitignore and added x-ui.db for etckeeper${plain}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ $release == "alpine" ]]; then
|
if [[ $release == "alpine" ]]; then
|
||||||
wget --inet4-only -O /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc
|
wget --inet4-only -O /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc
|
||||||
|
|
|
||||||
468
update.sh
468
update.sh
|
|
@ -70,33 +70,415 @@ arch() {
|
||||||
|
|
||||||
echo "Arch: $(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/urandom | fold -w "$length" | head -n 1)
|
||||||
|
echo "$random_string"
|
||||||
|
}
|
||||||
|
|
||||||
install_base() {
|
install_base() {
|
||||||
echo -e "${green}Updating and install dependency packages...${plain}"
|
echo -e "${green}Updating and install dependency packages...${plain}"
|
||||||
case "${release}" in
|
case "${release}" in
|
||||||
ubuntu | debian | armbian)
|
ubuntu | debian | armbian)
|
||||||
apt-get update >/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)
|
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)
|
centos)
|
||||||
if [[ "${VERSION_ID}" =~ ^7 ]]; then
|
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
|
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
|
fi
|
||||||
;;
|
;;
|
||||||
arch | manjaro | parch)
|
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)
|
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)
|
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" <<EOF
|
||||||
|
[req]
|
||||||
|
distinguished_name=req_distinguished_name
|
||||||
|
req_extensions=v3_req
|
||||||
|
[req_distinguished_name]
|
||||||
|
[v3_req]
|
||||||
|
subjectAltName=${sanExt}
|
||||||
|
EOF
|
||||||
|
openssl req -x509 -nodes -newkey rsa:2048 -days 365 \
|
||||||
|
-keyout "${certDir}/privkey.pem" \
|
||||||
|
-out "${certDir}/fullchain.pem" \
|
||||||
|
-subj "/CN=${name}" \
|
||||||
|
-config "$tmpCfg" -extensions v3_req >/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
|
esac
|
||||||
}
|
}
|
||||||
|
|
@ -105,6 +487,75 @@ config_after_update() {
|
||||||
echo -e "${yellow}x-ui settings:${plain}"
|
echo -e "${yellow}x-ui settings:${plain}"
|
||||||
/usr/local/x-ui/x-ui setting -show true
|
/usr/local/x-ui/x-ui setting -show true
|
||||||
/usr/local/x-ui/x-ui migrate
|
/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() {
|
update_x-ui() {
|
||||||
|
|
@ -204,6 +655,7 @@ update_x-ui() {
|
||||||
|
|
||||||
chmod +x /usr/local/x-ui/x-ui.sh >/dev/null 2>&1
|
chmod +x /usr/local/x-ui/x-ui.sh >/dev/null 2>&1
|
||||||
chmod +x /usr/bin/x-ui >/dev/null 2>&1
|
chmod +x /usr/bin/x-ui >/dev/null 2>&1
|
||||||
|
mkdir -p /var/log/x-ui >/dev/null 2>&1
|
||||||
|
|
||||||
echo -e "${green}Changing owner...${plain}"
|
echo -e "${green}Changing owner...${plain}"
|
||||||
chown -R root:root /usr/local/x-ui >/dev/null 2>&1
|
chown -R root:root /usr/local/x-ui >/dev/null 2>&1
|
||||||
|
|
|
||||||
2
web/assets/css/custom.min.css
vendored
2
web/assets/css/custom.min.css
vendored
File diff suppressed because one or more lines are too long
|
|
@ -120,6 +120,10 @@
|
||||||
oldAllSetting: new AllSetting(),
|
oldAllSetting: new AllSetting(),
|
||||||
allSetting: new AllSetting(),
|
allSetting: new AllSetting(),
|
||||||
saveBtnDisable: true,
|
saveBtnDisable: true,
|
||||||
|
entryHost: null,
|
||||||
|
entryPort: null,
|
||||||
|
entryProtocol: null,
|
||||||
|
entryIsIP: false,
|
||||||
user: {},
|
user: {},
|
||||||
lang: LanguageManager.getLanguage(),
|
lang: LanguageManager.getLanguage(),
|
||||||
inboundOptions: [],
|
inboundOptions: [],
|
||||||
|
|
@ -233,6 +237,31 @@
|
||||||
loading(spinning = true) {
|
loading(spinning = true) {
|
||||||
this.loadingStates.spinning = spinning;
|
this.loadingStates.spinning = spinning;
|
||||||
},
|
},
|
||||||
|
_isIp(h) {
|
||||||
|
if (typeof h !== "string") return false;
|
||||||
|
|
||||||
|
// IPv4: four dot-separated octets 0-255
|
||||||
|
const v4 = h.split(".");
|
||||||
|
if (
|
||||||
|
v4.length === 4 &&
|
||||||
|
v4.every(p => /^\d{1,3}$/.test(p) && Number(p) <= 255)
|
||||||
|
) return true;
|
||||||
|
|
||||||
|
// IPv6: hex groups, optional single :: compression
|
||||||
|
if (!h.includes(":") || h.includes(":::")) return false;
|
||||||
|
const parts = h.split("::");
|
||||||
|
if (parts.length > 2) return false;
|
||||||
|
|
||||||
|
const splitGroups = s => (s ? s.split(":").filter(Boolean) : []);
|
||||||
|
const head = splitGroups(parts[0]);
|
||||||
|
const tail = splitGroups(parts[1]);
|
||||||
|
const validGroup = seg => /^[0-9a-fA-F]{1,4}$/.test(seg);
|
||||||
|
|
||||||
|
if (![...head, ...tail].every(validGroup)) return false;
|
||||||
|
const groups = head.length + tail.length;
|
||||||
|
|
||||||
|
return parts.length === 2 ? groups < 8 : groups === 8;
|
||||||
|
},
|
||||||
async getAllSetting() {
|
async getAllSetting() {
|
||||||
const msg = await HttpUtil.post("/panel/setting/all");
|
const msg = await HttpUtil.post("/panel/setting/all");
|
||||||
|
|
||||||
|
|
@ -307,16 +336,41 @@
|
||||||
this.loading(true);
|
this.loading(true);
|
||||||
const msg = await HttpUtil.post("/panel/setting/restartPanel");
|
const msg = await HttpUtil.post("/panel/setting/restartPanel");
|
||||||
this.loading(false);
|
this.loading(false);
|
||||||
if (msg.success) {
|
if (!msg.success) return;
|
||||||
this.loading(true);
|
|
||||||
await PromiseUtil.sleep(5000);
|
this.loading(true);
|
||||||
var { webCertFile, webKeyFile, webDomain: host, webPort: port, webBasePath: base } = this.allSetting;
|
await PromiseUtil.sleep(5000);
|
||||||
if (host == this.oldAllSetting.webDomain) host = null;
|
|
||||||
if (port == this.oldAllSetting.webPort) port = null;
|
const { webDomain, webPort, webBasePath, webCertFile, webKeyFile } = this.allSetting;
|
||||||
const isTLS = webCertFile !== "" || webKeyFile !== "";
|
const newProtocol = (webCertFile || webKeyFile) ? "https:" : "http:";
|
||||||
const url = URLBuilder.buildURL({ host, port, isTLS, base, path: "panel/settings" });
|
|
||||||
window.location.replace(url);
|
let base = webBasePath ? webBasePath.replace(/^\//, "") : "";
|
||||||
|
if (base && !base.endsWith("/")) base += "/";
|
||||||
|
|
||||||
|
if (!this.entryIsIP) {
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.pathname = `/${base}panel/settings`;
|
||||||
|
url.protocol = newProtocol;
|
||||||
|
window.location.replace(url.toString());
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let finalHost = this.entryHost;
|
||||||
|
let finalPort = this.entryPort || "";
|
||||||
|
|
||||||
|
if (webDomain && this._isIp(webDomain)) {
|
||||||
|
finalHost = webDomain;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (webPort && Number(webPort) !== Number(this.entryPort)) {
|
||||||
|
finalPort = String(webPort);
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(`${newProtocol}//${finalHost}`);
|
||||||
|
if (finalPort) url.port = finalPort;
|
||||||
|
url.pathname = `/${base}panel/settings`;
|
||||||
|
|
||||||
|
window.location.replace(url.toString());
|
||||||
},
|
},
|
||||||
toggleTwoFactor(newValue) {
|
toggleTwoFactor(newValue) {
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
|
|
@ -568,6 +622,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
|
this.entryHost = window.location.hostname;
|
||||||
|
this.entryPort = window.location.port;
|
||||||
|
this.entryProtocol = window.location.protocol;
|
||||||
|
this.entryIsIP = this._isIp(this.entryHost);
|
||||||
await this.getAllSetting();
|
await this.getAllSetting();
|
||||||
await this.loadInboundTags();
|
await this.loadInboundTags();
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|
|
||||||
|
|
@ -527,10 +527,10 @@
|
||||||
findOutboundTraffic(o) {
|
findOutboundTraffic(o) {
|
||||||
for (const otraffic of this.outboundsTraffic) {
|
for (const otraffic of this.outboundsTraffic) {
|
||||||
if (otraffic.tag == o.tag) {
|
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) {
|
findOutboundAddress(o) {
|
||||||
serverObj = null;
|
serverObj = null;
|
||||||
|
|
|
||||||
|
|
@ -529,6 +529,18 @@ func (s *ServerService) GetXrayVersions() ([]string, error) {
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Check HTTP status code - GitHub API returns object instead of array on error
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||||
|
var errorResponse struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
if json.Unmarshal(bodyBytes, &errorResponse) == nil && errorResponse.Message != "" {
|
||||||
|
return nil, fmt.Errorf("GitHub API error: %s", errorResponse.Message)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("GitHub API returned status %d: %s", resp.StatusCode, resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
buffer := bytes.NewBuffer(make([]byte, bufferSize))
|
buffer := bytes.NewBuffer(make([]byte, bufferSize))
|
||||||
buffer.Reset()
|
buffer.Reset()
|
||||||
if _, err := buffer.ReadFrom(resp.Body); err != nil {
|
if _, err := buffer.ReadFrom(resp.Body); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,10 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If Start is called again (e.g. during reload), ensure any previous long-polling
|
||||||
|
// loop is stopped before creating a new bot / receiver.
|
||||||
|
StopBot()
|
||||||
|
|
||||||
// Initialize hash storage to store callback queries
|
// Initialize hash storage to store callback queries
|
||||||
hashStorage = global.NewHashStorage(20 * time.Minute)
|
hashStorage = global.NewHashStorage(20 * time.Minute)
|
||||||
|
|
||||||
|
|
@ -207,6 +211,7 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parsedAdminIds := make([]int64, 0)
|
||||||
// Parse admin IDs from comma-separated string
|
// Parse admin IDs from comma-separated string
|
||||||
if tgBotID != "" {
|
if tgBotID != "" {
|
||||||
for _, adminID := range strings.Split(tgBotID, ",") {
|
for _, adminID := range strings.Split(tgBotID, ",") {
|
||||||
|
|
@ -215,9 +220,12 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
|
||||||
logger.Warning("Failed to parse admin ID from Telegram bot chat ID:", err)
|
logger.Warning("Failed to parse admin ID from Telegram bot chat ID:", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
adminIds = append(adminIds, int64(id))
|
parsedAdminIds = append(parsedAdminIds, int64(id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
tgBotMutex.Lock()
|
||||||
|
adminIds = parsedAdminIds
|
||||||
|
tgBotMutex.Unlock()
|
||||||
|
|
||||||
// Get Telegram bot proxy URL
|
// Get Telegram bot proxy URL
|
||||||
tgBotProxy, err := t.settingService.GetTgBotProxy()
|
tgBotProxy, err := t.settingService.GetTgBotProxy()
|
||||||
|
|
@ -252,10 +260,12 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start receiving Telegram bot messages
|
// Start receiving Telegram bot messages
|
||||||
if !isRunning {
|
tgBotMutex.Lock()
|
||||||
|
alreadyRunning := isRunning || botCancel != nil
|
||||||
|
tgBotMutex.Unlock()
|
||||||
|
if !alreadyRunning {
|
||||||
logger.Info("Telegram bot receiver started")
|
logger.Info("Telegram bot receiver started")
|
||||||
go t.OnReceive()
|
go t.OnReceive()
|
||||||
isRunning = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -300,6 +310,8 @@ func (t *Tgbot) NewBot(token string, proxyUrl string, apiServerUrl string) (*tel
|
||||||
|
|
||||||
// IsRunning checks if the Telegram bot is currently running.
|
// IsRunning checks if the Telegram bot is currently running.
|
||||||
func (t *Tgbot) IsRunning() bool {
|
func (t *Tgbot) IsRunning() bool {
|
||||||
|
tgBotMutex.Lock()
|
||||||
|
defer tgBotMutex.Unlock()
|
||||||
return isRunning
|
return isRunning
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -317,34 +329,34 @@ func (t *Tgbot) SetHostname() {
|
||||||
// Stop safely stops the Telegram bot's Long Polling operation.
|
// Stop safely stops the Telegram bot's Long Polling operation.
|
||||||
// This method now calls the global StopBot function and cleans up other resources.
|
// This method now calls the global StopBot function and cleans up other resources.
|
||||||
func (t *Tgbot) Stop() {
|
func (t *Tgbot) Stop() {
|
||||||
// Call the global StopBot function to gracefully shut down Long Polling
|
|
||||||
StopBot()
|
StopBot()
|
||||||
|
|
||||||
// Stop the bot handler (in case the goroutine hasn't exited yet)
|
|
||||||
if botHandler != nil {
|
|
||||||
botHandler.Stop()
|
|
||||||
}
|
|
||||||
logger.Info("Stop Telegram receiver ...")
|
logger.Info("Stop Telegram receiver ...")
|
||||||
isRunning = false
|
tgBotMutex.Lock()
|
||||||
adminIds = nil
|
adminIds = nil
|
||||||
|
tgBotMutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// StopBot safely stops the Telegram bot's Long Polling operation by cancelling its context.
|
// StopBot safely stops the Telegram bot's Long Polling operation by cancelling its context.
|
||||||
// This is the global function called from main.go's signal handler and t.Stop().
|
// This is the global function called from main.go's signal handler and t.Stop().
|
||||||
func StopBot() {
|
func StopBot() {
|
||||||
|
// Don't hold the mutex while cancelling/waiting.
|
||||||
tgBotMutex.Lock()
|
tgBotMutex.Lock()
|
||||||
defer tgBotMutex.Unlock()
|
cancel := botCancel
|
||||||
|
botCancel = nil
|
||||||
|
handler := botHandler
|
||||||
|
botHandler = nil
|
||||||
|
isRunning = false
|
||||||
|
tgBotMutex.Unlock()
|
||||||
|
|
||||||
if botCancel != nil {
|
if handler != nil {
|
||||||
|
handler.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
if cancel != nil {
|
||||||
logger.Info("Sending cancellation signal to Telegram bot...")
|
logger.Info("Sending cancellation signal to Telegram bot...")
|
||||||
|
// Cancels the context passed to UpdatesViaLongPolling; this closes updates channel
|
||||||
// Calling botCancel() cancels the context passed to UpdatesViaLongPolling,
|
// and lets botHandler.Start() exit cleanly.
|
||||||
// which stops the Long Polling operation and closes the updates channel,
|
cancel()
|
||||||
// allowing the th.Start() goroutine to exit cleanly.
|
|
||||||
botCancel()
|
|
||||||
|
|
||||||
botCancel = nil
|
|
||||||
// Giving the goroutine a small delay to exit cleanly.
|
|
||||||
botWG.Wait()
|
botWG.Wait()
|
||||||
logger.Info("Telegram bot successfully stopped.")
|
logger.Info("Telegram bot successfully stopped.")
|
||||||
}
|
}
|
||||||
|
|
@ -379,36 +391,38 @@ func (t *Tgbot) OnReceive() {
|
||||||
params := telego.GetUpdatesParams{
|
params := telego.GetUpdatesParams{
|
||||||
Timeout: 30, // Increased timeout to reduce API calls
|
Timeout: 30, // Increased timeout to reduce API calls
|
||||||
}
|
}
|
||||||
// --- GRACEFUL SHUTDOWN FIX: Context creation ---
|
// Strict singleton: never start a second long-polling loop.
|
||||||
tgBotMutex.Lock()
|
tgBotMutex.Lock()
|
||||||
|
if botCancel != nil || isRunning {
|
||||||
// Create a context with cancellation and store the cancel function.
|
tgBotMutex.Unlock()
|
||||||
var ctx context.Context
|
logger.Warning("TgBot OnReceive called while already running; ignoring.")
|
||||||
|
return
|
||||||
// Check if botCancel is already set (to prevent race condition overwrite and goroutine leak)
|
|
||||||
if botCancel == nil {
|
|
||||||
ctx, botCancel = context.WithCancel(context.Background())
|
|
||||||
} else {
|
|
||||||
// If botCancel is already set, use a non-cancellable context for this redundant call.
|
|
||||||
// This prevents overwriting the active botCancel and causing a goroutine leak from the previous call.
|
|
||||||
logger.Warning("TgBot OnReceive called concurrently. Using background context for redundant call.")
|
|
||||||
ctx = context.Background() // <<< ИЗМЕНЕНИЕ
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
botCancel = cancel
|
||||||
|
isRunning = true
|
||||||
|
// Add to WaitGroup before releasing the lock so StopBot() can't return
|
||||||
|
// before this receiver goroutine is accounted for.
|
||||||
|
botWG.Add(1)
|
||||||
tgBotMutex.Unlock()
|
tgBotMutex.Unlock()
|
||||||
|
|
||||||
// Get updates channel using the context.
|
// Get updates channel using the context.
|
||||||
updates, _ := bot.UpdatesViaLongPolling(ctx, ¶ms)
|
updates, _ := bot.UpdatesViaLongPolling(ctx, ¶ms)
|
||||||
botWG.Go(func() {
|
go func() {
|
||||||
|
defer botWG.Done()
|
||||||
|
h, _ := th.NewBotHandler(bot, updates)
|
||||||
|
tgBotMutex.Lock()
|
||||||
|
botHandler = h
|
||||||
|
tgBotMutex.Unlock()
|
||||||
|
|
||||||
botHandler, _ = th.NewBotHandler(bot, updates)
|
h.HandleMessage(func(ctx *th.Context, message telego.Message) error {
|
||||||
botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error {
|
|
||||||
delete(userStates, message.Chat.ID)
|
delete(userStates, message.Chat.ID)
|
||||||
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.keyboardClosed"), tu.ReplyKeyboardRemove())
|
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.keyboardClosed"), tu.ReplyKeyboardRemove())
|
||||||
return nil
|
return nil
|
||||||
}, th.TextEqual(t.I18nBot("tgbot.buttons.closeKeyboard")))
|
}, th.TextEqual(t.I18nBot("tgbot.buttons.closeKeyboard")))
|
||||||
|
|
||||||
botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error {
|
h.HandleMessage(func(ctx *th.Context, message telego.Message) error {
|
||||||
// Use goroutine with worker pool for concurrent command processing
|
// Use goroutine with worker pool for concurrent command processing
|
||||||
go func() {
|
go func() {
|
||||||
messageWorkerPool <- struct{}{} // Acquire worker
|
messageWorkerPool <- struct{}{} // Acquire worker
|
||||||
|
|
@ -420,7 +434,7 @@ func (t *Tgbot) OnReceive() {
|
||||||
return nil
|
return nil
|
||||||
}, th.AnyCommand())
|
}, th.AnyCommand())
|
||||||
|
|
||||||
botHandler.HandleCallbackQuery(func(ctx *th.Context, query telego.CallbackQuery) error {
|
h.HandleCallbackQuery(func(ctx *th.Context, query telego.CallbackQuery) error {
|
||||||
// Use goroutine with worker pool for concurrent callback processing
|
// Use goroutine with worker pool for concurrent callback processing
|
||||||
go func() {
|
go func() {
|
||||||
messageWorkerPool <- struct{}{} // Acquire worker
|
messageWorkerPool <- struct{}{} // Acquire worker
|
||||||
|
|
@ -432,7 +446,7 @@ func (t *Tgbot) OnReceive() {
|
||||||
return nil
|
return nil
|
||||||
}, th.AnyCallbackQueryWithMessage())
|
}, th.AnyCallbackQueryWithMessage())
|
||||||
|
|
||||||
botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error {
|
h.HandleMessage(func(ctx *th.Context, message telego.Message) error {
|
||||||
if userState, exists := userStates[message.Chat.ID]; exists {
|
if userState, exists := userStates[message.Chat.ID]; exists {
|
||||||
switch userState {
|
switch userState {
|
||||||
case "awaiting_id":
|
case "awaiting_id":
|
||||||
|
|
@ -578,8 +592,8 @@ func (t *Tgbot) OnReceive() {
|
||||||
return nil
|
return nil
|
||||||
}, th.AnyMessage())
|
}, th.AnyMessage())
|
||||||
|
|
||||||
botHandler.Start()
|
h.Start()
|
||||||
})
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
// answerCommand processes incoming command messages from Telegram users.
|
// answerCommand processes incoming command messages from Telegram users.
|
||||||
|
|
|
||||||
270
x-ui.sh
270
x-ui.sh
|
|
@ -19,6 +19,20 @@ function LOGI() {
|
||||||
echo -e "${green}[INF] $* ${plain}"
|
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
|
# check root
|
||||||
[[ $EUID -ne 0 ]] && LOGE "ERROR: You must be root to run this script! \n" && exit 1
|
[[ $EUID -ne 0 ]] && LOGE "ERROR: You must be root to run this script! \n" && exit 1
|
||||||
|
|
||||||
|
|
@ -39,7 +53,8 @@ os_version=""
|
||||||
os_version=$(grep "^VERSION_ID" /etc/os-release | cut -d '=' -f2 | tr -d '"' | tr -d '.')
|
os_version=$(grep "^VERSION_ID" /etc/os-release | cut -d '=' -f2 | tr -d '"' | tr -d '.')
|
||||||
|
|
||||||
# Declare Variables
|
# Declare Variables
|
||||||
log_folder="${XUI_LOG_FOLDER:=/var/log}"
|
log_folder="${XUI_LOG_FOLDER:=/var/log/x-ui}"
|
||||||
|
mkdir -p "${log_folder}"
|
||||||
iplimit_log_path="${log_folder}/3xipl.log"
|
iplimit_log_path="${log_folder}/3xipl.log"
|
||||||
iplimit_banned_log_path="${log_folder}/3xipl-banned.log"
|
iplimit_banned_log_path="${log_folder}/3xipl-banned.log"
|
||||||
|
|
||||||
|
|
@ -213,6 +228,57 @@ gen_random_string() {
|
||||||
echo "$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" <<EOF
|
||||||
|
[req]
|
||||||
|
distinguished_name=req_distinguished_name
|
||||||
|
req_extensions=v3_req
|
||||||
|
[req_distinguished_name]
|
||||||
|
[v3_req]
|
||||||
|
subjectAltName=${sanExt}
|
||||||
|
EOF
|
||||||
|
openssl req -x509 -nodes -newkey rsa:2048 -days 365 \
|
||||||
|
-keyout "${certDir}/privkey.pem" \
|
||||||
|
-out "${certDir}/fullchain.pem" \
|
||||||
|
-subj "/CN=${name}" \
|
||||||
|
-config "$tmpCfg" -extensions v3_req >/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() {
|
reset_webbasepath() {
|
||||||
echo -e "${yellow}Resetting Web Base Path${plain}"
|
echo -e "${yellow}Resetting Web Base Path${plain}"
|
||||||
|
|
||||||
|
|
@ -256,7 +322,7 @@ check_config() {
|
||||||
|
|
||||||
local existing_webBasePath=$(echo "$info" | grep -Eo 'webBasePath: .+' | awk '{print $2}')
|
local existing_webBasePath=$(echo "$info" | grep -Eo 'webBasePath: .+' | awk '{print $2}')
|
||||||
local existing_port=$(echo "$info" | grep -Eo 'port: .+' | 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)
|
local server_ip=$(curl -s --max-time 3 https://api.ipify.org)
|
||||||
if [ -z "$server_ip" ]; then
|
if [ -z "$server_ip" ]; then
|
||||||
server_ip=$(curl -s --max-time 3 https://4.ident.me)
|
server_ip=$(curl -s --max-time 3 https://4.ident.me)
|
||||||
|
|
@ -271,7 +337,22 @@ check_config() {
|
||||||
echo -e "${green}Access URL: https://${server_ip}:${existing_port}${existing_webBasePath}${plain}"
|
echo -e "${green}Access URL: https://${server_ip}:${existing_port}${existing_webBasePath}${plain}"
|
||||||
fi
|
fi
|
||||||
else
|
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
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -958,6 +1039,7 @@ ssl_cert_issue_main() {
|
||||||
echo -e "${green}\t3.${plain} Force Renew"
|
echo -e "${green}\t3.${plain} Force Renew"
|
||||||
echo -e "${green}\t4.${plain} Show Existing Domains"
|
echo -e "${green}\t4.${plain} Show Existing Domains"
|
||||||
echo -e "${green}\t5.${plain} Set Cert paths for the panel"
|
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"
|
echo -e "${green}\t0.${plain} Back to Main Menu"
|
||||||
|
|
||||||
read -rp "Choose an option: " choice
|
read -rp "Choose an option: " choice
|
||||||
|
|
@ -1051,6 +1133,16 @@ ssl_cert_issue_main() {
|
||||||
fi
|
fi
|
||||||
ssl_cert_issue_main
|
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"
|
echo -e "${red}Invalid option. Please select a valid number.${plain}\n"
|
||||||
|
|
@ -1059,6 +1151,134 @@ ssl_cert_issue_main() {
|
||||||
esac
|
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() {
|
ssl_cert_issue() {
|
||||||
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}')
|
||||||
local existing_port=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')
|
local existing_port=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')
|
||||||
|
|
@ -1072,33 +1292,32 @@ ssl_cert_issue() {
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# install socat second
|
# install socat
|
||||||
case "${release}" in
|
case "${release}" in
|
||||||
ubuntu | debian | armbian)
|
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)
|
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)
|
centos)
|
||||||
if [[ "${VERSION_ID}" =~ ^7 ]]; then
|
if [[ "${VERSION_ID}" =~ ^7 ]]; then
|
||||||
yum -y update && yum -y install socat
|
yum -y update >/dev/null 2>&1 && yum -y install socat >/dev/null 2>&1
|
||||||
else
|
else
|
||||||
dnf -y update && dnf -y install socat
|
dnf -y update >/dev/null 2>&1 && dnf -y install socat >/dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
arch | manjaro | parch)
|
arch | manjaro | parch)
|
||||||
pacman -Sy --noconfirm socat
|
pacman -Sy --noconfirm socat >/dev/null 2>&1
|
||||||
;;
|
;;
|
||||||
opensuse-tumbleweed | opensuse-leap)
|
opensuse-tumbleweed | opensuse-leap)
|
||||||
zypper refresh && zypper -q install -y socat
|
zypper refresh >/dev/null 2>&1 && zypper -q install -y socat >/dev/null 2>&1
|
||||||
;;
|
;;
|
||||||
alpine)
|
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"
|
LOGW "Unsupported OS for automatic socat installation"
|
||||||
exit 1
|
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
|
|
@ -1110,7 +1329,22 @@ ssl_cert_issue() {
|
||||||
|
|
||||||
# get the domain here, and we need to verify it
|
# get the domain here, and we need to verify it
|
||||||
local domain=""
|
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..."
|
LOGD "Your domain is: ${domain}, checking it..."
|
||||||
|
|
||||||
# check if there already exists a certificate
|
# check if there already exists a certificate
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue