Compare commits

...

6 commits

Author SHA1 Message Date
MHSanaei
094d8fb0a0
fix 2026-01-03 03:56:54 +01:00
Sanaei
6c192b35f4
Merge branch 'main' into usrlocal 2026-01-03 03:52:00 +01:00
MHSanaei
9af1c2a078
fix 2026-01-03 03:47:38 +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
7 changed files with 148 additions and 31 deletions

View file

@ -17,7 +17,8 @@ on:
- '**.go'
- 'go.mod'
- 'go.sum'
- 'x-ui.service'
- 'x-ui.service.debian'
- 'x-ui.service.rhel'
jobs:
build:
@ -78,7 +79,8 @@ jobs:
mkdir 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/
mv x-ui/xui-release x-ui/x-ui
mkdir x-ui/bin

View file

@ -161,7 +161,7 @@ setup_ssl_certificate() {
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
${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" >/dev/null 2>&1
echo -e "${green}SSL certificate installed and configured successfully!${plain}"
return 0
else
@ -218,15 +218,15 @@ EOF
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
${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}"
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:]')
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:]')
# check for acme.sh first
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
@ -369,7 +369,7 @@ ssl_cert_issue() {
local webKeyFile="/root/cert/${domain}/privkey.pem"
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 File: $webCertFile${plain}"
echo -e "${green}Private Key File: $webKeyFile${plain}"
@ -455,7 +455,7 @@ prompt_and_setup_ssl() {
config_after_install() {
local existing_hasDefaultCredential=$(${xui_folder}/x-ui setting -show true | grep -Eo 'hasDefaultCredential: .+' | awk '{print $2}')
local existing_webBasePath=$(${xui_folder}/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}' | sed 's#^/##')
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
local existing_cert=$(${xui_folder}/x-ui setting -getCert true | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]')
@ -554,7 +554,7 @@ config_after_install() {
# 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:]')
existing_cert=$(${xui_folder}/x-ui setting -getCert true | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]')
if [[ -z "$existing_cert" ]]; then
echo ""
echo -e "${green}═══════════════════════════════════════════${plain}"
@ -646,6 +646,20 @@ install_x-ui() {
chmod +x /usr/bin/x-ui
mkdir -p /var/log/x-ui
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
wget --inet4-only -O /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc
@ -657,7 +671,18 @@ install_x-ui() {
rc-update add x-ui
rc-service x-ui start
else
cp -f x-ui.service ${xui_service}/
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 enable x-ui
systemctl start x-ui

View file

@ -193,7 +193,7 @@ setup_ssl_certificate() {
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
${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" >/dev/null 2>&1
echo -e "${green}SSL certificate installed and configured successfully!${plain}"
return 0
else
@ -249,14 +249,14 @@ EOF
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
${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}"
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:]')
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:]')
# check for acme.sh first
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
@ -399,7 +399,7 @@ ssl_cert_issue() {
local webKeyFile="/root/cert/${domain}/privkey.pem"
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 File: $webCertFile${plain}"
echo -e "${green}Private Key File: $webKeyFile${plain}"
@ -616,8 +616,10 @@ update_x-ui() {
fi
fi
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 ${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.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}"
@ -680,8 +682,21 @@ update_x-ui() {
rc-update add x-ui >/dev/null 2>&1
rc-service x-ui start >/dev/null 2>&1
else
echo -e "${green}Installing systemd unit...${plain}"
cp -f x-ui.service ${xui_service}/ >/dev/null 2>&1
if [ -f "x-ui.service" ]; then
echo -e "${green}Installing systemd unit...${plain}"
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 enable x-ui >/dev/null 2>&1

View file

@ -120,6 +120,10 @@
oldAllSetting: new AllSetting(),
allSetting: new AllSetting(),
saveBtnDisable: true,
entryHost: null,
entryPort: null,
entryProtocol: null,
entryIsIP: false,
user: {},
lang: LanguageManager.getLanguage(),
inboundOptions: [],
@ -233,6 +237,31 @@
loading(spinning = true) {
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() {
const msg = await HttpUtil.post("/panel/setting/all");
@ -307,16 +336,41 @@
this.loading(true);
const msg = await HttpUtil.post("/panel/setting/restartPanel");
this.loading(false);
if (msg.success) {
this.loading(true);
await PromiseUtil.sleep(5000);
var { webCertFile, webKeyFile, webDomain: host, webPort: port, webBasePath: base } = this.allSetting;
if (host == this.oldAllSetting.webDomain) host = null;
if (port == this.oldAllSetting.webPort) port = null;
const isTLS = webCertFile !== "" || webKeyFile !== "";
const url = URLBuilder.buildURL({ host, port, isTLS, base, path: "panel/settings" });
window.location.replace(url);
if (!msg.success) return;
this.loading(true);
await PromiseUtil.sleep(5000);
const { webDomain, webPort, webBasePath, webCertFile, webKeyFile } = this.allSetting;
const newProtocol = (webCertFile || webKeyFile) ? "https:" : "http:";
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) {
if (newValue) {
@ -568,6 +622,10 @@
}
},
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.loadInboundTags();
while (true) {

View file

@ -4,6 +4,7 @@ After=network.target
Wants=network.target
[Service]
EnvironmentFile=-/etc/default/x-ui
Environment="XRAY_VMESS_AEAD_FORCED=false"
Type=simple
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

View file

@ -276,7 +276,7 @@ EOF
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
${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."
return 0
}
@ -1156,8 +1156,8 @@ ssl_cert_issue_main() {
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}')
local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}')
local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')
# Get server IP
local server_ip=$(curl -s --max-time 3 https://api.ipify.org)
@ -1267,7 +1267,7 @@ ssl_cert_issue_for_ip() {
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"
${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile"
LOGI "Certificate configured for panel"
LOGI " - Certificate File: $webCertFile"
LOGI " - Private Key File: $webKeyFile"