3x-ui/update_zh_cn.sh
2026-05-16 11:44:55 +08:00

991 lines
42 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/bash
red='\033[0;31m'
green='\033[0;32m'
blue='\033[0;34m'
yellow='\033[0;33m'
plain='\033[0m'
xui_folder="${XUI_MAIN_FOLDER:=/usr/local/x-ui}"
xui_service="${XUI_SERVICE:=/etc/systemd/system}"
# 不要编辑此配置
b_source="${BASH_SOURCE[0]}"
while [ -h "$b_source" ]; do
b_dir="$(cd -P "$(dirname "$b_source")" > /dev/null 2>&1 && pwd || pwd -P)"
b_source="$(readlink "$b_source")"
[[ $b_source != /* ]] && b_source="$b_dir/$b_source"
done
cur_dir="$(cd -P "$(dirname "$b_source")" > /dev/null 2>&1 && pwd || pwd -P)"
script_name=$(basename "$0")
# 检查命令是否存在函数
_command_exists() {
type "$1" &> /dev/null
}
# 失败、记录并退出脚本函数
_fail() {
local msg=${1}
echo -e "${red}${msg}${plain}"
exit 2
}
# 检查 root 权限
[[ $EUID -ne 0 ]] && _fail "致命错误:请以 root 权限运行此脚本。"
if _command_exists curl; then
curl_bin=$(which curl)
else
_fail "错误:未找到命令 'curl'。"
fi
# 检查操作系统并设置 release 变量
if [[ -f /etc/os-release ]]; then
source /etc/os-release
release=$ID
elif [[ -f /usr/lib/os-release ]]; then
source /usr/lib/os-release
release=$ID
else
_fail "无法检查系统操作系统,请联系作者!"
fi
echo "操作系统版本为:$release"
arch() {
case "$(uname -m)" in
x86_64 | x64 | amd64) echo 'amd64' ;;
i*86 | x86) echo '386' ;;
armv8* | armv8 | arm64 | aarch64) echo 'arm64' ;;
armv7* | armv7 | arm) echo 'armv7' ;;
armv6* | armv6) echo 'armv6' ;;
armv5* | armv5) echo 'armv5' ;;
s390x) echo 's390x' ;;
*) echo -e "${red}不支持的 CPU 架构!${plain}" && rm -f "${cur_dir}/${script_name}" > /dev/null 2>&1 && exit 2 ;;
esac
}
echo "架构:$(arch)"
# 简单辅助函数
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])*\.)+(xn--[a-z0-9]{2,}|[A-Za-z]{2,})$ ]] && return 0 || return 1
}
# 端口辅助函数
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
}
gen_random_string() {
local length="$1"
openssl rand -base64 $((length * 2)) \
| tr -dc 'a-zA-Z0-9' \
| head -c "$length"
}
install_base() {
echo -e "${green}正在更新并安装依赖包...${plain}"
case "${release}" in
ubuntu | debian | armbian)
apt-get update > /dev/null 2>&1 && apt-get install -y -q cron curl tar tzdata socat openssl > /dev/null 2>&1
;;
fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
dnf -y update > /dev/null 2>&1 && dnf install -y -q cronie curl tar tzdata socat openssl > /dev/null 2>&1
;;
centos)
if [[ "${VERSION_ID}" =~ ^7 ]]; then
yum -y update > /dev/null 2>&1 && yum install -y -q cronie curl tar tzdata socat openssl > /dev/null 2>&1
else
dnf -y update > /dev/null 2>&1 && dnf install -y -q cronie curl tar tzdata socat openssl > /dev/null 2>&1
fi
;;
arch | manjaro | parch)
pacman -Syu > /dev/null 2>&1 && pacman -Syu --noconfirm cronie curl tar tzdata socat openssl > /dev/null 2>&1
;;
opensuse-tumbleweed | opensuse-leap)
zypper refresh > /dev/null 2>&1 && zypper -q install -y cron curl tar timezone socat openssl > /dev/null 2>&1
;;
alpine)
apk update > /dev/null 2>&1 && apk add dcron curl tar tzdata socat openssl > /dev/null 2>&1
;;
*)
apt-get update > /dev/null 2>&1 && apt install -y -q cron curl tar tzdata socat openssl > /dev/null 2>&1
;;
esac
}
install_acme() {
echo -e "${green}正在安装 acme.sh 以管理 SSL 证书...${plain}"
cd ~ || return 1
curl -s https://get.acme.sh | sh > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo -e "${red}安装 acme.sh 失败${plain}"
return 1
else
echo -e "${green}acme.sh 安装成功${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}正在设置 SSL 证书...${plain}"
# 检查是否已安装 acme.sh
if ! command -v ~/.acme.sh/acme.sh &> /dev/null; then
install_acme
if [ $? -ne 0 ]; then
echo -e "${yellow}安装 acme.sh 失败,跳过 SSL 设置${plain}"
return 1
fi
fi
# 创建证书目录
local certPath="/root/cert/${domain}"
mkdir -p "$certPath"
# 颁发证书
echo -e "${green}正在为 ${domain} 颁发 SSL 证书...${plain}"
echo -e "${yellow}注意:端口 80 必须开放并可从互联网访问${plain}"
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force > /dev/null 2>&1
~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport 80 --force
if [ $? -ne 0 ]; then
echo -e "${yellow}${domain} 颁发证书失败${plain}"
echo -e "${yellow}请确保端口 80 开放,稍后使用 x-ui 重试${plain}"
rm -rf ~/.acme.sh/${domain} 2> /dev/null
rm -rf "$certPath" 2> /dev/null
return 1
fi
# 安装证书
~/.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}安装证书失败${plain}"
return 1
fi
# 启用自动续期
~/.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
# 为面板设置证书
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" > /dev/null 2>&1
echo -e "${green}SSL 证书安装并配置成功!${plain}"
return 0
else
echo -e "${yellow}未找到证书文件${plain}"
return 1
fi
}
# 使用短期配置文件颁发 Let's Encrypt IP 证书(有效期约 6 天)
# 需要 acme.sh 和开放的端口 80 用于 HTTP-01 验证
setup_ip_certificate() {
local ipv4="$1"
local ipv6="$2" # 可选
echo -e "${green}正在设置 Let's Encrypt IP 证书(短期配置文件)...${plain}"
echo -e "${yellow}注意IP 证书有效期约为 6 天,将自动续期。${plain}"
echo -e "${yellow}默认监听端口为 80。如果您选择其他端口请确保外部端口 80 转发到该端口。${plain}"
# 检查 acme.sh
if ! command -v ~/.acme.sh/acme.sh &> /dev/null; then
install_acme
if [ $? -ne 0 ]; then
echo -e "${red}安装 acme.sh 失败${plain}"
return 1
fi
fi
# 验证 IP 地址
if [[ -z "$ipv4" ]]; then
echo -e "${red}需要 IPv4 地址${plain}"
return 1
fi
if ! is_ipv4 "$ipv4"; then
echo -e "${red}无效的 IPv4 地址:$ipv4${plain}"
return 1
fi
# 创建证书目录
local certDir="/root/cert/ip"
mkdir -p "$certDir"
# 构建域参数
local domain_args="-d ${ipv4}"
if [[ -n "$ipv6" ]] && is_ipv6 "$ipv6"; then
domain_args="${domain_args} -d ${ipv6}"
echo -e "${green}包含 IPv6 地址:${ipv6}${plain}"
fi
# 设置自动续期的重载命令(添加 || true 以便在服务停止时不会失败)
local reloadCmd="systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null || true"
# 选择 HTTP-01 监听器的端口(默认 80提示覆盖
local WebPort=""
read -rp "用于 ACME HTTP-01 监听器的端口(默认 80" WebPort
WebPort="${WebPort:-80}"
if ! [[ "${WebPort}" =~ ^[0-9]+$ ]] || ((WebPort < 1 || WebPort > 65535)); then
echo -e "${red}提供的端口无效。回退到 80。${plain}"
WebPort=80
fi
echo -e "${green}使用端口 ${WebPort} 进行独立验证。${plain}"
if [[ "${WebPort}" -ne 80 ]]; then
echo -e "${yellow}提醒Let's Encrypt 仍然通过端口 80 连接;将外部端口 80 转发到 ${WebPort}${plain}"
fi
# 确保所选端口可用
while true; do
if is_port_in_use "${WebPort}"; then
echo -e "${yellow}端口 ${WebPort} 当前正在使用中。${plain}"
local alt_port=""
read -rp "输入另一个用于 acme.sh 独立监听器的端口(留空则中止):" alt_port
alt_port="${alt_port// /}"
if [[ -z "${alt_port}" ]]; then
echo -e "${red}端口 ${WebPort} 繁忙;无法继续。${plain}"
return 1
fi
if ! [[ "${alt_port}" =~ ^[0-9]+$ ]] || ((alt_port < 1 || alt_port > 65535)); then
echo -e "${red}提供的端口无效。${plain}"
return 1
fi
WebPort="${alt_port}"
continue
else
echo -e "${green}端口 ${WebPort} 空闲,可以进行独立验证。${plain}"
break
fi
done
# 使用短期配置文件颁发证书
echo -e "${green}正在为 ${ipv4} 颁发 IP 证书...${plain}"
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force > /dev/null 2>&1
~/.acme.sh/acme.sh --issue \
${domain_args} \
--standalone \
--server letsencrypt \
--certificate-profile shortlived \
--days 6 \
--httpport ${WebPort} \
--force
if [ $? -ne 0 ]; then
echo -e "${red}颁发 IP 证书失败${plain}"
echo -e "${yellow}请确保端口 ${WebPort} 可访问(或从外部端口 80 转发)${plain}"
# 清理 IPv4 和 IPv6如果指定的 acme.sh 数据
rm -rf ~/.acme.sh/${ipv4} 2> /dev/null
[[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2> /dev/null
rm -rf ${certDir} 2> /dev/null
return 1
fi
echo -e "${green}证书颁发成功,正在安装...${plain}"
# 安装证书
# 注意:如果 reloadcmd 失败acme.sh 可能会报告“Reload error”并以非零退出
# 但证书文件仍然已安装。我们检查文件而不是退出代码。
~/.acme.sh/acme.sh --installcert -d ${ipv4} \
--key-file "${certDir}/privkey.pem" \
--fullchain-file "${certDir}/fullchain.pem" \
--reloadcmd "${reloadCmd}" 2>&1 || true
# 验证证书文件是否存在(不要依赖退出代码 - reloadcmd 失败会导致非零)
if [[ ! -f "${certDir}/fullchain.pem" || ! -f "${certDir}/privkey.pem" ]]; then
echo -e "${red}安装后未找到证书文件${plain}"
# 清理 IPv4 和 IPv6如果指定的 acme.sh 数据
rm -rf ~/.acme.sh/${ipv4} 2> /dev/null
[[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2> /dev/null
rm -rf ${certDir} 2> /dev/null
return 1
fi
echo -e "${green}证书文件安装成功${plain}"
# 为 acme.sh 启用自动升级(确俜 cron 任务运行)
~/.acme.sh/acme.sh --upgrade --auto-upgrade > /dev/null 2>&1
chmod 600 ${certDir}/privkey.pem 2> /dev/null
chmod 644 ${certDir}/fullchain.pem 2> /dev/null
# 配置面板使用证书
echo -e "${green}正在为面板设置证书路径...${plain}"
${xui_folder}/x-ui cert -webCert "${certDir}/fullchain.pem" -webCertKey "${certDir}/privkey.pem"
if [ $? -ne 0 ]; then
echo -e "${yellow}警告:无法自动设置证书路径。${plain}"
echo -e "${yellow}您可能需要在面板设置中手动设置它们。${plain}"
echo -e "${yellow}证书路径:${certDir}/fullchain.pem${plain}"
echo -e "${yellow}密钥路径:${certDir}/privkey.pem${plain}"
else
echo -e "${green}证书路径设置成功!${plain}"
fi
echo -e "${green}IP 证书安装并配置成功!${plain}"
echo -e "${green}证书有效期约为 6 天,通过 acme.sh cron 任务自动续期。${plain}"
echo -e "${yellow}每次续期后面板将自动重启。${plain}"
return 0
}
# 通过 acme.sh 进行全面的 SSL 证书手动颁发
ssl_cert_issue() {
local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep 'webBasePath:' | awk -F': ' '{print $2}' | tr -d '[:space:]' | sed 's#^/##')
local existing_port=$(${xui_folder}/x-ui setting -show true | grep 'port:' | awk -F': ' '{print $2}' | tr -d '[:space:]')
# 首先检查 acme.sh
if ! command -v ~/.acme.sh/acme.sh &> /dev/null; then
echo "未找到 acme.sh。正在安装..."
cd ~ || return 1
curl -s https://get.acme.sh | sh
if [ $? -ne 0 ]; then
echo -e "${red}安装 acme.sh 失败${plain}"
return 1
else
echo -e "${green}acme.sh 安装成功${plain}"
fi
fi
# 获取域名,我们需要验证它
local domain=""
while true; do
read -rp "请输入您的域名:" domain
domain="${domain// /}" # 删除空格
if [[ -z "$domain" ]]; then
echo -e "${red}域名不能为空。请重试。${plain}"
continue
fi
if ! is_domain "$domain"; then
echo -e "${red}无效的域名格式:${domain}。请输入有效的域名。${plain}"
continue
fi
break
done
echo -e "${green}您的域名为:${domain},正在检查...${plain}"
SSL_ISSUED_DOMAIN="${domain}"
# 检测现有证书,如果存在则重用
local cert_exists=0
if ~/.acme.sh/acme.sh --list 2> /dev/null | awk '{print $1}' | grep -Fxq "${domain}"; then
cert_exists=1
local certInfo=$(~/.acme.sh/acme.sh --list 2> /dev/null | grep -F "${domain}")
echo -e "${yellow}找到 ${domain} 的现有证书,将重用它。${plain}"
[[ -n "${certInfo}" ]] && echo "$certInfo"
else
echo -e "${green}您的域名已准备好颁发证书...${plain}"
fi
# 为证书创建目录
certPath="/root/cert/${domain}"
if [ ! -d "$certPath" ]; then
mkdir -p "$certPath"
else
rm -rf "$certPath"
mkdir -p "$certPath"
fi
# 获取独立服务器的端口号
local WebPort=80
read -rp "请选择要使用的端口(默认为 80" WebPort
if [[ ${WebPort} -gt 65535 || ${WebPort} -lt 1 ]]; then
echo -e "${yellow}您输入的 ${WebPort} 无效,将使用默认端口 80。${plain}"
WebPort=80
fi
echo -e "${green}将使用端口:${WebPort} 颁发证书。请确保此端口已开放。${plain}"
# 临时停止面板
echo -e "${yellow}正在临时停止面板...${plain}"
systemctl stop x-ui 2> /dev/null || rc-service x-ui stop 2> /dev/null
if [[ ${cert_exists} -eq 0 ]]; then
# 颁发证书
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force
~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport ${WebPort} --force
if [ $? -ne 0 ]; then
echo -e "${red}颁发证书失败,请检查日志。${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}颁发证书成功,正在安装证书...${plain}"
fi
else
echo -e "${green}使用现有证书,正在安装证书...${plain}"
fi
# 设置重载命令
reloadCmd="systemctl restart x-ui || rc-service x-ui restart"
echo -e "${green}ACME 的默认 --reloadcmd 为:${yellow}systemctl restart x-ui || rc-service x-ui restart${plain}"
echo -e "${green}此命令将在每次颁发和续期证书时运行。${plain}"
read -rp "您是否要修改 ACME 的 --reloadcmdy/n" setReloadcmd
if [[ "$setReloadcmd" == "y" || "$setReloadcmd" == "Y" ]]; then
echo -e "\n${green}\t1.${plain} 预设systemctl reload nginx ; systemctl restart x-ui"
echo -e "${green}\t2.${plain} 输入您自己的命令"
echo -e "${green}\t0.${plain} 保持默认 reloadcmd"
read -rp "选择一个选项:" choice
case "$choice" in
1)
echo -e "${green}Reloadcmd 为systemctl reload nginx ; systemctl restart x-ui${plain}"
reloadCmd="systemctl reload nginx ; systemctl restart x-ui"
;;
2)
echo -e "${yellow}建议将 x-ui restart 放在最后${plain}"
read -rp "请输入您的自定义 reloadcmd" reloadCmd
echo -e "${green}Reloadcmd 为:${reloadCmd}${plain}"
;;
*)
echo -e "${green}保持默认 reloadcmd${plain}"
;;
esac
fi
# 安装证书
local installOutput=""
installOutput=$(~/.acme.sh/acme.sh --installcert -d ${domain} \
--key-file /root/cert/${domain}/privkey.pem \
--fullchain-file /root/cert/${domain}/fullchain.pem --reloadcmd "${reloadCmd}" 2>&1)
local installRc=$?
echo "${installOutput}"
local installWroteFiles=0
if echo "${installOutput}" | grep -q "Installing key to:" && echo "${installOutput}" | grep -q "Installing full chain to:"; then
installWroteFiles=1
fi
if [[ -f "/root/cert/${domain}/privkey.pem" && -f "/root/cert/${domain}/fullchain.pem" && (${installRc} -eq 0 || ${installWroteFiles} -eq 1) ]]; then
echo -e "${green}安装证书成功,正在启用自动续期...${plain}"
else
echo -e "${red}安装证书失败,退出。${plain}"
if [[ ${cert_exists} -eq 0 ]]; then
rm -rf ~/.acme.sh/${domain}
fi
systemctl start x-ui 2> /dev/null || rc-service x-ui start 2> /dev/null
return 1
fi
# 启用自动续期
~/.acme.sh/acme.sh --upgrade --auto-upgrade
if [ $? -ne 0 ]; then
echo -e "${yellow}自动续期设置出现问题,证书详情:${plain}"
ls -lah /root/cert/${domain}/
chmod 600 $certPath/privkey.pem
chmod 644 $certPath/fullchain.pem
else
echo -e "${green}自动续期成功,证书详情:${plain}"
ls -lah /root/cert/${domain}/
chmod 600 $certPath/privkey.pem
chmod 644 $certPath/fullchain.pem
fi
# 重启面板
systemctl start x-ui 2> /dev/null || rc-service x-ui start 2> /dev/null
# 提示用户在成功安装证书后设置面板路径
read -rp "您是否要为面板设置此证书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"
echo -e "${green}已为面板设置证书路径${plain}"
echo -e "${green}证书文件:$webCertFile${plain}"
echo -e "${green}私钥文件:$webKeyFile${plain}"
echo ""
echo -e "${green}访问 URLhttps://${domain}:${existing_port}/${existing_webBasePath}${plain}"
echo -e "${yellow}面板将重启以应用 SSL 证书...${plain}"
systemctl restart x-ui 2> /dev/null || rc-service x-ui restart 2> /dev/null
else
echo -e "${red}错误:未找到域名的证书或私钥文件:$domain${plain}"
fi
else
echo -e "${yellow}跳过面板路径设置。${plain}"
fi
return 0
}
# 统一的交互式 SSL 设置(域或 IP
# 将全局 `SSL_HOST` 设置为所选的域/IP
prompt_and_setup_ssl() {
local panel_port="$1"
local web_base_path="$2" # 预期不带前导斜杠
local server_ip="$3"
local ssl_choice=""
echo -e "${yellow}选择 SSL 证书设置方法:${plain}"
echo -e "${green}1.${plain} Let's Encrypt 域名证书90 天有效期,自动续期)"
echo -e "${green}2.${plain} Let's Encrypt IP 地址证书6 天有效期,自动续期)"
echo -e "${green}3.${plain} 自定义 SSL 证书(现有文件路径)"
echo -e "${blue}注意:${plain}选项 1 和 2 需要开放端口 80。选项 3 需要手动指定路径。"
read -rp "选择一个选项(默认为 2 用于 IP" ssl_choice
ssl_choice="${ssl_choice// /}" # 删除空格
# 如果输入为空或无效(不是 1 或 3则默认为 2IP 证书)
if [[ "$ssl_choice" != "1" && "$ssl_choice" != "3" ]]; then
ssl_choice="2"
fi
case "$ssl_choice" in
1)
# 用户选择了 Let's Encrypt 域名选项
echo -e "${green}使用 Let's Encrypt 颁发域名证书...${plain}"
if ssl_cert_issue; then
local cert_domain="${SSL_ISSUED_DOMAIN}"
if [[ -z "${cert_domain}" ]]; then
cert_domain=$(~/.acme.sh/acme.sh --list 2> /dev/null | tail -1 | awk '{print $1}')
fi
if [[ -n "${cert_domain}" ]]; then
SSL_HOST="${cert_domain}"
echo -e "${green}✓ SSL 证书配置成功,域名:${cert_domain}${plain}"
else
echo -e "${yellow}SSL 设置可能已完成,但域提取失败${plain}"
SSL_HOST="${server_ip}"
fi
else
echo -e "${red}域名模式的 SSL 证书设置失败。${plain}"
SSL_HOST="${server_ip}"
fi
;;
2)
# 用户选择了 Let's Encrypt IP 证书选项
echo -e "${green}使用 Let's Encrypt 颁发 IP 证书(短期配置文件)...${plain}"
# 询问可选的 IPv6
local ipv6_addr=""
read -rp "您有要包含的 IPv6 地址吗?(留空则跳过):" ipv6_addr
ipv6_addr="${ipv6_addr// /}" # 删除空格
# 如果面板正在运行则停止(需要端口 80
if [[ $release == "alpine" ]]; then
rc-service x-ui stop > /dev/null 2>&1
else
systemctl stop x-ui > /dev/null 2>&1
fi
setup_ip_certificate "${server_ip}" "${ipv6_addr}"
if [ $? -eq 0 ]; then
SSL_HOST="${server_ip}"
echo -e "${green}✓ Let's Encrypt IP 证书配置成功${plain}"
else
echo -e "${red}✗ IP 证书设置失败。请检查端口 80 是否开放。${plain}"
SSL_HOST="${server_ip}"
fi
# SSL 配置后重启面板(重启应用新证书设置)
if [[ $release == "alpine" ]]; then
rc-service x-ui restart > /dev/null 2>&1
else
systemctl restart x-ui > /dev/null 2>&1
fi
;;
3)
# 用户选择了自定义路径(用户提供)选项
echo -e "${green}使用自定义现有证书...${plain}"
local custom_cert=""
local custom_key=""
local custom_domain=""
# 3.1 请求域名以稍后组成面板 URL
read -rp "请输入证书颁发的域名:" custom_domain
custom_domain="${custom_domain// /}" # 删除空格
# 3.2 循环获取证书路径
while true; do
read -rp "输入证书路径(关键词:.crt / fullchain" custom_cert
# 如果存在则删除引号
custom_cert=$(echo "$custom_cert" | tr -d '"' | tr -d "'")
if [[ -f "$custom_cert" && -r "$custom_cert" && -s "$custom_cert" ]]; then
break
elif [[ ! -f "$custom_cert" ]]; then
echo -e "${red}错误:文件不存在!请重试。${plain}"
elif [[ ! -r "$custom_cert" ]]; then
echo -e "${red}错误:文件存在但不可读(检查权限)!${plain}"
else
echo -e "${red}错误:文件为空!${plain}"
fi
done
# 3.3 循环获取私钥路径
while true; do
read -rp "输入私钥路径(关键词:.key / privatekey" custom_key
# 如果存在则删除引号
custom_key=$(echo "$custom_key" | tr -d '"' | tr -d "'")
if [[ -f "$custom_key" && -r "$custom_key" && -s "$custom_key" ]]; then
break
elif [[ ! -f "$custom_key" ]]; then
echo -e "${red}错误:文件不存在!请重试。${plain}"
elif [[ ! -r "$custom_key" ]]; then
echo -e "${red}错误:文件存在但不可读(检查权限)!${plain}"
else
echo -e "${red}错误:文件为空!${plain}"
fi
done
# 3.4 通过 x-ui 二进制文件应用设置
${xui_folder}/x-ui cert -webCert "$custom_cert" -webCertKey "$custom_key" > /dev/null 2>&1
# 设置 SSL_HOST 以组成面板 URL
if [[ -n "$custom_domain" ]]; then
SSL_HOST="$custom_domain"
else
SSL_HOST="${server_ip}"
fi
echo -e "${green}✓ 已应用自定义证书路径。${plain}"
echo -e "${yellow}注意:您需要负责在外部续期这些文件。${plain}"
systemctl restart x-ui > /dev/null 2>&1 || rc-service x-ui restart > /dev/null 2>&1
;;
*)
echo -e "${red}无效选项。跳过 SSL 设置。${plain}"
SSL_HOST="${server_ip}"
;;
esac
}
config_after_update() {
echo -e "${yellow}x-ui 设置:${plain}"
${xui_folder}/x-ui setting -show true
${xui_folder}/x-ui migrate
# 通过检查 cert: 行是否存在并且后面有内容来正确检测空证书
local existing_cert=$(${xui_folder}/x-ui setting -getCert true 2> /dev/null | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]')
local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')
local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}' | sed 's#^/##')
# 获取服务器 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
local response=$(curl -s -w "\n%{http_code}" --max-time 3 "${ip_address}" 2> /dev/null)
local http_code=$(echo "$response" | tail -n1)
local ip_result=$(echo "$response" | head -n-1 | tr -d '[:space:]"')
if [[ "${http_code}" == "200" && "${ip_result}" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
server_ip="${ip_result}"
break
fi
done
if [[ -z "$server_ip" ]]; then
echo -e "${yellow}无法从任何提供商自动检测服务器 IP。${plain}"
while [[ -z "$server_ip" ]]; do
read -rp "请输入您服务器的公共 IPv4 地址:" server_ip
server_ip="${server_ip// /}"
if [[ ! "$server_ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo -e "${red}无效的 IPv4 地址。请重试。${plain}"
server_ip=""
fi
done
fi
# 处理缺失/过短的 webBasePath
if [[ ${#existing_webBasePath} -lt 4 ]]; then
echo -e "${yellow}WebBasePath 缺失或太短。正在生成一个新的...${plain}"
local config_webBasePath=$(gen_random_string 18)
${xui_folder}/x-ui setting -webBasePath "${config_webBasePath}"
existing_webBasePath="${config_webBasePath}"
echo -e "${green}新的 WebBasePath${config_webBasePath}${plain}"
fi
# 如果缺少 SSL则检查并提示
if [[ -z "$existing_cert" ]]; then
echo ""
echo -e "${red}═══════════════════════════════════════════${plain}"
echo -e "${red} ⚠ 未检测到 SSL 证书 ⚠ ${plain}"
echo -e "${red}═══════════════════════════════════════════${plain}"
echo -e "${yellow}为了安全起见,所有面板都强制要求 SSL 证书。${plain}"
echo -e "${yellow}Let's Encrypt 现在支持域名和 IP 地址!${plain}"
echo ""
# 提示并设置 SSL域或 IP
prompt_and_setup_ssl "${existing_port}" "${existing_webBasePath}" "${server_ip}"
echo ""
echo -e "${green}═══════════════════════════════════════════${plain}"
echo -e "${green} 面板访问信息 ${plain}"
echo -e "${green}═══════════════════════════════════════════${plain}"
echo -e "${green}访问 URLhttps://${SSL_HOST}:${existing_port}/${existing_webBasePath}${plain}"
echo -e "${green}═══════════════════════════════════════════${plain}"
echo -e "${yellow}⚠ SSL 证书:已启用并配置${plain}"
else
echo -e "${green}SSL 证书已配置${plain}"
# 显示带有现有证书的访问 URL
local cert_domain=$(basename "$(dirname "$existing_cert")")
echo ""
echo -e "${green}═══════════════════════════════════════════${plain}"
echo -e "${green} 面板访问信息 ${plain}"
echo -e "${green}═══════════════════════════════════════════${plain}"
echo -e "${green}访问 URLhttps://${cert_domain}:${existing_port}/${existing_webBasePath}${plain}"
echo -e "${green}═══════════════════════════════════════════${plain}"
fi
}
update_x-ui() {
cd ${xui_folder%/x-ui}/
if [ -f "${xui_folder}/x-ui" ]; then
current_xui_version=$(${xui_folder}/x-ui -v)
echo -e "${green}当前 x-ui 版本:${current_xui_version}${plain}"
else
_fail "错误:当前 x-ui 版本:未知"
fi
echo -e "${green}正在下载新的 x-ui 版本...${plain}"
tag_version=$(${curl_bin} -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" 2> /dev/null | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
if [[ ! -n "$tag_version" ]]; then
echo -e "${yellow}尝试使用 IPv4 获取版本...${plain}"
tag_version=$(${curl_bin} -4 -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
if [[ ! -n "$tag_version" ]]; then
_fail "错误:无法获取 x-ui 版本,可能是由于 GitHub API 限制,请稍后重试"
fi
fi
echo -e "获取到 x-ui 最新版本:${tag_version},开始安装..."
${curl_bin} -fLRo ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz 2> /dev/null
if [[ $? -ne 0 ]]; then
echo -e "${yellow}尝试使用 IPv4 获取版本...${plain}"
${curl_bin} -4fLRo ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz 2> /dev/null
if [[ $? -ne 0 ]]; then
_fail "错误:无法下载 x-ui请确保您的服务器可以访问 GitHub"
fi
fi
if [[ -e ${xui_folder}/ ]]; then
echo -e "${green}正在停止 x-ui...${plain}"
if [[ $release == "alpine" ]]; then
if [ -f "/etc/init.d/x-ui" ]; then
rc-service x-ui stop > /dev/null 2>&1
rc-update del x-ui > /dev/null 2>&1
echo -e "${green}正在删除旧的服务单元版本...${plain}"
rm -f /etc/init.d/x-ui > /dev/null 2>&1
else
rm x-ui-linux-$(arch).tar.gz -f > /dev/null 2>&1
_fail "错误:未安装 x-ui 服务单元。"
fi
else
if [ -f "${xui_service}/x-ui.service" ]; then
systemctl stop x-ui > /dev/null 2>&1
systemctl disable x-ui > /dev/null 2>&1
echo -e "${green}正在删除旧的 systemd 单元版本...${plain}"
rm ${xui_service}/x-ui.service -f > /dev/null 2>&1
systemctl daemon-reload > /dev/null 2>&1
else
rm x-ui-linux-$(arch).tar.gz -f > /dev/null 2>&1
_fail "错误:未安装 x-ui systemd 单元。"
fi
fi
echo -e "${green}正在删除旧的 x-ui 版本...${plain}"
rm ${xui_folder} -f > /dev/null 2>&1
rm ${xui_folder}/x-ui.service -f > /dev/null 2>&1
rm ${xui_folder}/x-ui.service.debian -f > /dev/null 2>&1
rm ${xui_folder}/x-ui.service.arch -f > /dev/null 2>&1
rm ${xui_folder}/x-ui.service.rhel -f > /dev/null 2>&1
rm ${xui_folder}/x-ui -f > /dev/null 2>&1
rm ${xui_folder}/x-ui_zh_cn.sh -f > /dev/null 2>&1
echo -e "${green}正在删除旧的 xray 版本...${plain}"
rm ${xui_folder}/bin/xray-linux-amd64 -f > /dev/null 2>&1
echo -e "${green}正在删除旧的 README 和 LICENSE 文件...${plain}"
rm ${xui_folder}/bin/README.md -f > /dev/null 2>&1
rm ${xui_folder}/bin/LICENSE -f > /dev/null 2>&1
else
rm x-ui-linux-$(arch).tar.gz -f > /dev/null 2>&1
_fail "错误:未安装 x-ui。"
fi
echo -e "${green}正在安装新的 x-ui 版本...${plain}"
tar zxvf x-ui-linux-$(arch).tar.gz > /dev/null 2>&1
rm x-ui-linux-$(arch).tar.gz -f > /dev/null 2>&1
cd x-ui > /dev/null 2>&1
chmod +x x-ui > /dev/null 2>&1
# 检查系统架构并相应地重命名文件
if [[ $(arch) == "armv5" || $(arch) == "armv6" || $(arch) == "armv7" ]]; then
mv bin/xray-linux-$(arch) bin/xray-linux-arm > /dev/null 2>&1
chmod +x bin/xray-linux-arm > /dev/null 2>&1
fi
chmod +x x-ui bin/xray-linux-$(arch) > /dev/null 2>&1
echo -e "${green}正在下载并安装 x-ui_zh_cn.sh 脚本...${plain}"
${curl_bin} -fLRo /usr/bin/x-ui https://raw.githubusercontent.com/xsdxq-null/3X-UI-CN/main/x-ui_zh_cn.sh > /dev/null 2>&1
if [[ $? -ne 0 ]]; then
echo -e "${yellow}尝试使用 IPv4 获取 x-ui...${plain}"
${curl_bin} -4fLRo /usr/bin/x-ui https://raw.githubusercontent.com/xsdxq-null/3X-UI-CN/main/x-ui_zh_cn.sh > /dev/null 2>&1
if [[ $? -ne 0 ]]; then
_fail "错误:无法下载 x-ui_zh_cn.sh 脚本,请确保您的服务器可以访问 GitHub"
fi
fi
chmod +x ${xui_folder}/x-ui_zh_cn.sh > /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}正在更改所有者...${plain}"
chown -R root:root ${xui_folder} > /dev/null 2>&1
if [ -f "${xui_folder}/bin/config.json" ]; then
echo -e "${green}正在更改配置文件权限...${plain}"
chmod 640 ${xui_folder}/bin/config.json > /dev/null 2>&1
fi
if [[ $release == "alpine" ]]; then
echo -e "${green}正在下载并安装启动单元 x-ui.rc...${plain}"
${curl_bin} -fLRo /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc > /dev/null 2>&1
if [[ $? -ne 0 ]]; then
${curl_bin} -4fLRo /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc > /dev/null 2>&1
if [[ $? -ne 0 ]]; then
_fail "错误:无法下载启动单元 x-ui.rc请确保您的服务器可以访问 GitHub"
fi
fi
chmod +x /etc/init.d/x-ui > /dev/null 2>&1
chown root:root /etc/init.d/x-ui > /dev/null 2>&1
rc-update add x-ui > /dev/null 2>&1
rc-service x-ui start > /dev/null 2>&1
else
if [ -f "x-ui.service" ]; then
echo -e "${green}正在安装 systemd 单元...${plain}"
cp -f x-ui.service ${xui_service}/ > /dev/null 2>&1
if [[ $? -ne 0 ]]; then
echo -e "${red}复制 x-ui.service 失败${plain}"
exit 1
fi
else
service_installed=false
case "${release}" in
ubuntu | debian | armbian)
if [ -f "x-ui.service.debian" ]; then
echo -e "${green}正在安装类 debian 的 systemd 单元...${plain}"
cp -f x-ui.service.debian ${xui_service}/x-ui.service > /dev/null 2>&1
if [[ $? -eq 0 ]]; then
service_installed=true
fi
fi
;;
arch | manjaro | parch)
if [ -f "x-ui.service.arch" ]; then
echo -e "${green}正在安装类 arch 的 systemd 单元...${plain}"
cp -f x-ui.service.arch ${xui_service}/x-ui.service > /dev/null 2>&1
if [[ $? -eq 0 ]]; then
service_installed=true
fi
fi
;;
*)
if [ -f "x-ui.service.rhel" ]; then
echo -e "${green}正在安装类 rhel 的 systemd 单元...${plain}"
cp -f x-ui.service.rhel ${xui_service}/x-ui.service > /dev/null 2>&1
if [[ $? -eq 0 ]]; then
service_installed=true
fi
fi
;;
esac
# 如果在 tar.gz 中未找到服务文件,则从 GitHub 下载
if [ "$service_installed" = false ]; then
echo -e "${yellow}在 tar.gz 中未找到服务文件,正在从 GitHub 下载...${plain}"
case "${release}" in
ubuntu | debian | armbian)
${curl_bin} -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.debian > /dev/null 2>&1
;;
arch | manjaro | parch)
${curl_bin} -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.arch > /dev/null 2>&1
;;
*)
${curl_bin} -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.rhel > /dev/null 2>&1
;;
esac
if [[ $? -ne 0 ]]; then
echo -e "${red}从 GitHub 安装 x-ui.service 失败${plain}"
exit 1
fi
fi
fi
chown root:root ${xui_service}/x-ui.service > /dev/null 2>&1
chmod 644 ${xui_service}/x-ui.service > /dev/null 2>&1
systemctl daemon-reload > /dev/null 2>&1
systemctl enable x-ui > /dev/null 2>&1
systemctl start x-ui > /dev/null 2>&1
fi
config_after_update
echo -e "${green}x-ui ${tag_version}${plain} 更新完成,正在运行..."
echo -e ""
echo -e "
╔══════════════════════════════════════════════════════╗
${blue}x-ui 控制菜单用法(子命令):${plain}
║ ║
${blue}x-ui${plain} - 管理脚本 ║
${blue}x-ui start${plain} - 启动 ║
${blue}x-ui stop${plain} - 停止 ║
${blue}x-ui restart${plain} - 重启 ║
${blue}x-ui status${plain} - 当前状态 ║
${blue}x-ui settings${plain} - 当前设置 ║
${blue}x-ui enable${plain} - 启用开机自启 ║
${blue}x-ui disable${plain} - 禁用开机自启 ║
${blue}x-ui log${plain} - 查看日志 ║
${blue}x-ui banlog${plain} - 查看 Fail2ban 禁止日志 ║
${blue}x-ui update${plain} - 更新 ║
${blue}x-ui legacy${plain} - 旧版本 ║
${blue}x-ui install${plain} - 安装 ║
${blue}x-ui uninstall${plain} - 卸载 ║
╚══════════════════════════════════════════════════════╝"
}
echo -e "${green}正在运行...${plain}"
install_base
update_x-ui $1