Compare commits

..

9 commits

Author SHA1 Message Date
Rs.Nest
06529b4451
Merge c76c0bd7cb into 692a73788a 2026-01-03 06:26:56 +03:00
Nebulosa
692a73788a
Set variables for packaging purposes (#3600)
* Set Variables for settings
2026-01-03 03:57:19 +01:00
Mikhail Grigorev
3287fa4d80
Added EnvironmentFile to systemd unit (#3606)
* Added EnvironmentFile to systemd unit

* Added support for older releases

* Remove ARGS

* Fixed copy unit

* Fixed unit filename

* Update update.sh
2026-01-03 03:37:48 +01:00
weekend sorrow
1393f981bc
feat: Add etckeeper compatibility (#3602) 2026-01-03 03:13:00 +01:00
Ilya Kryuchkov
9a2c1c6b43
Fix: panel redirecting to old port after restart (#3594)
* Fix panel redirect logic

* Fix panel redirect logic

* remove duplicate code

* Cr fixes
2026-01-03 03:05:10 +01:00
Vlad Yaroslavlev
278aa1c85c
Fix telegram bot issue (#3608)
Some checks are pending
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (push) Waiting to run
* fix: improve Telegram bot handling for concurrent starts and graceful shutdown

- Added logic to stop any existing long-polling loop when Start is called again.
- Introduced a mutex to manage access to shared state variables, ensuring thread safety.
- Updated the OnReceive method to prevent multiple concurrent executions.
- Enhanced Stop method to ensure proper cleanup of resources and state management.

* fix: enhance Telegram bot's long-polling management

- Improved handling of concurrent starts by stopping existing long-polling loops.
- Implemented mutex for thread-safe access to shared state variables.
- Updated OnReceive method to prevent multiple executions.
- Enhanced Stop method for better resource cleanup and state management.

* .
2026-01-02 16:13:32 +01:00
Anton Petrov
8fe297ef9d
Fix QR codes colors inversion (#3607) 2026-01-02 16:12:30 +01:00
Zhenyu Qi
c881d1015a
fix: handle GitHub API error responses in GetXrayVersions (#3609)
GitHub API returns JSON object instead of array when encountering errors
(e.g., rate limit exceeded). This causes JSON unmarshal error:
'cannot unmarshal object into Go value of type []service.Release'

Add HTTP status code check to handle error responses gracefully and
return user-friendly error messages instead of JSON parsing errors.

Fixes issue where getXrayVersion fails with unmarshal error when
GitHub API rate limit is exceeded.
2026-01-02 16:12:13 +01:00
Nebulosa
c061337ce7
Set log folder variable to /var/log/3x-ui (#3599)
* Set log folder variable to /var/log/3x-ui

* Set log folder as x-ui and create the log folder

* Create the log folder in install and update scripts
2026-01-02 16:11:32 +01:00
11 changed files with 292 additions and 138 deletions

View file

@ -17,7 +17,8 @@ on:
- '**.go' - '**.go'
- 'go.mod' - 'go.mod'
- 'go.sum' - 'go.sum'
- 'x-ui.service' - 'x-ui.service.debian'
- 'x-ui.service.rhel'
jobs: jobs:
build: build:
@ -78,7 +79,8 @@ jobs:
mkdir x-ui mkdir x-ui
cp xui-release x-ui/ cp xui-release x-ui/
cp x-ui.service x-ui/ cp x-ui.service.debian x-ui/
cp x-ui.service.rhel x-ui/
cp x-ui.sh x-ui/ cp x-ui.sh x-ui/
mv x-ui/xui-release x-ui/x-ui mv x-ui/xui-release x-ui/x-ui
mkdir x-ui/bin mkdir x-ui/bin

View file

@ -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 {

View file

@ -8,6 +8,9 @@ plain='\033[0m'
cur_dir=$(pwd) cur_dir=$(pwd)
xui_folder="${XUI_MAIN_FOLDER:=/usr/local/x-ui}"
xui_service="${XUI_SERVICE:=/etc/systemd/system}"
# check root # check root
[[ $EUID -ne 0 ]] && echo -e "${red}Fatal error: ${plain} Please run this script with root privilege \n " && exit 1 [[ $EUID -ne 0 ]] && echo -e "${red}Fatal error: ${plain} Please run this script with root privilege \n " && exit 1
@ -158,7 +161,7 @@ setup_ssl_certificate() {
local webKeyFile="/root/cert/${domain}/privkey.pem" local webKeyFile="/root/cert/${domain}/privkey.pem"
if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then
/usr/local/x-ui/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" >/dev/null 2>&1 ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" >/dev/null 2>&1
echo -e "${green}SSL certificate installed and configured successfully!${plain}" echo -e "${green}SSL certificate installed and configured successfully!${plain}"
return 0 return 0
else else
@ -215,15 +218,15 @@ EOF
fi fi
chmod 755 ${certDir}/* 2>/dev/null 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 ${xui_folder}/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}" echo -e "${yellow}Self-signed certificate configured. Browsers will show a warning.${plain}"
return 0 return 0
} }
# Comprehensive manual SSL certificate issuance via acme.sh # Comprehensive manual SSL certificate issuance via acme.sh
ssl_cert_issue() { 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_webBasePath=$(${xui_folder}/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:]') local existing_port=$(${xui_folder}/x-ui setting -show true | grep 'port:' | awk -F': ' '{print $2}' | tr -d '[:space:]')
# check for acme.sh first # check for acme.sh first
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
@ -366,7 +369,7 @@ ssl_cert_issue() {
local webKeyFile="/root/cert/${domain}/privkey.pem" local webKeyFile="/root/cert/${domain}/privkey.pem"
if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then
/usr/local/x-ui/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile"
echo -e "${green}Certificate paths set for the panel${plain}" echo -e "${green}Certificate paths set for the panel${plain}"
echo -e "${green}Certificate File: $webCertFile${plain}" echo -e "${green}Certificate File: $webCertFile${plain}"
echo -e "${green}Private Key File: $webKeyFile${plain}" echo -e "${green}Private Key File: $webKeyFile${plain}"
@ -451,11 +454,11 @@ prompt_and_setup_ssl() {
} }
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=$(${xui_folder}/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}' | sed 's#^/##') local existing_webBasePath=$(${xui_folder}/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=$(${xui_folder}/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 # 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 existing_cert=$(${xui_folder}/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"
@ -487,7 +490,7 @@ config_after_install() {
echo -e "${yellow}Generated random port: ${config_port}${plain}" echo -e "${yellow}Generated random port: ${config_port}${plain}"
fi fi
/usr/local/x-ui/x-ui setting -username "${config_username}" -password "${config_password}" -port "${config_port}" -webBasePath "${config_webBasePath}" ${xui_folder}/x-ui setting -username "${config_username}" -password "${config_password}" -port "${config_port}" -webBasePath "${config_webBasePath}"
echo "" echo ""
echo -e "${green}═══════════════════════════════════════════${plain}" echo -e "${green}═══════════════════════════════════════════${plain}"
@ -515,7 +518,7 @@ config_after_install() {
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}" ${xui_folder}/x-ui setting -webBasePath "${config_webBasePath}"
echo -e "${green}New WebBasePath: ${config_webBasePath}${plain}" echo -e "${green}New WebBasePath: ${config_webBasePath}${plain}"
# If the panel is already installed but no certificate is configured, prompt for SSL now # If the panel is already installed but no certificate is configured, prompt for SSL now
@ -539,7 +542,7 @@ config_after_install() {
local config_password=$(gen_random_string 10) local config_password=$(gen_random_string 10)
echo -e "${yellow}Default credentials detected. Security update required...${plain}" echo -e "${yellow}Default credentials detected. Security update required...${plain}"
/usr/local/x-ui/x-ui setting -username "${config_username}" -password "${config_password}" ${xui_folder}/x-ui setting -username "${config_username}" -password "${config_password}"
echo -e "Generated new random login credentials:" echo -e "Generated new random login credentials:"
echo -e "###############################################" echo -e "###############################################"
echo -e "${green}Username: ${config_username}${plain}" echo -e "${green}Username: ${config_username}${plain}"
@ -551,7 +554,7 @@ config_after_install() {
# Existing install: if no cert configured, prompt user to set domain or self-signed # 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 # 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:]') existing_cert=$(${xui_folder}/x-ui setting -getCert true | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]')
if [[ -z "$existing_cert" ]]; then if [[ -z "$existing_cert" ]]; then
echo "" echo ""
echo -e "${green}═══════════════════════════════════════════${plain}" echo -e "${green}═══════════════════════════════════════════${plain}"
@ -566,11 +569,11 @@ config_after_install() {
fi fi
fi fi
/usr/local/x-ui/x-ui migrate ${xui_folder}/x-ui migrate
} }
install_x-ui() { install_x-ui() {
cd /usr/local/ cd ${xui_folder%/x-ui}/
# Download resources # Download resources
if [ $# == 0 ]; then if [ $# == 0 ]; then
@ -584,7 +587,7 @@ install_x-ui() {
fi fi
fi fi
echo -e "Got x-ui latest version: ${tag_version}, beginning the installation..." echo -e "Got x-ui latest version: ${tag_version}, beginning the installation..."
wget --inet4-only -N -O /usr/local/x-ui-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz wget --inet4-only -N -O ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz
if [[ $? -ne 0 ]]; then if [[ $? -ne 0 ]]; then
echo -e "${red}Downloading x-ui failed, please be sure that your server can access GitHub ${plain}" echo -e "${red}Downloading x-ui failed, please be sure that your server can access GitHub ${plain}"
exit 1 exit 1
@ -601,7 +604,7 @@ install_x-ui() {
url="https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz" url="https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz"
echo -e "Beginning to install x-ui $1" echo -e "Beginning to install x-ui $1"
wget --inet4-only -N -O /usr/local/x-ui-linux-$(arch).tar.gz ${url} wget --inet4-only -N -O ${xui_folder}-linux-$(arch).tar.gz ${url}
if [[ $? -ne 0 ]]; then if [[ $? -ne 0 ]]; then
echo -e "${red}Download x-ui $1 failed, please check if the version exists ${plain}" echo -e "${red}Download x-ui $1 failed, please check if the version exists ${plain}"
exit 1 exit 1
@ -614,13 +617,13 @@ install_x-ui() {
fi fi
# Stop x-ui service and remove old resources # Stop x-ui service and remove old resources
if [[ -e /usr/local/x-ui/ ]]; then if [[ -e ${xui_folder}/ ]]; then
if [[ $release == "alpine" ]]; then if [[ $release == "alpine" ]]; then
rc-service x-ui stop rc-service x-ui stop
else else
systemctl stop x-ui systemctl stop x-ui
fi fi
rm /usr/local/x-ui/ -rf rm ${xui_folder}/ -rf
fi fi
# Extract resources and set permissions # Extract resources and set permissions
@ -641,8 +644,23 @@ 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
if [[ $? -ne 0 ]]; then if [[ $? -ne 0 ]]; then
@ -653,7 +671,18 @@ install_x-ui() {
rc-update add x-ui rc-update add x-ui
rc-service x-ui start rc-service x-ui start
else else
cp -f x-ui.service /etc/systemd/system/ if [ -f "x-ui.service" ]; then
cp -f x-ui.service ${xui_service}/
else
case "${release}" in
ubuntu | debian | armbian)
cp -f x-ui.service.debian ${xui_service}/x-ui.service
;;
*)
cp -f x-ui.service.rhel ${xui_service}/x-ui.service
;;
esac
fi
systemctl daemon-reload systemctl daemon-reload
systemctl enable x-ui systemctl enable x-ui
systemctl start x-ui systemctl start x-ui

View file

@ -6,6 +6,9 @@ blue='\033[0;34m'
yellow='\033[0;33m' yellow='\033[0;33m'
plain='\033[0m' plain='\033[0m'
xui_folder="${XUI_MAIN_FOLDER:=/usr/local/x-ui}"
xui_service="${XUI_SERVICE:=/etc/systemd/system}"
# Don't edit this config # Don't edit this config
b_source="${BASH_SOURCE[0]}" b_source="${BASH_SOURCE[0]}"
while [ -h "$b_source" ]; do while [ -h "$b_source" ]; do
@ -190,7 +193,7 @@ setup_ssl_certificate() {
local webKeyFile="/root/cert/${domain}/privkey.pem" local webKeyFile="/root/cert/${domain}/privkey.pem"
if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then
/usr/local/x-ui/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" >/dev/null 2>&1 ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" >/dev/null 2>&1
echo -e "${green}SSL certificate installed and configured successfully!${plain}" echo -e "${green}SSL certificate installed and configured successfully!${plain}"
return 0 return 0
else else
@ -246,14 +249,14 @@ EOF
fi fi
chmod 755 ${certDir}/* 2>/dev/null 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 ${xui_folder}/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}" echo -e "${yellow}Self-signed certificate configured. Browsers will show a warning.${plain}"
return 0 return 0
} }
# Comprehensive manual SSL certificate issuance via acme.sh # Comprehensive manual SSL certificate issuance via acme.sh
ssl_cert_issue() { 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_webBasePath=$(${xui_folder}/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:]') local existing_port=$(${xui_folder}/x-ui setting -show true | grep 'port:' | awk -F': ' '{print $2}' | tr -d '[:space:]')
# check for acme.sh first # check for acme.sh first
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
@ -396,7 +399,7 @@ ssl_cert_issue() {
local webKeyFile="/root/cert/${domain}/privkey.pem" local webKeyFile="/root/cert/${domain}/privkey.pem"
if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then
/usr/local/x-ui/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile"
echo -e "${green}Certificate paths set for the panel${plain}" echo -e "${green}Certificate paths set for the panel${plain}"
echo -e "${green}Certificate File: $webCertFile${plain}" echo -e "${green}Certificate File: $webCertFile${plain}"
echo -e "${green}Private Key File: $webKeyFile${plain}" echo -e "${green}Private Key File: $webKeyFile${plain}"
@ -485,13 +488,13 @@ prompt_and_setup_ssl() {
config_after_update() { 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 ${xui_folder}/x-ui setting -show true
/usr/local/x-ui/x-ui migrate ${xui_folder}/x-ui migrate
# Properly detect empty cert by checking if cert: line exists and has content after it # 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_cert=$(${xui_folder}/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_port=$(${xui_folder}/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#^/##') local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}' | sed 's#^/##')
# Get server IP # Get server IP
local URL_lists=( local URL_lists=(
@ -514,7 +517,7 @@ config_after_update() {
if [[ ${#existing_webBasePath} -lt 4 ]]; then if [[ ${#existing_webBasePath} -lt 4 ]]; then
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}"
local config_webBasePath=$(gen_random_string 18) local config_webBasePath=$(gen_random_string 18)
/usr/local/x-ui/x-ui setting -webBasePath "${config_webBasePath}" ${xui_folder}/x-ui setting -webBasePath "${config_webBasePath}"
existing_webBasePath="${config_webBasePath}" existing_webBasePath="${config_webBasePath}"
echo -e "${green}New WebBasePath: ${config_webBasePath}${plain}" echo -e "${green}New WebBasePath: ${config_webBasePath}${plain}"
fi fi
@ -559,10 +562,10 @@ config_after_update() {
} }
update_x-ui() { update_x-ui() {
cd /usr/local/ cd ${xui_folder%/x-ui}/
if [ -f "/usr/local/x-ui/x-ui" ]; then if [ -f "${xui_folder}/x-ui" ]; then
current_xui_version=$(/usr/local/x-ui/x-ui -v) current_xui_version=$(${xui_folder}/x-ui -v)
echo -e "${green}Current x-ui version: ${current_xui_version}${plain}" echo -e "${green}Current x-ui version: ${current_xui_version}${plain}"
else else
_fail "ERROR: Current x-ui version: unknown" _fail "ERROR: Current x-ui version: unknown"
@ -579,16 +582,16 @@ update_x-ui() {
fi fi
fi fi
echo -e "Got x-ui latest version: ${tag_version}, beginning the installation..." echo -e "Got x-ui latest version: ${tag_version}, beginning the installation..."
${wget_bin} -N -O /usr/local/x-ui-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz 2>/dev/null ${wget_bin} -N -O ${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 if [[ $? -ne 0 ]]; then
echo -e "${yellow}Trying to fetch version with IPv4...${plain}" echo -e "${yellow}Trying to fetch version with IPv4...${plain}"
${wget_bin} --inet4-only -N -O /usr/local/x-ui-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz 2>/dev/null ${wget_bin} --inet4-only -N -O ${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 if [[ $? -ne 0 ]]; then
_fail "ERROR: Failed to download x-ui, please be sure that your server can access GitHub" _fail "ERROR: Failed to download x-ui, please be sure that your server can access GitHub"
fi fi
fi fi
if [[ -e /usr/local/x-ui/ ]]; then if [[ -e ${xui_folder}/ ]]; then
echo -e "${green}Stopping x-ui...${plain}" echo -e "${green}Stopping x-ui...${plain}"
if [[ $release == "alpine" ]]; then if [[ $release == "alpine" ]]; then
if [ -f "/etc/init.d/x-ui" ]; then if [ -f "/etc/init.d/x-ui" ]; then
@ -601,11 +604,11 @@ update_x-ui() {
_fail "ERROR: x-ui service unit not installed." _fail "ERROR: x-ui service unit not installed."
fi fi
else else
if [ -f "/etc/systemd/system/x-ui.service" ]; then if [ -f "${xui_service}/x-ui.service" ]; then
systemctl stop x-ui >/dev/null 2>&1 systemctl stop x-ui >/dev/null 2>&1
systemctl disable x-ui >/dev/null 2>&1 systemctl disable x-ui >/dev/null 2>&1
echo -e "${green}Removing old systemd unit version...${plain}" echo -e "${green}Removing old systemd unit version...${plain}"
rm /etc/systemd/system/x-ui.service -f >/dev/null 2>&1 rm ${xui_service}/x-ui.service -f >/dev/null 2>&1
systemctl daemon-reload >/dev/null 2>&1 systemctl daemon-reload >/dev/null 2>&1
else else
rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1 rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1
@ -613,15 +616,17 @@ update_x-ui() {
fi fi
fi fi
echo -e "${green}Removing old x-ui version...${plain}" echo -e "${green}Removing old x-ui version...${plain}"
rm /usr/bin/x-ui -f >/dev/null 2>&1 rm ${xui_folder} -f >/dev/null 2>&1
rm /usr/local/x-ui/x-ui.service -f >/dev/null 2>&1 rm ${xui_folder}/x-ui.service -f >/dev/null 2>&1
rm /usr/local/x-ui/x-ui -f >/dev/null 2>&1 rm ${xui_folder}/x-ui.service.debian -f >/dev/null 2>&1
rm /usr/local/x-ui/x-ui.sh -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.sh -f >/dev/null 2>&1
echo -e "${green}Removing old xray version...${plain}" echo -e "${green}Removing old xray version...${plain}"
rm /usr/local/x-ui/bin/xray-linux-amd64 -f >/dev/null 2>&1 rm ${xui_folder}/bin/xray-linux-amd64 -f >/dev/null 2>&1
echo -e "${green}Removing old README and LICENSE file...${plain}" echo -e "${green}Removing old README and LICENSE file...${plain}"
rm /usr/local/x-ui/bin/README.md -f >/dev/null 2>&1 rm ${xui_folder}/bin/README.md -f >/dev/null 2>&1
rm /usr/local/x-ui/bin/LICENSE -f >/dev/null 2>&1 rm ${xui_folder}/bin/LICENSE -f >/dev/null 2>&1
else else
rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1 rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1
_fail "ERROR: x-ui not installed." _fail "ERROR: x-ui not installed."
@ -651,15 +656,16 @@ update_x-ui() {
fi fi
fi fi
chmod +x /usr/local/x-ui/x-ui.sh >/dev/null 2>&1 chmod +x ${xui_folder}/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 ${xui_folder} >/dev/null 2>&1
if [ -f "/usr/local/x-ui/bin/config.json" ]; then if [ -f "${xui_folder}/bin/config.json" ]; then
echo -e "${green}Changing on config file permissions...${plain}" echo -e "${green}Changing on config file permissions...${plain}"
chmod 640 /usr/local/x-ui/bin/config.json >/dev/null 2>&1 chmod 640 ${xui_folder}/bin/config.json >/dev/null 2>&1
fi fi
if [[ $release == "alpine" ]]; then if [[ $release == "alpine" ]]; then
@ -676,9 +682,22 @@ update_x-ui() {
rc-update add x-ui >/dev/null 2>&1 rc-update add x-ui >/dev/null 2>&1
rc-service x-ui start >/dev/null 2>&1 rc-service x-ui start >/dev/null 2>&1
else else
echo -e "${green}Installing systemd unit...${plain}" if [ -f "x-ui.service" ]; then
cp -f x-ui.service /etc/systemd/system/ >/dev/null 2>&1 echo -e "${green}Installing systemd unit...${plain}"
chown root:root /etc/systemd/system/x-ui.service >/dev/null 2>&1 cp -f x-ui.service ${xui_service}/ >/dev/null 2>&1
else
case "${release}" in
ubuntu | debian | armbian)
echo -e "${green}Installing debian-like systemd unit...${plain}"
cp -f x-ui.service.debian ${xui_service}/x-ui.service >/dev/null 2>&1
;;
*)
echo -e "${green}Installing rhel-like systemd unit...${plain}"
cp -f x-ui.service.rhel ${xui_service}/x-ui.service >/dev/null 2>&1
;;
esac
fi
chown root:root ${xui_service}/x-ui.service >/dev/null 2>&1
systemctl daemon-reload >/dev/null 2>&1 systemctl daemon-reload >/dev/null 2>&1
systemctl enable x-ui >/dev/null 2>&1 systemctl enable x-ui >/dev/null 2>&1
systemctl start x-ui >/dev/null 2>&1 systemctl start x-ui >/dev/null 2>&1

File diff suppressed because one or more lines are too long

View file

@ -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) {

View file

@ -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 {

View file

@ -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, &params) updates, _ := bot.UpdatesViaLongPolling(ctx, &params)
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.

View file

@ -4,6 +4,7 @@ After=network.target
Wants=network.target Wants=network.target
[Service] [Service]
EnvironmentFile=-/etc/default/x-ui
Environment="XRAY_VMESS_AEAD_FORCED=false" Environment="XRAY_VMESS_AEAD_FORCED=false"
Type=simple Type=simple
WorkingDirectory=/usr/local/x-ui/ WorkingDirectory=/usr/local/x-ui/

16
x-ui.service.rhel Normal file
View file

@ -0,0 +1,16 @@
[Unit]
Description=x-ui Service
After=network.target
Wants=network.target
[Service]
EnvironmentFile=-/etc/sysconfig/x-ui
Environment="XRAY_VMESS_AEAD_FORCED=false"
Type=simple
WorkingDirectory=/usr/local/x-ui/
ExecStart=/usr/local/x-ui/x-ui
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target

65
x-ui.sh
View file

@ -53,7 +53,10 @@ 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}" xui_folder="${XUI_MAIN_FOLDER:=/usr/local/x-ui}"
xui_service="${XUI_SERVICE:=/etc/systemd/system}"
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"
@ -126,7 +129,7 @@ update_menu() {
fi fi
wget -O /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh wget -O /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh
chmod +x /usr/local/x-ui/x-ui.sh chmod +x ${xui_folder}/x-ui.sh
chmod +x /usr/bin/x-ui chmod +x /usr/bin/x-ui
if [[ $? == 0 ]]; then if [[ $? == 0 ]]; then
@ -175,13 +178,13 @@ uninstall() {
else else
systemctl stop x-ui systemctl stop x-ui
systemctl disable x-ui systemctl disable x-ui
rm /etc/systemd/system/x-ui.service -f rm ${xui_service}/x-ui.service -f
systemctl daemon-reload systemctl daemon-reload
systemctl reset-failed systemctl reset-failed
fi fi
rm /etc/x-ui/ -rf rm /etc/x-ui/ -rf
rm /usr/local/x-ui/ -rf rm ${xui_folder}/ -rf
echo "" echo ""
echo -e "Uninstalled Successfully.\n" echo -e "Uninstalled Successfully.\n"
@ -209,9 +212,9 @@ reset_user() {
read -rp "Do you want to disable currently configured two-factor authentication? (y/n): " twoFactorConfirm read -rp "Do you want to disable currently configured two-factor authentication? (y/n): " twoFactorConfirm
if [[ $twoFactorConfirm != "y" && $twoFactorConfirm != "Y" ]]; then if [[ $twoFactorConfirm != "y" && $twoFactorConfirm != "Y" ]]; then
/usr/local/x-ui/x-ui setting -username ${config_account} -password ${config_password} -resetTwoFactor false >/dev/null 2>&1 ${xui_folder}/x-ui setting -username ${config_account} -password ${config_password} -resetTwoFactor false >/dev/null 2>&1
else else
/usr/local/x-ui/x-ui setting -username ${config_account} -password ${config_password} -resetTwoFactor true >/dev/null 2>&1 ${xui_folder}/x-ui setting -username ${config_account} -password ${config_password} -resetTwoFactor true >/dev/null 2>&1
echo -e "Two factor authentication has been disabled." echo -e "Two factor authentication has been disabled."
fi fi
@ -273,7 +276,7 @@ EOF
fi fi
chmod 755 ${certDir}/* >/dev/null 2>&1 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 ${xui_folder}/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." LOGI "Self-signed certificate configured. Browsers will show a warning."
return 0 return 0
} }
@ -290,7 +293,7 @@ reset_webbasepath() {
config_webBasePath=$(gen_random_string 18) config_webBasePath=$(gen_random_string 18)
# Apply the new web base path setting # Apply the new web base path setting
/usr/local/x-ui/x-ui setting -webBasePath "${config_webBasePath}" >/dev/null 2>&1 ${xui_folder}/x-ui setting -webBasePath "${config_webBasePath}" >/dev/null 2>&1
echo -e "Web base path has been reset to: ${green}${config_webBasePath}${plain}" echo -e "Web base path has been reset to: ${green}${config_webBasePath}${plain}"
echo -e "${green}Please use the new web base path to access the panel.${plain}" echo -e "${green}Please use the new web base path to access the panel.${plain}"
@ -305,13 +308,13 @@ reset_config() {
fi fi
return 0 return 0
fi fi
/usr/local/x-ui/x-ui setting -reset ${xui_folder}/x-ui setting -reset
echo -e "All panel settings have been reset to default." echo -e "All panel settings have been reset to default."
restart restart
} }
check_config() { check_config() {
local info=$(/usr/local/x-ui/x-ui setting -show true) local info=$(${xui_folder}/x-ui setting -show true)
if [[ $? != 0 ]]; then if [[ $? != 0 ]]; then
LOGE "get current settings error, please check logs" LOGE "get current settings error, please check logs"
show_menu show_menu
@ -321,7 +324,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 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]') local existing_cert=$(${xui_folder}/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)
@ -362,7 +365,7 @@ set_port() {
LOGD "Cancelled" LOGD "Cancelled"
before_show_menu before_show_menu
else else
/usr/local/x-ui/x-ui setting -port ${port} ${xui_folder}/x-ui setting -port ${port}
echo -e "The port is set, Please restart the panel now, and use the new port ${green}${port}${plain} to access web panel" echo -e "The port is set, Please restart the panel now, and use the new port ${green}${port}${plain} to access web panel"
confirm_restart confirm_restart
fi fi
@ -654,7 +657,7 @@ check_status() {
return 1 return 1
fi fi
else else
if [[ ! -f /etc/systemd/system/x-ui.service ]]; then if [[ ! -f ${xui_service}/x-ui.service ]]; then
return 2 return 2
fi fi
temp=$(systemctl status x-ui | grep Active | awk '{print $3}' | cut -d "(" -f2 | cut -d ")" -f1) temp=$(systemctl status x-ui | grep Active | awk '{print $3}' | cut -d "(" -f2 | cut -d ")" -f1)
@ -976,7 +979,7 @@ update_geo() {
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
cd /usr/local/x-ui/bin cd ${xui_folder}/bin
case "$choice" in case "$choice" in
0) 0)
@ -1118,7 +1121,7 @@ ssl_cert_issue_main() {
local webKeyFile="/root/cert/${domain}/privkey.pem" local webKeyFile="/root/cert/${domain}/privkey.pem"
if [[ -f "${webCertFile}" && -f "${webKeyFile}" ]]; then if [[ -f "${webCertFile}" && -f "${webKeyFile}" ]]; then
/usr/local/x-ui/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile"
echo "Panel paths set for domain: $domain" echo "Panel paths set for domain: $domain"
echo " - Certificate File: $webCertFile" echo " - Certificate File: $webCertFile"
echo " - Private Key File: $webKeyFile" echo " - Private Key File: $webKeyFile"
@ -1153,8 +1156,8 @@ ssl_cert_issue_main() {
ssl_cert_issue_for_ip() { ssl_cert_issue_for_ip() {
LOGI "Starting automatic SSL certificate generation for server 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_webBasePath=$(${xui_folder}/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=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')
# Get server IP # Get server IP
local server_ip=$(curl -s --max-time 3 https://api.ipify.org) local server_ip=$(curl -s --max-time 3 https://api.ipify.org)
@ -1264,7 +1267,7 @@ ssl_cert_issue_for_ip() {
local webKeyFile="/root/cert/${server_ip}/privkey.pem" local webKeyFile="/root/cert/${server_ip}/privkey.pem"
if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then
/usr/local/x-ui/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile"
LOGI "Certificate configured for panel" LOGI "Certificate configured for panel"
LOGI " - Certificate File: $webCertFile" LOGI " - Certificate File: $webCertFile"
LOGI " - Private Key File: $webKeyFile" LOGI " - Private Key File: $webKeyFile"
@ -1279,8 +1282,8 @@ ssl_cert_issue_for_ip() {
} }
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=$(${xui_folder}/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=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')
# check for acme.sh first # check for acme.sh first
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
echo "acme.sh could not be found. we will install it" echo "acme.sh could not be found. we will install it"
@ -1445,7 +1448,7 @@ ssl_cert_issue() {
local webKeyFile="/root/cert/${domain}/privkey.pem" local webKeyFile="/root/cert/${domain}/privkey.pem"
if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then
/usr/local/x-ui/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile"
LOGI "Panel paths set for domain: $domain" LOGI "Panel paths set for domain: $domain"
LOGI " - Certificate File: $webCertFile" LOGI " - Certificate File: $webCertFile"
LOGI " - Private Key File: $webKeyFile" LOGI " - Private Key File: $webKeyFile"
@ -1460,8 +1463,8 @@ ssl_cert_issue() {
} }
ssl_cert_issue_CF() { ssl_cert_issue_CF() {
local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}') local existing_webBasePath=$(${xui_folder}/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=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')
LOGI "****** Instructions for Use ******" LOGI "****** Instructions for Use ******"
LOGI "Follow the steps below to complete the process:" LOGI "Follow the steps below to complete the process:"
LOGI "1. Cloudflare Registered E-mail." LOGI "1. Cloudflare Registered E-mail."
@ -1585,7 +1588,7 @@ ssl_cert_issue_CF() {
local webKeyFile="${certPath}/privkey.pem" local webKeyFile="${certPath}/privkey.pem"
if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then
/usr/local/x-ui/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile"
LOGI "Panel paths set for domain: $CF_Domain" LOGI "Panel paths set for domain: $CF_Domain"
LOGI " - Certificate File: $webCertFile" LOGI " - Certificate File: $webCertFile"
LOGI " - Private Key File: $webKeyFile" LOGI " - Private Key File: $webKeyFile"
@ -2049,11 +2052,11 @@ SSH_port_forwarding() {
break break
fi fi
done done
local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}') local existing_webBasePath=$(${xui_folder}/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=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')
local existing_listenIP=$(/usr/local/x-ui/x-ui setting -getListen true | grep -Eo 'listenIP: .+' | awk '{print $2}') local existing_listenIP=$(${xui_folder}/x-ui setting -getListen true | grep -Eo 'listenIP: .+' | awk '{print $2}')
local existing_cert=$(/usr/local/x-ui/x-ui setting -getCert true | grep -Eo 'cert: .+' | awk '{print $2}') local existing_cert=$(${xui_folder}/x-ui setting -getCert true | grep -Eo 'cert: .+' | awk '{print $2}')
local existing_key=$(/usr/local/x-ui/x-ui setting -getCert true | grep -Eo 'key: .+' | awk '{print $2}') local existing_key=$(${xui_folder}/x-ui setting -getCert true | grep -Eo 'key: .+' | awk '{print $2}')
local config_listenIP="" local config_listenIP=""
local listen_choice="" local listen_choice=""
@ -2094,7 +2097,7 @@ SSH_port_forwarding() {
config_listenIP="127.0.0.1" config_listenIP="127.0.0.1"
[[ "$listen_choice" == "2" ]] && read -rp "Enter custom IP to listen on: " config_listenIP [[ "$listen_choice" == "2" ]] && read -rp "Enter custom IP to listen on: " config_listenIP
/usr/local/x-ui/x-ui setting -listenIP "${config_listenIP}" >/dev/null 2>&1 ${xui_folder}/x-ui setting -listenIP "${config_listenIP}" >/dev/null 2>&1
echo -e "${green}listen IP has been set to ${config_listenIP}.${plain}" echo -e "${green}listen IP has been set to ${config_listenIP}.${plain}"
echo -e "\n${green}SSH Port Forwarding Configuration:${plain}" echo -e "\n${green}SSH Port Forwarding Configuration:${plain}"
echo -e "Standard SSH command:" echo -e "Standard SSH command:"
@ -2110,7 +2113,7 @@ SSH_port_forwarding() {
fi fi
;; ;;
2) 2)
/usr/local/x-ui/x-ui setting -listenIP 0.0.0.0 >/dev/null 2>&1 ${xui_folder}/x-ui setting -listenIP 0.0.0.0 >/dev/null 2>&1
echo -e "${green}Listen IP has been cleared.${plain}" echo -e "${green}Listen IP has been cleared.${plain}"
restart restart
;; ;;