TLS: Remove ECH Force Query

This commit is contained in:
MHSanaei
2026-05-04 13:20:24 +02:00
parent 51e2fb6dbf
commit e19061d513
66 changed files with 4378 additions and 4636 deletions

View File

@@ -37,4 +37,4 @@ curl -sfLRo geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/release
curl -sfLRo geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat curl -sfLRo geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
curl -sfLRo geoip_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat curl -sfLRo geoip_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat
curl -sfLRo geosite_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat curl -sfLRo geosite_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat
cd ../../ cd ../../

View File

@@ -18,7 +18,7 @@ xui_service="${XUI_SERVICE:=/etc/systemd/system}"
if [[ -f /etc/os-release ]]; then if [[ -f /etc/os-release ]]; then
source /etc/os-release source /etc/os-release
release=$ID release=$ID
elif [[ -f /usr/lib/os-release ]]; then elif [[ -f /usr/lib/os-release ]]; then
source /usr/lib/os-release source /usr/lib/os-release
release=$ID release=$ID
else else
@@ -59,16 +59,16 @@ is_domain() {
# Port helpers # Port helpers
is_port_in_use() { is_port_in_use() {
local port="$1" local port="$1"
if command -v ss >/dev/null 2>&1; then 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}' ss -ltn 2> /dev/null | awk -v p=":${port}$" '$4 ~ p {exit 0} END {exit 1}'
return return
fi fi
if command -v netstat >/dev/null 2>&1; then 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}' netstat -lnt 2> /dev/null | awk -v p=":${port} " '$4 ~ p {exit 0} END {exit 1}'
return return
fi fi
if command -v lsof >/dev/null 2>&1; then if command -v lsof > /dev/null 2>&1; then
lsof -nP -iTCP:${port} -sTCP:LISTEN >/dev/null 2>&1 && return 0 lsof -nP -iTCP:${port} -sTCP:LISTEN > /dev/null 2>&1 && return 0
fi fi
return 1 return 1
} }
@@ -77,35 +77,35 @@ install_base() {
case "${release}" in case "${release}" in
ubuntu | debian | armbian) ubuntu | debian | armbian)
apt-get update && apt-get install -y -q cron curl tar tzdata socat ca-certificates openssl apt-get update && apt-get install -y -q cron curl tar tzdata socat ca-certificates openssl
;; ;;
fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
dnf -y update && dnf install -y -q cronie curl tar tzdata socat ca-certificates openssl dnf -y update && dnf install -y -q cronie curl tar tzdata socat ca-certificates openssl
;; ;;
centos) centos)
if [[ "${VERSION_ID}" =~ ^7 ]]; then if [[ "${VERSION_ID}" =~ ^7 ]]; then
yum -y update && yum install -y cronie curl tar tzdata socat ca-certificates openssl yum -y update && yum install -y cronie curl tar tzdata socat ca-certificates openssl
else else
dnf -y update && dnf install -y -q cronie curl tar tzdata socat ca-certificates openssl dnf -y update && dnf install -y -q cronie curl tar tzdata socat ca-certificates openssl
fi fi
;; ;;
arch | manjaro | parch) arch | manjaro | parch)
pacman -Syu && pacman -Syu --noconfirm cronie curl tar tzdata socat ca-certificates openssl pacman -Syu && pacman -Syu --noconfirm cronie curl tar tzdata socat ca-certificates openssl
;; ;;
opensuse-tumbleweed | opensuse-leap) opensuse-tumbleweed | opensuse-leap)
zypper refresh && zypper -q install -y cron curl tar timezone socat ca-certificates openssl zypper refresh && zypper -q install -y cron curl tar timezone socat ca-certificates openssl
;; ;;
alpine) alpine)
apk update && apk add dcron curl tar tzdata socat ca-certificates openssl apk update && apk add dcron curl tar tzdata socat ca-certificates openssl
;; ;;
*) *)
apt-get update && apt-get install -y -q cron curl tar tzdata socat ca-certificates openssl apt-get update && apt-get install -y -q cron curl tar tzdata socat ca-certificates openssl
;; ;;
esac esac
} }
gen_random_string() { gen_random_string() {
local length="$1" local length="$1"
openssl rand -base64 $(( length * 2 )) \ openssl rand -base64 $((length * 2)) \
| tr -dc 'a-zA-Z0-9' \ | tr -dc 'a-zA-Z0-9' \
| head -c "$length" | head -c "$length"
} }
@@ -113,7 +113,7 @@ gen_random_string() {
install_acme() { install_acme() {
echo -e "${green}Installing acme.sh for SSL certificate management...${plain}" echo -e "${green}Installing acme.sh for SSL certificate management...${plain}"
cd ~ || return 1 cd ~ || return 1
curl -s https://get.acme.sh | sh >/dev/null 2>&1 curl -s https://get.acme.sh | sh > /dev/null 2>&1
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo -e "${red}Failed to install acme.sh${plain}" echo -e "${red}Failed to install acme.sh${plain}"
return 1 return 1
@@ -128,60 +128,60 @@ setup_ssl_certificate() {
local server_ip="$2" local server_ip="$2"
local existing_port="$3" local existing_port="$3"
local existing_webBasePath="$4" local existing_webBasePath="$4"
echo -e "${green}Setting up SSL certificate...${plain}" echo -e "${green}Setting up SSL certificate...${plain}"
# Check if acme.sh is installed # Check if acme.sh is installed
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then if ! command -v ~/.acme.sh/acme.sh &> /dev/null; then
install_acme install_acme
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo -e "${yellow}Failed to install acme.sh, skipping SSL setup${plain}" echo -e "${yellow}Failed to install acme.sh, skipping SSL setup${plain}"
return 1 return 1
fi fi
fi fi
# Create certificate directory # Create certificate directory
local certPath="/root/cert/${domain}" local certPath="/root/cert/${domain}"
mkdir -p "$certPath" mkdir -p "$certPath"
# Issue certificate # Issue certificate
echo -e "${green}Issuing SSL certificate for ${domain}...${plain}" echo -e "${green}Issuing SSL certificate for ${domain}...${plain}"
echo -e "${yellow}Note: Port 80 must be open and accessible from the internet${plain}" echo -e "${yellow}Note: Port 80 must be open and accessible from the internet${plain}"
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force >/dev/null 2>&1 ~/.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 ~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport 80 --force
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo -e "${yellow}Failed to issue certificate for ${domain}${plain}" echo -e "${yellow}Failed to issue certificate for ${domain}${plain}"
echo -e "${yellow}Please ensure port 80 is open and try again later with: x-ui${plain}" echo -e "${yellow}Please ensure port 80 is open and try again later with: x-ui${plain}"
rm -rf ~/.acme.sh/${domain} 2>/dev/null rm -rf ~/.acme.sh/${domain} 2> /dev/null
rm -rf "$certPath" 2>/dev/null rm -rf "$certPath" 2> /dev/null
return 1 return 1
fi fi
# Install certificate # Install certificate
~/.acme.sh/acme.sh --installcert -d ${domain} \ ~/.acme.sh/acme.sh --installcert -d ${domain} \
--key-file /root/cert/${domain}/privkey.pem \ --key-file /root/cert/${domain}/privkey.pem \
--fullchain-file /root/cert/${domain}/fullchain.pem \ --fullchain-file /root/cert/${domain}/fullchain.pem \
--reloadcmd "systemctl restart x-ui" >/dev/null 2>&1 --reloadcmd "systemctl restart x-ui" > /dev/null 2>&1
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo -e "${yellow}Failed to install certificate${plain}" echo -e "${yellow}Failed to install certificate${plain}"
return 1 return 1
fi fi
# Enable auto-renew # Enable auto-renew
~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1 ~/.acme.sh/acme.sh --upgrade --auto-upgrade > /dev/null 2>&1
# Secure permissions: private key readable only by owner # Secure permissions: private key readable only by owner
chmod 600 $certPath/privkey.pem 2>/dev/null chmod 600 $certPath/privkey.pem 2> /dev/null
chmod 644 $certPath/fullchain.pem 2>/dev/null chmod 644 $certPath/fullchain.pem 2> /dev/null
# Set certificate for panel # Set certificate for panel
local webCertFile="/root/cert/${domain}/fullchain.pem" local webCertFile="/root/cert/${domain}/fullchain.pem"
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
${xui_folder}/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
@@ -194,14 +194,14 @@ setup_ssl_certificate() {
# Requires acme.sh and port 80 open for HTTP-01 challenge # Requires acme.sh and port 80 open for HTTP-01 challenge
setup_ip_certificate() { setup_ip_certificate() {
local ipv4="$1" local ipv4="$1"
local ipv6="$2" # optional local ipv6="$2" # optional
echo -e "${green}Setting up Let's Encrypt IP certificate (shortlived profile)...${plain}" echo -e "${green}Setting up Let's Encrypt IP certificate (shortlived profile)...${plain}"
echo -e "${yellow}Note: IP certificates are valid for ~6 days and will auto-renew.${plain}" echo -e "${yellow}Note: IP certificates are valid for ~6 days and will auto-renew.${plain}"
echo -e "${yellow}Default listener is port 80. If you choose another port, ensure external port 80 forwards to it.${plain}" echo -e "${yellow}Default listener is port 80. If you choose another port, ensure external port 80 forwards to it.${plain}"
# Check for acme.sh # Check for acme.sh
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then if ! command -v ~/.acme.sh/acme.sh &> /dev/null; then
install_acme install_acme
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo -e "${red}Failed to install acme.sh${plain}" echo -e "${red}Failed to install acme.sh${plain}"
@@ -273,8 +273,8 @@ setup_ip_certificate() {
# Issue certificate with shortlived profile # Issue certificate with shortlived profile
echo -e "${green}Issuing IP certificate for ${ipv4}...${plain}" echo -e "${green}Issuing IP certificate for ${ipv4}...${plain}"
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force >/dev/null 2>&1 ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force > /dev/null 2>&1
~/.acme.sh/acme.sh --issue \ ~/.acme.sh/acme.sh --issue \
${domain_args} \ ${domain_args} \
--standalone \ --standalone \
@@ -288,9 +288,9 @@ setup_ip_certificate() {
echo -e "${red}Failed to issue IP certificate${plain}" echo -e "${red}Failed to issue IP certificate${plain}"
echo -e "${yellow}Please ensure port ${WebPort} is reachable (or forwarded from external port 80)${plain}" echo -e "${yellow}Please ensure port ${WebPort} is reachable (or forwarded from external port 80)${plain}"
# Cleanup acme.sh data for both IPv4 and IPv6 if specified # Cleanup acme.sh data for both IPv4 and IPv6 if specified
rm -rf ~/.acme.sh/${ipv4} 2>/dev/null rm -rf ~/.acme.sh/${ipv4} 2> /dev/null
[[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null [[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2> /dev/null
rm -rf ${certDir} 2>/dev/null rm -rf ${certDir} 2> /dev/null
return 1 return 1
fi fi
@@ -308,25 +308,25 @@ setup_ip_certificate() {
if [[ ! -f "${certDir}/fullchain.pem" || ! -f "${certDir}/privkey.pem" ]]; then if [[ ! -f "${certDir}/fullchain.pem" || ! -f "${certDir}/privkey.pem" ]]; then
echo -e "${red}Certificate files not found after installation${plain}" echo -e "${red}Certificate files not found after installation${plain}"
# Cleanup acme.sh data for both IPv4 and IPv6 if specified # Cleanup acme.sh data for both IPv4 and IPv6 if specified
rm -rf ~/.acme.sh/${ipv4} 2>/dev/null rm -rf ~/.acme.sh/${ipv4} 2> /dev/null
[[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null [[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2> /dev/null
rm -rf ${certDir} 2>/dev/null rm -rf ${certDir} 2> /dev/null
return 1 return 1
fi fi
echo -e "${green}Certificate files installed successfully${plain}" echo -e "${green}Certificate files installed successfully${plain}"
# Enable auto-upgrade for acme.sh (ensures cron job runs) # Enable auto-upgrade for acme.sh (ensures cron job runs)
~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1 ~/.acme.sh/acme.sh --upgrade --auto-upgrade > /dev/null 2>&1
# Secure permissions: private key readable only by owner # Secure permissions: private key readable only by owner
chmod 600 ${certDir}/privkey.pem 2>/dev/null chmod 600 ${certDir}/privkey.pem 2> /dev/null
chmod 644 ${certDir}/fullchain.pem 2>/dev/null chmod 644 ${certDir}/fullchain.pem 2> /dev/null
# Configure panel to use the certificate # Configure panel to use the certificate
echo -e "${green}Setting certificate paths for the panel...${plain}" echo -e "${green}Setting certificate paths for the panel...${plain}"
${xui_folder}/x-ui cert -webCert "${certDir}/fullchain.pem" -webCertKey "${certDir}/privkey.pem" ${xui_folder}/x-ui cert -webCert "${certDir}/fullchain.pem" -webCertKey "${certDir}/privkey.pem"
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo -e "${yellow}Warning: Could not set certificate paths automatically${plain}" echo -e "${yellow}Warning: Could not set certificate paths automatically${plain}"
echo -e "${yellow}Certificate files are at:${plain}" echo -e "${yellow}Certificate files are at:${plain}"
@@ -346,9 +346,9 @@ setup_ip_certificate() {
ssl_cert_issue() { 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_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:]') 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
echo "acme.sh could not be found. Installing now..." echo "acme.sh could not be found. Installing now..."
cd ~ || return 1 cd ~ || return 1
curl -s https://get.acme.sh | sh curl -s https://get.acme.sh | sh
@@ -364,18 +364,18 @@ ssl_cert_issue() {
local domain="" local domain=""
while true; do while true; do
read -rp "Please enter your domain name: " domain read -rp "Please enter your domain name: " domain
domain="${domain// /}" # Trim whitespace domain="${domain// /}" # Trim whitespace
if [[ -z "$domain" ]]; then if [[ -z "$domain" ]]; then
echo -e "${red}Domain name cannot be empty. Please try again.${plain}" echo -e "${red}Domain name cannot be empty. Please try again.${plain}"
continue continue
fi fi
if ! is_domain "$domain"; then if ! is_domain "$domain"; then
echo -e "${red}Invalid domain format: ${domain}. Please enter a valid domain name.${plain}" echo -e "${red}Invalid domain format: ${domain}. Please enter a valid domain name.${plain}"
continue continue
fi fi
break break
done done
echo -e "${green}Your domain is: ${domain}, checking it...${plain}" echo -e "${green}Your domain is: ${domain}, checking it...${plain}"
@@ -383,9 +383,9 @@ ssl_cert_issue() {
# detect existing certificate and reuse it if present # detect existing certificate and reuse it if present
local cert_exists=0 local cert_exists=0
if ~/.acme.sh/acme.sh --list 2>/dev/null | awk '{print $1}' | grep -Fxq "${domain}"; then if ~/.acme.sh/acme.sh --list 2> /dev/null | awk '{print $1}' | grep -Fxq "${domain}"; then
cert_exists=1 cert_exists=1
local certInfo=$(~/.acme.sh/acme.sh --list 2>/dev/null | grep -F "${domain}") local certInfo=$(~/.acme.sh/acme.sh --list 2> /dev/null | grep -F "${domain}")
echo -e "${yellow}Existing certificate found for ${domain}, will reuse it.${plain}" echo -e "${yellow}Existing certificate found for ${domain}, will reuse it.${plain}"
[[ -n "${certInfo}" ]] && echo "$certInfo" [[ -n "${certInfo}" ]] && echo "$certInfo"
else else
@@ -412,7 +412,7 @@ ssl_cert_issue() {
# Stop panel temporarily # Stop panel temporarily
echo -e "${yellow}Stopping panel temporarily...${plain}" echo -e "${yellow}Stopping panel temporarily...${plain}"
systemctl stop x-ui 2>/dev/null || rc-service x-ui stop 2>/dev/null systemctl stop x-ui 2> /dev/null || rc-service x-ui stop 2> /dev/null
if [[ ${cert_exists} -eq 0 ]]; then if [[ ${cert_exists} -eq 0 ]]; then
# issue the certificate # issue the certificate
@@ -421,7 +421,7 @@ ssl_cert_issue() {
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo -e "${red}Issuing certificate failed, please check logs.${plain}" echo -e "${red}Issuing certificate failed, please check logs.${plain}"
rm -rf ~/.acme.sh/${domain} rm -rf ~/.acme.sh/${domain}
systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null systemctl start x-ui 2> /dev/null || rc-service x-ui start 2> /dev/null
return 1 return 1
else else
echo -e "${green}Issuing certificate succeeded, installing certificates...${plain}" echo -e "${green}Issuing certificate succeeded, installing certificates...${plain}"
@@ -441,18 +441,18 @@ ssl_cert_issue() {
echo -e "${green}\t0.${plain} Keep default reloadcmd" echo -e "${green}\t0.${plain} Keep default reloadcmd"
read -rp "Choose an option: " choice read -rp "Choose an option: " choice
case "$choice" in case "$choice" in
1) 1)
echo -e "${green}Reloadcmd is: systemctl reload nginx ; systemctl restart x-ui${plain}" echo -e "${green}Reloadcmd is: systemctl reload nginx ; systemctl restart x-ui${plain}"
reloadCmd="systemctl reload nginx ; systemctl restart x-ui" reloadCmd="systemctl reload nginx ; systemctl restart x-ui"
;; ;;
2) 2)
echo -e "${yellow}It's recommended to put x-ui restart at the end${plain}" echo -e "${yellow}It's recommended to put x-ui restart at the end${plain}"
read -rp "Please enter your custom reloadcmd: " reloadCmd read -rp "Please enter your custom reloadcmd: " reloadCmd
echo -e "${green}Reloadcmd is: ${reloadCmd}${plain}" echo -e "${green}Reloadcmd is: ${reloadCmd}${plain}"
;; ;;
*) *)
echo -e "${green}Keeping default reloadcmd${plain}" echo -e "${green}Keeping default reloadcmd${plain}"
;; ;;
esac esac
fi fi
@@ -469,14 +469,14 @@ ssl_cert_issue() {
installWroteFiles=1 installWroteFiles=1
fi fi
if [[ -f "/root/cert/${domain}/privkey.pem" && -f "/root/cert/${domain}/fullchain.pem" && ( ${installRc} -eq 0 || ${installWroteFiles} -eq 1 ) ]]; then if [[ -f "/root/cert/${domain}/privkey.pem" && -f "/root/cert/${domain}/fullchain.pem" && (${installRc} -eq 0 || ${installWroteFiles} -eq 1) ]]; then
echo -e "${green}Installing certificate succeeded, enabling auto renew...${plain}" echo -e "${green}Installing certificate succeeded, enabling auto renew...${plain}"
else else
echo -e "${red}Installing certificate failed, exiting.${plain}" echo -e "${red}Installing certificate failed, exiting.${plain}"
if [[ ${cert_exists} -eq 0 ]]; then if [[ ${cert_exists} -eq 0 ]]; then
rm -rf ~/.acme.sh/${domain} rm -rf ~/.acme.sh/${domain}
fi fi
systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null systemctl start x-ui 2> /dev/null || rc-service x-ui start 2> /dev/null
return 1 return 1
fi fi
@@ -486,18 +486,18 @@ ssl_cert_issue() {
echo -e "${yellow}Auto renew setup had issues, certificate details:${plain}" echo -e "${yellow}Auto renew setup had issues, certificate details:${plain}"
ls -lah /root/cert/${domain}/ ls -lah /root/cert/${domain}/
# Secure permissions: private key readable only by owner # Secure permissions: private key readable only by owner
chmod 600 $certPath/privkey.pem 2>/dev/null chmod 600 $certPath/privkey.pem 2> /dev/null
chmod 644 $certPath/fullchain.pem 2>/dev/null chmod 644 $certPath/fullchain.pem 2> /dev/null
else else
echo -e "${green}Auto renew succeeded, certificate details:${plain}" echo -e "${green}Auto renew succeeded, certificate details:${plain}"
ls -lah /root/cert/${domain}/ ls -lah /root/cert/${domain}/
# Secure permissions: private key readable only by owner # Secure permissions: private key readable only by owner
chmod 600 $certPath/privkey.pem 2>/dev/null chmod 600 $certPath/privkey.pem 2> /dev/null
chmod 644 $certPath/fullchain.pem 2>/dev/null chmod 644 $certPath/fullchain.pem 2> /dev/null
fi fi
# start panel # start panel
systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null systemctl start x-ui 2> /dev/null || rc-service x-ui start 2> /dev/null
# Prompt user to set panel paths after successful certificate installation # Prompt user to set panel paths after successful certificate installation
read -rp "Would you like to set this certificate for the panel? (y/n): " setPanel read -rp "Would you like to set this certificate for the panel? (y/n): " setPanel
@@ -513,14 +513,14 @@ ssl_cert_issue() {
echo "" echo ""
echo -e "${green}Access URL: https://${domain}:${existing_port}/${existing_webBasePath}${plain}" echo -e "${green}Access URL: https://${domain}:${existing_port}/${existing_webBasePath}${plain}"
echo -e "${yellow}Panel will restart to apply SSL certificate...${plain}" echo -e "${yellow}Panel will restart to apply SSL certificate...${plain}"
systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null systemctl restart x-ui 2> /dev/null || rc-service x-ui restart 2> /dev/null
else else
echo -e "${red}Error: Certificate or private key file not found for domain: $domain.${plain}" echo -e "${red}Error: Certificate or private key file not found for domain: $domain.${plain}"
fi fi
else else
echo -e "${yellow}Skipping panel path setting.${plain}" echo -e "${yellow}Skipping panel path setting.${plain}"
fi fi
return 0 return 0
} }
@@ -528,7 +528,7 @@ ssl_cert_issue() {
# Sets global `SSL_HOST` to the chosen domain/IP for Access URL usage # Sets global `SSL_HOST` to the chosen domain/IP for Access URL usage
prompt_and_setup_ssl() { prompt_and_setup_ssl() {
local panel_port="$1" local panel_port="$1"
local web_base_path="$2" # expected without leading slash local web_base_path="$2" # expected without leading slash
local server_ip="$3" local server_ip="$3"
local ssl_choice="" local ssl_choice=""
@@ -539,124 +539,124 @@ prompt_and_setup_ssl() {
echo -e "${green}3.${plain} Custom SSL Certificate (Path to existing files)" echo -e "${green}3.${plain} Custom SSL Certificate (Path to existing files)"
echo -e "${blue}Note:${plain} Options 1 & 2 require port 80 open. Option 3 requires manual paths." echo -e "${blue}Note:${plain} Options 1 & 2 require port 80 open. Option 3 requires manual paths."
read -rp "Choose an option (default 2 for IP): " ssl_choice read -rp "Choose an option (default 2 for IP): " ssl_choice
ssl_choice="${ssl_choice// /}" # Trim whitespace ssl_choice="${ssl_choice// /}" # Trim whitespace
# Default to 2 (IP cert) if input is empty or invalid (not 1 or 3) # Default to 2 (IP cert) if input is empty or invalid (not 1 or 3)
if [[ "$ssl_choice" != "1" && "$ssl_choice" != "3" ]]; then if [[ "$ssl_choice" != "1" && "$ssl_choice" != "3" ]]; then
ssl_choice="2" ssl_choice="2"
fi fi
case "$ssl_choice" in case "$ssl_choice" in
1) 1)
# User chose Let's Encrypt domain option # User chose Let's Encrypt domain option
echo -e "${green}Using Let's Encrypt for domain certificate...${plain}" echo -e "${green}Using Let's Encrypt for domain certificate...${plain}"
if ssl_cert_issue; then if ssl_cert_issue; then
local cert_domain="${SSL_ISSUED_DOMAIN}" local cert_domain="${SSL_ISSUED_DOMAIN}"
if [[ -z "${cert_domain}" ]]; then if [[ -z "${cert_domain}" ]]; then
cert_domain=$(~/.acme.sh/acme.sh --list 2>/dev/null | tail -1 | awk '{print $1}') cert_domain=$(~/.acme.sh/acme.sh --list 2> /dev/null | tail -1 | awk '{print $1}')
fi fi
if [[ -n "${cert_domain}" ]]; then if [[ -n "${cert_domain}" ]]; then
SSL_HOST="${cert_domain}" SSL_HOST="${cert_domain}"
echo -e "${green}✓ SSL certificate configured successfully with domain: ${cert_domain}${plain}" echo -e "${green}✓ SSL certificate configured successfully with domain: ${cert_domain}${plain}"
else
echo -e "${yellow}SSL setup may have completed, but domain extraction failed${plain}"
SSL_HOST="${server_ip}"
fi
else else
echo -e "${yellow}SSL setup may have completed, but domain extraction failed${plain}" echo -e "${red}SSL certificate setup failed for domain mode.${plain}"
SSL_HOST="${server_ip}" SSL_HOST="${server_ip}"
fi fi
else ;;
echo -e "${red}SSL certificate setup failed for domain mode.${plain}" 2)
SSL_HOST="${server_ip}" # User chose Let's Encrypt IP certificate option
fi echo -e "${green}Using Let's Encrypt for IP certificate (shortlived profile)...${plain}"
;;
2)
# User chose Let's Encrypt IP certificate option
echo -e "${green}Using Let's Encrypt for IP certificate (shortlived profile)...${plain}"
# Ask for optional IPv6
local ipv6_addr=""
read -rp "Do you have an IPv6 address to include? (leave empty to skip): " ipv6_addr
ipv6_addr="${ipv6_addr// /}" # Trim whitespace
# Stop panel if running (port 80 needed)
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 certificate configured successfully${plain}"
else
echo -e "${red}✗ IP certificate setup failed. Please check port 80 is open.${plain}"
SSL_HOST="${server_ip}"
fi
;;
3)
# User chose Custom Paths (User Provided) option
echo -e "${green}Using custom existing certificate...${plain}"
local custom_cert=""
local custom_key=""
local custom_domain=""
# 3.1 Request Domain to compose Panel URL later # Ask for optional IPv6
read -rp "Please enter domain name certificate issued for: " custom_domain local ipv6_addr=""
custom_domain="${custom_domain// /}" # Remove spaces read -rp "Do you have an IPv6 address to include? (leave empty to skip): " ipv6_addr
ipv6_addr="${ipv6_addr// /}" # Trim whitespace
# 3.2 Loop for Certificate Path # Stop panel if running (port 80 needed)
while true; do if [[ $release == "alpine" ]]; then
read -rp "Input certificate path (keywords: .crt / fullchain): " custom_cert rc-service x-ui stop > /dev/null 2>&1
# Strip quotes if present
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}Error: File does not exist! Try again.${plain}"
elif [[ ! -r "$custom_cert" ]]; then
echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}"
else else
echo -e "${red}Error: File is empty!${plain}" systemctl stop x-ui > /dev/null 2>&1
fi fi
done
# 3.3 Loop for Private Key Path setup_ip_certificate "${server_ip}" "${ipv6_addr}"
while true; do if [ $? -eq 0 ]; then
read -rp "Input private key path (keywords: .key / privatekey): " custom_key SSL_HOST="${server_ip}"
# Strip quotes if present echo -e "${green}✓ Let's Encrypt IP certificate configured successfully${plain}"
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}Error: File does not exist! Try again.${plain}"
elif [[ ! -r "$custom_key" ]]; then
echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}"
else else
echo -e "${red}Error: File is empty!${plain}" echo -e "${red}✗ IP certificate setup failed. Please check port 80 is open.${plain}"
SSL_HOST="${server_ip}"
fi fi
done ;;
3)
# User chose Custom Paths (User Provided) option
echo -e "${green}Using custom existing certificate...${plain}"
local custom_cert=""
local custom_key=""
local custom_domain=""
# 3.4 Apply Settings via x-ui binary # 3.1 Request Domain to compose Panel URL later
${xui_folder}/x-ui cert -webCert "$custom_cert" -webCertKey "$custom_key" >/dev/null 2>&1 read -rp "Please enter domain name certificate issued for: " custom_domain
custom_domain="${custom_domain// /}" # Remove spaces
# Set SSL_HOST for composing Panel URL
if [[ -n "$custom_domain" ]]; then # 3.2 Loop for Certificate Path
SSL_HOST="$custom_domain" while true; do
else read -rp "Input certificate path (keywords: .crt / fullchain): " custom_cert
# Strip quotes if present
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}Error: File does not exist! Try again.${plain}"
elif [[ ! -r "$custom_cert" ]]; then
echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}"
else
echo -e "${red}Error: File is empty!${plain}"
fi
done
# 3.3 Loop for Private Key Path
while true; do
read -rp "Input private key path (keywords: .key / privatekey): " custom_key
# Strip quotes if present
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}Error: File does not exist! Try again.${plain}"
elif [[ ! -r "$custom_key" ]]; then
echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}"
else
echo -e "${red}Error: File is empty!${plain}"
fi
done
# 3.4 Apply Settings via x-ui binary
${xui_folder}/x-ui cert -webCert "$custom_cert" -webCertKey "$custom_key" > /dev/null 2>&1
# Set SSL_HOST for composing Panel URL
if [[ -n "$custom_domain" ]]; then
SSL_HOST="$custom_domain"
else
SSL_HOST="${server_ip}"
fi
echo -e "${green}✓ Custom certificate paths applied.${plain}"
echo -e "${yellow}Note: You are responsible for renewing these files externally.${plain}"
systemctl restart x-ui > /dev/null 2>&1 || rc-service x-ui restart > /dev/null 2>&1
;;
*)
echo -e "${red}Invalid option. Skipping SSL setup.${plain}"
SSL_HOST="${server_ip}" SSL_HOST="${server_ip}"
fi ;;
echo -e "${green}✓ Custom certificate paths applied.${plain}"
echo -e "${yellow}Note: You are responsible for renewing these files externally.${plain}"
systemctl restart x-ui >/dev/null 2>&1 || rc-service x-ui restart >/dev/null 2>&1
;;
*)
echo -e "${red}Invalid option. Skipping SSL setup.${plain}"
SSL_HOST="${server_ip}"
;;
esac esac
} }
@@ -676,7 +676,7 @@ config_after_install() {
) )
local server_ip="" local server_ip=""
for ip_address in "${URL_lists[@]}"; do for ip_address in "${URL_lists[@]}"; do
local response=$(curl -s -w "\n%{http_code}" --max-time 3 "${ip_address}" 2>/dev/null) local response=$(curl -s -w "\n%{http_code}" --max-time 3 "${ip_address}" 2> /dev/null)
local http_code=$(echo "$response" | tail -n1) local http_code=$(echo "$response" | tail -n1)
local ip_result=$(echo "$response" | head -n-1 | tr -d '[:space:]') local ip_result=$(echo "$response" | head -n-1 | tr -d '[:space:]')
if [[ "${http_code}" == "200" && -n "${ip_result}" ]]; then if [[ "${http_code}" == "200" && -n "${ip_result}" ]]; then
@@ -684,13 +684,13 @@ config_after_install() {
break break
fi fi
done done
if [[ ${#existing_webBasePath} -lt 4 ]]; then if [[ ${#existing_webBasePath} -lt 4 ]]; then
if [[ "$existing_hasDefaultCredential" == "true" ]]; then if [[ "$existing_hasDefaultCredential" == "true" ]]; then
local config_webBasePath=$(gen_random_string 18) local config_webBasePath=$(gen_random_string 18)
local config_username=$(gen_random_string 10) local config_username=$(gen_random_string 10)
local config_password=$(gen_random_string 10) local config_password=$(gen_random_string 10)
read -rp "Would you like to customize the Panel Port settings? (If not, a random port will be applied) [y/n]: " config_confirm read -rp "Would you like to customize the Panel Port settings? (If not, a random port will be applied) [y/n]: " config_confirm
if [[ "${config_confirm}" == "y" || "${config_confirm}" == "Y" ]]; then if [[ "${config_confirm}" == "y" || "${config_confirm}" == "Y" ]]; then
read -rp "Please set up the panel port: " config_port read -rp "Please set up the panel port: " config_port
@@ -699,9 +699,9 @@ config_after_install() {
local config_port=$(shuf -i 1024-62000 -n 1) local config_port=$(shuf -i 1024-62000 -n 1)
echo -e "${yellow}Generated random port: ${config_port}${plain}" echo -e "${yellow}Generated random port: ${config_port}${plain}"
fi fi
${xui_folder}/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}"
echo -e "${green} SSL Certificate Setup (MANDATORY) ${plain}" echo -e "${green} SSL Certificate Setup (MANDATORY) ${plain}"
@@ -711,7 +711,7 @@ config_after_install() {
echo "" echo ""
prompt_and_setup_ssl "${config_port}" "${config_webBasePath}" "${server_ip}" prompt_and_setup_ssl "${config_port}" "${config_webBasePath}" "${server_ip}"
# Display final credentials and access information # Display final credentials and access information
echo "" echo ""
echo -e "${green}═══════════════════════════════════════════${plain}" echo -e "${green}═══════════════════════════════════════════${plain}"
@@ -750,7 +750,7 @@ config_after_install() {
if [[ "$existing_hasDefaultCredential" == "true" ]]; then if [[ "$existing_hasDefaultCredential" == "true" ]]; then
local config_username=$(gen_random_string 10) local config_username=$(gen_random_string 10)
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}"
${xui_folder}/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:"
@@ -778,13 +778,13 @@ config_after_install() {
echo -e "${green}SSL certificate already configured. No action needed.${plain}" echo -e "${green}SSL certificate already configured. No action needed.${plain}"
fi fi
fi fi
${xui_folder}/x-ui migrate ${xui_folder}/x-ui migrate
} }
install_x-ui() { install_x-ui() {
cd ${xui_folder%/x-ui}/ cd ${xui_folder%/x-ui}/
# Download resources # Download resources
if [ $# == 0 ]; then if [ $# == 0 ]; then
tag_version=$(curl -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') tag_version=$(curl -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
@@ -806,12 +806,12 @@ install_x-ui() {
tag_version=$1 tag_version=$1
tag_version_numeric=${tag_version#v} tag_version_numeric=${tag_version#v}
min_version="2.3.5" min_version="2.3.5"
if [[ "$(printf '%s\n' "$min_version" "$tag_version_numeric" | sort -V | head -n1)" != "$min_version" ]]; then if [[ "$(printf '%s\n' "$min_version" "$tag_version_numeric" | sort -V | head -n1)" != "$min_version" ]]; then
echo -e "${red}Please use a newer version (at least v2.3.5). Exiting installation.${plain}" echo -e "${red}Please use a newer version (at least v2.3.5). Exiting installation.${plain}"
exit 1 exit 1
fi fi
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"
curl -4fLRo ${xui_folder}-linux-$(arch).tar.gz ${url} curl -4fLRo ${xui_folder}-linux-$(arch).tar.gz ${url}
@@ -825,7 +825,7 @@ install_x-ui() {
echo -e "${red}Failed to download x-ui.sh${plain}" echo -e "${red}Failed to download x-ui.sh${plain}"
exit 1 exit 1
fi fi
# Stop x-ui service and remove old resources # Stop x-ui service and remove old resources
if [[ -e ${xui_folder}/ ]]; then if [[ -e ${xui_folder}/ ]]; then
if [[ $release == "alpine" ]]; then if [[ $release == "alpine" ]]; then
@@ -835,22 +835,22 @@ install_x-ui() {
fi fi
rm ${xui_folder}/ -rf rm ${xui_folder}/ -rf
fi fi
# Extract resources and set permissions # Extract resources and set permissions
tar zxvf x-ui-linux-$(arch).tar.gz tar zxvf x-ui-linux-$(arch).tar.gz
rm x-ui-linux-$(arch).tar.gz -f rm x-ui-linux-$(arch).tar.gz -f
cd x-ui cd x-ui
chmod +x x-ui chmod +x x-ui
chmod +x x-ui.sh chmod +x x-ui.sh
# Check the system's architecture and rename the file accordingly # Check the system's architecture and rename the file accordingly
if [[ $(arch) == "armv5" || $(arch) == "armv6" || $(arch) == "armv7" ]]; then if [[ $(arch) == "armv5" || $(arch) == "armv6" || $(arch) == "armv7" ]]; then
mv bin/xray-linux-$(arch) bin/xray-linux-arm mv bin/xray-linux-$(arch) bin/xray-linux-arm
chmod +x bin/xray-linux-arm chmod +x bin/xray-linux-arm
fi fi
chmod +x x-ui bin/xray-linux-$(arch) chmod +x x-ui bin/xray-linux-$(arch)
# 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
@@ -870,7 +870,7 @@ install_x-ui() {
echo -e "${green}Created /etc/.gitignore and added x-ui.db for etckeeper${plain}" echo -e "${green}Created /etc/.gitignore and added x-ui.db for etckeeper${plain}"
fi fi
fi fi
if [[ $release == "alpine" ]]; then if [[ $release == "alpine" ]]; then
curl -4fLRo /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc curl -4fLRo /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc
if [[ $? -ne 0 ]]; then if [[ $? -ne 0 ]]; then
@@ -883,73 +883,73 @@ install_x-ui() {
else else
# Install systemd service file # Install systemd service file
service_installed=false service_installed=false
if [ -f "x-ui.service" ]; then if [ -f "x-ui.service" ]; then
echo -e "${green}Found x-ui.service in extracted files, installing...${plain}" echo -e "${green}Found x-ui.service in extracted files, installing...${plain}"
cp -f x-ui.service ${xui_service}/ >/dev/null 2>&1 cp -f x-ui.service ${xui_service}/ > /dev/null 2>&1
if [[ $? -eq 0 ]]; then if [[ $? -eq 0 ]]; then
service_installed=true service_installed=true
fi fi
fi fi
if [ "$service_installed" = false ]; then if [ "$service_installed" = false ]; then
case "${release}" in case "${release}" in
ubuntu | debian | armbian) ubuntu | debian | armbian)
if [ -f "x-ui.service.debian" ]; then if [ -f "x-ui.service.debian" ]; then
echo -e "${green}Found x-ui.service.debian in extracted files, installing...${plain}" echo -e "${green}Found x-ui.service.debian in extracted files, installing...${plain}"
cp -f x-ui.service.debian ${xui_service}/x-ui.service >/dev/null 2>&1 cp -f x-ui.service.debian ${xui_service}/x-ui.service > /dev/null 2>&1
if [[ $? -eq 0 ]]; then if [[ $? -eq 0 ]]; then
service_installed=true service_installed=true
fi fi
fi fi
;; ;;
arch | manjaro | parch) arch | manjaro | parch)
if [ -f "x-ui.service.arch" ]; then if [ -f "x-ui.service.arch" ]; then
echo -e "${green}Found x-ui.service.arch in extracted files, installing...${plain}" echo -e "${green}Found x-ui.service.arch in extracted files, installing...${plain}"
cp -f x-ui.service.arch ${xui_service}/x-ui.service >/dev/null 2>&1 cp -f x-ui.service.arch ${xui_service}/x-ui.service > /dev/null 2>&1
if [[ $? -eq 0 ]]; then if [[ $? -eq 0 ]]; then
service_installed=true service_installed=true
fi fi
fi fi
;; ;;
*) *)
if [ -f "x-ui.service.rhel" ]; then if [ -f "x-ui.service.rhel" ]; then
echo -e "${green}Found x-ui.service.rhel in extracted files, installing...${plain}" echo -e "${green}Found x-ui.service.rhel in extracted files, installing...${plain}"
cp -f x-ui.service.rhel ${xui_service}/x-ui.service >/dev/null 2>&1 cp -f x-ui.service.rhel ${xui_service}/x-ui.service > /dev/null 2>&1
if [[ $? -eq 0 ]]; then if [[ $? -eq 0 ]]; then
service_installed=true service_installed=true
fi fi
fi fi
;; ;;
esac esac
fi fi
# If service file not found in tar.gz, download from GitHub # If service file not found in tar.gz, download from GitHub
if [ "$service_installed" = false ]; then if [ "$service_installed" = false ]; then
echo -e "${yellow}Service files not found in tar.gz, downloading from GitHub...${plain}" echo -e "${yellow}Service files not found in tar.gz, downloading from GitHub...${plain}"
case "${release}" in case "${release}" in
ubuntu | debian | armbian) ubuntu | debian | armbian)
curl -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.debian >/dev/null 2>&1 curl -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) arch | manjaro | parch)
curl -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.arch >/dev/null 2>&1 curl -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.arch > /dev/null 2>&1
;; ;;
*) *)
curl -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.rhel >/dev/null 2>&1 curl -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.rhel > /dev/null 2>&1
;; ;;
esac esac
if [[ $? -ne 0 ]]; then if [[ $? -ne 0 ]]; then
echo -e "${red}Failed to install x-ui.service from GitHub${plain}" echo -e "${red}Failed to install x-ui.service from GitHub${plain}"
exit 1 exit 1
fi fi
service_installed=true service_installed=true
fi fi
if [ "$service_installed" = true ]; then if [ "$service_installed" = true ]; then
echo -e "${green}Setting up systemd unit...${plain}" echo -e "${green}Setting up systemd unit...${plain}"
chown root:root ${xui_service}/x-ui.service >/dev/null 2>&1 chown root:root ${xui_service}/x-ui.service > /dev/null 2>&1
chmod 644 ${xui_service}/x-ui.service >/dev/null 2>&1 chmod 644 ${xui_service}/x-ui.service > /dev/null 2>&1
systemctl daemon-reload systemctl daemon-reload
systemctl enable x-ui systemctl enable x-ui
systemctl start x-ui systemctl start x-ui
@@ -958,7 +958,7 @@ install_x-ui() {
exit 1 exit 1
fi fi
fi fi
echo -e "${green}x-ui ${tag_version}${plain} installation finished, it is running now..." echo -e "${green}x-ui ${tag_version}${plain} installation finished, it is running now..."
echo -e "" echo -e ""
echo -e "┌───────────────────────────────────────────────────────┐ echo -e "┌───────────────────────────────────────────────────────┐

564
update.sh
View File

@@ -12,16 +12,16 @@ 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
b_dir="$(cd -P "$(dirname "$b_source")" >/dev/null 2>&1 && pwd || pwd -P)" b_dir="$(cd -P "$(dirname "$b_source")" > /dev/null 2>&1 && pwd || pwd -P)"
b_source="$(readlink "$b_source")" b_source="$(readlink "$b_source")"
[[ $b_source != /* ]] && b_source="$b_dir/$b_source" [[ $b_source != /* ]] && b_source="$b_dir/$b_source"
done done
cur_dir="$(cd -P "$(dirname "$b_source")" >/dev/null 2>&1 && pwd || pwd -P)" cur_dir="$(cd -P "$(dirname "$b_source")" > /dev/null 2>&1 && pwd || pwd -P)"
script_name=$(basename "$0") script_name=$(basename "$0")
# Check command exist function # Check command exist function
_command_exists() { _command_exists() {
type "$1" &>/dev/null type "$1" &> /dev/null
} }
# Fail, log and exit script function # Fail, log and exit script function
@@ -44,7 +44,7 @@ fi
if [[ -f /etc/os-release ]]; then if [[ -f /etc/os-release ]]; then
source /etc/os-release source /etc/os-release
release=$ID release=$ID
elif [[ -f /usr/lib/os-release ]]; then elif [[ -f /usr/lib/os-release ]]; then
source /usr/lib/os-release source /usr/lib/os-release
release=$ID release=$ID
else else
@@ -61,7 +61,7 @@ arch() {
armv6* | armv6) echo 'armv6' ;; armv6* | armv6) echo 'armv6' ;;
armv5* | armv5) echo 'armv5' ;; armv5* | armv5) echo 'armv5' ;;
s390x) echo 's390x' ;; s390x) echo 's390x' ;;
*) echo -e "${red}Unsupported CPU architecture!${plain}" && rm -f "${cur_dir}/${script_name}" >/dev/null 2>&1 && exit 2;; *) echo -e "${red}Unsupported CPU architecture!${plain}" && rm -f "${cur_dir}/${script_name}" > /dev/null 2>&1 && exit 2 ;;
esac esac
} }
@@ -84,23 +84,23 @@ is_domain() {
# Port helpers # Port helpers
is_port_in_use() { is_port_in_use() {
local port="$1" local port="$1"
if command -v ss >/dev/null 2>&1; then 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}' ss -ltn 2> /dev/null | awk -v p=":${port}$" '$4 ~ p {exit 0} END {exit 1}'
return return
fi fi
if command -v netstat >/dev/null 2>&1; then 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}' netstat -lnt 2> /dev/null | awk -v p=":${port} " '$4 ~ p {exit 0} END {exit 1}'
return return
fi fi
if command -v lsof >/dev/null 2>&1; then if command -v lsof > /dev/null 2>&1; then
lsof -nP -iTCP:${port} -sTCP:LISTEN >/dev/null 2>&1 && return 0 lsof -nP -iTCP:${port} -sTCP:LISTEN > /dev/null 2>&1 && return 0
fi fi
return 1 return 1
} }
gen_random_string() { gen_random_string() {
local length="$1" local length="$1"
openssl rand -base64 $(( length * 2 )) \ openssl rand -base64 $((length * 2)) \
| tr -dc 'a-zA-Z0-9' \ | tr -dc 'a-zA-Z0-9' \
| head -c "$length" | head -c "$length"
} }
@@ -109,37 +109,37 @@ install_base() {
echo -e "${green}Updating and install dependency packages...${plain}" echo -e "${green}Updating and install dependency packages...${plain}"
case "${release}" in case "${release}" in
ubuntu | debian | armbian) ubuntu | debian | armbian)
apt-get update >/dev/null 2>&1 && apt-get install -y -q cron curl tar tzdata socat openssl >/dev/null 2>&1 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) 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 dnf -y update > /dev/null 2>&1 && dnf install -y -q cronie curl tar tzdata socat openssl > /dev/null 2>&1
;; ;;
centos) centos)
if [[ "${VERSION_ID}" =~ ^7 ]]; then if [[ "${VERSION_ID}" =~ ^7 ]]; then
yum -y update >/dev/null 2>&1 && yum install -y -q cronie curl tar tzdata socat openssl >/dev/null 2>&1 yum -y update > /dev/null 2>&1 && yum install -y -q cronie curl tar tzdata socat openssl > /dev/null 2>&1
else else
dnf -y update >/dev/null 2>&1 && dnf install -y -q cronie curl tar tzdata socat openssl >/dev/null 2>&1 dnf -y update > /dev/null 2>&1 && dnf install -y -q cronie curl tar tzdata socat openssl > /dev/null 2>&1
fi fi
;; ;;
arch | manjaro | parch) arch | manjaro | parch)
pacman -Syu >/dev/null 2>&1 && pacman -Syu --noconfirm cronie curl tar tzdata socat openssl >/dev/null 2>&1 pacman -Syu > /dev/null 2>&1 && pacman -Syu --noconfirm cronie curl tar tzdata socat openssl > /dev/null 2>&1
;; ;;
opensuse-tumbleweed | opensuse-leap) opensuse-tumbleweed | opensuse-leap)
zypper refresh >/dev/null 2>&1 && zypper -q install -y cron curl tar timezone socat openssl >/dev/null 2>&1 zypper refresh > /dev/null 2>&1 && zypper -q install -y cron curl tar timezone socat openssl > /dev/null 2>&1
;; ;;
alpine) alpine)
apk update >/dev/null 2>&1 && apk add dcron curl tar tzdata socat openssl>/dev/null 2>&1 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 apt-get update > /dev/null 2>&1 && apt install -y -q cron curl tar tzdata socat openssl > /dev/null 2>&1
;; ;;
esac esac
} }
install_acme() { install_acme() {
echo -e "${green}Installing acme.sh for SSL certificate management...${plain}" echo -e "${green}Installing acme.sh for SSL certificate management...${plain}"
cd ~ || return 1 cd ~ || return 1
curl -s https://get.acme.sh | sh >/dev/null 2>&1 curl -s https://get.acme.sh | sh > /dev/null 2>&1
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo -e "${red}Failed to install acme.sh${plain}" echo -e "${red}Failed to install acme.sh${plain}"
return 1 return 1
@@ -154,59 +154,59 @@ setup_ssl_certificate() {
local server_ip="$2" local server_ip="$2"
local existing_port="$3" local existing_port="$3"
local existing_webBasePath="$4" local existing_webBasePath="$4"
echo -e "${green}Setting up SSL certificate...${plain}" echo -e "${green}Setting up SSL certificate...${plain}"
# Check if acme.sh is installed # Check if acme.sh is installed
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then if ! command -v ~/.acme.sh/acme.sh &> /dev/null; then
install_acme install_acme
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo -e "${yellow}Failed to install acme.sh, skipping SSL setup${plain}" echo -e "${yellow}Failed to install acme.sh, skipping SSL setup${plain}"
return 1 return 1
fi fi
fi fi
# Create certificate directory # Create certificate directory
local certPath="/root/cert/${domain}" local certPath="/root/cert/${domain}"
mkdir -p "$certPath" mkdir -p "$certPath"
# Issue certificate # Issue certificate
echo -e "${green}Issuing SSL certificate for ${domain}...${plain}" echo -e "${green}Issuing SSL certificate for ${domain}...${plain}"
echo -e "${yellow}Note: Port 80 must be open and accessible from the internet${plain}" echo -e "${yellow}Note: Port 80 must be open and accessible from the internet${plain}"
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force >/dev/null 2>&1 ~/.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 ~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport 80 --force
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo -e "${yellow}Failed to issue certificate for ${domain}${plain}" echo -e "${yellow}Failed to issue certificate for ${domain}${plain}"
echo -e "${yellow}Please ensure port 80 is open and try again later with: x-ui${plain}" echo -e "${yellow}Please ensure port 80 is open and try again later with: x-ui${plain}"
rm -rf ~/.acme.sh/${domain} 2>/dev/null rm -rf ~/.acme.sh/${domain} 2> /dev/null
rm -rf "$certPath" 2>/dev/null rm -rf "$certPath" 2> /dev/null
return 1 return 1
fi fi
# Install certificate # Install certificate
~/.acme.sh/acme.sh --installcert -d ${domain} \ ~/.acme.sh/acme.sh --installcert -d ${domain} \
--key-file /root/cert/${domain}/privkey.pem \ --key-file /root/cert/${domain}/privkey.pem \
--fullchain-file /root/cert/${domain}/fullchain.pem \ --fullchain-file /root/cert/${domain}/fullchain.pem \
--reloadcmd "systemctl restart x-ui" >/dev/null 2>&1 --reloadcmd "systemctl restart x-ui" > /dev/null 2>&1
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo -e "${yellow}Failed to install certificate${plain}" echo -e "${yellow}Failed to install certificate${plain}"
return 1 return 1
fi fi
# Enable auto-renew # Enable auto-renew
~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1 ~/.acme.sh/acme.sh --upgrade --auto-upgrade > /dev/null 2>&1
chmod 600 $certPath/privkey.pem 2>/dev/null chmod 600 $certPath/privkey.pem 2> /dev/null
chmod 644 $certPath/fullchain.pem 2>/dev/null chmod 644 $certPath/fullchain.pem 2> /dev/null
# Set certificate for panel # Set certificate for panel
local webCertFile="/root/cert/${domain}/fullchain.pem" local webCertFile="/root/cert/${domain}/fullchain.pem"
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
${xui_folder}/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
@@ -219,14 +219,14 @@ setup_ssl_certificate() {
# Requires acme.sh and port 80 open for HTTP-01 challenge # Requires acme.sh and port 80 open for HTTP-01 challenge
setup_ip_certificate() { setup_ip_certificate() {
local ipv4="$1" local ipv4="$1"
local ipv6="$2" # optional local ipv6="$2" # optional
echo -e "${green}Setting up Let's Encrypt IP certificate (shortlived profile)...${plain}" echo -e "${green}Setting up Let's Encrypt IP certificate (shortlived profile)...${plain}"
echo -e "${yellow}Note: IP certificates are valid for ~6 days and will auto-renew.${plain}" echo -e "${yellow}Note: IP certificates are valid for ~6 days and will auto-renew.${plain}"
echo -e "${yellow}Default listener is port 80. If you choose another port, ensure external port 80 forwards to it.${plain}" echo -e "${yellow}Default listener is port 80. If you choose another port, ensure external port 80 forwards to it.${plain}"
# Check for acme.sh # Check for acme.sh
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then if ! command -v ~/.acme.sh/acme.sh &> /dev/null; then
install_acme install_acme
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo -e "${red}Failed to install acme.sh${plain}" echo -e "${red}Failed to install acme.sh${plain}"
@@ -298,8 +298,8 @@ setup_ip_certificate() {
# Issue certificate with shortlived profile # Issue certificate with shortlived profile
echo -e "${green}Issuing IP certificate for ${ipv4}...${plain}" echo -e "${green}Issuing IP certificate for ${ipv4}...${plain}"
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force >/dev/null 2>&1 ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force > /dev/null 2>&1
~/.acme.sh/acme.sh --issue \ ~/.acme.sh/acme.sh --issue \
${domain_args} \ ${domain_args} \
--standalone \ --standalone \
@@ -313,9 +313,9 @@ setup_ip_certificate() {
echo -e "${red}Failed to issue IP certificate${plain}" echo -e "${red}Failed to issue IP certificate${plain}"
echo -e "${yellow}Please ensure port ${WebPort} is reachable (or forwarded from external port 80)${plain}" echo -e "${yellow}Please ensure port ${WebPort} is reachable (or forwarded from external port 80)${plain}"
# Cleanup acme.sh data for both IPv4 and IPv6 if specified # Cleanup acme.sh data for both IPv4 and IPv6 if specified
rm -rf ~/.acme.sh/${ipv4} 2>/dev/null rm -rf ~/.acme.sh/${ipv4} 2> /dev/null
[[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null [[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2> /dev/null
rm -rf ${certDir} 2>/dev/null rm -rf ${certDir} 2> /dev/null
return 1 return 1
fi fi
@@ -333,19 +333,19 @@ setup_ip_certificate() {
if [[ ! -f "${certDir}/fullchain.pem" || ! -f "${certDir}/privkey.pem" ]]; then if [[ ! -f "${certDir}/fullchain.pem" || ! -f "${certDir}/privkey.pem" ]]; then
echo -e "${red}Certificate files not found after installation${plain}" echo -e "${red}Certificate files not found after installation${plain}"
# Cleanup acme.sh data for both IPv4 and IPv6 if specified # Cleanup acme.sh data for both IPv4 and IPv6 if specified
rm -rf ~/.acme.sh/${ipv4} 2>/dev/null rm -rf ~/.acme.sh/${ipv4} 2> /dev/null
[[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null [[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2> /dev/null
rm -rf ${certDir} 2>/dev/null rm -rf ${certDir} 2> /dev/null
return 1 return 1
fi fi
echo -e "${green}Certificate files installed successfully${plain}" echo -e "${green}Certificate files installed successfully${plain}"
# Enable auto-upgrade for acme.sh (ensures cron job runs) # Enable auto-upgrade for acme.sh (ensures cron job runs)
~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1 ~/.acme.sh/acme.sh --upgrade --auto-upgrade > /dev/null 2>&1
chmod 600 ${certDir}/privkey.pem 2>/dev/null chmod 600 ${certDir}/privkey.pem 2> /dev/null
chmod 644 ${certDir}/fullchain.pem 2>/dev/null chmod 644 ${certDir}/fullchain.pem 2> /dev/null
# Configure panel to use the certificate # Configure panel to use the certificate
echo -e "${green}Setting certificate paths for the panel...${plain}" echo -e "${green}Setting certificate paths for the panel...${plain}"
@@ -369,9 +369,9 @@ setup_ip_certificate() {
ssl_cert_issue() { 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_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:]') 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
echo "acme.sh could not be found. Installing now..." echo "acme.sh could not be found. Installing now..."
cd ~ || return 1 cd ~ || return 1
curl -s https://get.acme.sh | sh curl -s https://get.acme.sh | sh
@@ -387,18 +387,18 @@ ssl_cert_issue() {
local domain="" local domain=""
while true; do while true; do
read -rp "Please enter your domain name: " domain read -rp "Please enter your domain name: " domain
domain="${domain// /}" # Trim whitespace domain="${domain// /}" # Trim whitespace
if [[ -z "$domain" ]]; then if [[ -z "$domain" ]]; then
echo -e "${red}Domain name cannot be empty. Please try again.${plain}" echo -e "${red}Domain name cannot be empty. Please try again.${plain}"
continue continue
fi fi
if ! is_domain "$domain"; then if ! is_domain "$domain"; then
echo -e "${red}Invalid domain format: ${domain}. Please enter a valid domain name.${plain}" echo -e "${red}Invalid domain format: ${domain}. Please enter a valid domain name.${plain}"
continue continue
fi fi
break break
done done
echo -e "${green}Your domain is: ${domain}, checking it...${plain}" echo -e "${green}Your domain is: ${domain}, checking it...${plain}"
@@ -406,9 +406,9 @@ ssl_cert_issue() {
# detect existing certificate and reuse it if present # detect existing certificate and reuse it if present
local cert_exists=0 local cert_exists=0
if ~/.acme.sh/acme.sh --list 2>/dev/null | awk '{print $1}' | grep -Fxq "${domain}"; then if ~/.acme.sh/acme.sh --list 2> /dev/null | awk '{print $1}' | grep -Fxq "${domain}"; then
cert_exists=1 cert_exists=1
local certInfo=$(~/.acme.sh/acme.sh --list 2>/dev/null | grep -F "${domain}") local certInfo=$(~/.acme.sh/acme.sh --list 2> /dev/null | grep -F "${domain}")
echo -e "${yellow}Existing certificate found for ${domain}, will reuse it.${plain}" echo -e "${yellow}Existing certificate found for ${domain}, will reuse it.${plain}"
[[ -n "${certInfo}" ]] && echo "$certInfo" [[ -n "${certInfo}" ]] && echo "$certInfo"
else else
@@ -435,7 +435,7 @@ ssl_cert_issue() {
# Stop panel temporarily # Stop panel temporarily
echo -e "${yellow}Stopping panel temporarily...${plain}" echo -e "${yellow}Stopping panel temporarily...${plain}"
systemctl stop x-ui 2>/dev/null || rc-service x-ui stop 2>/dev/null systemctl stop x-ui 2> /dev/null || rc-service x-ui stop 2> /dev/null
if [[ ${cert_exists} -eq 0 ]]; then if [[ ${cert_exists} -eq 0 ]]; then
# issue the certificate # issue the certificate
@@ -444,7 +444,7 @@ ssl_cert_issue() {
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo -e "${red}Issuing certificate failed, please check logs.${plain}" echo -e "${red}Issuing certificate failed, please check logs.${plain}"
rm -rf ~/.acme.sh/${domain} rm -rf ~/.acme.sh/${domain}
systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null systemctl start x-ui 2> /dev/null || rc-service x-ui start 2> /dev/null
return 1 return 1
else else
echo -e "${green}Issuing certificate succeeded, installing certificates...${plain}" echo -e "${green}Issuing certificate succeeded, installing certificates...${plain}"
@@ -464,18 +464,18 @@ ssl_cert_issue() {
echo -e "${green}\t0.${plain} Keep default reloadcmd" echo -e "${green}\t0.${plain} Keep default reloadcmd"
read -rp "Choose an option: " choice read -rp "Choose an option: " choice
case "$choice" in case "$choice" in
1) 1)
echo -e "${green}Reloadcmd is: systemctl reload nginx ; systemctl restart x-ui${plain}" echo -e "${green}Reloadcmd is: systemctl reload nginx ; systemctl restart x-ui${plain}"
reloadCmd="systemctl reload nginx ; systemctl restart x-ui" reloadCmd="systemctl reload nginx ; systemctl restart x-ui"
;; ;;
2) 2)
echo -e "${yellow}It's recommended to put x-ui restart at the end${plain}" echo -e "${yellow}It's recommended to put x-ui restart at the end${plain}"
read -rp "Please enter your custom reloadcmd: " reloadCmd read -rp "Please enter your custom reloadcmd: " reloadCmd
echo -e "${green}Reloadcmd is: ${reloadCmd}${plain}" echo -e "${green}Reloadcmd is: ${reloadCmd}${plain}"
;; ;;
*) *)
echo -e "${green}Keeping default reloadcmd${plain}" echo -e "${green}Keeping default reloadcmd${plain}"
;; ;;
esac esac
fi fi
@@ -492,14 +492,14 @@ ssl_cert_issue() {
installWroteFiles=1 installWroteFiles=1
fi fi
if [[ -f "/root/cert/${domain}/privkey.pem" && -f "/root/cert/${domain}/fullchain.pem" && ( ${installRc} -eq 0 || ${installWroteFiles} -eq 1 ) ]]; then if [[ -f "/root/cert/${domain}/privkey.pem" && -f "/root/cert/${domain}/fullchain.pem" && (${installRc} -eq 0 || ${installWroteFiles} -eq 1) ]]; then
echo -e "${green}Installing certificate succeeded, enabling auto renew...${plain}" echo -e "${green}Installing certificate succeeded, enabling auto renew...${plain}"
else else
echo -e "${red}Installing certificate failed, exiting.${plain}" echo -e "${red}Installing certificate failed, exiting.${plain}"
if [[ ${cert_exists} -eq 0 ]]; then if [[ ${cert_exists} -eq 0 ]]; then
rm -rf ~/.acme.sh/${domain} rm -rf ~/.acme.sh/${domain}
fi fi
systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null systemctl start x-ui 2> /dev/null || rc-service x-ui start 2> /dev/null
return 1 return 1
fi fi
@@ -518,7 +518,7 @@ ssl_cert_issue() {
fi fi
# Restart panel # Restart panel
systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null systemctl start x-ui 2> /dev/null || rc-service x-ui start 2> /dev/null
# Prompt user to set panel paths after successful certificate installation # Prompt user to set panel paths after successful certificate installation
read -rp "Would you like to set this certificate for the panel? (y/n): " setPanel read -rp "Would you like to set this certificate for the panel? (y/n): " setPanel
@@ -534,21 +534,21 @@ ssl_cert_issue() {
echo "" echo ""
echo -e "${green}Access URL: https://${domain}:${existing_port}/${existing_webBasePath}${plain}" echo -e "${green}Access URL: https://${domain}:${existing_port}/${existing_webBasePath}${plain}"
echo -e "${yellow}Panel will restart to apply SSL certificate...${plain}" echo -e "${yellow}Panel will restart to apply SSL certificate...${plain}"
systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null systemctl restart x-ui 2> /dev/null || rc-service x-ui restart 2> /dev/null
else else
echo -e "${red}Error: Certificate or private key file not found for domain: $domain.${plain}" echo -e "${red}Error: Certificate or private key file not found for domain: $domain.${plain}"
fi fi
else else
echo -e "${yellow}Skipping panel path setting.${plain}" echo -e "${yellow}Skipping panel path setting.${plain}"
fi fi
return 0 return 0
} }
# Unified interactive SSL setup (domain or IP) # Unified interactive SSL setup (domain or IP)
# Sets global `SSL_HOST` to the chosen domain/IP # Sets global `SSL_HOST` to the chosen domain/IP
prompt_and_setup_ssl() { prompt_and_setup_ssl() {
local panel_port="$1" local panel_port="$1"
local web_base_path="$2" # expected without leading slash local web_base_path="$2" # expected without leading slash
local server_ip="$3" local server_ip="$3"
local ssl_choice="" local ssl_choice=""
@@ -559,132 +559,132 @@ prompt_and_setup_ssl() {
echo -e "${green}3.${plain} Custom SSL Certificate (Path to existing files)" echo -e "${green}3.${plain} Custom SSL Certificate (Path to existing files)"
echo -e "${blue}Note:${plain} Options 1 & 2 require port 80 open. Option 3 requires manual paths." echo -e "${blue}Note:${plain} Options 1 & 2 require port 80 open. Option 3 requires manual paths."
read -rp "Choose an option (default 2 for IP): " ssl_choice read -rp "Choose an option (default 2 for IP): " ssl_choice
ssl_choice="${ssl_choice// /}" # Trim whitespace ssl_choice="${ssl_choice// /}" # Trim whitespace
# Default to 2 (IP cert) if input is empty or invalid (not 1 or 3) # Default to 2 (IP cert) if input is empty or invalid (not 1 or 3)
if [[ "$ssl_choice" != "1" && "$ssl_choice" != "3" ]]; then if [[ "$ssl_choice" != "1" && "$ssl_choice" != "3" ]]; then
ssl_choice="2" ssl_choice="2"
fi fi
case "$ssl_choice" in case "$ssl_choice" in
1) 1)
# User chose Let's Encrypt domain option # User chose Let's Encrypt domain option
echo -e "${green}Using Let's Encrypt for domain certificate...${plain}" echo -e "${green}Using Let's Encrypt for domain certificate...${plain}"
if ssl_cert_issue; then if ssl_cert_issue; then
local cert_domain="${SSL_ISSUED_DOMAIN}" local cert_domain="${SSL_ISSUED_DOMAIN}"
if [[ -z "${cert_domain}" ]]; then if [[ -z "${cert_domain}" ]]; then
cert_domain=$(~/.acme.sh/acme.sh --list 2>/dev/null | tail -1 | awk '{print $1}') cert_domain=$(~/.acme.sh/acme.sh --list 2> /dev/null | tail -1 | awk '{print $1}')
fi fi
if [[ -n "${cert_domain}" ]]; then if [[ -n "${cert_domain}" ]]; then
SSL_HOST="${cert_domain}" SSL_HOST="${cert_domain}"
echo -e "${green}✓ SSL certificate configured successfully with domain: ${cert_domain}${plain}" echo -e "${green}✓ SSL certificate configured successfully with domain: ${cert_domain}${plain}"
else
echo -e "${yellow}SSL setup may have completed, but domain extraction failed${plain}"
SSL_HOST="${server_ip}"
fi
else else
echo -e "${yellow}SSL setup may have completed, but domain extraction failed${plain}" echo -e "${red}SSL certificate setup failed for domain mode.${plain}"
SSL_HOST="${server_ip}" SSL_HOST="${server_ip}"
fi fi
else ;;
echo -e "${red}SSL certificate setup failed for domain mode.${plain}" 2)
SSL_HOST="${server_ip}" # User chose Let's Encrypt IP certificate option
fi echo -e "${green}Using Let's Encrypt for IP certificate (shortlived profile)...${plain}"
;;
2)
# User chose Let's Encrypt IP certificate option
echo -e "${green}Using Let's Encrypt for IP certificate (shortlived profile)...${plain}"
# Ask for optional IPv6
local ipv6_addr=""
read -rp "Do you have an IPv6 address to include? (leave empty to skip): " ipv6_addr
ipv6_addr="${ipv6_addr// /}" # Trim whitespace
# Stop panel if running (port 80 needed)
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 certificate configured successfully${plain}"
else
echo -e "${red}✗ IP certificate setup failed. Please check port 80 is open.${plain}"
SSL_HOST="${server_ip}"
fi
# Restart panel after SSL is configured (restart applies new cert settings)
if [[ $release == "alpine" ]]; then
rc-service x-ui restart >/dev/null 2>&1
else
systemctl restart x-ui >/dev/null 2>&1
fi
;; # Ask for optional IPv6
3) local ipv6_addr=""
# User chose Custom Paths (User Provided) option read -rp "Do you have an IPv6 address to include? (leave empty to skip): " ipv6_addr
echo -e "${green}Using custom existing certificate...${plain}" ipv6_addr="${ipv6_addr// /}" # Trim whitespace
local custom_cert=""
local custom_key=""
local custom_domain=""
# 3.1 Request Domain to compose Panel URL later # Stop panel if running (port 80 needed)
read -rp "Please enter domain name certificate issued for: " custom_domain if [[ $release == "alpine" ]]; then
custom_domain="${custom_domain// /}" # Remove spaces rc-service x-ui stop > /dev/null 2>&1
# 3.2 Loop for Certificate Path
while true; do
read -rp "Input certificate path (keywords: .crt / fullchain): " custom_cert
# Strip quotes if present
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}Error: File does not exist! Try again.${plain}"
elif [[ ! -r "$custom_cert" ]]; then
echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}"
else else
echo -e "${red}Error: File is empty!${plain}" systemctl stop x-ui > /dev/null 2>&1
fi fi
done
# 3.3 Loop for Private Key Path setup_ip_certificate "${server_ip}" "${ipv6_addr}"
while true; do if [ $? -eq 0 ]; then
read -rp "Input private key path (keywords: .key / privatekey): " custom_key SSL_HOST="${server_ip}"
# Strip quotes if present echo -e "${green}✓ Let's Encrypt IP certificate configured successfully${plain}"
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}Error: File does not exist! Try again.${plain}"
elif [[ ! -r "$custom_key" ]]; then
echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}"
else else
echo -e "${red}Error: File is empty!${plain}" echo -e "${red}✗ IP certificate setup failed. Please check port 80 is open.${plain}"
SSL_HOST="${server_ip}"
fi fi
done
# 3.4 Apply Settings via x-ui binary # Restart panel after SSL is configured (restart applies new cert settings)
${xui_folder}/x-ui cert -webCert "$custom_cert" -webCertKey "$custom_key" >/dev/null 2>&1 if [[ $release == "alpine" ]]; then
rc-service x-ui restart > /dev/null 2>&1
else
systemctl restart x-ui > /dev/null 2>&1
fi
# Set SSL_HOST for composing Panel URL ;;
if [[ -n "$custom_domain" ]]; then 3)
SSL_HOST="$custom_domain" # User chose Custom Paths (User Provided) option
else echo -e "${green}Using custom existing certificate...${plain}"
local custom_cert=""
local custom_key=""
local custom_domain=""
# 3.1 Request Domain to compose Panel URL later
read -rp "Please enter domain name certificate issued for: " custom_domain
custom_domain="${custom_domain// /}" # Remove spaces
# 3.2 Loop for Certificate Path
while true; do
read -rp "Input certificate path (keywords: .crt / fullchain): " custom_cert
# Strip quotes if present
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}Error: File does not exist! Try again.${plain}"
elif [[ ! -r "$custom_cert" ]]; then
echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}"
else
echo -e "${red}Error: File is empty!${plain}"
fi
done
# 3.3 Loop for Private Key Path
while true; do
read -rp "Input private key path (keywords: .key / privatekey): " custom_key
# Strip quotes if present
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}Error: File does not exist! Try again.${plain}"
elif [[ ! -r "$custom_key" ]]; then
echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}"
else
echo -e "${red}Error: File is empty!${plain}"
fi
done
# 3.4 Apply Settings via x-ui binary
${xui_folder}/x-ui cert -webCert "$custom_cert" -webCertKey "$custom_key" > /dev/null 2>&1
# Set SSL_HOST for composing Panel URL
if [[ -n "$custom_domain" ]]; then
SSL_HOST="$custom_domain"
else
SSL_HOST="${server_ip}"
fi
echo -e "${green}✓ Custom certificate paths applied.${plain}"
echo -e "${yellow}Note: You are responsible for renewing these files externally.${plain}"
systemctl restart x-ui > /dev/null 2>&1 || rc-service x-ui restart > /dev/null 2>&1
;;
*)
echo -e "${red}Invalid option. Skipping SSL setup.${plain}"
SSL_HOST="${server_ip}" SSL_HOST="${server_ip}"
fi ;;
echo -e "${green}✓ Custom certificate paths applied.${plain}"
echo -e "${yellow}Note: You are responsible for renewing these files externally.${plain}"
systemctl restart x-ui >/dev/null 2>&1 || rc-service x-ui restart >/dev/null 2>&1
;;
*)
echo -e "${red}Invalid option. Skipping SSL setup.${plain}"
SSL_HOST="${server_ip}"
;;
esac esac
} }
@@ -692,12 +692,12 @@ config_after_update() {
echo -e "${yellow}x-ui settings:${plain}" echo -e "${yellow}x-ui settings:${plain}"
${xui_folder}/x-ui setting -show true ${xui_folder}/x-ui setting -show true
${xui_folder}/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=$(${xui_folder}/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=$(${xui_folder}/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=$(${xui_folder}/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=(
"https://api4.ipify.org" "https://api4.ipify.org"
@@ -709,7 +709,7 @@ config_after_update() {
) )
local server_ip="" local server_ip=""
for ip_address in "${URL_lists[@]}"; do for ip_address in "${URL_lists[@]}"; do
local response=$(curl -s -w "\n%{http_code}" --max-time 3 "${ip_address}" 2>/dev/null) local response=$(curl -s -w "\n%{http_code}" --max-time 3 "${ip_address}" 2> /dev/null)
local http_code=$(echo "$response" | tail -n1) local http_code=$(echo "$response" | tail -n1)
local ip_result=$(echo "$response" | head -n-1 | tr -d '[:space:]') local ip_result=$(echo "$response" | head -n-1 | tr -d '[:space:]')
if [[ "${http_code}" == "200" && -n "${ip_result}" ]]; then if [[ "${http_code}" == "200" && -n "${ip_result}" ]]; then
@@ -717,7 +717,7 @@ config_after_update() {
break break
fi fi
done done
# Handle missing/short webBasePath # Handle missing/short webBasePath
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}"
@@ -726,7 +726,7 @@ config_after_update() {
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
# Check and prompt for SSL if missing # Check and prompt for SSL if missing
if [[ -z "$existing_cert" ]]; then if [[ -z "$existing_cert" ]]; then
echo "" echo ""
@@ -736,16 +736,16 @@ config_after_update() {
echo -e "${yellow}For security, SSL certificate is MANDATORY for all panels.${plain}" echo -e "${yellow}For security, SSL certificate is MANDATORY for all panels.${plain}"
echo -e "${yellow}Let's Encrypt now supports both domains and IP addresses!${plain}" echo -e "${yellow}Let's Encrypt now supports both domains and IP addresses!${plain}"
echo "" echo ""
if [[ -z "${server_ip}" ]]; then if [[ -z "${server_ip}" ]]; then
echo -e "${red}Failed to detect server IP${plain}" echo -e "${red}Failed to detect server IP${plain}"
echo -e "${yellow}Please configure SSL manually using: x-ui${plain}" echo -e "${yellow}Please configure SSL manually using: x-ui${plain}"
return return
fi fi
# Prompt and setup SSL (domain or IP) # Prompt and setup SSL (domain or IP)
prompt_and_setup_ssl "${existing_port}" "${existing_webBasePath}" "${server_ip}" prompt_and_setup_ssl "${existing_port}" "${existing_webBasePath}" "${server_ip}"
echo "" echo ""
echo -e "${green}═══════════════════════════════════════════${plain}" echo -e "${green}═══════════════════════════════════════════${plain}"
echo -e "${green} Panel Access Information ${plain}" echo -e "${green} Panel Access Information ${plain}"
@@ -768,17 +768,17 @@ config_after_update() {
update_x-ui() { update_x-ui() {
cd ${xui_folder%/x-ui}/ cd ${xui_folder%/x-ui}/
if [ -f "${xui_folder}/x-ui" ]; then if [ -f "${xui_folder}/x-ui" ]; then
current_xui_version=$(${xui_folder}/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"
fi fi
echo -e "${green}Downloading new x-ui version...${plain}" echo -e "${green}Downloading new x-ui version...${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/') 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 if [[ ! -n "$tag_version" ]]; then
echo -e "${yellow}Trying to fetch version with IPv4...${plain}" echo -e "${yellow}Trying to fetch version with 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/') tag_version=$(${curl_bin} -4 -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
@@ -787,110 +787,110 @@ 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..."
${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 ${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 if [[ $? -ne 0 ]]; then
echo -e "${yellow}Trying to fetch version with IPv4...${plain}" echo -e "${yellow}Trying to fetch version with 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 ${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 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 ${xui_folder}/ ]]; 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
rc-service x-ui stop >/dev/null 2>&1 rc-service x-ui stop > /dev/null 2>&1
rc-update del x-ui >/dev/null 2>&1 rc-update del x-ui > /dev/null 2>&1
echo -e "${green}Removing old service unit version...${plain}" echo -e "${green}Removing old service unit version...${plain}"
rm -f /etc/init.d/x-ui >/dev/null 2>&1 rm -f /etc/init.d/x-ui > /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 service unit not installed." _fail "ERROR: x-ui service unit not installed."
fi fi
else else
if [ -f "${xui_service}/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 ${xui_service}/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
_fail "ERROR: x-ui systemd unit not installed." _fail "ERROR: x-ui systemd unit not installed."
fi fi
fi fi
echo -e "${green}Removing old x-ui version...${plain}" echo -e "${green}Removing old x-ui version...${plain}"
rm ${xui_folder} -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 -f > /dev/null 2>&1
rm ${xui_folder}/x-ui.service.debian -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.arch -f > /dev/null 2>&1
rm ${xui_folder}/x-ui.service.rhel -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 -f > /dev/null 2>&1
rm ${xui_folder}/x-ui.sh -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 ${xui_folder}/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 ${xui_folder}/bin/README.md -f >/dev/null 2>&1 rm ${xui_folder}/bin/README.md -f > /dev/null 2>&1
rm ${xui_folder}/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."
fi fi
echo -e "${green}Installing new x-ui version...${plain}" echo -e "${green}Installing new x-ui version...${plain}"
tar zxvf x-ui-linux-$(arch).tar.gz >/dev/null 2>&1 tar zxvf x-ui-linux-$(arch).tar.gz > /dev/null 2>&1
rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1 rm x-ui-linux-$(arch).tar.gz -f > /dev/null 2>&1
cd x-ui >/dev/null 2>&1 cd x-ui > /dev/null 2>&1
chmod +x x-ui >/dev/null 2>&1 chmod +x x-ui > /dev/null 2>&1
# Check the system's architecture and rename the file accordingly # Check the system's architecture and rename the file accordingly
if [[ $(arch) == "armv5" || $(arch) == "armv6" || $(arch) == "armv7" ]]; then if [[ $(arch) == "armv5" || $(arch) == "armv6" || $(arch) == "armv7" ]]; then
mv bin/xray-linux-$(arch) bin/xray-linux-arm >/dev/null 2>&1 mv bin/xray-linux-$(arch) bin/xray-linux-arm > /dev/null 2>&1
chmod +x bin/xray-linux-arm >/dev/null 2>&1 chmod +x bin/xray-linux-arm > /dev/null 2>&1
fi fi
chmod +x x-ui bin/xray-linux-$(arch) >/dev/null 2>&1 chmod +x x-ui bin/xray-linux-$(arch) > /dev/null 2>&1
echo -e "${green}Downloading and installing x-ui.sh script...${plain}" echo -e "${green}Downloading and installing x-ui.sh script...${plain}"
${curl_bin} -fLRo /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh >/dev/null 2>&1 ${curl_bin} -fLRo /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh > /dev/null 2>&1
if [[ $? -ne 0 ]]; then if [[ $? -ne 0 ]]; then
echo -e "${yellow}Trying to fetch x-ui with IPv4...${plain}" echo -e "${yellow}Trying to fetch x-ui with IPv4...${plain}"
${curl_bin} -4fLRo /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh >/dev/null 2>&1 ${curl_bin} -4fLRo /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh > /dev/null 2>&1
if [[ $? -ne 0 ]]; then if [[ $? -ne 0 ]]; then
_fail "ERROR: Failed to download x-ui.sh script, please be sure that your server can access GitHub" _fail "ERROR: Failed to download x-ui.sh script, please be sure that your server can access GitHub"
fi fi
fi fi
chmod +x ${xui_folder}/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 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 ${xui_folder} >/dev/null 2>&1 chown -R root:root ${xui_folder} > /dev/null 2>&1
if [ -f "${xui_folder}/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 ${xui_folder}/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
echo -e "${green}Downloading and installing startup unit x-ui.rc...${plain}" echo -e "${green}Downloading and installing startup unit 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 ${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 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 ${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 if [[ $? -ne 0 ]]; then
_fail "ERROR: Failed to download startup unit x-ui.rc, please be sure that your server can access GitHub" _fail "ERROR: Failed to download startup unit x-ui.rc, please be sure that your server can access GitHub"
fi fi
fi fi
chmod +x /etc/init.d/x-ui >/dev/null 2>&1 chmod +x /etc/init.d/x-ui > /dev/null 2>&1
chown root:root /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-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
if [ -f "x-ui.service" ]; then if [ -f "x-ui.service" ]; then
echo -e "${green}Installing systemd unit...${plain}" echo -e "${green}Installing systemd unit...${plain}"
cp -f x-ui.service ${xui_service}/ >/dev/null 2>&1 cp -f x-ui.service ${xui_service}/ > /dev/null 2>&1
if [[ $? -ne 0 ]]; then if [[ $? -ne 0 ]]; then
echo -e "${red}Failed to copy x-ui.service${plain}" echo -e "${red}Failed to copy x-ui.service${plain}"
exit 1 exit 1
@@ -901,62 +901,62 @@ update_x-ui() {
ubuntu | debian | armbian) ubuntu | debian | armbian)
if [ -f "x-ui.service.debian" ]; then if [ -f "x-ui.service.debian" ]; then
echo -e "${green}Installing debian-like systemd unit...${plain}" echo -e "${green}Installing debian-like systemd unit...${plain}"
cp -f x-ui.service.debian ${xui_service}/x-ui.service >/dev/null 2>&1 cp -f x-ui.service.debian ${xui_service}/x-ui.service > /dev/null 2>&1
if [[ $? -eq 0 ]]; then if [[ $? -eq 0 ]]; then
service_installed=true service_installed=true
fi fi
fi fi
;; ;;
arch | manjaro | parch) arch | manjaro | parch)
if [ -f "x-ui.service.arch" ]; then if [ -f "x-ui.service.arch" ]; then
echo -e "${green}Installing arch-like systemd unit...${plain}" echo -e "${green}Installing arch-like systemd unit...${plain}"
cp -f x-ui.service.arch ${xui_service}/x-ui.service >/dev/null 2>&1 cp -f x-ui.service.arch ${xui_service}/x-ui.service > /dev/null 2>&1
if [[ $? -eq 0 ]]; then if [[ $? -eq 0 ]]; then
service_installed=true service_installed=true
fi fi
fi fi
;; ;;
*) *)
if [ -f "x-ui.service.rhel" ]; then if [ -f "x-ui.service.rhel" ]; then
echo -e "${green}Installing rhel-like systemd unit...${plain}" echo -e "${green}Installing rhel-like systemd unit...${plain}"
cp -f x-ui.service.rhel ${xui_service}/x-ui.service >/dev/null 2>&1 cp -f x-ui.service.rhel ${xui_service}/x-ui.service > /dev/null 2>&1
if [[ $? -eq 0 ]]; then if [[ $? -eq 0 ]]; then
service_installed=true service_installed=true
fi fi
fi fi
;; ;;
esac esac
# If service file not found in tar.gz, download from GitHub # If service file not found in tar.gz, download from GitHub
if [ "$service_installed" = false ]; then if [ "$service_installed" = false ]; then
echo -e "${yellow}Service files not found in tar.gz, downloading from GitHub...${plain}" echo -e "${yellow}Service files not found in tar.gz, downloading from GitHub...${plain}"
case "${release}" in case "${release}" in
ubuntu | debian | armbian) 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 ${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) 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.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 ${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 esac
if [[ $? -ne 0 ]]; then if [[ $? -ne 0 ]]; then
echo -e "${red}Failed to install x-ui.service from GitHub${plain}" echo -e "${red}Failed to install x-ui.service from GitHub${plain}"
exit 1 exit 1
fi fi
fi fi
fi fi
chown root:root ${xui_service}/x-ui.service >/dev/null 2>&1 chown root:root ${xui_service}/x-ui.service > /dev/null 2>&1
chmod 644 ${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 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
fi fi
config_after_update config_after_update
echo -e "${green}x-ui ${tag_version}${plain} updating finished, it is running now..." echo -e "${green}x-ui ${tag_version}${plain} updating finished, it is running now..."
echo -e "" echo -e ""
echo -e "┌───────────────────────────────────────────────────────┐ echo -e "┌───────────────────────────────────────────────────────┐

View File

@@ -697,7 +697,6 @@ class TlsStreamSettings extends XrayCommonClass {
certificates = [new TlsStreamSettings.Cert()], certificates = [new TlsStreamSettings.Cert()],
alpn = [ALPN_OPTION.H2, ALPN_OPTION.HTTP1], alpn = [ALPN_OPTION.H2, ALPN_OPTION.HTTP1],
echServerKeys = '', echServerKeys = '',
echForceQuery = 'none',
settings = new TlsStreamSettings.Settings() settings = new TlsStreamSettings.Settings()
) { ) {
super(); super();
@@ -711,7 +710,6 @@ class TlsStreamSettings extends XrayCommonClass {
this.certs = certificates; this.certs = certificates;
this.alpn = alpn; this.alpn = alpn;
this.echServerKeys = echServerKeys; this.echServerKeys = echServerKeys;
this.echForceQuery = echForceQuery;
this.settings = settings; this.settings = settings;
} }
@@ -744,7 +742,6 @@ class TlsStreamSettings extends XrayCommonClass {
certs, certs,
json.alpn, json.alpn,
json.echServerKeys, json.echServerKeys,
json.echForceQuery,
settings, settings,
); );
} }
@@ -761,7 +758,6 @@ class TlsStreamSettings extends XrayCommonClass {
certificates: TlsStreamSettings.toJsonArray(this.certs), certificates: TlsStreamSettings.toJsonArray(this.certs),
alpn: this.alpn, alpn: this.alpn,
echServerKeys: this.echServerKeys, echServerKeys: this.echServerKeys,
echForceQuery: this.echForceQuery,
settings: this.settings, settings: this.settings,
}; };
} }

View File

@@ -1,6 +1,7 @@
{{ define "page/head_start" }} {{ define "page/head_start" }}
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="renderer" content="webkit"> <meta name="renderer" content="webkit">
@@ -12,6 +13,7 @@
[v-cloak] { [v-cloak] {
display: none; display: none;
} }
/* vazirmatn-regular - arabic_latin_latin-ext */ /* vazirmatn-regular - arabic_latin_latin-ext */
@font-face { @font-face {
font-display: swap; font-display: swap;
@@ -21,10 +23,11 @@
src: url('{{ .base_path }}assets/Vazirmatn-UI-NL-Regular.woff2') format('woff2'); src: url('{{ .base_path }}assets/Vazirmatn-UI-NL-Regular.woff2') format('woff2');
unicode-range: U+0600-06FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE80-FEFC, U+0030-0039; unicode-range: U+0600-06FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE80-FEFC, U+0030-0039;
} }
body { body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Vazirmatn', 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Vazirmatn', 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
} }
/* mobile touch scrolling for tabs */ /* mobile touch scrolling for tabs */
@media (max-width: 576px) { @media (max-width: 576px) {
.ant-tabs-nav-container { .ant-tabs-nav-container {
@@ -34,59 +37,69 @@
overscroll-behavior-x: contain; overscroll-behavior-x: contain;
white-space: nowrap; white-space: nowrap;
max-width: 100%; max-width: 100%;
padding: 0 !important; /* Remove padding for arrows */ padding: 0 !important;
/* Remove padding for arrows */
} }
.ant-tabs-nav-wrap { .ant-tabs-nav-wrap {
overflow: visible !important; overflow: visible !important;
padding: 0 !important; padding: 0 !important;
} }
.ant-tabs-nav-scroll { .ant-tabs-nav-scroll {
overflow: visible !important; overflow: visible !important;
box-shadow: none !important; box-shadow: none !important;
} }
.ant-tabs-nav { .ant-tabs-nav {
display: flex !important; display: flex !important;
transform: none !important; /* Disable JS transform */ transform: none !important;
width: auto !important; /* Disable JS transform */
margin: 0 !important; width: auto !important;
margin: 0 !important;
} }
.ant-tabs-tab-prev, .ant-tabs-tab-prev,
.ant-tabs-tab-next { .ant-tabs-tab-next {
display: none !important; /* Hide arrows */ display: none !important;
/* Hide arrows */
} }
.ant-tabs-nav-container::-webkit-scrollbar { .ant-tabs-nav-container::-webkit-scrollbar {
display: none; display: none;
} }
} }
</style> </style>
<title>{{ .host }} {{ i18n .title}}</title> <title>{{ .host }} {{ i18n .title}}</title>
{{ end }} {{ end }}
{{ define "page/head_end" }} {{ define "page/head_end" }}
</head> </head>
{{ end }} {{ end }}
{{ define "page/body_start" }} {{ define "page/body_start" }}
<body> <body>
<div id="message"></div> <div id="message"></div>
{{ end }} {{ end }}
{{ define "page/body_scripts" }} {{ define "page/body_scripts" }}
<script src="{{ .base_path }}assets/vue/vue.min.js?{{ .cur_ver }}"></script> <script src="{{ .base_path }}assets/vue/vue.min.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/moment/moment.min.js"></script> <script src="{{ .base_path }}assets/moment/moment.min.js"></script>
<script src="{{ .base_path }}assets/ant-design-vue/antd.min.js"></script> <script src="{{ .base_path }}assets/ant-design-vue/antd.min.js"></script>
<script src="{{ .base_path }}assets/axios/axios.min.js?{{ .cur_ver }}"></script> <script src="{{ .base_path }}assets/axios/axios.min.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/qs/qs.min.js"></script> <script src="{{ .base_path }}assets/qs/qs.min.js"></script>
<script src="{{ .base_path }}assets/js/axios-init.js?{{ .cur_ver }}"></script> <script src="{{ .base_path }}assets/js/axios-init.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/util/index.js?{{ .cur_ver }}"></script> <script src="{{ .base_path }}assets/js/util/index.js?{{ .cur_ver }}"></script>
<script> <script>
const basePath = '{{ .base_path }}'; const basePath = '{{ .base_path }}';
axios.defaults.baseURL = basePath; axios.defaults.baseURL = basePath;
</script> </script>
<script src="{{ .base_path }}assets/js/websocket.js?{{ .cur_ver }}"></script> <script src="{{ .base_path }}assets/js/websocket.js?{{ .cur_ver }}"></script>
{{ end }} {{ end }}
{{ define "page/body_end" }} {{ define "page/body_end" }}
</body> </body>
</html> </html>
{{ end }} {{ end }}

View File

@@ -2,30 +2,39 @@
<template slot="actions" slot-scope="text, client, index"> <template slot="actions" slot-scope="text, client, index">
<a-tooltip> <a-tooltip>
<template slot="title">{{ i18n "qrCode" }}</template> <template slot="title">{{ i18n "qrCode" }}</template>
<a-icon :style="{ fontSize: '22px', marginInlineStart: '14px' }" class="normal-icon" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record.id,client);"></a-icon> <a-icon :style="{ fontSize: '22px', marginInlineStart: '14px' }" class="normal-icon" type="qrcode"
v-if="record.hasLink()" @click="showQrcode(record.id,client);"></a-icon>
</a-tooltip> </a-tooltip>
<a-tooltip> <a-tooltip>
<template slot="title">{{ i18n "pages.client.edit" }}</template> <template slot="title">{{ i18n "pages.client.edit" }}</template>
<a-icon :style="{ fontSize: '22px' }" class="normal-icon" type="edit" @click="openEditClient(record.id,client);"></a-icon> <a-icon :style="{ fontSize: '22px' }" class="normal-icon" type="edit"
@click="openEditClient(record.id,client);"></a-icon>
</a-tooltip> </a-tooltip>
<a-tooltip> <a-tooltip>
<template slot="title">{{ i18n "info" }}</template> <template slot="title">{{ i18n "info" }}</template>
<a-icon :style="{ fontSize: '22px' }" class="normal-icon" type="info-circle" @click="showInfo(record.id,client);"></a-icon> <a-icon :style="{ fontSize: '22px' }" class="normal-icon" type="info-circle"
@click="showInfo(record.id,client);"></a-icon>
</a-tooltip> </a-tooltip>
<a-tooltip> <a-tooltip>
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template> <template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
<a-popconfirm @confirm="resetClientTraffic(client,record.id,false)" title='{{ i18n "pages.inbounds.resetTrafficContent"}}' :overlay-class-name="themeSwitcher.currentTheme" ok-text='{{ i18n "reset"}}' cancel-text='{{ i18n "cancel"}}'> <a-popconfirm @confirm="resetClientTraffic(client,record.id,false)"
title='{{ i18n "pages.inbounds.resetTrafficContent"}}' :overlay-class-name="themeSwitcher.currentTheme"
ok-text='{{ i18n "reset"}}' cancel-text='{{ i18n "cancel"}}'>
<a-icon slot="icon" type="question-circle-o" :style="{ color: 'var(--color-primary-100)'}"></a-icon> <a-icon slot="icon" type="question-circle-o" :style="{ color: 'var(--color-primary-100)'}"></a-icon>
<a-icon :style="{ fontSize: '22px', cursor: 'pointer' }" class="normal-icon" type="retweet" v-if="client.email.length > 0"></a-icon> <a-icon :style="{ fontSize: '22px', cursor: 'pointer' }" class="normal-icon" type="retweet"
v-if="client.email.length > 0"></a-icon>
</a-popconfirm> </a-popconfirm>
</a-tooltip> </a-tooltip>
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span :style="{ color: '#FF4D4F' }"> {{ i18n "delete"}}</span> <span :style="{ color: '#FF4D4F' }"> {{ i18n "delete"}}</span>
</template> </template>
<a-popconfirm @confirm="delClient(record.id,client,false)" title='{{ i18n "pages.inbounds.deleteClientContent"}}' :overlay-class-name="themeSwitcher.currentTheme" ok-text='{{ i18n "delete"}}' ok-type="danger" cancel-text='{{ i18n "cancel"}}'> <a-popconfirm @confirm="delClient(record.id,client,false)" title='{{ i18n "pages.inbounds.deleteClientContent"}}'
:overlay-class-name="themeSwitcher.currentTheme" ok-text='{{ i18n "delete"}}' ok-type="danger"
cancel-text='{{ i18n "cancel"}}'>
<a-icon slot="icon" type="question-circle-o" :style="{ color: '#e04141' }"></a-icon> <a-icon slot="icon" type="question-circle-o" :style="{ color: '#e04141' }"></a-icon>
<a-icon :style="{ fontSize: '22px', cursor: 'pointer' }" class="delete-icon" type="delete" v-if="isRemovable(record.id)"></a-icon> <a-icon :style="{ fontSize: '22px', cursor: 'pointer' }" class="delete-icon" type="delete"
v-if="isRemovable(record.id)"></a-icon>
</a-popconfirm> </a-popconfirm>
</a-tooltip> </a-tooltip>
</template> </template>
@@ -34,7 +43,7 @@
</template> </template>
<template slot="online" slot-scope="text, client, index"> <template slot="online" slot-scope="text, client, index">
<a-popover :overlay-class-name="themeSwitcher.currentTheme"> <a-popover :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content" > <template slot="content">
{{ i18n "lastOnline" }}: [[ formatLastOnline(client.email) ]] {{ i18n "lastOnline" }}: [[ formatLastOnline(client.email) ]]
</template> </template>
<template v-if="client.enable && isClientOnline(client.email)"> <template v-if="client.enable && isClientOnline(client.email)">
@@ -53,7 +62,8 @@
<template v-else-if="!client.enable">{{ i18n "disabled" }}</template> <template v-else-if="!client.enable">{{ i18n "disabled" }}</template>
<template v-else-if="client.enable && isClientOnline(client.email)">{{ i18n "online" }}</template> <template v-else-if="client.enable && isClientOnline(client.email)">{{ i18n "online" }}</template>
</template> </template>
<a-badge :class="isClientOnline(client.email)? 'online-animation' : ''" :color="client.enable ? statsExpColor(record, client.email) : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'"></a-badge> <a-badge :class="isClientOnline(client.email)? 'online-animation' : ''"
:color="client.enable ? statsExpColor(record, client.email) : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'"></a-badge>
</a-tooltip> </a-tooltip>
<a-space direction="vertical" :size="2"> <a-space direction="vertical" :size="2">
<span class="client-email">[[ client.email ]]</span> <span class="client-email">[[ client.email ]]</span>
@@ -87,10 +97,13 @@
<tr class="tr-table-box"> <tr class="tr-table-box">
<td class="tr-table-rt"> [[ SizeFormatter.sizeFormat(getSumStats(record, client.email)) ]] </td> <td class="tr-table-rt"> [[ SizeFormatter.sizeFormat(getSumStats(record, client.email)) ]] </td>
<td class="tr-table-bar" v-if="!client.enable"> <td class="tr-table-bar" v-if="!client.enable">
<a-progress :stroke-color="themeSwitcher.isDarkTheme ? 'rgb(72 84 105)' : '#bcbcbc'" :show-info="false" :percent="statsProgress(record, client.email)" /> <a-progress :stroke-color="themeSwitcher.isDarkTheme ? 'rgb(72 84 105)' : '#bcbcbc'" :show-info="false"
:percent="statsProgress(record, client.email)" />
</td> </td>
<td class="tr-table-bar" v-else-if="client.totalGB > 0"> <td class="tr-table-bar" v-else-if="client.totalGB > 0">
<a-progress :stroke-color="clientStatsColor(record, client.email)" :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''" :percent="statsProgress(record, client.email)" /> <a-progress :stroke-color="clientStatsColor(record, client.email)" :show-info="false"
:status="isClientDepleted(record, client.email)? 'exception' : ''"
:percent="statsProgress(record, client.email)" />
</td> </td>
<td v-else class="infinite-bar tr-table-bar"> <td v-else class="infinite-bar tr-table-bar">
<a-progress :show-info="false" :percent="100"></a-progress> <a-progress :show-info="false" :percent="100"></a-progress>
@@ -118,7 +131,8 @@
<tr class="tr-table-box"> <tr class="tr-table-box">
<td class="tr-table-rt"> [[ IntlUtil.formatRelativeTime(client.expiryTime) ]] </td> <td class="tr-table-rt"> [[ IntlUtil.formatRelativeTime(client.expiryTime) ]] </td>
<td class="infinite-bar tr-table-bar"> <td class="infinite-bar tr-table-bar">
<a-progress :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''" :percent="expireProgress(client.expiryTime, client.reset)" /> <a-progress :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''"
:percent="expireProgress(client.expiryTime, client.reset)" />
</td> </td>
<td class="tr-table-lt">[[ client.reset + "d" ]]</td> <td class="tr-table-lt">[[ client.reset + "d" ]]</td>
</tr> </tr>
@@ -131,11 +145,16 @@
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span> <span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span>
<span v-else>[[ IntlUtil.formatDate(client.expiryTime) ]]</span> <span v-else>[[ IntlUtil.formatDate(client.expiryTime) ]]</span>
</template> </template>
<a-tag :style="{ minWidth: '50px', border: 'none' }" :color="ColorUtils.userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)"> [[ IntlUtil.formatRelativeTime(client.expiryTime) ]] </a-tag> <a-tag :style="{ minWidth: '50px', border: 'none' }"
:color="ColorUtils.userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)"> [[
IntlUtil.formatRelativeTime(client.expiryTime) ]] </a-tag>
</a-popover> </a-popover>
<a-tag v-else :color="ColorUtils.userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)" :style="{ border: 'none' }" class="infinite-tag"> <a-tag v-else :color="ColorUtils.userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)"
:style="{ border: 'none' }" class="infinite-tag">
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor"> <svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
<path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path> <path
d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z"
fill="currentColor"></path>
</svg> </svg>
</a-tag> </a-tag>
</template> </template>
@@ -165,7 +184,8 @@
<span :style="{ color: '#FF4D4F' }"> {{ i18n "delete"}}</span> <span :style="{ color: '#FF4D4F' }"> {{ i18n "delete"}}</span>
</a-menu-item> </a-menu-item>
<a-menu-item> <a-menu-item>
<a-switch v-model="client.enable" size="small" @change="switchEnableClient(record.id, client, $event)"></a-switch> <a-switch v-model="client.enable" size="small"
@change="switchEnableClient(record.id, client, $event)"></a-switch>
{{ i18n "enable"}} {{ i18n "enable"}}
</a-menu-item> </a-menu-item>
</a-menu> </a-menu>
@@ -179,9 +199,11 @@
<td colspan="3" :style="{ textAlign: 'center' }">{{ i18n "pages.inbounds.traffic" }}</td> <td colspan="3" :style="{ textAlign: 'center' }">{{ i18n "pages.inbounds.traffic" }}</td>
</tr> </tr>
<tr> <tr>
<td width="80px" :style="{ margin: '0', textAlign: 'right', fontSize: '1em' }"> [[ SizeFormatter.sizeFormat(getUpStats(record, client.email) + getDownStats(record, client.email)) ]] </td> <td width="80px" :style="{ margin: '0', textAlign: 'right', fontSize: '1em' }"> [[
SizeFormatter.sizeFormat(getUpStats(record, client.email) + getDownStats(record, client.email)) ]] </td>
<td width="120px" v-if="!client.enable"> <td width="120px" v-if="!client.enable">
<a-progress :stroke-color="themeSwitcher.isDarkTheme ? 'rgb(72 84 105)' : '#bcbcbc'" :show-info="false" :percent="statsProgress(record, client.email)" /> <a-progress :stroke-color="themeSwitcher.isDarkTheme ? 'rgb(72 84 105)' : '#bcbcbc'" :show-info="false"
:percent="statsProgress(record, client.email)" />
</td> </td>
<td width="120px" v-else-if="client.totalGB > 0"> <td width="120px" v-else-if="client.totalGB > 0">
<a-popover :overlay-class-name="themeSwitcher.currentTheme"> <a-popover :overlay-class-name="themeSwitcher.currentTheme">
@@ -197,11 +219,14 @@
</tr> </tr>
</table> </table>
</template> </template>
<a-progress :stroke-color="clientStatsColor(record, client.email)" :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''" :percent="statsProgress(record, client.email)" /> <a-progress :stroke-color="clientStatsColor(record, client.email)" :show-info="false"
:status="isClientDepleted(record, client.email)? 'exception' : ''"
:percent="statsProgress(record, client.email)" />
</a-popover> </a-popover>
</td> </td>
<td width="120px" v-else class="infinite-bar"> <td width="120px" v-else class="infinite-bar">
<a-progress :stroke-color="themeSwitcher.isDarkTheme ? '#2c1e32':'#F2EAF1'" :show-info="false" :percent="100"></a-progress> <a-progress :stroke-color="themeSwitcher.isDarkTheme ? '#2c1e32':'#F2EAF1'" :show-info="false"
:percent="100"></a-progress>
</td> </td>
<td width="80px"> <td width="80px">
<template v-if="client.totalGB > 0">[[ client._totalGB + "GB" ]]</template> <template v-if="client.totalGB > 0">[[ client._totalGB + "GB" ]]</template>
@@ -216,14 +241,16 @@
</tr> </tr>
<tr> <tr>
<template v-if="client.expiryTime !=0 && client.reset >0"> <template v-if="client.expiryTime !=0 && client.reset >0">
<td width="80px" :style="{ margin: '0', textAlign: 'right', fontSize: '1em' }"> [[ IntlUtil.formatRelativeTime(client.expiryTime) ]] </td> <td width="80px" :style="{ margin: '0', textAlign: 'right', fontSize: '1em' }"> [[
IntlUtil.formatRelativeTime(client.expiryTime) ]] </td>
<td width="120px" class="infinite-bar"> <td width="120px" class="infinite-bar">
<a-popover :overlay-class-name="themeSwitcher.currentTheme"> <a-popover :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content"> <template slot="content">
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span> <span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span>
<span v-else>[[ IntlUtil.formatDate(client.expiryTime) ]]</span> <span v-else>[[ IntlUtil.formatDate(client.expiryTime) ]]</span>
</template> </template>
<a-progress :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''" :percent="expireProgress(client.expiryTime, client.reset)" /> <a-progress :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''"
:percent="expireProgress(client.expiryTime, client.reset)" />
</a-popover> </a-popover>
</td> </td>
<td width="60px">[[ client.reset + "d" ]]</td> <td width="60px">[[ client.reset + "d" ]]</td>
@@ -235,11 +262,16 @@
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span> <span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span>
<span v-else>[[ IntlUtil.formatDate(client.expiryTime) ]]</span> <span v-else>[[ IntlUtil.formatDate(client.expiryTime) ]]</span>
</template> </template>
<a-tag :style="{ minWidth: '50px', border: 'none' }" :color="ColorUtils.userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)"> [[ IntlUtil.formatRelativeTime(client.expiryTime) ]] </a-tag> <a-tag :style="{ minWidth: '50px', border: 'none' }"
:color="ColorUtils.userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)"> [[
IntlUtil.formatRelativeTime(client.expiryTime) ]] </a-tag>
</a-popover> </a-popover>
<a-tag v-else :color="client.enable ? 'purple' : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'" class="infinite-tag"> <a-tag v-else :color="client.enable ? 'purple' : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'"
class="infinite-tag">
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor"> <svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
<path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path> <path
d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z"
fill="currentColor"></path>
</svg> </svg>
</a-tag> </a-tag>
</template> </template>
@@ -248,7 +280,8 @@
</table> </table>
</template> </template>
<a-badge> <a-badge>
<a-icon v-if="!client.enable" slot="count" type="pause-circle" theme="filled" :style="{ color: themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc' }"></a-icon> <a-icon v-if="!client.enable" slot="count" type="pause-circle" theme="filled"
:style="{ color: themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc' }"></a-icon>
<a-button shape="round" size="small" :style="{ fontSize: '14px', padding: '0 10px' }"> <a-button shape="round" size="small" :style="{ fontSize: '14px', padding: '0 10px' }">
<a-icon type="solution"></a-icon> <a-icon type="solution"></a-icon>
</a-button> </a-button>
@@ -271,4 +304,4 @@
- -
</template> </template>
</template> </template>
{{end}} {{end}}

View File

@@ -1,13 +1,13 @@
{{define "component/customStatistic"}} {{define "component/customStatistic"}}
<template> <template>
<a-statistic :title="title" :value="value"> <a-statistic :title="title" :value="value">
<template #prefix> <template #prefix>
<slot name="prefix"></slot> <slot name="prefix"></slot>
</template> </template>
<template #suffix> <template #suffix>
<slot name="suffix"></slot> <slot name="suffix"></slot>
</template> </template>
</a-statistic> </a-statistic>
</template> </template>
{{end}} {{end}}
@@ -16,9 +16,11 @@
.dark .ant-statistic-content { .dark .ant-statistic-content {
color: var(--dark-color-text-primary) color: var(--dark-color-text-primary)
} }
.dark .ant-statistic-title { .dark .ant-statistic-title {
color: rgba(255, 255, 255, 0.55) color: rgba(255, 255, 255, 0.55)
} }
.ant-statistic-content { .ant-statistic-content {
font-size: 16px; font-size: 16px;
} }

View File

@@ -42,7 +42,7 @@
}; };
}, },
watch: { watch: {
value: function (date) { value: function(date) {
this.date = this.convertToJalalian(date) this.date = this.convertToJalalian(date)
} }
}, },
@@ -52,7 +52,8 @@
}, },
methods: { methods: {
convertToGregorian(date) { convertToGregorian(date) {
return date ? moment(moment(date, 'jYYYY/jMM/jDD HH:mm:ss').format('YYYY-MM-DD HH:mm:ss')) : null return date ? moment(moment(date, 'jYYYY/jMM/jDD HH:mm:ss').format('YYYY-MM-DD HH:mm:ss')) :
null
}, },
convertToJalalian(date) { convertToJalalian(date) {
return date && moment.isMoment(date) ? date.format('jYYYY/jMM/jDD HH:mm:ss') : null return date && moment.isMoment(date) ? date.format('jYYYY/jMM/jDD HH:mm:ss') : null

View File

@@ -26,7 +26,7 @@
type: String, type: String,
required: false, required: false,
defaultValue: "default", defaultValue: "default",
validator: function (value) { validator: function(value) {
return ['small', 'default'].includes(value) return ['small', 'default'].includes(value)
} }
} }
@@ -46,4 +46,4 @@
} }
}) })
</script> </script>
{{end}} {{end}}

View File

@@ -43,8 +43,7 @@
Vue.component('a-sidebar', { Vue.component('a-sidebar', {
data() { data() {
return { return {
tabs: [ tabs: [{
{
key: '{{ .base_path }}panel/', key: '{{ .base_path }}panel/',
icon: 'dashboard', icon: 'dashboard',
title: '{{ i18n "menu.dashboard"}}' title: '{{ i18n "menu.dashboard"}}'
@@ -79,8 +78,8 @@
}, },
methods: { methods: {
openLink(key) { openLink(key) {
return key.startsWith('http') ? return key.startsWith('http') ?
window.open(key) : window.open(key) :
location.href = key location.href = key
}, },
closeDrawer() { closeDrawer() {

View File

@@ -1,6 +1,6 @@
{{define "component/sortableTableTrigger"}} {{define "component/sortableTableTrigger"}}
<a-icon type="drag" class="sortable-icon" :style="{ cursor: 'move' }" @mouseup="mouseUpHandler" @mousedown="mouseDownHandler" <a-icon type="drag" class="sortable-icon" :style="{ cursor: 'move' }" @mouseup="mouseUpHandler"
@click="clickHandler" /> @mousedown="mouseDownHandler" @click="clickHandler" />
{{end}} {{end}}
{{define "component/aTableSortable"}} {{define "component/aTableSortable"}}
@@ -49,7 +49,7 @@
sortable, sortable,
} }
}, },
render: function (createElement) { render: function(createElement) {
return createElement('a-table', { return createElement('a-table', {
class: { class: {
'ant-table-is-sorting': this.isDragging(), 'ant-table-is-sorting': this.isDragging(),
@@ -64,12 +64,12 @@
drop: (e) => this.dropHandler(e), drop: (e) => this.dropHandler(e),
}, },
scopedSlots: this.$scopedSlots, scopedSlots: this.$scopedSlots,
locale: { locale: {
filterConfirm: `{{ i18n "confirm" }}`, filterConfirm: `{{ i18n "confirm" }}`,
filterReset: `{{ i18n "reset" }}`, filterReset: `{{ i18n "reset" }}`,
emptyText: `{{ i18n "noData" }}` emptyText: `{{ i18n "noData" }}`
} }
}, this.$slots.default,) }, this.$slots.default, )
}, },
created() { created() {
this.$memoSort = {}; this.$memoSort = {};
@@ -148,7 +148,8 @@
class: { class: {
...(parentMethodResult?.class || {}), ...(parentMethodResult?.class || {}),
[DRAGGABLE_ROW_CLASS]: true, [DRAGGABLE_ROW_CLASS]: true,
['dragging']: this.isDragging() ? (newIndex === null ? index === currentIndex : index === newIndex) : false, ['dragging']: this.isDragging() ? (newIndex === null ? index === currentIndex : index === newIndex) :
false,
}, },
}; };
} }

View File

@@ -24,9 +24,11 @@
{{define "component/themeSwitchTemplateLogin"}} {{define "component/themeSwitchTemplateLogin"}}
<template> <template>
<a-space @mousedown="themeSwitcher.animationsOff()" id="change-theme" direction="vertical" :size="10" :style="{ width: '100%' }"> <a-space @mousedown="themeSwitcher.animationsOff()" id="change-theme" direction="vertical" :size="10"
:style="{ width: '100%' }">
<a-space direction="horizontal" size="small"> <a-space direction="horizontal" size="small">
<a-switch size="small" :default-checked="themeSwitcher.isDarkTheme" @change="themeSwitcher.toggleTheme()"></a-switch> <a-switch size="small" :default-checked="themeSwitcher.isDarkTheme"
@change="themeSwitcher.toggleTheme()"></a-switch>
<span>{{ i18n "menu.dark" }}</span> <span>{{ i18n "menu.dark" }}</span>
</a-space> </a-space>
<a-space v-if="themeSwitcher.isDarkTheme" direction="horizontal" size="small"> <a-space v-if="themeSwitcher.isDarkTheme" direction="horizontal" size="small">

View File

@@ -1,11 +1,5 @@
{{define "form/client"}} {{define "form/client"}}
<a-form <a-form layout="horizontal" v-if="client" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
layout="horizontal"
v-if="client"
:colon="false"
:label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"
>
<a-form-item label='{{ i18n "pages.inbounds.enable" }}'> <a-form-item label='{{ i18n "pages.inbounds.enable" }}'>
<a-switch v-model="client.enable"></a-switch> <a-switch v-model="client.enable"></a-switch>
</a-form-item> </a-form-item>
@@ -16,33 +10,22 @@
<span>{{ i18n "pages.inbounds.emailDesc" }}</span> <span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template> </template>
{{ i18n "pages.inbounds.email" }} {{ i18n "pages.inbounds.email" }}
<a-icon <a-icon type="sync" @click="client.email = RandomUtil.randomLowerAndNum(9)"></a-icon>
type="sync"
@click="client.email = RandomUtil.randomLowerAndNum(9)"
></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input v-model.trim="client.email"></a-input> <a-input v-model.trim="client.email"></a-input>
</a-form-item> </a-form-item>
<a-form-item <a-form-item v-if="inbound.protocol === Protocols.TROJAN || inbound.protocol === Protocols.SHADOWSOCKS">
v-if="inbound.protocol === Protocols.TROJAN || inbound.protocol === Protocols.SHADOWSOCKS"
>
<template slot="label"> <template slot="label">
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "reset" }}</span> <span>{{ i18n "reset" }}</span>
</template> </template>
{{ i18n "password" }} {{ i18n "password" }}
<a-icon <a-icon v-if="inbound.protocol === Protocols.SHADOWSOCKS"
v-if="inbound.protocol === Protocols.SHADOWSOCKS" @click="client.password = RandomUtil.randomShadowsocksPassword(inbound.settings.method)" type="sync"></a-icon>
@click="client.password = RandomUtil.randomShadowsocksPassword(inbound.settings.method)" <a-icon v-if="inbound.protocol === Protocols.TROJAN" @click="client.password = RandomUtil.randomSeq(10)"
type="sync" type="sync">
></a-icon>
<a-icon
v-if="inbound.protocol === Protocols.TROJAN"
@click="client.password = RandomUtil.randomSeq(10)"
type="sync"
>
</a-icon> </a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
@@ -55,42 +38,26 @@
<span>{{ i18n "reset" }}</span> <span>{{ i18n "reset" }}</span>
</template> </template>
Auth Password Auth Password
<a-icon <a-icon @click="client.auth = RandomUtil.randomSeq(10)" type="sync"></a-icon>
@click="client.auth = RandomUtil.randomSeq(10)"
type="sync"
></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input v-model.trim="client.auth"></a-input> <a-input v-model.trim="client.auth"></a-input>
</a-form-item> </a-form-item>
<a-form-item <a-form-item v-if="inbound.protocol === Protocols.VMESS || inbound.protocol === Protocols.VLESS">
v-if="inbound.protocol === Protocols.VMESS || inbound.protocol === Protocols.VLESS"
>
<template slot="label"> <template slot="label">
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "reset" }}</span> <span>{{ i18n "reset" }}</span>
</template> </template>
ID ID
<a-icon <a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"></a-icon>
@click="client.id = RandomUtil.randomUUID()"
type="sync"
></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input v-model.trim="client.id"></a-input> <a-input v-model.trim="client.id"></a-input>
</a-form-item> </a-form-item>
<a-form-item <a-form-item v-if="inbound.protocol === Protocols.VMESS" label='{{ i18n "security" }}'>
v-if="inbound.protocol === Protocols.VMESS" <a-select v-model="client.security" :dropdown-class-name="themeSwitcher.currentTheme">
label='{{ i18n "security" }}' <a-select-option v-for="key in USERS_SECURITY" :value="key">[[ key ]]</a-select-option>
>
<a-select
v-model="client.security"
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select-option v-for="key in USERS_SECURITY" :value="key"
>[[ key ]]</a-select-option
>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item v-if="client.email && app.subSettings?.enable"> <a-form-item v-if="client.email && app.subSettings?.enable">
@@ -100,10 +67,7 @@
<span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span> <span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span>
</template> </template>
Subscription Subscription
<a-icon <a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"></a-icon>
@click="client.subId = RandomUtil.randomLowerAndNum(16)"
type="sync"
></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input v-model.trim="client.subId"></a-input> <a-input v-model.trim="client.subId"></a-input>
@@ -118,11 +82,7 @@
<a-icon type="question-circle"></a-icon> <a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input-number <a-input-number :style="{ width: '50%' }" v-model.number="client.tgId" min="0"></a-input-number>
:style="{ width: '50%' }"
v-model.number="client.tgId"
min="0"
></a-input-number>
</a-form-item> </a-form-item>
<a-form-item v-if="client.email" label='{{ i18n "comment" }}'> <a-form-item v-if="client.email" label='{{ i18n "comment" }}'>
<a-input v-model.trim="client.comment"></a-input> <a-input v-model.trim="client.comment"></a-input>
@@ -139,9 +99,7 @@
</template> </template>
<a-input-number v-model.number="client.limitIp" min="0"></a-input-number> <a-input-number v-model.number="client.limitIp" min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item <a-form-item v-if="app.ipLimitEnable && client.limitIp > 0 && client.email && isEdit">
v-if="app.ipLimitEnable && client.limitIp > 0 && client.email && isEdit"
>
<template slot="label"> <template slot="label">
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
@@ -160,25 +118,15 @@
</span> </span>
</a-tooltip> </a-tooltip>
<a-form layout="block"> <a-form layout="block">
<a-textarea <a-textarea id="clientIPs" readonly @click="getDBClientIps(client.email)" placeholder="Click To Get IPs"
id="clientIPs" :auto-size="{ minRows: 5, maxRows: 10 }">
readonly
@click="getDBClientIps(client.email)"
placeholder="Click To Get IPs"
:auto-size="{ minRows: 5, maxRows: 10 }"
>
</a-textarea> </a-textarea>
</a-form> </a-form>
</a-form-item> </a-form-item>
<a-form-item v-if="inbound.canEnableTlsFlow()" label="Flow"> <a-form-item v-if="inbound.canEnableTlsFlow()" label="Flow">
<a-select <a-select v-model="client.flow" :dropdown-class-name="themeSwitcher.currentTheme">
v-model="client.flow"
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select-option value selected>{{ i18n "none" }}</a-select-option> <a-select-option value selected>{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key" <a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
>[[ key ]]</a-select-option
>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
@@ -201,45 +149,28 @@
</a-tag> </a-tag>
<a-tooltip> <a-tooltip>
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template> <template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
<a-icon <a-icon type="retweet" @click="resetClientTraffic(client.email,clientStats.inboundId,$event.target)"
type="retweet" v-if="client.email.length > 0"></a-icon>
@click="resetClientTraffic(client.email,clientStats.inboundId,$event.target)"
v-if="client.email.length > 0"
></a-icon>
</a-tooltip> </a-tooltip>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.client.delayedStart" }}'> <a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
<a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch> <a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
</a-form-item> </a-form-item>
<a-form-item v-if="delayedStart" label='{{ i18n "pages.client.expireDays" }}'> <a-form-item v-if="delayedStart" label='{{ i18n "pages.client.expireDays" }}'>
<a-input-number <a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
v-model.number="delayedExpireDays"
:min="0"
></a-input-number>
</a-form-item> </a-form-item>
<a-form-item v-else> <a-form-item v-else>
<template slot="label"> <template slot="label">
<a-tooltip> <a-tooltip>
<template slot="title" <template slot="title">{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</template>
>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</template
>
{{ i18n "pages.inbounds.expireDate" }} {{ i18n "pages.inbounds.expireDate" }}
<a-icon type="question-circle"></a-icon> <a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-date-picker <a-date-picker v-if="datepicker == 'gregorian'" :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
v-if="datepicker == 'gregorian'" :dropdown-class-name="themeSwitcher.currentTheme" v-model="client._expiryTime"></a-date-picker>
:show-time="{ format: 'HH:mm:ss' }" <a-persian-datepicker v-else placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}'
format="YYYY-MM-DD HH:mm:ss" value="client._expiryTime" v-model="client._expiryTime"></a-persian-datepicker>
:dropdown-class-name="themeSwitcher.currentTheme"
v-model="client._expiryTime"
></a-date-picker>
<a-persian-datepicker
v-else
placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}'
value="client._expiryTime"
v-model="client._expiryTime"
></a-persian-datepicker>
<a-tag color="red" v-if="isEdit && isExpiry">Expired</a-tag> <a-tag color="red" v-if="isEdit && isExpiry">Expired</a-tag>
</a-form-item> </a-form-item>
<a-form-item v-if="client.expiryTime != 0"> <a-form-item v-if="client.expiryTime != 0">
@@ -253,4 +184,4 @@
<a-input-number v-model.number="client.reset" :min="0"></a-input-number> <a-input-number v-model.number="client.reset" :min="0"></a-input-number>
</a-form-item> </a-form-item>
</a-form> </a-form>
{{end}} {{end}}

File diff suppressed because it is too large Load Diff

View File

@@ -25,7 +25,7 @@
<a-select-option value="tcp">TCP</a-select-option> <a-select-option value="tcp">TCP</a-select-option>
<a-select-option value="udp">UDP</a-select-option> <a-select-option value="udp">UDP</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label='Follow Redirect'> <a-form-item label='Follow Redirect'>
<a-switch v-model="inbound.settings.followRedirect"></a-switch> <a-switch v-model="inbound.settings.followRedirect"></a-switch>
</a-form-item> </a-form-item>
@@ -34,4 +34,4 @@
<template> <template>
{{template "form/streamSockopt"}} {{template "form/streamSockopt"}}
</template> </template>
{{end}} {{end}}

View File

@@ -5,7 +5,8 @@
<td width="45%">{{ i18n "username" }}</td> <td width="45%">{{ i18n "username" }}</td>
<td width="45%">{{ i18n "password" }}</td> <td width="45%">{{ i18n "password" }}</td>
<td> <td>
<a-button icon="plus" size="small" @click="inbound.settings.addAccount(new Inbound.HttpSettings.HttpAccount())"></a-button> <a-button icon="plus" size="small"
@click="inbound.settings.addAccount(new Inbound.HttpSettings.HttpAccount())"></a-button>
</td> </td>
</tr> </tr>
</table> </table>
@@ -23,4 +24,4 @@
<a-switch v-model="inbound.settings.allowTransparent" /> <a-switch v-model="inbound.settings.allowTransparent" />
</a-form-item> </a-form-item>
</a-form> </a-form>
{{end}} {{end}}

View File

@@ -1,7 +1,5 @@
{{define "form/hysteria"}} {{define "form/hysteria"}}
<a-collapse activeKey="0" <a-collapse activeKey="0" v-for="(client, index) in inbound.settings.hysterias.slice(0,1)" v-if="!isEdit">
v-for="(client, index) in inbound.settings.hysterias.slice(0,1)"
v-if="!isEdit">
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'> <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
{{template "form/client"}} {{template "form/client"}}
</a-collapse-panel> </a-collapse-panel>
@@ -22,11 +20,9 @@
</table> </table>
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
<a-form :colon="false" :label-col="{ md: {span:8} }" <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
:wrapper-col="{ md: {span:14} }">
<a-form-item :label="'{{ i18n "pages.inbounds.stream.tcp.version" }}'"> <a-form-item :label="'{{ i18n "pages.inbounds.stream.tcp.version" }}'">
<a-input-number v-model.number="inbound.settings.version" :min="2" <a-input-number v-model.number="inbound.settings.version" :min="2" :max="2" disabled></a-input-number>
:max="2" disabled></a-input-number>
</a-form-item> </a-form-item>
</a-form> </a-form>
{{end}} {{end}}

View File

@@ -1,10 +1,6 @@
{{define "form/shadowsocks"}} {{define "form/shadowsocks"}}
<template v-if="inbound.isSSMultiUser"> <template v-if="inbound.isSSMultiUser">
<a-collapse <a-collapse activeKey="0" v-for="(client, index) in inbound.settings.shadowsockses.slice(0,1)" v-if="!isEdit">
activeKey="0"
v-for="(client, index) in inbound.settings.shadowsockses.slice(0,1)"
v-if="!isEdit"
>
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'> <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
{{template "form/client"}} {{template "form/client"}}
</a-collapse-panel> </a-collapse-panel>
@@ -16,10 +12,8 @@
<th>{{ i18n "pages.inbounds.email" }}</th> <th>{{ i18n "pages.inbounds.email" }}</th>
<th>Password</th> <th>Password</th>
</tr> </tr>
<tr <tr v-for="(client, index) in inbound.settings.shadowsockses"
v-for="(client, index) in inbound.settings.shadowsockses" :class="index % 2 == 1 ? ' client-table-odd-row' : ''">
:class="index % 2 == 1 ? ' client-table-odd-row' : ''"
>
<td>[[ client.email ]]</td> <td>[[ client.email ]]</td>
<td>[[ client.password ]]</td> <td>[[ client.password ]]</td>
</tr> </tr>
@@ -27,20 +21,11 @@
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
</template> </template>
<a-form <a-form :colon=" false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
:colon=" false"
:label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"
>
<a-form-item label='{{ i18n "encryption" }}'> <a-form-item label='{{ i18n "encryption" }}'>
<a-select <a-select v-model="inbound.settings.method" @change="SSMethodChange"
v-model="inbound.settings.method" :dropdown-class-name="themeSwitcher.currentTheme">
@change="SSMethodChange" <a-select-option v-for="(method,method_name) in SSMethods" :value="method">[[ method_name ]]</a-select-option>
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select-option v-for="(method,method_name) in SSMethods" :value="method"
>[[ method_name ]]</a-select-option
>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item v-if="inbound.isSS2022"> <a-form-item v-if="inbound.isSS2022">
@@ -50,20 +35,15 @@
<span>{{ i18n "reset" }}</span> <span>{{ i18n "reset" }}</span>
</template> </template>
Password Password
<a-icon <a-icon @click="inbound.settings.password = RandomUtil.randomShadowsocksPassword(inbound.settings.method)"
@click="inbound.settings.password = RandomUtil.randomShadowsocksPassword(inbound.settings.method)" type="sync"></a-icon>
type="sync"
></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input v-model.trim="inbound.settings.password"></a-input> <a-input v-model.trim="inbound.settings.password"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.network" }}'> <a-form-item label='{{ i18n "pages.inbounds.network" }}'>
<a-select <a-select v-model="inbound.settings.network" :style="{ width: '100px' }"
v-model="inbound.settings.network" :dropdown-class-name="themeSwitcher.currentTheme">
:style="{ width: '100px' }"
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select-option value="tcp,udp">TCP,UDP</a-select-option> <a-select-option value="tcp,udp">TCP,UDP</a-select-option>
<a-select-option value="tcp">TCP</a-select-option> <a-select-option value="tcp">TCP</a-select-option>
<a-select-option value="udp">UDP</a-select-option> <a-select-option value="udp">UDP</a-select-option>
@@ -73,4 +53,4 @@
<a-switch v-model="inbound.settings.ivCheck"></a-switch> <a-switch v-model="inbound.settings.ivCheck"></a-switch>
</a-form-item> </a-form-item>
</a-form> </a-form>
{{end}} {{end}}

View File

@@ -1,9 +1,5 @@
{{define "form/mixed"}} {{define "form/mixed"}}
<a-form <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
:colon="false"
:label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"
>
<a-form-item label='{{ i18n "pages.inbounds.enable" }} UDP'> <a-form-item label='{{ i18n "pages.inbounds.enable" }} UDP'>
<a-switch v-model="inbound.settings.udp"></a-switch> <a-switch v-model="inbound.settings.udp"></a-switch>
</a-form-item> </a-form-item>
@@ -11,10 +7,8 @@
<a-input v-model.trim="inbound.settings.ip"></a-input> <a-input v-model.trim="inbound.settings.ip"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "password" }}'> <a-form-item label='{{ i18n "password" }}'>
<a-switch <a-switch :checked="inbound.settings.auth === 'password'"
:checked="inbound.settings.auth === 'password'" @change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"></a-switch>
@change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"
></a-switch>
</a-form-item> </a-form-item>
<template v-if="inbound.settings.auth === 'password'"> <template v-if="inbound.settings.auth === 'password'">
<table :style="{ width: '100%', textAlign: 'center', margin: '1rem 0' }"> <table :style="{ width: '100%', textAlign: 'center', margin: '1rem 0' }">
@@ -22,42 +16,21 @@
<td width="45%">{{ i18n "username" }}</td> <td width="45%">{{ i18n "username" }}</td>
<td width="45%">{{ i18n "password" }}</td> <td width="45%">{{ i18n "password" }}</td>
<td> <td>
<a-button <a-button icon="plus" size="small"
icon="plus" @click="inbound.settings.addAccount(new Inbound.MixedSettings.SocksAccount())"></a-button>
size="small"
@click="inbound.settings.addAccount(new Inbound.MixedSettings.SocksAccount())"
></a-button>
</td> </td>
</tr> </tr>
</table> </table>
<a-input-group <a-input-group compact v-for="(account, index) in inbound.settings.accounts" :style="{ marginBottom: '10px' }">
compact <a-input :style="{ width: '50%' }" v-model.trim="account.user" placeholder='{{ i18n "username" }}'>
v-for="(account, index) in inbound.settings.accounts" <template slot="addonBefore" :style="{ margin: '0' }">[[ index+1 ]]</template>
:style="{ marginBottom: '10px' }"
>
<a-input
:style="{ width: '50%' }"
v-model.trim="account.user"
placeholder='{{ i18n "username" }}'
>
<template slot="addonBefore" :style="{ margin: '0' }"
>[[ index+1 ]]</template
>
</a-input> </a-input>
<a-input <a-input :style="{ width: '50%' }" v-model.trim="account.pass" placeholder='{{ i18n "password" }}'>
:style="{ width: '50%' }"
v-model.trim="account.pass"
placeholder='{{ i18n "password" }}'
>
<template slot="addonAfter"> <template slot="addonAfter">
<a-button <a-button icon="minus" size="small" @click="inbound.settings.delAccount(index)"></a-button>
icon="minus"
size="small"
@click="inbound.settings.delAccount(index)"
></a-button>
</template> </template>
</a-input> </a-input>
</a-input-group> </a-input-group>
</template> </template>
</a-form> </a-form>
{{end}} {{end}}

View File

@@ -19,35 +19,35 @@
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
<template v-if=" inbound.isTcp"> <template v-if=" inbound.isTcp">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="Fallbacks"> <a-form-item label="Fallbacks">
<a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button> <a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button>
</a-form-item> </a-form-item>
</a-form> </a-form>
<!-- trojan fallbacks --> <!-- trojan fallbacks -->
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:8} }" <a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"> :wrapper-col="{ md: {span:14} }">
<a-divider :style="{ margin: '0' }"> Fallback [[ index + 1 ]] <a-icon type="delete" <a-divider :style="{ margin: '0' }"> Fallback [[ index + 1 ]] <a-icon type="delete"
@click="() => inbound.settings.delFallback(index)" @click="() => inbound.settings.delFallback(index)"
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon> :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
</a-divider> </a-divider>
<a-form-item label='SNI'> <a-form-item label='SNI'>
<a-input v-model="fallback.name"></a-input> <a-input v-model="fallback.name"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='ALPN'> <a-form-item label='ALPN'>
<a-input v-model="fallback.alpn"></a-input> <a-input v-model="fallback.alpn"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='Path'> <a-form-item label='Path'>
<a-input v-model="fallback.path"></a-input> <a-input v-model="fallback.path"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='Dest'> <a-form-item label='Dest'>
<a-input v-model="fallback.dest"></a-input> <a-input v-model="fallback.dest"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='xVer'> <a-form-item label='xVer'>
<a-input-number v-model.number="fallback.xver" :min="0" :max="2"></a-input-number> <a-input-number v-model.number="fallback.xver" :min="0" :max="2"></a-input-number>
</a-form-item> </a-form-item>
</a-form> </a-form>
<a-divider style="margin:5px 0;"></a-divider> <a-divider style="margin:5px 0;"></a-divider>
</template> </template>
{{end}} {{end}}

View File

@@ -1,9 +1,5 @@
{{define "form/tun"}} {{define "form/tun"}}
<a-form <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
:colon="false"
:label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"
>
<a-form-item> <a-form-item>
<template slot="label"> <template slot="label">
<a-tooltip> <a-tooltip>
@@ -26,38 +22,18 @@
<a-icon type="question-circle"></a-icon> <a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input-number <a-input-number v-model.number="inbound.settings.mtu[0]" :min="1" :max="9000" placeholder="1500"></a-input-number>
v-model.number="inbound.settings.mtu[0]"
:min="1"
:max="9000"
placeholder="1500"
></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label="MTU IPv6"> <a-form-item label="MTU IPv6">
<a-input-number <a-input-number v-model.number="inbound.settings.mtu[1]" :min="1" :max="9000" placeholder="1280"></a-input-number>
v-model.number="inbound.settings.mtu[1]"
:min="1"
:max="9000"
placeholder="1280"
></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label="Gateway"> <a-form-item label="Gateway">
<a-select <a-select mode="tags" v-model="inbound.settings.gateway" :style="{ width: '100%' }" :token-separators="[',']"
mode="tags" placeholder="IPv4/IPv6 gateway"></a-select>
v-model="inbound.settings.gateway"
:style="{ width: '100%' }"
:token-separators="[',']"
placeholder="IPv4/IPv6 gateway"
></a-select>
</a-form-item> </a-form-item>
<a-form-item label="DNS"> <a-form-item label="DNS">
<a-select <a-select mode="tags" v-model="inbound.settings.dns" :style="{ width: '100%' }" :token-separators="[',']"
mode="tags" placeholder="DNS servers"></a-select>
v-model="inbound.settings.dns"
:style="{ width: '100%' }"
:token-separators="[',']"
placeholder="DNS servers"
></a-select>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<template slot="label"> <template slot="label">
@@ -69,26 +45,14 @@
<a-icon type="question-circle"></a-icon> <a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input-number <a-input-number v-model.number="inbound.settings.userLevel" :min="0" placeholder="0"></a-input-number>
v-model.number="inbound.settings.userLevel"
:min="0"
placeholder="0"
></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label="Auto Routing Table"> <a-form-item label="Auto Routing Table">
<a-select <a-select mode="tags" v-model="inbound.settings.autoSystemRoutingTable" :style="{ width: '100%' }"
mode="tags" :token-separators="[',']" placeholder="e.g. vpn, proxy"></a-select>
v-model="inbound.settings.autoSystemRoutingTable"
:style="{ width: '100%' }"
:token-separators="[',']"
placeholder="e.g. vpn, proxy"
></a-select>
</a-form-item> </a-form-item>
<a-form-item label="Auto Outbounds"> <a-form-item label="Auto Outbounds">
<a-input <a-input v-model.trim="inbound.settings.autoOutboundsInterface" placeholder="auto"></a-input>
v-model.trim="inbound.settings.autoOutboundsInterface"
placeholder="auto"
></a-input>
</a-form-item> </a-form-item>
</a-form> </a-form>
{{end}} {{end}}

View File

@@ -12,8 +12,7 @@
<th>{{ i18n "pages.inbounds.email" }}</th> <th>{{ i18n "pages.inbounds.email" }}</th>
<th>ID</th> <th>ID</th>
</tr> </tr>
<tr v-for="(client, index) in inbound.settings.vlesses" <tr v-for="(client, index) in inbound.settings.vlesses" :class="index % 2 == 1 ? ' client-table-odd-row' : ''">
:class="index % 2 == 1 ? ' client-table-odd-row' : ''">
<td>[[ client.email ]]</td> <td>[[ client.email ]]</td>
<td>[[ client.id ]]</td> <td>[[ client.id ]]</td>
</tr> </tr>
@@ -21,104 +20,104 @@
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
<template v-if=" !inbound.stream.isTLS || !inbound.stream.isReality"> <template v-if=" !inbound.stream.isTLS || !inbound.stream.isReality">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="Authentication"> <a-form-item label="Authentication">
<a-select v-model="inbound.settings.selectedAuth" @change="getNewVlessEnc" <a-select v-model="inbound.settings.selectedAuth" @change="getNewVlessEnc"
:dropdown-class-name="themeSwitcher.currentTheme"> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="undefined">None</a-select-option> <a-select-option :value="undefined">None</a-select-option>
<a-select-option value="X25519, not Post-Quantum">X25519 (not <a-select-option value="X25519, not Post-Quantum">X25519 (not
Post-Quantum)</a-select-option> Post-Quantum)</a-select-option>
<a-select-option value="ML-KEM-768, Post-Quantum">ML-KEM-768 <a-select-option value="ML-KEM-768, Post-Quantum">ML-KEM-768
(Post-Quantum)</a-select-option> (Post-Quantum)</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="decryption"> <a-form-item label="decryption">
<a-input v-model.trim="inbound.settings.decryption"></a-input> <a-input v-model.trim="inbound.settings.decryption"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="encryption"> <a-form-item label="encryption">
<a-input v-model="inbound.settings.encryption"></a-input> <a-input v-model="inbound.settings.encryption"></a-input>
</a-form-item> </a-form-item>
<a-form-item label=" "> <a-form-item label=" ">
<a-space> <a-space>
<a-button type="primary" icon="import" @click="getNewVlessEnc">Get New <a-button type="primary" icon="import" @click="getNewVlessEnc">Get New
keys</a-button> keys</a-button>
<a-button danger @click="clearVlessEnc">Clear</a-button> <a-button danger @click="clearVlessEnc">Clear</a-button>
</a-space> </a-space>
</a-form-item> </a-form-item>
</a-form> </a-form>
<a-divider :style="{ margin: '5px 0' }"></a-divider> <a-divider :style="{ margin: '5px 0' }"></a-divider>
</template> </template>
<template v-if="inbound.isTcp && !inbound.settings.selectedAuth"> <template v-if="inbound.isTcp && !inbound.settings.selectedAuth">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="Fallbacks"> <a-form-item label="Fallbacks">
<a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button> <a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button>
</a-form-item> </a-form-item>
</a-form> </a-form>
<!-- vless fallbacks --> <!-- vless fallbacks -->
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:8} }" <a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"> :wrapper-col="{ md: {span:14} }">
<a-divider :style="{ margin: '0' }"> Fallback [[ index + 1 ]] <a-icon type="delete" <a-divider :style="{ margin: '0' }"> Fallback [[ index + 1 ]] <a-icon type="delete"
@click="() => inbound.settings.delFallback(index)" @click="() => inbound.settings.delFallback(index)"
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon> :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
</a-divider> </a-divider>
<a-form-item label='SNI'> <a-form-item label='SNI'>
<a-input v-model="fallback.name"></a-input> <a-input v-model="fallback.name"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='ALPN'> <a-form-item label='ALPN'>
<a-input v-model="fallback.alpn"></a-input> <a-input v-model="fallback.alpn"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='Path'> <a-form-item label='Path'>
<a-input v-model="fallback.path"></a-input> <a-input v-model="fallback.path"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='Dest'> <a-form-item label='Dest'>
<a-input v-model="fallback.dest"></a-input> <a-input v-model="fallback.dest"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='xVer'> <a-form-item label='xVer'>
<a-input-number v-model.number="fallback.xver" :min="0" :max="2"></a-input-number> <a-input-number v-model.number="fallback.xver" :min="0" :max="2"></a-input-number>
</a-form-item> </a-form-item>
</a-form> </a-form>
<a-divider :style="{ margin: '5px 0' }"></a-divider> <a-divider :style="{ margin: '5px 0' }"></a-divider>
</template> </template>
<template v-if="inbound.canEnableVisionSeed()"> <template v-if="inbound.canEnableVisionSeed()">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="Vision Seed"> <a-form-item label="Vision Seed">
<a-row :gutter="8"> <a-row :gutter="8">
<a-col :span="6"> <a-col :span="6">
<a-input-number <a-input-number
:value="(inbound.settings.testseed && inbound.settings.testseed[0] !== undefined) ? inbound.settings.testseed[0] : 900" :value="(inbound.settings.testseed && inbound.settings.testseed[0] !== undefined) ? inbound.settings.testseed[0] : 900"
@change="(val) => updateTestseed(0, val)" :min="0" :max="9999" :style="{ width: '100%' }" @change="(val) => updateTestseed(0, val)" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="900"
placeholder="900" addon-before="[0]"></a-input-number> addon-before="[0]"></a-input-number>
</a-col> </a-col>
<a-col :span="6"> <a-col :span="6">
<a-input-number <a-input-number
:value="(inbound.settings.testseed && inbound.settings.testseed[1] !== undefined) ? inbound.settings.testseed[1] : 500" :value="(inbound.settings.testseed && inbound.settings.testseed[1] !== undefined) ? inbound.settings.testseed[1] : 500"
@change="(val) => updateTestseed(1, val)" :min="0" :max="9999" :style="{ width: '100%' }" @change="(val) => updateTestseed(1, val)" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="500"
placeholder="500" addon-before="[1]"></a-input-number> addon-before="[1]"></a-input-number>
</a-col> </a-col>
<a-col :span="6"> <a-col :span="6">
<a-input-number <a-input-number
:value="(inbound.settings.testseed && inbound.settings.testseed[2] !== undefined) ? inbound.settings.testseed[2] : 900" :value="(inbound.settings.testseed && inbound.settings.testseed[2] !== undefined) ? inbound.settings.testseed[2] : 900"
@change="(val) => updateTestseed(2, val)" :min="0" :max="9999" :style="{ width: '100%' }" @change="(val) => updateTestseed(2, val)" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="900"
placeholder="900" addon-before="[2]"></a-input-number> addon-before="[2]"></a-input-number>
</a-col> </a-col>
<a-col :span="6"> <a-col :span="6">
<a-input-number <a-input-number
:value="(inbound.settings.testseed && inbound.settings.testseed[3] !== undefined) ? inbound.settings.testseed[3] : 256" :value="(inbound.settings.testseed && inbound.settings.testseed[3] !== undefined) ? inbound.settings.testseed[3] : 256"
@change="(val) => updateTestseed(3, val)" :min="0" :max="9999" :style="{ width: '100%' }" @change="(val) => updateTestseed(3, val)" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="256"
placeholder="256" addon-before="[3]"></a-input-number> addon-before="[3]"></a-input-number>
</a-col> </a-col>
</a-row> </a-row>
<a-space :size="8" :style="{ marginTop: '8px' }"> <a-space :size="8" :style="{ marginTop: '8px' }">
<a-button type="primary" @click="setRandomTestseed"> <a-button type="primary" @click="setRandomTestseed">
Rand Rand
</a-button> </a-button>
<a-button @click="resetTestseed"> <a-button @click="resetTestseed">
Reset Reset
</a-button> </a-button>
</a-space> </a-space>
</a-form-item> </a-form-item>
</a-form> </a-form>
<a-divider :style="{ margin: '5px 0' }"></a-divider> <a-divider :style="{ margin: '5px 0' }"></a-divider>
</template> </template>
{{end}} {{end}}

View File

@@ -12,8 +12,8 @@
<th>ID</th> <th>ID</th>
<th>{{ i18n "security" }}</th> <th>{{ i18n "security" }}</th>
</tr> </tr>
<tr v-for="(client, index) in inbound.settings.vmesses" <tr v-for="(client, index) in inbound.settings.vmesses"
:class="index % 2 == 1 ? ' client-table-odd-row' : ''"> :class="index % 2 == 1 ? ' client-table-odd-row' : ''">
<td>[[ client.email ]]</td> <td>[[ client.email ]]</td>
<td>[[ client.id ]]</td> <td>[[ client.id ]]</td>
<td>[[ client.security ]]</td> <td>[[ client.security ]]</td>

View File

@@ -1,9 +1,5 @@
{{define "form/sniffing"}} {{define "form/sniffing"}}
<a-form <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
:colon="false"
:label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"
>
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
{{ i18n "enabled" }} {{ i18n "enabled" }}
@@ -19,9 +15,7 @@
<template v-if="inbound.sniffing.enabled"> <template v-if="inbound.sniffing.enabled">
<a-form-item :wrapper-col="{span:24}"> <a-form-item :wrapper-col="{span:24}">
<a-checkbox-group v-model="inbound.sniffing.destOverride"> <a-checkbox-group v-model="inbound.sniffing.destOverride">
<a-checkbox v-for="key,value in SNIFFING_OPTION" :value="key" <a-checkbox v-for="key,value in SNIFFING_OPTION" :value="key">[[ value ]]</a-checkbox>
>[[ value ]]</a-checkbox
>
</a-checkbox-group> </a-checkbox-group>
</a-form-item> </a-form-item>
<a-form-item label="Metadata Only"> <a-form-item label="Metadata Only">
@@ -31,23 +25,13 @@
<a-switch v-model="inbound.sniffing.routeOnly"></a-switch> <a-switch v-model="inbound.sniffing.routeOnly"></a-switch>
</a-form-item> </a-form-item>
<a-form-item label="IPs Excluded"> <a-form-item label="IPs Excluded">
<a-select <a-select mode="tags" v-model="inbound.sniffing.ipsExcluded" :style="{ width: '100%' }" :token-separators="[',']"
mode="tags" placeholder="IP/CIDR/geoip:*/ext:*"></a-select>
v-model="inbound.sniffing.ipsExcluded"
:style="{ width: '100%' }"
:token-separators="[',']"
placeholder="IP/CIDR/geoip:*/ext:*"
></a-select>
</a-form-item> </a-form-item>
<a-form-item label="Domains Excluded"> <a-form-item label="Domains Excluded">
<a-select <a-select mode="tags" v-model="inbound.sniffing.domainsExcluded" :style="{ width: '100%' }"
mode="tags" :token-separators="[',']" placeholder="domain:*/ext:*"></a-select>
v-model="inbound.sniffing.domainsExcluded"
:style="{ width: '100%' }"
:token-separators="[',']"
placeholder="domain:*/ext:*"
></a-select>
</a-form-item> </a-form-item>
</template> </template>
</a-form> </a-form>
{{end}} {{end}}

View File

@@ -1,67 +1,31 @@
{{define "form/externalProxy"}} {{define "form/externalProxy"}}
<a-form <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
:colon="false"
:label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"
>
<a-divider :style="{ margin: '5px 0 0' }"></a-divider> <a-divider :style="{ margin: '5px 0 0' }"></a-divider>
<a-form-item label="External Proxy"> <a-form-item label="External Proxy">
<a-switch v-model="externalProxy"></a-switch> <a-switch v-model="externalProxy"></a-switch>
<a-button <a-button icon="plus" v-if="externalProxy" type="primary" :style="{ marginLeft: '10px' }" size="small"
icon="plus" @click="inbound.stream.externalProxy.push({forceTls: 'same', dest: '', port: 443, remark: ''})"></a-button>
v-if="externalProxy"
type="primary"
:style="{ marginLeft: '10px' }"
size="small"
@click="inbound.stream.externalProxy.push({forceTls: 'same', dest: '', port: 443, remark: ''})"
></a-button>
</a-form-item> </a-form-item>
<a-input-group <a-input-group :style="{ margin: '8px 0' }" compact v-for="(row, index) in inbound.stream.externalProxy">
:style="{ margin: '8px 0' }"
compact
v-for="(row, index) in inbound.stream.externalProxy"
>
<template> <template>
<a-tooltip title="Force TLS"> <a-tooltip title="Force TLS">
<a-select <a-select v-model="row.forceTls" :style="{ width: '20%', margin: '0px' }"
v-model="row.forceTls" :dropdown-class-name="themeSwitcher.currentTheme">
:style="{ width: '20%', margin: '0px' }" <a-select-option value="same">{{ i18n "pages.inbounds.same" }}</a-select-option>
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select-option value="same"
>{{ i18n "pages.inbounds.same" }}</a-select-option
>
<a-select-option value="none">{{ i18n "none" }}</a-select-option> <a-select-option value="none">{{ i18n "none" }}</a-select-option>
<a-select-option value="tls">TLS</a-select-option> <a-select-option value="tls">TLS</a-select-option>
</a-select> </a-select>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input <a-input :style="{ width: '30%' }" v-model.trim="row.dest" placeholder='{{ i18n "host" }}'></a-input>
:style="{ width: '30%' }"
v-model.trim="row.dest"
placeholder='{{ i18n "host" }}'
></a-input>
<a-tooltip title='{{ i18n "pages.inbounds.port" }}'> <a-tooltip title='{{ i18n "pages.inbounds.port" }}'>
<a-input-number <a-input-number :style="{ width: '15%' }" v-model.number="row.port" min="1" max="65535"></a-input-number>
:style="{ width: '15%' }"
v-model.number="row.port"
min="1"
max="65535"
></a-input-number>
</a-tooltip> </a-tooltip>
<a-input <a-input :style="{ width: '30%', top: '0' }" v-model.trim="row.remark" placeholder='{{ i18n "remark" }}'>
:style="{ width: '30%', top: '0' }"
v-model.trim="row.remark"
placeholder='{{ i18n "remark" }}'
>
<template slot="addonAfter"> <template slot="addonAfter">
<a-button <a-button icon="minus" size="small" @click="inbound.stream.externalProxy.splice(index, 1)"></a-button>
icon="minus"
size="small"
@click="inbound.stream.externalProxy.splice(index, 1)"
></a-button>
</template> </template>
</a-input> </a-input>
</a-input-group> </a-input-group>
</a-form> </a-form>
{{end}} {{end}}

View File

@@ -1,446 +1,339 @@
{{define "form/streamFinalMask"}} {{define "form/streamFinalMask"}}
<a-form <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"
:colon="false" v-if="inbound.protocol == Protocols.HYSTERIA || ['kcp', 'xhttp', 'raw', 'tcp', 'httpupgrade', 'ws', 'grpc'].includes(inbound.stream.network)">
:label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"
v-if="inbound.protocol == Protocols.HYSTERIA || ['kcp', 'xhttp', 'raw', 'tcp', 'httpupgrade', 'ws', 'grpc'].includes(inbound.stream.network)"
>
<a-divider :style="{ margin: '5px 0 0' }"></a-divider> <a-divider :style="{ margin: '5px 0 0' }"></a-divider>
<!-- TCP Masks for raw/tcp/httpupgrade/ws/grpc/xhttp --> <!-- TCP Masks for raw/tcp/httpupgrade/ws/grpc/xhttp -->
<template v-if="['raw', 'tcp', 'httpupgrade', 'ws', 'grpc', 'xhttp'].includes(inbound.stream.network)"> <template v-if="['raw', 'tcp', 'httpupgrade', 'ws', 'grpc', 'xhttp'].includes(inbound.stream.network)">
<a-form-item label="TCP Masks"> <a-form-item label="TCP Masks">
<a-button <a-button icon="plus" type="primary" size="small" @click="inbound.stream.addTcpMask('fragment')"></a-button>
icon="plus" </a-form-item>
type="primary" <template v-if="inbound.stream.finalmask.tcp && inbound.stream.finalmask.tcp.length > 0">
size="small" <a-form v-for="(mask, index) in inbound.stream.finalmask.tcp" :key="index" :colon="false"
@click="inbound.stream.addTcpMask('fragment')" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
></a-button> <a-divider :style="{ margin: '0' }">
</a-form-item> TCP Mask [[ index + 1 ]]
<template v-if="inbound.stream.finalmask.tcp && inbound.stream.finalmask.tcp.length > 0"> <a-icon type="delete" @click="() => inbound.stream.delTcpMask(index)"
<a-form :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
v-for="(mask, index) in inbound.stream.finalmask.tcp" </a-divider>
:key="index" <a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
:colon="false" <a-select v-model="mask.type" @change="(type) => { mask.settings = mask._getDefaultSettings(type, {}); }"
:label-col="{ md: {span:8} }" :dropdown-class-name="themeSwitcher.currentTheme">
:wrapper-col="{ md: {span:14} }" <a-select-option value="fragment">Fragment</a-select-option>
> <a-select-option value="header-custom">Header Custom</a-select-option>
<a-divider :style="{ margin: '0' }"> <a-select-option value="sudoku">Sudoku</a-select-option>
TCP Mask [[ index + 1 ]]
<a-icon
type="delete"
@click="() => inbound.stream.delTcpMask(index)"
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"
></a-icon>
</a-divider>
<a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
<a-select
v-model="mask.type"
@change="(type) => { mask.settings = mask._getDefaultSettings(type, {}); }"
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select-option value="fragment">Fragment</a-select-option>
<a-select-option value="header-custom">Header Custom</a-select-option>
<a-select-option value="sudoku">Sudoku</a-select-option>
</a-select>
</a-form-item>
<!-- Fragment settings -->
<template v-if="mask.type === 'fragment'">
<a-form-item label="Packets">
<a-select
v-model="mask.settings.packets"
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select-option value="tlshello">tlshello</a-select-option>
<a-select-option value="1-3">1-3</a-select-option>
<a-select-option value="1-5">1-5</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="Length">
<a-input v-model.trim="mask.settings.length" placeholder="e.g. 100-200" />
</a-form-item>
<a-form-item label="Delay">
<a-input v-model.trim="mask.settings.delay" placeholder="e.g. 10-20" />
</a-form-item>
<a-form-item label="Max Split">
<a-input v-model.trim="mask.settings.maxSplit" placeholder="e.g. 3-6" />
</a-form-item>
</template>
<!-- Sudoku settings (TCP) --> <!-- Fragment settings -->
<template v-if="mask.type === 'sudoku'"> <template v-if="mask.type === 'fragment'">
<a-form-item label="Password"> <a-form-item label="Packets">
<a-input v-model.trim="mask.settings.password" placeholder="Obfuscation password" /> <a-select v-model="mask.settings.packets" :dropdown-class-name="themeSwitcher.currentTheme">
</a-form-item> <a-select-option value="tlshello">tlshello</a-select-option>
<a-form-item label="ASCII"> <a-select-option value="1-3">1-3</a-select-option>
<a-input v-model.trim="mask.settings.ascii" placeholder="ASCII" /> <a-select-option value="1-5">1-5</a-select-option>
</a-form-item> </a-select>
<a-form-item label="Custom Table"> </a-form-item>
<a-input v-model.trim="mask.settings.customTable" placeholder="Custom Table" /> <a-form-item label="Length">
</a-form-item> <a-input v-model.trim="mask.settings.length" placeholder="e.g. 100-200" />
<a-form-item label="Custom Tables"> </a-form-item>
<a-input v-model.trim="mask.settings.customTables" placeholder="Custom Tables" /> <a-form-item label="Delay">
</a-form-item> <a-input v-model.trim="mask.settings.delay" placeholder="e.g. 10-20" />
<a-form-item label="Padding Min"> </a-form-item>
<a-input-number v-model.number="mask.settings.paddingMin" :min="0" /> <a-form-item label="Max Split">
</a-form-item> <a-input v-model.trim="mask.settings.maxSplit" placeholder="e.g. 3-6" />
<a-form-item label="Padding Max"> </a-form-item>
<a-input-number v-model.number="mask.settings.paddingMax" :min="0" /> </template>
</a-form-item>
</template>
<!-- Header Custom (TCP) clients/servers/errors are 2D arrays of groups --> <!-- Sudoku settings (TCP) -->
<template v-if="mask.type === 'header-custom'"> <template v-if="mask.type === 'sudoku'">
<!-- Clients --> <a-form-item label="Password">
<a-form-item label="Clients"> <a-input v-model.trim="mask.settings.password" placeholder="Obfuscation password" />
<a-button </a-form-item>
icon="plus" <a-form-item label="ASCII">
type="primary" <a-input v-model.trim="mask.settings.ascii" placeholder="ASCII" />
size="small" </a-form-item>
@click="mask.settings.clients.push([{delay: 0, rand: 0, randRange: '0-255', type: 'array', packet: []}])" <a-form-item label="Custom Table">
></a-button> <a-input v-model.trim="mask.settings.customTable" placeholder="Custom Table" />
</a-form-item> </a-form-item>
<template v-for="(group, gi) in mask.settings.clients" :key="'cg'+gi"> <a-form-item label="Custom Tables">
<a-divider :style="{ margin: '0' }"> <a-input v-model.trim="mask.settings.customTables" placeholder="Custom Tables" />
Clients Group [[ gi + 1 ]] </a-form-item>
<a-icon <a-form-item label="Padding Min">
type="delete" <a-input-number v-model.number="mask.settings.paddingMin" :min="0" />
@click="mask.settings.clients.splice(gi, 1)" </a-form-item>
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer', marginLeft: '8px' }" <a-form-item label="Padding Max">
></a-icon> <a-input-number v-model.number="mask.settings.paddingMax" :min="0" />
</a-divider> </a-form-item>
<template v-for="(item, ii) in group" :key="'ci'+ii"> </template>
<a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
<a-select v-model="item.type" :dropdown-class-name="themeSwitcher.currentTheme" <!-- Header Custom (TCP) clients/servers/errors are 2D arrays of groups -->
@change="t => { if(t === 'base64') item.packet = RandomUtil.randomBase64(); else if(t === 'array') { item.rand = 0; item.packet = []; } else { item.packet = ''; } }"> <template v-if="mask.type === 'header-custom'">
<a-select-option value="array">Array</a-select-option> <!-- Clients -->
<a-select-option value="str">String</a-select-option> <a-form-item label="Clients">
<a-select-option value="hex">Hex</a-select-option> <a-button icon="plus" type="primary" size="small"
<a-select-option value="base64">Base64</a-select-option> @click="mask.settings.clients.push([{delay: 0, rand: 0, randRange: '0-255', type: 'array', packet: []}])"></a-button>
</a-select> </a-form-item>
</a-form-item> <template v-for="(group, gi) in mask.settings.clients" :key="'cg'+gi">
<a-form-item label="Delay (ms)"> <a-divider :style="{ margin: '0' }">
<a-input-number v-model.number="item.delay" :min="0" /> Clients Group [[ gi + 1 ]]
</a-form-item> <a-icon type="delete" @click="mask.settings.clients.splice(gi, 1)"
<template v-if="item.type === 'array'"> :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer', marginLeft: '8px' }"></a-icon>
<a-form-item label="Rand"> </a-divider>
<a-input-number v-model.number="item.rand" :min="0" /> <template v-for="(item, ii) in group" :key="'ci'+ii">
<a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
<a-select v-model="item.type" :dropdown-class-name="themeSwitcher.currentTheme"
@change="t => { if(t === 'base64') item.packet = RandomUtil.randomBase64(); else if(t === 'array') { item.rand = 0; item.packet = []; } else { item.packet = ''; } }">
<a-select-option value="array">Array</a-select-option>
<a-select-option value="str">String</a-select-option>
<a-select-option value="hex">Hex</a-select-option>
<a-select-option value="base64">Base64</a-select-option>
</a-select>
</a-form-item> </a-form-item>
<a-form-item label="Rand Range"> <a-form-item label="Delay (ms)">
<a-input v-model.trim="item.randRange" placeholder="0-255" /> <a-input-number v-model.number="item.delay" :min="0" />
</a-form-item>
<template v-if="item.type === 'array'">
<a-form-item label="Rand">
<a-input-number v-model.number="item.rand" :min="0" />
</a-form-item>
<a-form-item label="Rand Range">
<a-input v-model.trim="item.randRange" placeholder="0-255" />
</a-form-item>
</template>
<a-form-item v-else label="Packet">
<a-input-group compact v-if="item.type === 'base64'">
<a-input v-model.trim="item.packet" placeholder="binary data"
:style="{ width: 'calc(100% - 32px)' }" />
<a-button icon="reload" @click="item.packet = RandomUtil.randomBase64()" />
</a-input-group>
<a-input v-else v-model.trim="item.packet" placeholder="binary data" />
</a-form-item>
</template>
</template>
<!-- Servers -->
<a-form-item label="Servers">
<a-button icon="plus" type="primary" size="small"
@click="mask.settings.servers.push([{delay: 0, rand: 0, randRange: '0-255', type: 'array', packet: []}])"></a-button>
</a-form-item>
<template v-for="(group, gi) in mask.settings.servers" :key="'sg'+gi">
<a-divider :style="{ margin: '0' }">
Servers Group [[ gi + 1 ]]
<a-icon type="delete" @click="mask.settings.servers.splice(gi, 1)"
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer', marginLeft: '8px' }"></a-icon>
</a-divider>
<template v-for="(item, ii) in group" :key="'si'+ii">
<a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
<a-select v-model="item.type" :dropdown-class-name="themeSwitcher.currentTheme"
@change="t => { if(t === 'base64') item.packet = RandomUtil.randomBase64(); else if(t === 'array') { item.rand = 0; item.packet = []; } else { item.packet = ''; } }">
<a-select-option value="array">Array</a-select-option>
<a-select-option value="str">String</a-select-option>
<a-select-option value="hex">Hex</a-select-option>
<a-select-option value="base64">Base64</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Delay (ms)">
<a-input-number v-model.number="item.delay" :min="0" />
</a-form-item>
<template v-if="item.type === 'array'">
<a-form-item label="Rand">
<a-input-number v-model.number="item.rand" :min="0" />
</a-form-item>
<a-form-item label="Rand Range">
<a-input v-model.trim="item.randRange" placeholder="0-255" />
</a-form-item>
</template>
<a-form-item v-else label="Packet">
<a-input-group compact v-if="item.type === 'base64'">
<a-input v-model.trim="item.packet" placeholder="binary data"
:style="{ width: 'calc(100% - 32px)' }" />
<a-button icon="reload" @click="item.packet = RandomUtil.randomBase64()" />
</a-input-group>
<a-input v-else v-model.trim="item.packet" placeholder="binary data" />
</a-form-item> </a-form-item>
</template> </template>
<a-form-item v-else label="Packet">
<a-input-group compact v-if="item.type === 'base64'">
<a-input v-model.trim="item.packet" placeholder="binary data" :style="{ width: 'calc(100% - 32px)' }" />
<a-button icon="reload" @click="item.packet = RandomUtil.randomBase64()" />
</a-input-group>
<a-input v-else v-model.trim="item.packet" placeholder="binary data" />
</a-form-item>
</template> </template>
</template> </template>
<!-- Servers --> </a-form>
<a-form-item label="Servers"> </template>
<a-button
icon="plus"
type="primary"
size="small"
@click="mask.settings.servers.push([{delay: 0, rand: 0, randRange: '0-255', type: 'array', packet: []}])"
></a-button>
</a-form-item>
<template v-for="(group, gi) in mask.settings.servers" :key="'sg'+gi">
<a-divider :style="{ margin: '0' }">
Servers Group [[ gi + 1 ]]
<a-icon
type="delete"
@click="mask.settings.servers.splice(gi, 1)"
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer', marginLeft: '8px' }"
></a-icon>
</a-divider>
<template v-for="(item, ii) in group" :key="'si'+ii">
<a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
<a-select v-model="item.type" :dropdown-class-name="themeSwitcher.currentTheme"
@change="t => { if(t === 'base64') item.packet = RandomUtil.randomBase64(); else if(t === 'array') { item.rand = 0; item.packet = []; } else { item.packet = ''; } }">
<a-select-option value="array">Array</a-select-option>
<a-select-option value="str">String</a-select-option>
<a-select-option value="hex">Hex</a-select-option>
<a-select-option value="base64">Base64</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Delay (ms)">
<a-input-number v-model.number="item.delay" :min="0" />
</a-form-item>
<template v-if="item.type === 'array'">
<a-form-item label="Rand">
<a-input-number v-model.number="item.rand" :min="0" />
</a-form-item>
<a-form-item label="Rand Range">
<a-input v-model.trim="item.randRange" placeholder="0-255" />
</a-form-item>
</template>
<a-form-item v-else label="Packet">
<a-input-group compact v-if="item.type === 'base64'">
<a-input v-model.trim="item.packet" placeholder="binary data" :style="{ width: 'calc(100% - 32px)' }" />
<a-button icon="reload" @click="item.packet = RandomUtil.randomBase64()" />
</a-input-group>
<a-input v-else v-model.trim="item.packet" placeholder="binary data" />
</a-form-item>
</template>
</template>
</template>
</a-form>
</template>
</template> </template>
<template v-if="inbound.protocol == Protocols.HYSTERIA || inbound.stream.network == 'kcp'"> <template v-if="inbound.protocol == Protocols.HYSTERIA || inbound.stream.network == 'kcp'">
<a-form-item label="UDP Masks"> <a-form-item label="UDP Masks">
<a-button <a-button icon="plus" type="primary" size="small"
icon="plus" @click="inbound.stream.addUdpMask(inbound.protocol === Protocols.HYSTERIA ? 'salamander' : 'mkcp-aes128gcm')"></a-button>
type="primary" </a-form-item>
size="small" <template v-if="inbound.stream.finalmask.udp && inbound.stream.finalmask.udp.length > 0">
@click="inbound.stream.addUdpMask(inbound.protocol === Protocols.HYSTERIA ? 'salamander' : 'mkcp-aes128gcm')" <a-form v-for="(mask, index) in inbound.stream.finalmask.udp" :key="index" :colon="false"
></a-button> :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
</a-form-item> <a-divider :style="{ margin: '0' }">
<template UDP Mask [[ index + 1 ]]
v-if="inbound.stream.finalmask.udp && inbound.stream.finalmask.udp.length > 0" <a-icon type="delete" @click="() => inbound.stream.delUdpMask(index)"
> :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
<a-form </a-divider>
v-for="(mask, index) in inbound.stream.finalmask.udp" <a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
:key="index" <a-select v-model="mask.type"
:colon="false" @change="(type) => { mask.settings = mask._getDefaultSettings(type, {}); if(inbound.stream.network === 'kcp') { inbound.stream.kcp.mtu = type === 'xdns' ? 900 : 1350; } }"
:label-col="{ md: {span:8} }" :dropdown-class-name="themeSwitcher.currentTheme">
:wrapper-col="{ md: {span:14} }" <template v-if="inbound.protocol === Protocols.HYSTERIA">
> <a-select-option value="salamander">Salamander (Hysteria2)</a-select-option>
<a-divider :style="{ margin: '0' }"> </template>
UDP Mask [[ index + 1 ]] <template v-else>
<a-icon <a-select-option value="mkcp-aes128gcm">mKCP AES-128-GCM</a-select-option>
type="delete" <a-select-option value="header-dns">Header DNS</a-select-option>
@click="() => inbound.stream.delUdpMask(index)" <a-select-option value="header-dtls">Header DTLS 1.2</a-select-option>
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }" <a-select-option value="header-srtp">Header SRTP</a-select-option>
></a-icon> <a-select-option value="header-utp">Header uTP</a-select-option>
</a-divider> <a-select-option value="header-wechat">Header WeChat Video</a-select-option>
<a-form-item label='{{ i18n "pages.xray.outbound.type" }}'> <a-select-option value="header-wireguard">Header WireGuard</a-select-option>
<a-select <a-select-option value="mkcp-original">mKCP Original</a-select-option>
v-model="mask.type" <a-select-option value="xdns">xDNS</a-select-option>
@change="(type) => { mask.settings = mask._getDefaultSettings(type, {}); if(inbound.stream.network === 'kcp') { inbound.stream.kcp.mtu = type === 'xdns' ? 900 : 1350; } }" <a-select-option value="xicmp">xICMP</a-select-option>
:dropdown-class-name="themeSwitcher.currentTheme" <a-select-option value="header-custom">Header Custom</a-select-option>
> <a-select-option value="noise">Noise</a-select-option>
<template v-if="inbound.protocol === Protocols.HYSTERIA"> </template>
<a-select-option value="salamander" </a-select>
>Salamander (Hysteria2)</a-select-option
>
</template>
<template v-else>
<a-select-option value="mkcp-aes128gcm"
>mKCP AES-128-GCM</a-select-option
>
<a-select-option value="header-dns">Header DNS</a-select-option>
<a-select-option value="header-dtls"
>Header DTLS 1.2</a-select-option
>
<a-select-option value="header-srtp">Header SRTP</a-select-option>
<a-select-option value="header-utp">Header uTP</a-select-option>
<a-select-option value="header-wechat"
>Header WeChat Video</a-select-option
>
<a-select-option value="header-wireguard"
>Header WireGuard</a-select-option
>
<a-select-option value="mkcp-original"
>mKCP Original</a-select-option
>
<a-select-option value="xdns">xDNS</a-select-option>
<a-select-option value="xicmp">xICMP</a-select-option>
<a-select-option value="header-custom">Header Custom</a-select-option>
<a-select-option value="noise">Noise</a-select-option>
</template>
</a-select>
</a-form-item>
<a-form-item
label="Password"
v-if="['mkcp-aes128gcm', 'salamander'].includes(mask.type)"
>
<a-input
v-model.trim="mask.settings.password"
placeholder="Obfuscation password"
></a-input>
</a-form-item>
<a-form-item label="Domain" v-if="mask.type === 'header-dns'">
<a-input
v-model.trim="mask.settings.domain"
placeholder="e.g., www.example.com"
></a-input>
</a-form-item>
<template v-if="mask.type === 'xdns'">
<a-form-item label="Domains">
<a-select
mode="tags"
v-model="mask.settings.domains"
:style="{ width: '100%' }"
:token-separators="[',']"
placeholder="e.g., www.example.com"
></a-select>
</a-form-item> </a-form-item>
</template> <a-form-item label="Password" v-if="['mkcp-aes128gcm', 'salamander'].includes(mask.type)">
<template v-if="mask.type === 'noise'"> <a-input v-model.trim="mask.settings.password" placeholder="Obfuscation password"></a-input>
<a-form-item label="Reset">
<a-input-number v-model.number="mask.settings.reset" :min="0" />
</a-form-item> </a-form-item>
<a-form-item label="Noise"> <a-form-item label="Domain" v-if="mask.type === 'header-dns'">
<a-button <a-input v-model.trim="mask.settings.domain" placeholder="e.g., www.example.com"></a-input>
icon="plus"
type="primary"
size="small"
@click="mask.settings.noise.push({rand: '1-8192', randRange: '0-255', type: 'array', packet: [], delay: '10-20'})"
></a-button>
</a-form-item> </a-form-item>
<template v-for="(n, index) in mask.settings.noise" :key="index"> <template v-if="mask.type === 'xdns'">
<a-divider :style="{ margin: '0' }"> <a-form-item label="Domains">
Noise [[ index + 1 ]] <a-select mode="tags" v-model="mask.settings.domains" :style="{ width: '100%' }" :token-separators="[',']"
<a-icon placeholder="e.g., www.example.com"></a-select>
type="delete"
@click="() => mask.settings.noise.splice(index, 1)"
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"
></a-icon>
</a-divider>
<a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
<a-select
v-model="n.type"
:dropdown-class-name="themeSwitcher.currentTheme"
@change="t => { if(t === 'base64') n.packet = RandomUtil.randomBase64(); else if(t === 'array') n.packet = []; else n.packet = ''; }"
>
<a-select-option value="array">Array</a-select-option>
<a-select-option value="str">String</a-select-option>
<a-select-option value="hex">Hex</a-select-option>
<a-select-option value="base64">Base64</a-select-option>
</a-select>
</a-form-item>
<template v-if="n.type === 'array'">
<a-form-item label="Rand">
<a-input v-model.trim="n.rand" placeholder="0 or 1-8192" />
</a-form-item>
<a-form-item label="Rand Range">
<a-input v-model.trim="n.randRange" placeholder="0-255" />
</a-form-item>
</template>
<a-form-item v-else label="Packet">
<a-input-group compact v-if="n.type === 'base64'">
<a-input v-model.trim="n.packet" placeholder="binary data" :style="{ width: 'calc(100% - 32px)' }" />
<a-button icon="reload" @click="n.packet = RandomUtil.randomBase64()" />
</a-input-group>
<a-input v-else v-model.trim="n.packet" placeholder="binary data" />
</a-form-item>
<a-form-item label="Delay">
<a-input v-model.trim="n.delay" placeholder="10-20" />
</a-form-item> </a-form-item>
</template> </template>
</template> <template v-if="mask.type === 'noise'">
<template v-if="mask.type === 'header-custom'"> <a-form-item label="Reset">
<a-form-item label="Client"> <a-input-number v-model.number="mask.settings.reset" :min="0" />
<a-button
icon="plus"
type="primary"
size="small"
@click="mask.settings.client.push({rand: 0, randRange: '0-255', type: 'array', packet: []})"
></a-button>
</a-form-item>
<template v-for="(c, index) in mask.settings.client" :key="index">
<a-divider :style="{ margin: '0' }">
Client [[ index + 1 ]]
<a-icon
type="delete"
@click="mask.settings.client.splice(index, 1)"
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"
></a-icon>
</a-divider>
<a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
<a-select
v-model="c.type"
:dropdown-class-name="themeSwitcher.currentTheme"
@change="t => { if(t === 'base64') c.packet = RandomUtil.randomBase64(); else if(t === 'array') c.packet = []; else c.packet = ''; }"
>
<a-select-option value="array">Array</a-select-option>
<a-select-option value="str">String</a-select-option>
<a-select-option value="hex">Hex</a-select-option>
<a-select-option value="base64">Base64</a-select-option>
</a-select>
</a-form-item> </a-form-item>
<template v-if="c.type === 'array'"> <a-form-item label="Noise">
<a-form-item label="Rand"> <a-button icon="plus" type="primary" size="small"
<a-input-number v-model.number="c.rand" /> @click="mask.settings.noise.push({rand: '1-8192', randRange: '0-255', type: 'array', packet: [], delay: '10-20'})"></a-button>
</a-form-item>
<template v-for="(n, index) in mask.settings.noise" :key="index">
<a-divider :style="{ margin: '0' }">
Noise [[ index + 1 ]]
<a-icon type="delete" @click="() => mask.settings.noise.splice(index, 1)"
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
</a-divider>
<a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
<a-select v-model="n.type" :dropdown-class-name="themeSwitcher.currentTheme"
@change="t => { if(t === 'base64') n.packet = RandomUtil.randomBase64(); else if(t === 'array') n.packet = []; else n.packet = ''; }">
<a-select-option value="array">Array</a-select-option>
<a-select-option value="str">String</a-select-option>
<a-select-option value="hex">Hex</a-select-option>
<a-select-option value="base64">Base64</a-select-option>
</a-select>
</a-form-item> </a-form-item>
<a-form-item label="Rand Range"> <template v-if="n.type === 'array'">
<a-input v-model.trim="c.randRange" placeholder="0-255" /> <a-form-item label="Rand">
<a-input v-model.trim="n.rand" placeholder="0 or 1-8192" />
</a-form-item>
<a-form-item label="Rand Range">
<a-input v-model.trim="n.randRange" placeholder="0-255" />
</a-form-item>
</template>
<a-form-item v-else label="Packet">
<a-input-group compact v-if="n.type === 'base64'">
<a-input v-model.trim="n.packet" placeholder="binary data" :style="{ width: 'calc(100% - 32px)' }" />
<a-button icon="reload" @click="n.packet = RandomUtil.randomBase64()" />
</a-input-group>
<a-input v-else v-model.trim="n.packet" placeholder="binary data" />
</a-form-item>
<a-form-item label="Delay">
<a-input v-model.trim="n.delay" placeholder="10-20" />
</a-form-item> </a-form-item>
</template> </template>
<a-form-item v-else label="Packet">
<a-input-group compact v-if="c.type === 'base64'">
<a-input v-model.trim="c.packet" placeholder="binary data" :style="{ width: 'calc(100% - 32px)' }" />
<a-button icon="reload" @click="c.packet = RandomUtil.randomBase64()" />
</a-input-group>
<a-input v-else v-model.trim="c.packet" placeholder="binary data" />
</a-form-item>
</template> </template>
<a-divider :style="{ margin: '0' }"></a-divider> <template v-if="mask.type === 'header-custom'">
<a-form-item label="Server"> <a-form-item label="Client">
<a-button <a-button icon="plus" type="primary" size="small"
icon="plus" @click="mask.settings.client.push({rand: 0, randRange: '0-255', type: 'array', packet: []})"></a-button>
type="primary"
size="small"
@click="mask.settings.server.push({rand: 0, randRange: '0-255', type: 'array', packet: []})"
></a-button>
</a-form-item>
<template v-for="(s, index) in mask.settings.server" :key="index">
<a-divider :style="{ margin: '0' }">
Server [[ index + 1 ]]
<a-icon
type="delete"
@click="mask.settings.server.splice(index, 1)"
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"
></a-icon>
</a-divider>
<a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
<a-select
v-model="s.type"
:dropdown-class-name="themeSwitcher.currentTheme"
@change="t => { if(t === 'base64') s.packet = RandomUtil.randomBase64(); else if(t === 'array') s.packet = []; else s.packet = ''; }"
>
<a-select-option value="array">Array</a-select-option>
<a-select-option value="str">String</a-select-option>
<a-select-option value="hex">Hex</a-select-option>
<a-select-option value="base64">Base64</a-select-option>
</a-select>
</a-form-item> </a-form-item>
<template v-if="s.type === 'array'"> <template v-for="(c, index) in mask.settings.client" :key="index">
<a-form-item label="Rand"> <a-divider :style="{ margin: '0' }">
<a-input-number v-model.number="s.rand" /> Client [[ index + 1 ]]
<a-icon type="delete" @click="mask.settings.client.splice(index, 1)"
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
</a-divider>
<a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
<a-select v-model="c.type" :dropdown-class-name="themeSwitcher.currentTheme"
@change="t => { if(t === 'base64') c.packet = RandomUtil.randomBase64(); else if(t === 'array') c.packet = []; else c.packet = ''; }">
<a-select-option value="array">Array</a-select-option>
<a-select-option value="str">String</a-select-option>
<a-select-option value="hex">Hex</a-select-option>
<a-select-option value="base64">Base64</a-select-option>
</a-select>
</a-form-item> </a-form-item>
<a-form-item label="Rand Range"> <template v-if="c.type === 'array'">
<a-input v-model.trim="s.randRange" placeholder="0-255" /> <a-form-item label="Rand">
<a-input-number v-model.number="c.rand" />
</a-form-item>
<a-form-item label="Rand Range">
<a-input v-model.trim="c.randRange" placeholder="0-255" />
</a-form-item>
</template>
<a-form-item v-else label="Packet">
<a-input-group compact v-if="c.type === 'base64'">
<a-input v-model.trim="c.packet" placeholder="binary data" :style="{ width: 'calc(100% - 32px)' }" />
<a-button icon="reload" @click="c.packet = RandomUtil.randomBase64()" />
</a-input-group>
<a-input v-else v-model.trim="c.packet" placeholder="binary data" />
</a-form-item> </a-form-item>
</template> </template>
<a-form-item v-else label="Packet"> <a-divider :style="{ margin: '0' }"></a-divider>
<a-input-group compact v-if="s.type === 'base64'"> <a-form-item label="Server">
<a-input v-model.trim="s.packet" placeholder="binary data" :style="{ width: 'calc(100% - 32px)' }" /> <a-button icon="plus" type="primary" size="small"
<a-button icon="reload" @click="s.packet = RandomUtil.randomBase64()" /> @click="mask.settings.server.push({rand: 0, randRange: '0-255', type: 'array', packet: []})"></a-button>
</a-input-group> </a-form-item>
<a-input v-else v-model.trim="s.packet" placeholder="binary data" /> <template v-for="(s, index) in mask.settings.server" :key="index">
<a-divider :style="{ margin: '0' }">
Server [[ index + 1 ]]
<a-icon type="delete" @click="mask.settings.server.splice(index, 1)"
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
</a-divider>
<a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
<a-select v-model="s.type" :dropdown-class-name="themeSwitcher.currentTheme"
@change="t => { if(t === 'base64') s.packet = RandomUtil.randomBase64(); else if(t === 'array') s.packet = []; else s.packet = ''; }">
<a-select-option value="array">Array</a-select-option>
<a-select-option value="str">String</a-select-option>
<a-select-option value="hex">Hex</a-select-option>
<a-select-option value="base64">Base64</a-select-option>
</a-select>
</a-form-item>
<template v-if="s.type === 'array'">
<a-form-item label="Rand">
<a-input-number v-model.number="s.rand" />
</a-form-item>
<a-form-item label="Rand Range">
<a-input v-model.trim="s.randRange" placeholder="0-255" />
</a-form-item>
</template>
<a-form-item v-else label="Packet">
<a-input-group compact v-if="s.type === 'base64'">
<a-input v-model.trim="s.packet" placeholder="binary data" :style="{ width: 'calc(100% - 32px)' }" />
<a-button icon="reload" @click="s.packet = RandomUtil.randomBase64()" />
</a-input-group>
<a-input v-else v-model.trim="s.packet" placeholder="binary data" />
</a-form-item>
</template>
</template>
<template v-if="mask.type === 'xicmp'">
<a-form-item label="IP">
<a-input v-model.trim="mask.settings.ip" placeholder="0.0.0.0" />
</a-form-item>
<a-form-item label="ID">
<a-input-number v-model.number="mask.settings.id" :min="0" />
</a-form-item> </a-form-item>
</template> </template>
</template> </a-form>
<template v-if="mask.type === 'xicmp'"> </template>
<a-form-item label="IP">
<a-input v-model.trim="mask.settings.ip" placeholder="0.0.0.0" />
</a-form-item>
<a-form-item label="ID">
<a-input-number v-model.number="mask.settings.id" :min="0" />
</a-form-item>
</template>
</a-form>
</template>
</template> </template>
<!-- quicParams only for xhttp H3 and hysteria --> <!-- quicParams only for xhttp H3 and hysteria -->
@@ -450,10 +343,8 @@
</a-form-item> </a-form-item>
<template v-if="inbound.stream.finalmask.enableQuicParams"> <template v-if="inbound.stream.finalmask.enableQuicParams">
<a-form-item label="Congestion"> <a-form-item label="Congestion">
<a-select <a-select v-model="inbound.stream.finalmask.quicParams.congestion"
v-model="inbound.stream.finalmask.quicParams.congestion" :dropdown-class-name="themeSwitcher.currentTheme">
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select-option value="reno">Reno</a-select-option> <a-select-option value="reno">Reno</a-select-option>
<a-select-option value="bbr">BBR</a-select-option> <a-select-option value="bbr">BBR</a-select-option>
<a-select-option value="brutal">Brutal</a-select-option> <a-select-option value="brutal">Brutal</a-select-option>
@@ -492,21 +383,26 @@
<a-switch v-model="inbound.stream.finalmask.quicParams.disablePathMTUDiscovery"></a-switch> <a-switch v-model="inbound.stream.finalmask.quicParams.disablePathMTUDiscovery"></a-switch>
</a-form-item> </a-form-item>
<a-form-item label="Max Incoming Streams"> <a-form-item label="Max Incoming Streams">
<a-input-number v-model.number="inbound.stream.finalmask.quicParams.maxIncomingStreams" :min="0" placeholder="0 = default" /> <a-input-number v-model.number="inbound.stream.finalmask.quicParams.maxIncomingStreams" :min="0"
placeholder="0 = default" />
</a-form-item> </a-form-item>
<a-form-item label="Init Stream Window"> <a-form-item label="Init Stream Window">
<a-input-number v-model.number="inbound.stream.finalmask.quicParams.initStreamReceiveWindow" :min="0" placeholder="0 = default" /> <a-input-number v-model.number="inbound.stream.finalmask.quicParams.initStreamReceiveWindow" :min="0"
placeholder="0 = default" />
</a-form-item> </a-form-item>
<a-form-item label="Max Stream Window"> <a-form-item label="Max Stream Window">
<a-input-number v-model.number="inbound.stream.finalmask.quicParams.maxStreamReceiveWindow" :min="0" placeholder="0 = default" /> <a-input-number v-model.number="inbound.stream.finalmask.quicParams.maxStreamReceiveWindow" :min="0"
placeholder="0 = default" />
</a-form-item> </a-form-item>
<a-form-item label="Init Conn Window"> <a-form-item label="Init Conn Window">
<a-input-number v-model.number="inbound.stream.finalmask.quicParams.initConnectionReceiveWindow" :min="0" placeholder="0 = default" /> <a-input-number v-model.number="inbound.stream.finalmask.quicParams.initConnectionReceiveWindow" :min="0"
placeholder="0 = default" />
</a-form-item> </a-form-item>
<a-form-item label="Max Conn Window"> <a-form-item label="Max Conn Window">
<a-input-number v-model.number="inbound.stream.finalmask.quicParams.maxConnectionReceiveWindow" :min="0" placeholder="0 = default" /> <a-input-number v-model.number="inbound.stream.finalmask.quicParams.maxConnectionReceiveWindow" :min="0"
placeholder="0 = default" />
</a-form-item> </a-form-item>
</template> </template>
</template> </template>
</a-form> </a-form>
{{end}} {{end}}

View File

@@ -1,9 +1,5 @@
{{define "form/streamGRPC"}} {{define "form/streamGRPC"}}
<a-form <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
:colon="false"
:label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"
>
<a-form-item label="Service Name"> <a-form-item label="Service Name">
<a-input v-model.trim="inbound.stream.grpc.serviceName"></a-input> <a-input v-model.trim="inbound.stream.grpc.serviceName"></a-input>
</a-form-item> </a-form-item>
@@ -14,4 +10,4 @@
<a-switch v-model="inbound.stream.grpc.multiMode"></a-switch> <a-switch v-model="inbound.stream.grpc.multiMode"></a-switch>
</a-form-item> </a-form-item>
</a-form> </a-form>
{{end}} {{end}}

View File

@@ -1,13 +1,7 @@
{{define "form/streamHTTPUpgrade"}} {{define "form/streamHTTPUpgrade"}}
<a-form <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
:colon="false"
:label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"
>
<a-form-item label="Proxy Protocol"> <a-form-item label="Proxy Protocol">
<a-switch <a-switch v-model="inbound.stream.httpupgrade.acceptProxyProtocol"></a-switch>
v-model="inbound.stream.httpupgrade.acceptProxyProtocol"
></a-switch>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "host" }}'> <a-form-item label='{{ i18n "host" }}'>
<a-input v-model.trim="inbound.stream.httpupgrade.host"></a-input> <a-input v-model.trim="inbound.stream.httpupgrade.host"></a-input>
@@ -16,39 +10,20 @@
<a-input v-model.trim="inbound.stream.httpupgrade.path"></a-input> <a-input v-model.trim="inbound.stream.httpupgrade.path"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'> <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
<a-button <a-button icon="plus" size="small" @click="inbound.stream.httpupgrade.addHeader('', '')"></a-button>
icon="plus"
size="small"
@click="inbound.stream.httpupgrade.addHeader('', '')"
></a-button>
</a-form-item> </a-form-item>
<a-form-item :wrapper-col="{span:24}"> <a-form-item :wrapper-col="{span:24}">
<a-input-group <a-input-group compact v-for="(header, index) in inbound.stream.httpupgrade.headers">
compact <a-input :style="{ width: '50%' }" v-model.trim="header.name"
v-for="(header, index) in inbound.stream.httpupgrade.headers" placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'>
> <template slot="addonBefore" :style="{ margin: '0' }">[[ index+1 ]]</template>
<a-input
:style="{ width: '50%' }"
v-model.trim="header.name"
placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'
>
<template slot="addonBefore" :style="{ margin: '0' }"
>[[ index+1 ]]</template
>
</a-input> </a-input>
<a-input <a-input :style="{ width: '50%' }" v-model.trim="header.value"
:style="{ width: '50%' }" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
v-model.trim="header.value" <a-button icon="minus" slot="addonAfter" size="small"
placeholder='{{ i18n "pages.inbounds.stream.general.value" }}' @click="inbound.stream.httpupgrade.removeHeader(index)"></a-button>
>
<a-button
icon="minus"
slot="addonAfter"
size="small"
@click="inbound.stream.httpupgrade.removeHeader(index)"
></a-button>
</a-input> </a-input>
</a-input-group> </a-input-group>
</a-form-item> </a-form-item>
</a-form> </a-form>
{{end}} {{end}}

View File

@@ -1,14 +1,7 @@
{{define "form/streamHysteria"}} {{define "form/streamHysteria"}}
<a-form <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
:colon="false"
:label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"
>
<a-form-item label="UDP Idle Timeout"> <a-form-item label="UDP Idle Timeout">
<a-input-number <a-input-number v-model.number="inbound.stream.hysteria.udpIdleTimeout" :min="0"></a-input-number>
v-model.number="inbound.stream.hysteria.udpIdleTimeout"
:min="0"
></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label="Masquerade"> <a-form-item label="Masquerade">
<a-switch v-model="inbound.stream.hysteria.masqueradeSwitch"></a-switch> <a-switch v-model="inbound.stream.hysteria.masqueradeSwitch"></a-switch>
@@ -16,85 +9,50 @@
<template v-if="inbound.stream.hysteria.masqueradeSwitch"> <template v-if="inbound.stream.hysteria.masqueradeSwitch">
<a-divider :style="{ margin: '5px 0 0' }">Masquerade</a-divider> <a-divider :style="{ margin: '5px 0 0' }">Masquerade</a-divider>
<a-form-item label="Type"> <a-form-item label="Type">
<a-select <a-select v-model="inbound.stream.hysteria.masquerade.type" :dropdown-class-name="themeSwitcher.currentTheme">
v-model="inbound.stream.hysteria.masquerade.type"
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select-option value="file">File</a-select-option> <a-select-option value="file">File</a-select-option>
<a-select-option value="proxy">Proxy</a-select-option> <a-select-option value="proxy">Proxy</a-select-option>
<a-select-option value="string">String</a-select-option> <a-select-option value="string">String</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item <a-form-item label="Dir" v-if="inbound.stream.hysteria.masquerade.type === 'file'">
label="Dir"
v-if="inbound.stream.hysteria.masquerade.type === 'file'"
>
<a-input v-model.trim="inbound.stream.hysteria.masquerade.dir"></a-input> <a-input v-model.trim="inbound.stream.hysteria.masquerade.dir"></a-input>
</a-form-item> </a-form-item>
<template v-if="inbound.stream.hysteria.masquerade.type === 'proxy'"> <template v-if="inbound.stream.hysteria.masquerade.type === 'proxy'">
<a-form-item label="URL"> <a-form-item label="URL">
<a-input <a-input v-model.trim="inbound.stream.hysteria.masquerade.url"></a-input>
v-model.trim="inbound.stream.hysteria.masquerade.url"
></a-input>
</a-form-item> </a-form-item>
<a-form-item label="Rewrite Host"> <a-form-item label="Rewrite Host">
<a-switch <a-switch v-model="inbound.stream.hysteria.masquerade.rewriteHost"></a-switch>
v-model="inbound.stream.hysteria.masquerade.rewriteHost"
></a-switch>
</a-form-item> </a-form-item>
<a-form-item label="Insecure"> <a-form-item label="Insecure">
<a-switch <a-switch v-model="inbound.stream.hysteria.masquerade.insecure"></a-switch>
v-model="inbound.stream.hysteria.masquerade.insecure"
></a-switch>
</a-form-item> </a-form-item>
</template> </template>
<template v-if="inbound.stream.hysteria.masquerade.type === 'string'"> <template v-if="inbound.stream.hysteria.masquerade.type === 'string'">
<a-form-item label="Content"> <a-form-item label="Content">
<a-input <a-input v-model.trim="inbound.stream.hysteria.masquerade.content"></a-input>
v-model.trim="inbound.stream.hysteria.masquerade.content"
></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'> <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
<a-button <a-button size="small" @click="inbound.stream.hysteria.masquerade.addHeader('', '')">+</a-button>
size="small"
@click="inbound.stream.hysteria.masquerade.addHeader('', '')"
>+</a-button
>
</a-form-item> </a-form-item>
<a-form-item :wrapper-col="{span:24}"> <a-form-item :wrapper-col="{span:24}">
<a-input-group <a-input-group compact v-for="(header, index) in inbound.stream.hysteria.masquerade.headers">
compact <a-input style="width: 50%" v-model.trim="header.name"
v-for="(header, index) in inbound.stream.hysteria.masquerade.headers" placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'>
> <template slot="addonBefore" style="margin: 0">[[ index+1 ]]</template>
<a-input
style="width: 50%"
v-model.trim="header.name"
placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'
>
<template slot="addonBefore" style="margin: 0"
>[[ index+1 ]]</template
>
</a-input> </a-input>
<a-input <a-input style="width: 50%" v-model.trim="header.value"
style="width: 50%" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
v-model.trim="header.value" <a-button slot="addonAfter" size="small"
placeholder='{{ i18n "pages.inbounds.stream.general.value" }}' @click="inbound.stream.hysteria.masquerade.removeHeader(index)">-</a-button>
>
<a-button
slot="addonAfter"
size="small"
@click="inbound.stream.hysteria.masquerade.removeHeader(index)"
>-</a-button
>
</a-input> </a-input>
</a-input-group> </a-input-group>
</a-form-item> </a-form-item>
<a-form-item label="Status Code"> <a-form-item label="Status Code">
<a-input-number <a-input-number v-model.number="inbound.stream.hysteria.masquerade.statusCode"></a-input-number>
v-model.number="inbound.stream.hysteria.masquerade.statusCode"
></a-input-number>
</a-form-item> </a-form-item>
</template> </template>
</template> </template>
</a-form> </a-form>
{{end}} {{end}}

View File

@@ -1,46 +1,22 @@
{{define "form/streamKCP"}} {{define "form/streamKCP"}}
<a-form <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
:colon="false"
:label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"
>
<a-form-item label="MTU"> <a-form-item label="MTU">
<a-input-number <a-input-number v-model.number="inbound.stream.kcp.mtu" :min="576" :max="1460"></a-input-number>
v-model.number="inbound.stream.kcp.mtu"
:min="576"
:max="1460"
></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label="TTI (ms)"> <a-form-item label="TTI (ms)">
<a-input-number <a-input-number v-model.number="inbound.stream.kcp.tti" :min="10" :max="100"></a-input-number>
v-model.number="inbound.stream.kcp.tti"
:min="10"
:max="100"
></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label="Uplink (MB/s)"> <a-form-item label="Uplink (MB/s)">
<a-input-number <a-input-number v-model.number="inbound.stream.kcp.upCap" :min="0"></a-input-number>
v-model.number="inbound.stream.kcp.upCap"
:min="0"
></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label="Downlink (MB/s)"> <a-form-item label="Downlink (MB/s)">
<a-input-number <a-input-number v-model.number="inbound.stream.kcp.downCap" :min="0"></a-input-number>
v-model.number="inbound.stream.kcp.downCap"
:min="0"
></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label="CWND Multiplier"> <a-form-item label="CWND Multiplier">
<a-input-number <a-input-number v-model.number="inbound.stream.kcp.cwndMultiplier" :min="1"></a-input-number>
v-model.number="inbound.stream.kcp.cwndMultiplier"
:min="1"
></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label="Max Sending Window"> <a-form-item label="Max Sending Window">
<a-input-number <a-input-number v-model.number="inbound.stream.kcp.maxSendingWindow" :min="0"></a-input-number>
v-model.number="inbound.stream.kcp.maxSendingWindow"
:min="0"
></a-input-number>
</a-form-item> </a-form-item>
</a-form> </a-form>
{{end}} {{end}}

View File

@@ -1,18 +1,10 @@
{{define "form/streamSettings"}} {{define "form/streamSettings"}}
<!-- select stream network --> <!-- select stream network -->
<a-form <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"
:colon="false" v-if="inbound.protocol != Protocols.HYSTERIA">
:label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"
v-if="inbound.protocol != Protocols.HYSTERIA"
>
<a-form-item label='{{ i18n "transmission" }}'> <a-form-item label='{{ i18n "transmission" }}'>
<a-select <a-select v-model="inbound.stream.network" :style="{ width: '75%' }" @change="streamNetworkChange"
v-model="inbound.stream.network" :dropdown-class-name="themeSwitcher.currentTheme">
:style="{ width: '75%' }"
@change="streamNetworkChange"
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select-option value="tcp">TCP (RAW)</a-select-option> <a-select-option value="tcp">TCP (RAW)</a-select-option>
<a-select-option value="kcp">mKCP</a-select-option> <a-select-option value="kcp">mKCP</a-select-option>
<a-select-option value="ws">WebSocket</a-select-option> <a-select-option value="ws">WebSocket</a-select-option>
@@ -63,4 +55,4 @@
<!-- finalmask --> <!-- finalmask -->
<template> {{template "form/streamFinalMask"}} </template> <template> {{template "form/streamFinalMask"}} </template>
{{end}} {{end}}

View File

@@ -1,49 +1,27 @@
{{define "form/streamSockopt"}} {{define "form/streamSockopt"}}
<a-divider :style="{ margin: '5px 0 0' }"></a-divider> <a-divider :style="{ margin: '5px 0 0' }"></a-divider>
<a-form <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
:colon="false"
:label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"
>
<a-form-item label="Sockopt"> <a-form-item label="Sockopt">
<a-switch v-model="inbound.stream.sockoptSwitch"></a-switch> <a-switch v-model="inbound.stream.sockoptSwitch"></a-switch>
</a-form-item> </a-form-item>
<template v-if="inbound.stream.sockoptSwitch"> <template v-if="inbound.stream.sockoptSwitch">
<a-form-item label="Route Mark"> <a-form-item label="Route Mark">
<a-input-number <a-input-number v-model.number="inbound.stream.sockopt.mark" :min="0"></a-input-number>
v-model.number="inbound.stream.sockopt.mark"
:min="0"
></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label="TCP Keep Alive Interval"> <a-form-item label="TCP Keep Alive Interval">
<a-input-number <a-input-number v-model.number="inbound.stream.sockopt.tcpKeepAliveInterval" :min="0"></a-input-number>
v-model.number="inbound.stream.sockopt.tcpKeepAliveInterval"
:min="0"
></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label="TCP Keep Alive Idle"> <a-form-item label="TCP Keep Alive Idle">
<a-input-number <a-input-number v-model.number="inbound.stream.sockopt.tcpKeepAliveIdle" :min="0"></a-input-number>
v-model.number="inbound.stream.sockopt.tcpKeepAliveIdle"
:min="0"
></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label="TCP Max Seg"> <a-form-item label="TCP Max Seg">
<a-input-number <a-input-number v-model.number="inbound.stream.sockopt.tcpMaxSeg" :min="0"></a-input-number>
v-model.number="inbound.stream.sockopt.tcpMaxSeg"
:min="0"
></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label="TCP User Timeout"> <a-form-item label="TCP User Timeout">
<a-input-number <a-input-number v-model.number="inbound.stream.sockopt.tcpUserTimeout" :min="0"></a-input-number>
v-model.number="inbound.stream.sockopt.tcpUserTimeout"
:min="0"
></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label="TCP Window Clamp"> <a-form-item label="TCP Window Clamp">
<a-input-number <a-input-number v-model.number="inbound.stream.sockopt.tcpWindowClamp" :min="0"></a-input-number>
v-model.number="inbound.stream.sockopt.tcpWindowClamp"
:min="0"
></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label="Proxy Protocol"> <a-form-item label="Proxy Protocol">
<a-switch v-model="inbound.stream.sockopt.acceptProxyProtocol"></a-switch> <a-switch v-model="inbound.stream.sockopt.acceptProxyProtocol"></a-switch>
@@ -61,33 +39,20 @@
<a-switch v-model.trim="inbound.stream.sockopt.V6Only"></a-switch> <a-switch v-model.trim="inbound.stream.sockopt.V6Only"></a-switch>
</a-form-item> </a-form-item>
<a-form-item label="Domain Strategy"> <a-form-item label="Domain Strategy">
<a-select <a-select v-model="inbound.stream.sockopt.domainStrategy" :style="{ width: '50%' }"
v-model="inbound.stream.sockopt.domainStrategy" :dropdown-class-name="themeSwitcher.currentTheme">
:style="{ width: '50%' }" <a-select-option v-for="key in DOMAIN_STRATEGY_OPTION" :value="key">[[ key ]]</a-select-option>
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select-option v-for="key in DOMAIN_STRATEGY_OPTION" :value="key"
>[[ key ]]</a-select-option
>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="TCP Congestion"> <a-form-item label="TCP Congestion">
<a-select <a-select v-model="inbound.stream.sockopt.tcpcongestion" :style="{ width: '50%' }"
v-model="inbound.stream.sockopt.tcpcongestion" :dropdown-class-name="themeSwitcher.currentTheme">
:style="{ width: '50%' }" <a-select-option v-for="key in TCP_CONGESTION_OPTION" :value="key">[[ key ]]</a-select-option>
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select-option v-for="key in TCP_CONGESTION_OPTION" :value="key"
>[[ key ]]</a-select-option
>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="TProxy"> <a-form-item label="TProxy">
<a-select <a-select v-model="inbound.stream.sockopt.tproxy" :style="{ width: '50%' }"
v-model="inbound.stream.sockopt.tproxy" :dropdown-class-name="themeSwitcher.currentTheme">
:style="{ width: '50%' }"
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select-option value="off">Off</a-select-option> <a-select-option value="off">Off</a-select-option>
<a-select-option value="redirect">Redirect</a-select-option> <a-select-option value="redirect">Redirect</a-select-option>
<a-select-option value="tproxy">TProxy</a-select-option> <a-select-option value="tproxy">TProxy</a-select-option>
@@ -100,15 +65,9 @@
<a-input v-model="inbound.stream.sockopt.interfaceName"></a-input> <a-input v-model="inbound.stream.sockopt.interfaceName"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="Trusted X-Forwarded-For"> <a-form-item label="Trusted X-Forwarded-For">
<a-select <a-select mode="tags" v-model="inbound.stream.sockopt.trustedXForwardedFor" :style="{ width: '100%' }"
mode="tags" :dropdown-class-name="themeSwitcher.currentTheme">
v-model="inbound.stream.sockopt.trustedXForwardedFor" <a-select-option value="CF-Connecting-IP">CF-Connecting-IP</a-select-option>
:style="{ width: '100%' }"
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select-option value="CF-Connecting-IP"
>CF-Connecting-IP</a-select-option
>
<a-select-option value="X-Real-IP">X-Real-IP</a-select-option> <a-select-option value="X-Real-IP">X-Real-IP</a-select-option>
<a-select-option value="True-Client-IP">True-Client-IP</a-select-option> <a-select-option value="True-Client-IP">True-Client-IP</a-select-option>
<a-select-option value="X-Client-IP">X-Client-IP</a-select-option> <a-select-option value="X-Client-IP">X-Client-IP</a-select-option>
@@ -116,4 +75,4 @@
</a-form-item> </a-form-item>
</template> </template>
</a-form> </a-form>
{{end}} {{end}}

View File

@@ -1,31 +1,19 @@
{{define "form/streamTCP"}} {{define "form/streamTCP"}}
<!-- tcp type --> <!-- tcp type -->
<a-form <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
:colon="false"
:label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"
>
<a-form-item label="Proxy Protocol" v-if="inbound.canEnableTls()"> <a-form-item label="Proxy Protocol" v-if="inbound.canEnableTls()">
<a-switch v-model="inbound.stream.tcp.acceptProxyProtocol"></a-switch> <a-switch v-model="inbound.stream.tcp.acceptProxyProtocol"></a-switch>
</a-form-item> </a-form-item>
<a-form-item label='HTTP {{ i18n "camouflage" }}'> <a-form-item label='HTTP {{ i18n "camouflage" }}'>
<a-switch <a-switch :checked="inbound.stream.tcp.type === 'http'"
:checked="inbound.stream.tcp.type === 'http'" @change="checked => inbound.stream.tcp.type = checked ? 'http' : 'none'"></a-switch>
@change="checked => inbound.stream.tcp.type = checked ? 'http' : 'none'"
></a-switch>
</a-form-item> </a-form-item>
</a-form> </a-form>
<a-form <a-form v-if="inbound.stream.tcp.type === 'http'" :colon="false" :label-col="{ md: {span:8} }"
v-if="inbound.stream.tcp.type === 'http'" :wrapper-col="{ md: {span:14} }">
:colon="false"
:label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"
>
<!-- tcp request --> <!-- tcp request -->
<a-divider :style="{ margin: '0' }" <a-divider :style="{ margin: '0' }">{{ i18n "pages.inbounds.stream.general.request" }}</a-divider>
>{{ i18n "pages.inbounds.stream.general.request" }}</a-divider
>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.version" }}'> <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.version" }}'>
<a-input v-model.trim="inbound.stream.tcp.request.version"></a-input> <a-input v-model.trim="inbound.stream.tcp.request.version"></a-input>
</a-form-item> </a-form-item>
@@ -33,66 +21,35 @@
<a-input v-model.trim="inbound.stream.tcp.request.method"></a-input> <a-input v-model.trim="inbound.stream.tcp.request.method"></a-input>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<template slot="label" <template slot="label">{{ i18n "pages.inbounds.stream.tcp.path" }}
>{{ i18n "pages.inbounds.stream.tcp.path" }} <a-button icon="plus" size="small" @click="inbound.stream.tcp.request.addPath('/')"></a-button>
<a-button
icon="plus"
size="small"
@click="inbound.stream.tcp.request.addPath('/')"
></a-button>
</template> </template>
<template v-for="(path, index) in inbound.stream.tcp.request.path"> <template v-for="(path, index) in inbound.stream.tcp.request.path">
<a-input v-model.trim="inbound.stream.tcp.request.path[index]"> <a-input v-model.trim="inbound.stream.tcp.request.path[index]">
<a-button <a-button icon="minus" size="small" slot="addonAfter" @click="inbound.stream.tcp.request.removePath(index)"
icon="minus" v-if="inbound.stream.tcp.request.path.length>1"></a-button>
size="small"
slot="addonAfter"
@click="inbound.stream.tcp.request.removePath(index)"
v-if="inbound.stream.tcp.request.path.length>1"
></a-button>
</a-input> </a-input>
</template> </template>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'> <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
<a-button <a-button icon="plus" size="small" @click="inbound.stream.tcp.request.addHeader('Host', '')"></a-button>
icon="plus"
size="small"
@click="inbound.stream.tcp.request.addHeader('Host', '')"
></a-button>
</a-form-item> </a-form-item>
<a-form-item :wrapper-col="{span:24}"> <a-form-item :wrapper-col="{span:24}">
<a-input-group <a-input-group compact v-for="(header, index) in inbound.stream.tcp.request.headers">
compact <a-input :style="{ width: '50%' }" v-model.trim="header.name"
v-for="(header, index) in inbound.stream.tcp.request.headers" placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
> <template slot="addonBefore" :style="{ margin: '0' }">[[ index+1 ]]</template>
<a-input
:style="{ width: '50%' }"
v-model.trim="header.name"
placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'
>
<template slot="addonBefore" :style="{ margin: '0' }"
>[[ index+1 ]]</template
>
</a-input> </a-input>
<a-input <a-input :style="{ width: '50%' }" v-model.trim="header.value"
:style="{ width: '50%' }" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
v-model.trim="header.value" <a-button icon="minus" slot="addonAfter" size="small"
placeholder='{{ i18n "pages.inbounds.stream.general.value" }}' @click="inbound.stream.tcp.request.removeHeader(index)"></a-button>
>
<a-button
icon="minus"
slot="addonAfter"
size="small"
@click="inbound.stream.tcp.request.removeHeader(index)"
></a-button>
</a-input> </a-input>
</a-input-group> </a-input-group>
</a-form-item> </a-form-item>
<!-- tcp response --> <!-- tcp response -->
<a-divider :style="{ margin: '0' }" <a-divider :style="{ margin: '0' }">{{ i18n "pages.inbounds.stream.general.response" }}</a-divider>
>{{ i18n "pages.inbounds.stream.general.response" }}</a-divider
>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.version" }}'> <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.version" }}'>
<a-input v-model.trim="inbound.stream.tcp.response.version"></a-input> <a-input v-model.trim="inbound.stream.tcp.response.version"></a-input>
</a-form-item> </a-form-item>
@@ -103,40 +60,22 @@
<a-input v-model.trim="inbound.stream.tcp.response.reason"></a-input> <a-input v-model.trim="inbound.stream.tcp.response.reason"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseHeader" }}'> <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseHeader" }}'>
<a-button <a-button icon="plus" size="small"
icon="plus" @click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')"></a-button>
size="small"
@click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')"
></a-button>
</a-form-item> </a-form-item>
<a-form-item :wrapper-col="{span:24}"> <a-form-item :wrapper-col="{span:24}">
<a-input-group <a-input-group compact v-for="(header, index) in inbound.stream.tcp.response.headers">
compact <a-input :style="{ width: '50%' }" v-model.trim="header.name"
v-for="(header, index) in inbound.stream.tcp.response.headers" placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
> <template slot="addonBefore" :style="{ margin: '0' }">[[ index+1 ]]</template>
<a-input
:style="{ width: '50%' }"
v-model.trim="header.name"
placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'
>
<template slot="addonBefore" :style="{ margin: '0' }"
>[[ index+1 ]]</template
>
</a-input> </a-input>
<a-input <a-input :style="{ width: '50%' }" v-model.trim="header.value"
:style="{ width: '50%' }" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
v-model.trim="header.value"
placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'
>
<template slot="addonAfter"> <template slot="addonAfter">
<a-button <a-button icon="minus" size="small" @click="inbound.stream.tcp.response.removeHeader(index)"></a-button>
icon="minus"
size="small"
@click="inbound.stream.tcp.response.removeHeader(index)"
></a-button>
</template> </template>
</a-input> </a-input>
</a-input-group> </a-input-group>
</a-form-item> </a-form-item>
</a-form> </a-form>
{{end}} {{end}}

View File

@@ -1,9 +1,5 @@
{{define "form/streamWS"}} {{define "form/streamWS"}}
<a-form <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
:colon="false"
:label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"
>
<a-form-item label="Proxy Protocol"> <a-form-item label="Proxy Protocol">
<a-switch v-model="inbound.stream.ws.acceptProxyProtocol"></a-switch> <a-switch v-model="inbound.stream.ws.acceptProxyProtocol"></a-switch>
</a-form-item> </a-form-item>
@@ -14,42 +10,22 @@
<a-input v-model.trim="inbound.stream.ws.path"></a-input> <a-input v-model.trim="inbound.stream.ws.path"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="Heartbeat Period"> <a-form-item label="Heartbeat Period">
<a-input-number <a-input-number v-model.number="inbound.stream.ws.heartbeatPeriod" :min="0"></a-input-number>
v-model.number="inbound.stream.ws.heartbeatPeriod"
:min="0"
></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'> <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
<a-button <a-button icon="plus" size="small" @click="inbound.stream.ws.addHeader('', '')"></a-button>
icon="plus"
size="small"
@click="inbound.stream.ws.addHeader('', '')"
></a-button>
</a-form-item> </a-form-item>
<a-form-item :wrapper-col="{span:24}"> <a-form-item :wrapper-col="{span:24}">
<a-input-group compact v-for="(header, index) in inbound.stream.ws.headers"> <a-input-group compact v-for="(header, index) in inbound.stream.ws.headers">
<a-input <a-input :style="{ width: '50%' }" v-model.trim="header.name"
:style="{ width: '50%' }" placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'>
v-model.trim="header.name" <template slot="addonBefore" :style="{ margin: '0' }">[[ index+1 ]]</template>
placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'
>
<template slot="addonBefore" :style="{ margin: '0' }"
>[[ index+1 ]]</template
>
</a-input> </a-input>
<a-input <a-input :style="{ width: '50%' }" v-model.trim="header.value"
:style="{ width: '50%' }" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
v-model.trim="header.value" <a-button icon="minus" slot="addonAfter" size="small" @click="inbound.stream.ws.removeHeader(index)"></a-button>
placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'
>
<a-button
icon="minus"
slot="addonAfter"
size="small"
@click="inbound.stream.ws.removeHeader(index)"
></a-button>
</a-input> </a-input>
</a-input-group> </a-input-group>
</a-form-item> </a-form-item>
</a-form> </a-form>
{{end}} {{end}}

View File

@@ -1,9 +1,5 @@
{{define "form/streamXHTTP"}} {{define "form/streamXHTTP"}}
<a-form <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
:colon="false"
:label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"
>
<a-form-item label='{{ i18n "host" }}'> <a-form-item label='{{ i18n "host" }}'>
<a-input v-model.trim="inbound.stream.xhttp.host"></a-input> <a-input v-model.trim="inbound.stream.xhttp.host"></a-input>
</a-form-item> </a-form-item>
@@ -11,69 +7,34 @@
<a-input v-model.trim="inbound.stream.xhttp.path"></a-input> <a-input v-model.trim="inbound.stream.xhttp.path"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'> <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
<a-button <a-button icon="plus" size="small" @click="inbound.stream.xhttp.addHeader('', '')"></a-button>
icon="plus"
size="small"
@click="inbound.stream.xhttp.addHeader('', '')"
></a-button>
</a-form-item> </a-form-item>
<a-form-item :wrapper-col="{span:24}"> <a-form-item :wrapper-col="{span:24}">
<a-input-group <a-input-group compact v-for="(header, index) in inbound.stream.xhttp.headers">
compact <a-input :style="{ width: '50%' }" v-model.trim="header.name"
v-for="(header, index) in inbound.stream.xhttp.headers" placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'>
> <template slot="addonBefore" :style="{ margin: '0' }">[[ index+1 ]]</template>
<a-input
:style="{ width: '50%' }"
v-model.trim="header.name"
placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'
>
<template slot="addonBefore" :style="{ margin: '0' }"
>[[ index+1 ]]</template
>
</a-input> </a-input>
<a-input <a-input :style="{ width: '50%' }" v-model.trim="header.value"
:style="{ width: '50%' }" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
v-model.trim="header.value" <a-button icon="minus" slot="addonAfter" size="small"
placeholder='{{ i18n "pages.inbounds.stream.general.value" }}' @click="inbound.stream.xhttp.removeHeader(index)"></a-button>
>
<a-button
icon="minus"
slot="addonAfter"
size="small"
@click="inbound.stream.xhttp.removeHeader(index)"
></a-button>
</a-input> </a-input>
</a-input-group> </a-input-group>
</a-form-item> </a-form-item>
<a-form-item label="Mode"> <a-form-item label="Mode">
<a-select <a-select v-model="inbound.stream.xhttp.mode" :style="{ width: '50%' }"
v-model="inbound.stream.xhttp.mode" :dropdown-class-name="themeSwitcher.currentTheme">
:style="{ width: '50%' }" <a-select-option v-for="key in MODE_OPTION" :value="key">[[ key ]]</a-select-option>
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select-option v-for="key in MODE_OPTION" :value="key"
>[[ key ]]</a-select-option
>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item <a-form-item label="Max Buffered Upload" v-if="inbound.stream.xhttp.mode === 'packet-up'">
label="Max Buffered Upload" <a-input-number v-model.number="inbound.stream.xhttp.scMaxBufferedPosts"></a-input-number>
v-if="inbound.stream.xhttp.mode === 'packet-up'"
>
<a-input-number
v-model.number="inbound.stream.xhttp.scMaxBufferedPosts"
></a-input-number>
</a-form-item> </a-form-item>
<a-form-item <a-form-item label="Max Upload Size (Byte)" v-if="inbound.stream.xhttp.mode === 'packet-up'">
label="Max Upload Size (Byte)"
v-if="inbound.stream.xhttp.mode === 'packet-up'"
>
<a-input v-model.trim="inbound.stream.xhttp.scMaxEachPostBytes"></a-input> <a-input v-model.trim="inbound.stream.xhttp.scMaxEachPostBytes"></a-input>
</a-form-item> </a-form-item>
<a-form-item <a-form-item label="Stream-Up Server" v-if="inbound.stream.xhttp.mode === 'stream-up'">
label="Stream-Up Server"
v-if="inbound.stream.xhttp.mode === 'stream-up'"
>
<a-input v-model.trim="inbound.stream.xhttp.scStreamUpServerSecs"></a-input> <a-input v-model.trim="inbound.stream.xhttp.scStreamUpServerSecs"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="Padding Bytes"> <a-form-item label="Padding Bytes">
@@ -84,22 +45,13 @@
</a-form-item> </a-form-item>
<template v-if="inbound.stream.xhttp.xPaddingObfsMode"> <template v-if="inbound.stream.xhttp.xPaddingObfsMode">
<a-form-item label="Padding Key"> <a-form-item label="Padding Key">
<a-input <a-input v-model.trim="inbound.stream.xhttp.xPaddingKey" placeholder="x_padding"></a-input>
v-model.trim="inbound.stream.xhttp.xPaddingKey"
placeholder="x_padding"
></a-input>
</a-form-item> </a-form-item>
<a-form-item label="Padding Header"> <a-form-item label="Padding Header">
<a-input <a-input v-model.trim="inbound.stream.xhttp.xPaddingHeader" placeholder="X-Padding"></a-input>
v-model.trim="inbound.stream.xhttp.xPaddingHeader"
placeholder="X-Padding"
></a-input>
</a-form-item> </a-form-item>
<a-form-item label="Padding Placement"> <a-form-item label="Padding Placement">
<a-select <a-select v-model="inbound.stream.xhttp.xPaddingPlacement" :dropdown-class-name="themeSwitcher.currentTheme">
v-model="inbound.stream.xhttp.xPaddingPlacement"
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select-option value>Default (queryInHeader)</a-select-option> <a-select-option value>Default (queryInHeader)</a-select-option>
<a-select-option value="queryInHeader">queryInHeader</a-select-option> <a-select-option value="queryInHeader">queryInHeader</a-select-option>
<a-select-option value="header">header</a-select-option> <a-select-option value="header">header</a-select-option>
@@ -108,10 +60,7 @@
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="Padding Method"> <a-form-item label="Padding Method">
<a-select <a-select v-model="inbound.stream.xhttp.xPaddingMethod" :dropdown-class-name="themeSwitcher.currentTheme">
v-model="inbound.stream.xhttp.xPaddingMethod"
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select-option value>Default (repeat-x)</a-select-option> <a-select-option value>Default (repeat-x)</a-select-option>
<a-select-option value="repeat-x">repeat-x</a-select-option> <a-select-option value="repeat-x">repeat-x</a-select-option>
<a-select-option value="tokenish">tokenish</a-select-option> <a-select-option value="tokenish">tokenish</a-select-option>
@@ -119,10 +68,7 @@
</a-form-item> </a-form-item>
</template> </template>
<a-form-item label="Uplink HTTP Method"> <a-form-item label="Uplink HTTP Method">
<a-select <a-select v-model="inbound.stream.xhttp.uplinkHTTPMethod" :dropdown-class-name="themeSwitcher.currentTheme">
v-model="inbound.stream.xhttp.uplinkHTTPMethod"
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select-option value>Default (POST)</a-select-option> <a-select-option value>Default (POST)</a-select-option>
<a-select-option value="POST">POST</a-select-option> <a-select-option value="POST">POST</a-select-option>
<a-select-option value="PUT">PUT</a-select-option> <a-select-option value="PUT">PUT</a-select-option>
@@ -130,10 +76,7 @@
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="Session Placement"> <a-form-item label="Session Placement">
<a-select <a-select v-model="inbound.stream.xhttp.sessionPlacement" :dropdown-class-name="themeSwitcher.currentTheme">
v-model="inbound.stream.xhttp.sessionPlacement"
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select-option value>Default (path)</a-select-option> <a-select-option value>Default (path)</a-select-option>
<a-select-option value="path">path</a-select-option> <a-select-option value="path">path</a-select-option>
<a-select-option value="header">header</a-select-option> <a-select-option value="header">header</a-select-option>
@@ -141,20 +84,12 @@
<a-select-option value="query">query</a-select-option> <a-select-option value="query">query</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item <a-form-item label="Session Key"
label="Session Key" v-if="inbound.stream.xhttp.sessionPlacement && inbound.stream.xhttp.sessionPlacement !== 'path'">
v-if="inbound.stream.xhttp.sessionPlacement && inbound.stream.xhttp.sessionPlacement !== 'path'" <a-input v-model.trim="inbound.stream.xhttp.sessionKey" placeholder="x_session"></a-input>
>
<a-input
v-model.trim="inbound.stream.xhttp.sessionKey"
placeholder="x_session"
></a-input>
</a-form-item> </a-form-item>
<a-form-item label="Sequence Placement"> <a-form-item label="Sequence Placement">
<a-select <a-select v-model="inbound.stream.xhttp.seqPlacement" :dropdown-class-name="themeSwitcher.currentTheme">
v-model="inbound.stream.xhttp.seqPlacement"
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select-option value>Default (path)</a-select-option> <a-select-option value>Default (path)</a-select-option>
<a-select-option value="path">path</a-select-option> <a-select-option value="path">path</a-select-option>
<a-select-option value="header">header</a-select-option> <a-select-option value="header">header</a-select-option>
@@ -162,23 +97,12 @@
<a-select-option value="query">query</a-select-option> <a-select-option value="query">query</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item <a-form-item label="Sequence Key"
label="Sequence Key" v-if="inbound.stream.xhttp.seqPlacement && inbound.stream.xhttp.seqPlacement !== 'path'">
v-if="inbound.stream.xhttp.seqPlacement && inbound.stream.xhttp.seqPlacement !== 'path'" <a-input v-model.trim="inbound.stream.xhttp.seqKey" placeholder="x_seq"></a-input>
>
<a-input
v-model.trim="inbound.stream.xhttp.seqKey"
placeholder="x_seq"
></a-input>
</a-form-item> </a-form-item>
<a-form-item <a-form-item label="Uplink Data Placement" v-if="inbound.stream.xhttp.mode === 'packet-up'">
label="Uplink Data Placement" <a-select v-model="inbound.stream.xhttp.uplinkDataPlacement" :dropdown-class-name="themeSwitcher.currentTheme">
v-if="inbound.stream.xhttp.mode === 'packet-up'"
>
<a-select
v-model="inbound.stream.xhttp.uplinkDataPlacement"
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select-option value>Default (body)</a-select-option> <a-select-option value>Default (body)</a-select-option>
<a-select-option value="body">body</a-select-option> <a-select-option value="body">body</a-select-option>
<a-select-option value="header">header</a-select-option> <a-select-option value="header">header</a-select-option>
@@ -186,27 +110,17 @@
<a-select-option value="query">query</a-select-option> <a-select-option value="query">query</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item <a-form-item label="Uplink Data Key"
label="Uplink Data Key" v-if="inbound.stream.xhttp.mode === 'packet-up' && inbound.stream.xhttp.uplinkDataPlacement && inbound.stream.xhttp.uplinkDataPlacement !== 'body'">
v-if="inbound.stream.xhttp.mode === 'packet-up' && inbound.stream.xhttp.uplinkDataPlacement && inbound.stream.xhttp.uplinkDataPlacement !== 'body'" <a-input v-model.trim="inbound.stream.xhttp.uplinkDataKey" placeholder="x_data"></a-input>
>
<a-input
v-model.trim="inbound.stream.xhttp.uplinkDataKey"
placeholder="x_data"
></a-input>
</a-form-item> </a-form-item>
<a-form-item <a-form-item label="Uplink Chunk Size"
label="Uplink Chunk Size" v-if="inbound.stream.xhttp.mode === 'packet-up' && inbound.stream.xhttp.uplinkDataPlacement && inbound.stream.xhttp.uplinkDataPlacement !== 'body'">
v-if="inbound.stream.xhttp.mode === 'packet-up' && inbound.stream.xhttp.uplinkDataPlacement && inbound.stream.xhttp.uplinkDataPlacement !== 'body'" <a-input-number v-model.number="inbound.stream.xhttp.uplinkChunkSize" :min="0"
> placeholder="0 (unlimited)"></a-input-number>
<a-input-number
v-model.number="inbound.stream.xhttp.uplinkChunkSize"
:min="0"
placeholder="0 (unlimited)"
></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label="No SSE Header"> <a-form-item label="No SSE Header">
<a-switch v-model="inbound.stream.xhttp.noSSEHeader"></a-switch> <a-switch v-model="inbound.stream.xhttp.noSSEHeader"></a-switch>
</a-form-item> </a-form-item>
</a-form> </a-form>
{{end}} {{end}}

View File

@@ -1,14 +1,12 @@
{{define "form/tlsSettings"}} {{define "form/tlsSettings"}}
<!-- tls enable --> <!-- tls enable -->
<a-form v-if="inbound.canEnableTls()" :colon="false" <a-form v-if="inbound.canEnableTls()" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
:label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<template v-if="inbound.protocol !== Protocols.HYSTERIA"> <template v-if="inbound.protocol !== Protocols.HYSTERIA">
<a-divider :style="{ margin: '3px 0' }"></a-divider> <a-divider :style="{ margin: '3px 0' }"></a-divider>
<a-form-item label='{{ i18n "security" }}'> <a-form-item label='{{ i18n "security" }}'>
<a-radio-group v-model="inbound.stream.security" button-style="solid"> <a-radio-group v-model="inbound.stream.security" button-style="solid">
<a-radio-button value="none">{{ i18n "none" }}</a-radio-button> <a-radio-button value="none">{{ i18n "none" }}</a-radio-button>
<a-radio-button v-if="inbound.canEnableReality()" <a-radio-button v-if="inbound.canEnableReality()" value="reality">Reality</a-radio-button>
value="reality">Reality</a-radio-button>
<a-radio-button value="tls">TLS</a-radio-button> <a-radio-button value="tls">TLS</a-radio-button>
</a-radio-group> </a-radio-group>
</a-form-item> </a-form-item>
@@ -21,8 +19,7 @@
<a-input v-model.trim="inbound.stream.tls.sni"></a-input> <a-input v-model.trim="inbound.stream.tls.sni"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="Cipher Suites"> <a-form-item label="Cipher Suites">
<a-select v-model="inbound.stream.tls.cipherSuites" <a-select v-model="inbound.stream.tls.cipherSuites" :dropdown-class-name="themeSwitcher.currentTheme">
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value>Auto</a-select-option> <a-select-option value>Auto</a-select-option>
<a-select-option v-for="key,value in TLS_CIPHER_OPTION" :value="key">[[ <a-select-option v-for="key,value in TLS_CIPHER_OPTION" :value="key">[[
value ]]</a-select-option> value ]]</a-select-option>
@@ -30,14 +27,12 @@
</a-form-item> </a-form-item>
<a-form-item label="Min/Max Version"> <a-form-item label="Min/Max Version">
<a-input-group compact> <a-input-group compact>
<a-select v-model="inbound.stream.tls.minVersion" <a-select v-model="inbound.stream.tls.minVersion" :style="{ width: '50%' }"
:style="{ width: '50%' }"
:dropdown-class-name="themeSwitcher.currentTheme"> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key <a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key
]]</a-select-option> ]]</a-select-option>
</a-select> </a-select>
<a-select v-model="inbound.stream.tls.maxVersion" <a-select v-model="inbound.stream.tls.maxVersion" :style="{ width: '50%' }"
:style="{ width: '50%' }"
:dropdown-class-name="themeSwitcher.currentTheme"> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key <a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key
]]</a-select-option> ]]</a-select-option>
@@ -45,8 +40,7 @@
</a-input-group> </a-input-group>
</a-form-item> </a-form-item>
<a-form-item label="uTLS"> <a-form-item label="uTLS">
<a-select v-model="inbound.stream.tls.settings.fingerprint" <a-select v-model="inbound.stream.tls.settings.fingerprint" :style="{ width: '100%' }"
:style="{ width: '100%' }"
:dropdown-class-name="themeSwitcher.currentTheme"> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value>None</a-select-option> <a-select-option value>None</a-select-option>
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key <a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key
@@ -54,9 +48,7 @@
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="ALPN"> <a-form-item label="ALPN">
<a-select mode="multiple" <a-select mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme" v-model="inbound.stream.tls.alpn">
:dropdown-class-name="themeSwitcher.currentTheme"
v-model="inbound.stream.tls.alpn">
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn <a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn
]]</a-select-option> ]]</a-select-option>
</a-select> </a-select>
@@ -75,8 +67,7 @@
<a-form-item label='{{ i18n "certificate" }}'> <a-form-item label='{{ i18n "certificate" }}'>
<a-radio-group v-model="cert.useFile" button-style="solid" <a-radio-group v-model="cert.useFile" button-style="solid"
:style="{ display: 'inline-flex', whiteSpace: 'nowrap', maxWidth: '100%' }"> :style="{ display: 'inline-flex', whiteSpace: 'nowrap', maxWidth: '100%' }">
<a-radio-button :value="true" <a-radio-button :value="true" :style="{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }">{{
:style="{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }">{{
i18n "pages.inbounds.certificatePath" }}</a-radio-button> i18n "pages.inbounds.certificatePath" }}</a-radio-button>
<a-radio-button :value="false" <a-radio-button :value="false"
:style="{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }">{{ :style="{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }">{{
@@ -87,8 +78,7 @@
<a-space> <a-space>
<a-button icon="plus" v-if="index === 0" type="primary" size="small" <a-button icon="plus" v-if="index === 0" type="primary" size="small"
@click="inbound.stream.tls.addCert()"></a-button> @click="inbound.stream.tls.addCert()"></a-button>
<a-button icon="minus" v-if="inbound.stream.tls.certs.length>1" <a-button icon="minus" v-if="inbound.stream.tls.certs.length>1" type="primary" size="small"
type="primary" size="small"
@click="inbound.stream.tls.removeCert(index)"></a-button> @click="inbound.stream.tls.removeCert(index)"></a-button>
</a-space> </a-space>
</a-form-item> </a-form-item>
@@ -100,8 +90,7 @@
<a-input v-model.trim="cert.keyFile"></a-input> <a-input v-model.trim="cert.keyFile"></a-input>
</a-form-item> </a-form-item>
<a-form-item label=" "> <a-form-item label=" ">
<a-button type="primary" icon="import" <a-button type="primary" icon="import" @click="setDefaultCertData(index)">
@click="setDefaultCertData(index)">
{{ i18n "pages.inbounds.setDefaultCert" }}</a-button> {{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
</a-form-item> </a-form-item>
</template> </template>
@@ -117,8 +106,7 @@
<a-switch v-model="cert.oneTimeLoading"></a-switch> <a-switch v-model="cert.oneTimeLoading"></a-switch>
</a-form-item> </a-form-item>
<a-form-item label='Usage Option'> <a-form-item label='Usage Option'>
<a-select v-model="cert.usage" :style="{ width: '50%' }" <a-select v-model="cert.usage" :style="{ width: '50%' }" :dropdown-class-name="themeSwitcher.currentTheme">
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in USAGE_OPTION" :value="key">[[ key <a-select-option v-for="key in USAGE_OPTION" :value="key">[[ key
]]</a-select-option> ]]</a-select-option>
</a-select> </a-select>
@@ -133,13 +121,6 @@
<a-form-item label='ECH config'> <a-form-item label='ECH config'>
<a-input v-model="inbound.stream.tls.settings.echConfigList"></a-input> <a-input v-model="inbound.stream.tls.settings.echConfigList"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='ECH force query'>
<a-select v-model="inbound.stream.tls.echForceQuery"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in ['none', 'half', 'full']" :value="key">[[
key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label=" "> <a-form-item label=" ">
<a-space> <a-space>
<a-button type="primary" icon="import" @click="getNewEchCert">Get New <a-button type="primary" icon="import" @click="getNewEchCert">Get New

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,7 @@
padding: 2px 6px; padding: 2px 6px;
border-radius: 3px; border-radius: 3px;
} }
html[data-theme="ultra-dark"] body.dark .custom-geo-section code.custom-geo-ext-code { html[data-theme="ultra-dark"] body.dark .custom-geo-section code.custom-geo-ext-code {
color: var(--dark-color-text-primary, rgba(255, 255, 255, 0.88)); color: var(--dark-color-text-primary, rgba(255, 255, 255, 0.88));
background: var(--dark-color-surface-700, #111929); background: var(--dark-color-surface-700, #111929);
@@ -119,7 +120,8 @@
</a-row> </a-row>
</span> </span>
<template slot="content"> <template slot="content">
<span class="max-w-400" v-for="line in (status.xray.errorMsg || '').split('\n')">[[ line ]]</span> <span class="max-w-400" v-for="line in (status.xray.errorMsg || '').split('\n')">[[ line
]]</span>
</template> </template>
<a-badge :text="status.xray.stateMsg" :color="status.xray.color" <a-badge :text="status.xray.stateMsg" :color="status.xray.color"
:class="status.xray.color === 'red' ? 'xray-error-animation' : ''" /> :class="status.xray.color === 'red' ? 'xray-error-animation' : ''" />
@@ -170,10 +172,11 @@
<a-col :sm="24" :lg="12"> <a-col :sm="24" :lg="12">
<a-card title='3X-UI' hoverable> <a-card title='3X-UI' hoverable>
<template v-if="panelUpdateModal.info.updateAvailable" #extra> <template v-if="panelUpdateModal.info.updateAvailable" #extra>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme" :title='`{{ i18n "pages.index.updatePanel" }}: ${panelUpdateModal.info.latestVersion}`'> <a-tooltip :overlay-class-name="themeSwitcher.currentTheme"
:title='`{{ i18n "pages.index.updatePanel" }}: ${panelUpdateModal.info.latestVersion}`'>
<a-tag color="orange" style="cursor:pointer;margin:0" @click="openPanelUpdate"> <a-tag color="orange" style="cursor:pointer;margin:0" @click="openPanelUpdate">
<a-icon type="cloud-download"></a-icon>[[ panelUpdateModal.info.latestVersion ]] <a-icon type="cloud-download"></a-icon>[[ panelUpdateModal.info.latestVersion ]]
<span v-if="!isMobile">{{ i18n "pages.index.updatePanel" }}</span> <span v-if="!isMobile">{{ i18n "pages.index.updatePanel" }}</span>
</a-tag> </a-tag>
</a-tooltip> </a-tooltip>
</template> </template>
@@ -327,8 +330,7 @@
</a-layout> </a-layout>
<a-modal id="panel-update-modal" v-model="panelUpdateModal.visible" title='{{ i18n "pages.index.updatePanel" }}' <a-modal id="panel-update-modal" v-model="panelUpdateModal.visible" title='{{ i18n "pages.index.updatePanel" }}'
:closable="true" @ok="() => panelUpdateModal.visible = false" :class="themeSwitcher.currentTheme" footer=""> :closable="true" @ok="() => panelUpdateModal.visible = false" :class="themeSwitcher.currentTheme" footer="">
<a-alert type="warning" class="mb-12 w-100" message='{{ i18n "pages.index.panelUpdateDesc" }}' <a-alert type="warning" class="mb-12 w-100" message='{{ i18n "pages.index.panelUpdateDesc" }}' show-icon></a-alert>
show-icon></a-alert>
<a-list class="ant-version-list w-100" bordered> <a-list class="ant-version-list w-100" bordered>
<a-list-item class="ant-version-list-item"> <a-list-item class="ant-version-list-item">
<span>{{ i18n "pages.index.currentPanelVersion" }}</span> <span>{{ i18n "pages.index.currentPanelVersion" }}</span>
@@ -379,57 +381,62 @@
</a-collapse-panel> </a-collapse-panel>
<a-collapse-panel key="3" header='{{ i18n "pages.index.customGeoTitle" }}'> <a-collapse-panel key="3" header='{{ i18n "pages.index.customGeoTitle" }}'>
<div class="custom-geo-section"> <div class="custom-geo-section">
<a-alert type="info" show-icon class="mb-10" <a-alert type="info" show-icon class="mb-10"
message='{{ i18n "pages.index.customGeoRoutingHint" }}'></a-alert> message='{{ i18n "pages.index.customGeoRoutingHint" }}'></a-alert>
<div class="mb-10"> <div class="mb-10">
<a-button type="primary" icon="plus" @click="openCustomGeoModal(null)" :loading="customGeoLoading"> <a-button type="primary" icon="plus" @click="openCustomGeoModal(null)" :loading="customGeoLoading">
{{ i18n "pages.index.customGeoAdd" }} {{ i18n "pages.index.customGeoAdd" }}
</a-button> </a-button>
<a-button class="ml-8" icon="reload" @click="updateAllCustomGeo" :loading="customGeoUpdatingAll">{{ i18n <a-button class="ml-8" icon="reload" @click="updateAllCustomGeo" :loading="customGeoUpdatingAll">{{ i18n
"pages.index.geofilesUpdateAll" }}</a-button> "pages.index.geofilesUpdateAll" }}</a-button>
</div> </div>
<a-table :columns="customGeoColumns" :data-source="customGeoList" :pagination="false" :row-key="r => r.id" <a-table :columns="customGeoColumns" :data-source="customGeoList" :pagination="false" :row-key="r => r.id"
:loading="customGeoLoading" size="small" :scroll="{ x: 520 }"> :loading="customGeoLoading" size="small" :scroll="{ x: 520 }">
<template slot="extDat" slot-scope="text, record"> <template slot="extDat" slot-scope="text, record">
<code class="custom-geo-ext-code">[[ customGeoExtDisplay(record) ]]</code> <code class="custom-geo-ext-code">[[ customGeoExtDisplay(record) ]]</code>
</template> </template>
<template slot="lastUpdatedAt" slot-scope="text, record"> <template slot="lastUpdatedAt" slot-scope="text, record">
<span v-if="record.lastUpdatedAt">[[ customGeoFormatTime(record.lastUpdatedAt) ]]</span> <span v-if="record.lastUpdatedAt">[[ customGeoFormatTime(record.lastUpdatedAt) ]]</span>
<span v-else></span> <span v-else></span>
</template> </template>
<template slot="action" slot-scope="text, record"> <template slot="action" slot-scope="text, record">
<a-space size="small"> <a-space size="small">
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme"> <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
<template slot="title">{{ i18n "pages.index.customGeoEdit" }}</template> <template slot="title">{{ i18n "pages.index.customGeoEdit" }}</template>
<a-button type="link" size="small" icon="edit" @click="openCustomGeoModal(record)"></a-button> <a-button type="link" size="small" icon="edit" @click="openCustomGeoModal(record)"></a-button>
</a-tooltip> </a-tooltip>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme"> <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
<template slot="title">{{ i18n "pages.index.customGeoDownload" }}</template> <template slot="title">{{ i18n "pages.index.customGeoDownload" }}</template>
<a-button type="link" size="small" icon="reload" @click="downloadCustomGeo(record.id)" :loading="customGeoActionId === record.id"></a-button> <a-button type="link" size="small" icon="reload" @click="downloadCustomGeo(record.id)"
</a-tooltip> :loading="customGeoActionId === record.id"></a-button>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme"> </a-tooltip>
<template slot="title">{{ i18n "pages.index.customGeoDelete" }}</template> <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
<a-button type="link" size="small" icon="delete" @click="confirmDeleteCustomGeo(record)"></a-button> <template slot="title">{{ i18n "pages.index.customGeoDelete" }}</template>
</a-tooltip> <a-button type="link" size="small" icon="delete" @click="confirmDeleteCustomGeo(record)"></a-button>
</a-space> </a-tooltip>
</template> </a-space>
</a-table> </template>
</a-table>
</div> </div>
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
</a-modal> </a-modal>
<a-modal v-model="customGeoModal.visible" :title="customGeoModal.editId ? '{{ i18n "pages.index.customGeoModalEdit" }}' : '{{ i18n "pages.index.customGeoModalAdd" }}'" <a-modal v-model="customGeoModal.visible"
:confirm-loading="customGeoModal.saving" @ok="submitCustomGeo" :ok-text="'{{ i18n "pages.index.customGeoModalSave" }}'" :cancel-text="'{{ i18n "close" }}'" :title="customGeoModal.editId ? '{{ i18n "pages.index.customGeoModalEdit" }}' : '{{ i18n "pages.index.customGeoModalAdd" }}'"
:confirm-loading="customGeoModal.saving" @ok="submitCustomGeo"
:ok-text="'{{ i18n "pages.index.customGeoModalSave" }}'" :cancel-text="'{{ i18n "close" }}'"
:class="themeSwitcher.currentTheme"> :class="themeSwitcher.currentTheme">
<a-form layout="vertical"> <a-form layout="vertical">
<a-form-item label='{{ i18n "pages.index.customGeoType" }}'> <a-form-item label='{{ i18n "pages.index.customGeoType" }}'>
<a-select v-model="customGeoModal.form.type" :disabled="!!customGeoModal.editId" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="customGeoModal.form.type" :disabled="!!customGeoModal.editId"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="geosite">geosite</a-select-option> <a-select-option value="geosite">geosite</a-select-option>
<a-select-option value="geoip">geoip</a-select-option> <a-select-option value="geoip">geoip</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.index.customGeoAlias" }}'> <a-form-item label='{{ i18n "pages.index.customGeoAlias" }}'>
<a-input v-model.trim="customGeoModal.form.alias" :disabled="!!customGeoModal.editId" placeholder='{{ i18n "pages.index.customGeoAliasPlaceholder" }}'></a-input> <a-input v-model.trim="customGeoModal.form.alias" :disabled="!!customGeoModal.editId"
placeholder='{{ i18n "pages.index.customGeoAliasPlaceholder" }}'></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.index.customGeoUrl" }}'> <a-form-item label='{{ i18n "pages.index.customGeoUrl" }}'>
<a-input v-model.trim="customGeoModal.form.url" placeholder="https://"></a-input> <a-input v-model.trim="customGeoModal.form.url" placeholder="https://"></a-input>
@@ -469,7 +476,8 @@
<a-checkbox v-model="logModal.syslog" @change="openLogs()">SysLog</a-checkbox> <a-checkbox v-model="logModal.syslog" @change="openLogs()">SysLog</a-checkbox>
</a-form-item> </a-form-item>
<a-form-item style="float: right;"> <a-form-item style="float: right;">
<a-button type="primary" icon="download" @click="FileManager.downloadTextFile(logModal.logs?.join('\n'), 'x-ui.log')"></a-button> <a-button type="primary" icon="download"
@click="FileManager.downloadTextFile(logModal.logs?.join('\n'), 'x-ui.log')"></a-button>
</a-form-item> </a-form-item>
</a-form> </a-form>
<div class="ant-input log-container" v-html="logModal.formattedLogs"></div> <div class="ant-input log-container" v-html="logModal.formattedLogs"></div>
@@ -547,7 +555,8 @@
<sparkline :data="cpuHistoryLong" :labels="cpuHistoryLabels" :vb-width="840" :height="220" <sparkline :data="cpuHistoryLong" :labels="cpuHistoryLabels" :vb-width="840" :height="220"
:stroke="status.cpu.color" :stroke-width="2.2" :show-grid="true" :show-axes="true" :tick-count-x="5" :stroke="status.cpu.color" :stroke-width="2.2" :show-grid="true" :show-axes="true" :tick-count-x="5"
:max-points="cpuHistoryLong.length" :fill-opacity="0.18" :marker-radius="3.2" :show-tooltip="true" /> :max-points="cpuHistoryLong.length" :fill-opacity="0.18" :marker-radius="3.2" :show-tooltip="true" />
<div style="margin-top:4px;font-size:11px;opacity:0.65">Timeframe: [[ cpuHistoryModal.bucket ]] sec per point (total [[ cpuHistoryLong.length ]] points)</div> <div style="margin-top:4px;font-size:11px;opacity:0.65">Timeframe: [[ cpuHistoryModal.bucket ]] sec per point
(total [[ cpuHistoryLong.length ]] points)</div>
</div> </div>
</a-modal> </a-modal>
</a-layout> </a-layout>
@@ -560,28 +569,88 @@
// Tiny Sparkline component using an inline SVG polyline // Tiny Sparkline component using an inline SVG polyline
Vue.component('sparkline', { Vue.component('sparkline', {
props: { props: {
data: { type: Array, required: true }, data: {
type: Array,
required: true
},
// viewBox width for drawing space; SVG width will be 100% of container // viewBox width for drawing space; SVG width will be 100% of container
vbWidth: { type: Number, default: 320 }, vbWidth: {
height: { type: Number, default: 80 }, type: Number,
stroke: { type: String, default: '#008771' }, default: 320
strokeWidth: { type: Number, default: 2 }, },
maxPoints: { type: Number, default: 120 }, height: {
showGrid: { type: Boolean, default: true }, type: Number,
gridColor: { type: String, default: 'rgba(0,0,0,0.1)' }, default: 80
fillOpacity: { type: Number, default: 0.15 }, },
showMarker: { type: Boolean, default: true }, stroke: {
markerRadius: { type: Number, default: 2.8 }, type: String,
default: '#008771'
},
strokeWidth: {
type: Number,
default: 2
},
maxPoints: {
type: Number,
default: 120
},
showGrid: {
type: Boolean,
default: true
},
gridColor: {
type: String,
default: 'rgba(0,0,0,0.1)'
},
fillOpacity: {
type: Number,
default: 0.15
},
showMarker: {
type: Boolean,
default: true
},
markerRadius: {
type: Number,
default: 2.8
},
// New opts for axes/labels/tooltip // New opts for axes/labels/tooltip
labels: { type: Array, default: () => [] }, // same length as data for x labels (e.g., timestamps) labels: {
showAxes: { type: Boolean, default: false }, type: Array,
yTickStep: { type: Number, default: 25 }, // percent ticks default: () => []
tickCountX: { type: Number, default: 4 }, }, // same length as data for x labels (e.g., timestamps)
paddingLeft: { type: Number, default: 32 }, showAxes: {
paddingRight: { type: Number, default: 6 }, type: Boolean,
paddingTop: { type: Number, default: 6 }, default: false
paddingBottom: { type: Number, default: 20 }, },
showTooltip: { type: Boolean, default: false }, yTickStep: {
type: Number,
default: 25
}, // percent ticks
tickCountX: {
type: Number,
default: 4
},
paddingLeft: {
type: Number,
default: 32
},
paddingRight: {
type: Number,
default: 6
},
paddingTop: {
type: Number,
default: 6
},
paddingBottom: {
type: Number,
default: 20
},
showTooltip: {
type: Boolean,
default: false
},
}, },
data() { data() {
return { return {
@@ -644,7 +713,12 @@
// draw at 25%, 50%, 75% // draw at 25%, 50%, 75%
return [0, 0.25, 0.5, 0.75, 1] return [0, 0.25, 0.5, 0.75, 1]
.map(r => Math.round(this.paddingTop + h * r)) .map(r => Math.round(this.paddingTop + h * r))
.map(y => ({ x1: this.paddingLeft, y1: y, x2: this.paddingLeft + w, y2: y })) .map(y => ({
x1: this.paddingLeft,
y1: y,
x2: this.paddingLeft + w,
y2: y
}))
}, },
lastPoint() { lastPoint() {
if (this.pointsArr.length === 0) return null if (this.pointsArr.length === 0) return null
@@ -656,7 +730,10 @@
const ticks = [] const ticks = []
for (let p = 0; p <= 100; p += step) { for (let p = 0; p <= 100; p += step) {
const y = Math.round(this.paddingTop + (this.drawHeight - (p / 100) * this.drawHeight)) const y = Math.round(this.paddingTop + (this.drawHeight - (p / 100) * this.drawHeight))
ticks.push({ y, label: `${p}%` }) ticks.push({
y,
label: `${p}%`
})
} }
return ticks return ticks
}, },
@@ -677,7 +754,10 @@
positions.forEach(idx => { positions.forEach(idx => {
const label = labels[idx] != null ? String(labels[idx]) : String(idx) const label = labels[idx] != null ? String(labels[idx]) : String(idx)
const x = Math.round(this.paddingLeft + idx * dx) const x = Math.round(this.paddingLeft + idx * dx)
ticks.push({ x, label }) ticks.push({
x,
label
})
}) })
return ticks return ticks
}, },
@@ -778,17 +858,36 @@
this.disk = new CurTotal(0, 0); this.disk = new CurTotal(0, 0);
this.loads = [0, 0, 0]; this.loads = [0, 0, 0];
this.mem = new CurTotal(0, 0); this.mem = new CurTotal(0, 0);
this.netIO = { up: 0, down: 0 }; this.netIO = {
this.netTraffic = { sent: 0, recv: 0 }; up: 0,
this.publicIP = { ipv4: 0, ipv6: 0 }; down: 0
};
this.netTraffic = {
sent: 0,
recv: 0
};
this.publicIP = {
ipv4: 0,
ipv6: 0
};
this.swap = new CurTotal(0, 0); this.swap = new CurTotal(0, 0);
this.tcpCount = 0; this.tcpCount = 0;
this.udpCount = 0; this.udpCount = 0;
this.uptime = 0; this.uptime = 0;
this.appUptime = 0; this.appUptime = 0;
this.appStats = { threads: 0, mem: 0, uptime: 0 }; this.appStats = {
threads: 0,
mem: 0,
uptime: 0
};
this.xray = { state: 'stop', stateMsg: "", errorMsg: "", version: "", color: "" }; this.xray = {
state: 'stop',
stateMsg: "",
errorMsg: "",
version: "",
color: ""
};
if (data == null) { if (data == null) {
return; return;
@@ -918,20 +1017,20 @@
}; };
const xraylogModal = { const xraylogModal = {
visible: false, visible: false,
logs: [], logs: [],
rows: 20, rows: 20,
showDirect: true, showDirect: true,
showBlocked: true, showBlocked: true,
showProxy: true, showProxy: true,
loading: false, loading: false,
show(logs) { show(logs) {
this.visible = true; this.visible = true;
this.logs = logs; this.logs = logs;
this.formattedLogs = this.logs?.length > 0 ? this.formatLogs(this.logs) : "No Record..."; this.formattedLogs = this.logs?.length > 0 ? this.formatLogs(this.logs) : "No Record...";
}, },
formatLogs(logs) { formatLogs(logs) {
let formattedLogs = ` let formattedLogs = `
<style> <style>
table { table {
border-collapse: collapse; border-collapse: collapse;
@@ -954,21 +1053,20 @@
</tr> </tr>
`; `;
logs.reverse().forEach((log, index) => { logs.reverse().forEach((log, index) => {
let outboundColor = ''; let outboundColor = '';
if (log.Event === 1) { if (log.Event === 1) {
outboundColor = ' style="color: #e04141;"'; //red for blocked outboundColor = ' style="color: #e04141;"'; //red for blocked
} } else if (log.Event === 2) {
else if (log.Event === 2) { outboundColor = ' style="color: #3c89e8;"'; //blue for proxies
outboundColor = ' style="color: #3c89e8;"'; //blue for proxies }
}
let text = ``; let text = ``;
if (log.Email !== "") { if (log.Email !== "") {
text = `<td>${log.Email}</td>`; text = `<td>${log.Email}</td>`;
} }
formattedLogs += ` formattedLogs += `
<tr ${outboundColor}> <tr ${outboundColor}>
<td><b>${IntlUtil.formatDate(log.DateTime)}</b></td> <td><b>${IntlUtil.formatDate(log.DateTime)}</b></td>
<td>${log.FromAddress}</td> <td>${log.FromAddress}</td>
@@ -978,14 +1076,14 @@
${text} ${text}
</tr> </tr>
`; `;
}); });
return formattedLogs += "</table>"; return formattedLogs += "</table>";
}, },
hide() { hide() {
this.visible = false; this.visible = false;
}, },
}; };
const backupModal = { const backupModal = {
visible: false, visible: false,
show() { show() {
@@ -996,10 +1094,31 @@
}, },
}; };
const customGeoColumns = [ const customGeoColumns = [{
{ title: '{{ i18n "pages.index.customGeoExtColumn" }}', key: 'extDat', scopedSlots: { customRender: 'extDat' }, ellipsis: true }, title: '{{ i18n "pages.index.customGeoExtColumn" }}',
{ title: '{{ i18n "pages.index.customGeoLastUpdated" }}', key: 'lastUpdatedAt', scopedSlots: { customRender: 'lastUpdatedAt' }, width: 160 }, key: 'extDat',
{ title: '{{ i18n "pages.index.customGeoActions" }}', key: 'action', scopedSlots: { customRender: 'action' }, width: 120, fixed: 'right' }, scopedSlots: {
customRender: 'extDat'
},
ellipsis: true
},
{
title: '{{ i18n "pages.index.customGeoLastUpdated" }}',
key: 'lastUpdatedAt',
scopedSlots: {
customRender: 'lastUpdatedAt'
},
width: 160
},
{
title: '{{ i18n "pages.index.customGeoActions" }}',
key: 'action',
scopedSlots: {
customRender: 'action'
},
width: 120,
fixed: 'right'
},
]; ];
const app = new Vue({ const app = new Vue({
@@ -1016,7 +1135,10 @@
cpuHistory: [], // small live widget history cpuHistory: [], // small live widget history
cpuHistoryLong: [], // aggregated points from backend cpuHistoryLong: [], // aggregated points from backend
cpuHistoryLabels: [], cpuHistoryLabels: [],
cpuHistoryModal: { visible: false, bucket: 2 }, cpuHistoryModal: {
visible: false,
bucket: 2
},
versionModal, versionModal,
panelUpdateModal, panelUpdateModal,
logModal, logModal,
@@ -1092,16 +1214,16 @@
const labels = [] const labels = []
for (const p of msg.obj) { for (const p of msg.obj) {
const d = new Date(p.t * 1000) const d = new Date(p.t * 1000)
const hh = String(d.getHours()).padStart(2,'0') const hh = String(d.getHours()).padStart(2, '0')
const mm = String(d.getMinutes()).padStart(2,'0') const mm = String(d.getMinutes()).padStart(2, '0')
const ss = String(d.getSeconds()).padStart(2,'0') const ss = String(d.getSeconds()).padStart(2, '0')
labels.push(bucket>=60 ? `${hh}:${mm}` : `${hh}:${mm}:${ss}`) labels.push(bucket >= 60 ? `${hh}:${mm}` : `${hh}:${mm}:${ss}`)
vals.push(Math.max(0, Math.min(100, p.cpu))) vals.push(Math.max(0, Math.min(100, p.cpu)))
} }
this.cpuHistoryLabels = labels this.cpuHistoryLabels = labels
this.cpuHistoryLong = vals this.cpuHistoryLong = vals
} }
} catch(e) { } catch (e) {
console.error('Failed to fetch bucketed cpu history', e) console.error('Failed to fetch bucketed cpu history', e)
} }
}, },
@@ -1129,9 +1251,9 @@
return typeof moment !== 'undefined' ? moment(ts * 1000).format('YYYY-MM-DD HH:mm') : String(ts); return typeof moment !== 'undefined' ? moment(ts * 1000).format('YYYY-MM-DD HH:mm') : String(ts);
}, },
customGeoExtDisplay(record) { customGeoExtDisplay(record) {
const fn = record.type === 'geoip' const fn = record.type === 'geoip' ?
? `geoip_${record.alias}.dat` `geoip_${record.alias}.dat` :
: `geosite_${record.alias}.dat`; `geosite_${record.alias}.dat`;
return `ext:${fn}:tag`; return `ext:${fn}:tag`;
}, },
async loadCustomGeo() { async loadCustomGeo() {
@@ -1285,18 +1407,18 @@
const isSingleFile = !!fileName; const isSingleFile = !!fileName;
this.$confirm({ this.$confirm({
title: '{{ i18n "pages.index.geofileUpdateDialog" }}', title: '{{ i18n "pages.index.geofileUpdateDialog" }}',
content: isSingleFile content: isSingleFile ?
? '{{ i18n "pages.index.geofileUpdateDialogDesc" }}'.replace("#filename#", fileName) '{{ i18n "pages.index.geofileUpdateDialogDesc" }}'.replace("#filename#", fileName) :
: '{{ i18n "pages.index.geofilesUpdateDialogDesc" }}', '{{ i18n "pages.index.geofilesUpdateDialogDesc" }}',
okText: '{{ i18n "confirm"}}', okText: '{{ i18n "confirm"}}',
class: themeSwitcher.currentTheme, class: themeSwitcher.currentTheme,
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: async () => { onOk: async () => {
versionModal.hide(); versionModal.hide();
this.loading(true, '{{ i18n "pages.index.dontRefresh"}}'); this.loading(true, '{{ i18n "pages.index.dontRefresh"}}');
const url = isSingleFile const url = isSingleFile ?
? `/panel/api/server/updateGeofile/${fileName}` `/panel/api/server/updateGeofile/${fileName}` :
: `/panel/api/server/updateGeofile`; `/panel/api/server/updateGeofile`;
await HttpUtil.post(url); await HttpUtil.post(url);
this.loading(false); this.loading(false);
}, },
@@ -1320,7 +1442,10 @@
}, },
async openLogs() { async openLogs() {
logModal.loading = true; logModal.loading = true;
const msg = await HttpUtil.post('/panel/api/server/logs/' + logModal.rows, { level: logModal.level, syslog: logModal.syslog }); const msg = await HttpUtil.post('/panel/api/server/logs/' + logModal.rows, {
level: logModal.level,
syslog: logModal.syslog
});
if (!msg.success) { if (!msg.success) {
return; return;
} }
@@ -1330,7 +1455,12 @@
}, },
async openXrayLogs() { async openXrayLogs() {
xraylogModal.loading = true; xraylogModal.loading = true;
const msg = await HttpUtil.post('/panel/api/server/xraylogs/' + xraylogModal.rows, { filter: xraylogModal.filter, showDirect: xraylogModal.showDirect, showBlocked: xraylogModal.showBlocked, showProxy: xraylogModal.showProxy }); const msg = await HttpUtil.post('/panel/api/server/xraylogs/' + xraylogModal.rows, {
filter: xraylogModal.filter,
showDirect: xraylogModal.showDirect,
showBlocked: xraylogModal.showBlocked,
showProxy: xraylogModal.showProxy
});
if (!msg.success) { if (!msg.success) {
return; return;
} }
@@ -1347,10 +1477,15 @@
try { try {
const dt = l.DateTime ? new Date(l.DateTime) : null; const dt = l.DateTime ? new Date(l.DateTime) : null;
const dateStr = dt && !isNaN(dt.getTime()) ? dt.toISOString() : ''; const dateStr = dt && !isNaN(dt.getTime()) ? dt.toISOString() : '';
const eventMap = { 0: 'DIRECT', 1: 'BLOCKED', 2: 'PROXY' }; const eventMap = {
0: 'DIRECT',
1: 'BLOCKED',
2: 'PROXY'
};
const eventText = eventMap[l.Event] || String(l.Event ?? ''); const eventText = eventMap[l.Event] || String(l.Event ?? '');
const emailPart = l.Email ? ` Email=${l.Email}` : ''; const emailPart = l.Email ? ` Email=${l.Email}` : '';
return `${dateStr} FROM=${l.FromAddress || ''} TO=${l.ToAddress || ''} INBOUND=${l.Inbound || ''} OUTBOUND=${l.Outbound || ''}${emailPart} EVENT=${eventText}`.trim(); return `${dateStr} FROM=${l.FromAddress || ''} TO=${l.ToAddress || ''} INBOUND=${l.Inbound || ''} OUTBOUND=${l.Outbound || ''}${emailPart} EVENT=${eventText}`
.trim();
} catch (e) { } catch (e) {
return JSON.stringify(l); return JSON.stringify(l);
} }
@@ -1442,7 +1577,7 @@
// Setup WebSocket for real-time updates // Setup WebSocket for real-time updates
if (window.wsClient) { if (window.wsClient) {
window.wsClient.connect(); window.wsClient.connect();
// Listen for status updates // Listen for status updates
window.wsClient.on('status', (payload) => { window.wsClient.on('status', (payload) => {
this.setStatus(payload); this.setStatus(payload);
@@ -1491,4 +1626,4 @@
}, },
}); });
</script> </script>
{{ template "page/body_end" .}} {{ template "page/body_end" .}}

View File

@@ -108,8 +108,15 @@
el: '#app', el: '#app',
data: { data: {
themeSwitcher, themeSwitcher,
loadingStates: { fetched: false, spinning: false }, loadingStates: {
user: { username: "", password: "", twoFactorCode: "" }, fetched: false,
spinning: false
},
user: {
username: "",
password: "",
twoFactorCode: ""
},
twoFactorEnable: false, twoFactorEnable: false,
lang: "", lang: "",
animationStarted: false animationStarted: false
@@ -233,13 +240,18 @@
} }
} }
}); });
pm_wait_for_forms.observe(pm_host, { childList: true, subtree: true }); pm_wait_for_forms.observe(pm_host, {
childList: true,
subtree: true
});
} }
if (document.readyState === 'loading') { if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', pm_init, { once: true }); document.addEventListener('DOMContentLoaded', pm_init, {
once: true
});
} else { } else {
pm_init(); pm_init();
} }
</script> </script>
{{ template "page/body_end" .}} {{ template "page/body_end" .}}

View File

@@ -1,58 +1,41 @@
{{define "modals/clientsBulkModal"}} {{define "modals/clientsBulkModal"}}
<a-modal id="client-bulk-modal" v-model="clientsBulkModal.visible" <a-modal id="client-bulk-modal" v-model="clientsBulkModal.visible" :title="clientsBulkModal.title"
:title="clientsBulkModal.title" @ok="clientsBulkModal.ok" :confirm-loading="clientsBulkModal.confirmLoading" :closable="true" :mask-closable="false"
@ok="clientsBulkModal.ok" :confirm-loading="clientsBulkModal.confirmLoading" :ok-text="clientsBulkModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
:closable="true" :mask-closable="false" <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
:ok-text="clientsBulkModal.okText" cancel-text='{{ i18n "close" }}'
:class="themeSwitcher.currentTheme">
<a-form :colon="false" :label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "pages.client.method" }}'> <a-form-item label='{{ i18n "pages.client.method" }}'>
<a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid" <a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid"
:dropdown-class-name="themeSwitcher.currentTheme"> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="0">Random</a-select-option> <a-select-option :value="0">Random</a-select-option>
<a-select-option :value="1">Random+Prefix</a-select-option> <a-select-option :value="1">Random+Prefix</a-select-option>
<a-select-option :value="2">Random+Prefix+Num</a-select-option> <a-select-option :value="2">Random+Prefix+Num</a-select-option>
<a-select-option <a-select-option :value="3">Random+Prefix+Num+Postfix</a-select-option>
:value="3">Random+Prefix+Num+Postfix</a-select-option>
<a-select-option :value="4">Prefix+Num+Postfix</a-select-option> <a-select-option :value="4">Prefix+Num+Postfix</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.client.first" }}' <a-form-item label='{{ i18n "pages.client.first" }}' v-if="clientsBulkModal.emailMethod>1">
v-if="clientsBulkModal.emailMethod>1"> <a-input-number v-model.number="clientsBulkModal.firstNum" :min="1"></a-input-number>
<a-input-number v-model.number="clientsBulkModal.firstNum"
:min="1"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.client.last" }}' <a-form-item label='{{ i18n "pages.client.last" }}' v-if="clientsBulkModal.emailMethod>1">
v-if="clientsBulkModal.emailMethod>1"> <a-input-number v-model.number="clientsBulkModal.lastNum" :min="clientsBulkModal.firstNum"></a-input-number>
<a-input-number v-model.number="clientsBulkModal.lastNum"
:min="clientsBulkModal.firstNum"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.client.prefix" }}' <a-form-item label='{{ i18n "pages.client.prefix" }}' v-if="clientsBulkModal.emailMethod>0">
v-if="clientsBulkModal.emailMethod>0">
<a-input v-model.trim="clientsBulkModal.emailPrefix"></a-input> <a-input v-model.trim="clientsBulkModal.emailPrefix"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.client.postfix" }}' <a-form-item label='{{ i18n "pages.client.postfix" }}' v-if="clientsBulkModal.emailMethod>2">
v-if="clientsBulkModal.emailMethod>2">
<a-input v-model.trim="clientsBulkModal.emailPostfix"></a-input> <a-input v-model.trim="clientsBulkModal.emailPostfix"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.client.clientCount" }}' <a-form-item label='{{ i18n "pages.client.clientCount" }}' v-if="clientsBulkModal.emailMethod < 2">
v-if="clientsBulkModal.emailMethod < 2"> <a-input-number v-model.number="clientsBulkModal.quantity" :min="1" :max="500"></a-input-number>
<a-input-number v-model.number="clientsBulkModal.quantity" :min="1"
:max="500"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "security" }}' <a-form-item label='{{ i18n "security" }}' v-if="inbound.protocol === Protocols.VMESS">
v-if="inbound.protocol === Protocols.VMESS"> <a-select v-model="clientsBulkModal.security" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select v-model="clientsBulkModal.security"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in USERS_SECURITY" :value="key">[[ <a-select-option v-for="key in USERS_SECURITY" :value="key">[[
key ]]</a-select-option> key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label='Flow' <a-form-item label='Flow' v-if="clientsBulkModal.inbound.canEnableTlsFlow()">
v-if="clientsBulkModal.inbound.canEnableTlsFlow()"> <a-select v-model="clientsBulkModal.flow" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select v-model="clientsBulkModal.flow"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value selected>{{ i18n "none" <a-select-option value selected>{{ i18n "none"
}}</a-select-option> }}</a-select-option>
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ <a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[
@@ -67,9 +50,7 @@
}}</span> }}</span>
</template> </template>
Subscription Subscription
<a-icon <a-icon @click="clientsBulkModal.subId = RandomUtil.randomLowerAndNum(16)" type="sync"></a-icon>
@click="clientsBulkModal.subId = RandomUtil.randomLowerAndNum(16)"
type="sync"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input v-model.trim="clientsBulkModal.subId"></a-input> <a-input v-model.trim="clientsBulkModal.subId"></a-input>
@@ -84,8 +65,7 @@
<a-icon type="question-circle"></a-icon> <a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input-number :style="{ width: '50%' }" <a-input-number :style="{ width: '50%' }" v-model.number="clientsBulkModal.tgId" min="0"></a-input-number>
v-model.number="clientsBulkModal.tgId" min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item v-if="app.ipLimitEnable"> <a-form-item v-if="app.ipLimitEnable">
<template slot="label"> <template slot="label">
@@ -97,8 +77,7 @@
<a-icon type="question-circle"></a-icon> <a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input-number v-model.number="clientsBulkModal.limitIp" <a-input-number v-model.number="clientsBulkModal.limitIp" min="0"></a-input-number>
min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<template slot="label"> <template slot="label">
@@ -110,17 +89,13 @@
<a-icon type="question-circle"></a-icon> <a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input-number v-model.number="clientsBulkModal.totalGB" <a-input-number v-model.number="clientsBulkModal.totalGB" :min="0"></a-input-number>
:min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.client.delayedStart" }}'> <a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
<a-switch v-model="clientsBulkModal.delayedStart" <a-switch v-model="clientsBulkModal.delayedStart" @click="clientsBulkModal.expiryTime=0"></a-switch>
@click="clientsBulkModal.expiryTime=0"></a-switch>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.client.expireDays" }}' <a-form-item label='{{ i18n "pages.client.expireDays" }}' v-if="clientsBulkModal.delayedStart">
v-if="clientsBulkModal.delayedStart"> <a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
<a-input-number v-model.number="delayedExpireDays"
:min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item v-else> <a-form-item v-else>
<template slot="label"> <template slot="label">
@@ -133,15 +108,11 @@
<a-icon type="question-circle"></a-icon> <a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-date-picker v-if="datepicker == 'gregorian'" <a-date-picker v-if="datepicker == 'gregorian'" :show-time="{ format: 'HH:mm:ss' }"
:show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss" :dropdown-class-name="themeSwitcher.currentTheme"
format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.currentTheme"
v-model="clientsBulkModal.expiryTime"></a-date-picker> v-model="clientsBulkModal.expiryTime"></a-date-picker>
<a-persian-datepicker v-else <a-persian-datepicker v-else placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}'
placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}' value="clientsBulkModal.expiryTime" v-model="clientsBulkModal.expiryTime">
value="clientsBulkModal.expiryTime"
v-model="clientsBulkModal.expiryTime">
</a-persian-datepicker> </a-persian-datepicker>
</a-form-item> </a-form-item>
<a-form-item v-if="clientsBulkModal.expiryTime != 0"> <a-form-item v-if="clientsBulkModal.expiryTime != 0">
@@ -154,13 +125,11 @@
<a-icon type="question-circle"></a-icon> <a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input-number v-model.number="clientsBulkModal.reset" <a-input-number v-model.number="clientsBulkModal.reset" :min="0"></a-input-number>
:min="0"></a-input-number>
</a-form-item> </a-form-item>
</a-form> </a-form>
</a-modal> </a-modal>
<script> <script>
const clientsBulkModal = { const clientsBulkModal = {
visible: false, visible: false,
confirmLoading: false, confirmLoading: false,
@@ -219,7 +188,7 @@
title = '', title = '',
okText = '{{ i18n "sure" }}', okText = '{{ i18n "sure" }}',
dbInbound = null, dbInbound = null,
confirm = (inbound, dbInbound) => { } confirm = (inbound, dbInbound) => {}
}) { }) {
this.visible = true; this.visible = true;
this.title = title; this.title = title;
@@ -245,12 +214,19 @@
}, },
newClient(protocol) { newClient(protocol) {
switch (protocol) { switch (protocol) {
case Protocols.VMESS: return new Inbound.VmessSettings.VMESS(); case Protocols.VMESS:
case Protocols.VLESS: return new Inbound.VLESSSettings.VLESS(); return new Inbound.VmessSettings.VMESS();
case Protocols.TROJAN: return new Inbound.TrojanSettings.Trojan(); case Protocols.VLESS:
case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings.Shadowsocks(clientsBulkModal.inbound.settings.shadowsockses[0].method); return new Inbound.VLESSSettings.VLESS();
case Protocols.HYSTERIA: return new Inbound.HysteriaSettings.Hysteria(); case Protocols.TROJAN:
default: return null; return new Inbound.TrojanSettings.Trojan();
case Protocols.SHADOWSOCKS:
return new Inbound.ShadowsocksSettings.Shadowsocks(clientsBulkModal.inbound.settings
.shadowsockses[0].method);
case Protocols.HYSTERIA:
return new Inbound.HysteriaSettings.Hysteria();
default:
return null;
} }
}, },
close() { close() {
@@ -271,7 +247,8 @@
return this.clientsBulkModal.inbound; return this.clientsBulkModal.inbound;
}, },
get delayedExpireDays() { get delayedExpireDays() {
return this.clientsBulkModal.expiryTime < 0 ? this.clientsBulkModal.expiryTime / -86400000 : 0; return this.clientsBulkModal.expiryTime < 0 ? this.clientsBulkModal.expiryTime / -86400000 :
0;
}, },
get datepicker() { get datepicker() {
return app.datepicker; return app.datepicker;
@@ -281,6 +258,5 @@
}, },
}, },
}); });
</script> </script>
{{end}} {{end}}

View File

@@ -1,10 +1,7 @@
{{define "modals/clientsModal"}} {{define "modals/clientsModal"}}
<a-modal id="client-modal" v-model="clientModal.visible" <a-modal id="client-modal" v-model="clientModal.visible" :title="clientModal.title" @ok="clientModal.ok"
:title="clientModal.title" @ok="clientModal.ok" :confirm-loading="clientModal.confirmLoading" :closable="true" :mask-closable="false"
:confirm-loading="clientModal.confirmLoading" :closable="true" :class="themeSwitcher.currentTheme" :ok-text="clientModal.okText" cancel-text='{{ i18n "close" }}'>
:mask-closable="false"
:class="themeSwitcher.currentTheme"
:ok-text="clientModal.okText" cancel-text='{{ i18n "close" }}'>
<template v-if="isEdit"> <template v-if="isEdit">
<a-tag v-if="isExpiry || isTrafficExhausted" color="red" <a-tag v-if="isExpiry || isTrafficExhausted" color="red"
:style="{ marginBottom: '10px', display: 'block', textAlign: 'center' }">Account :style="{ marginBottom: '10px', display: 'block', textAlign: 'center' }">Account
@@ -13,7 +10,6 @@
{{template "form/client"}} {{template "form/client"}}
</a-modal> </a-modal>
<script> <script>
const clientModal = { const clientModal = {
visible: false, visible: false,
confirmLoading: false, confirmLoading: false,
@@ -30,12 +26,20 @@
delayedStart: false, delayedStart: false,
ok() { ok() {
if (clientModal.isEdit) { if (clientModal.isEdit) {
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id, clientModal.oldClientId); ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id, clientModal
.oldClientId);
} else { } else {
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id); ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id);
} }
}, },
show({ title = '', okText = '{{ i18n "sure" }}', index = null, dbInbound = null, confirm = () => { }, isEdit = false }) { show({
title = '',
okText = '{{ i18n "sure" }}',
index = null,
dbInbound = null,
confirm = () => {},
isEdit = false
}) {
this.visible = true; this.visible = true;
this.title = title; this.title = title;
this.okText = okText; this.okText = okText;
@@ -55,30 +59,41 @@
} }
this.clientStats = this.dbInbound.clientStats.find(row => row.email === this.clients[this.index].email); this.clientStats = this.dbInbound.clientStats.find(row => row.email === this.clients[this.index].email);
this.confirm = confirm; this.confirm = confirm;
}, },
getClientId(protocol, client) { getClientId(protocol, client) {
switch (protocol) { switch (protocol) {
case Protocols.TROJAN: return client.password; case Protocols.TROJAN:
case Protocols.SHADOWSOCKS: return client.email; return client.password;
case Protocols.HYSTERIA: return client.auth; case Protocols.SHADOWSOCKS:
default: return client.id; return client.email;
case Protocols.HYSTERIA:
return client.auth;
default:
return client.id;
} }
}, },
addClient(inbound, clients) { addClient(inbound, clients) {
switch (inbound.protocol) { switch (inbound.protocol) {
case Protocols.VMESS: return clients.push(new Inbound.VmessSettings.VMESS()); case Protocols.VMESS:
case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS()); return clients.push(new Inbound.VmessSettings.VMESS());
case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan()); case Protocols.VLESS:
case Protocols.SHADOWSOCKS: return clients.push(new Inbound.ShadowsocksSettings.Shadowsocks(clients[0].method, RandomUtil.randomShadowsocksPassword(inbound.settings.method))); return clients.push(new Inbound.VLESSSettings.VLESS());
case Protocols.HYSTERIA: return clients.push(new Inbound.HysteriaSettings.Hysteria()); case Protocols.TROJAN:
default: return null; return clients.push(new Inbound.TrojanSettings.Trojan());
case Protocols.SHADOWSOCKS:
return clients.push(new Inbound.ShadowsocksSettings.Shadowsocks(clients[0].method, RandomUtil
.randomShadowsocksPassword(inbound.settings.method)));
case Protocols.HYSTERIA:
return clients.push(new Inbound.HysteriaSettings.Hysteria());
default:
return null;
} }
}, },
close() { close() {
clientModal.visible = false; clientModal.visible = false;
clientModal.loading(false); clientModal.loading(false);
}, },
loading(loading=true) { loading(loading = true) {
clientModal.confirmLoading = loading; clientModal.confirmLoading = loading;
}, },
}; };
@@ -110,7 +125,8 @@
return true return true
}, },
get isExpiry() { get isExpiry() {
return this.clientModal.isEdit && this.client.expiryTime >0 ? (this.client.expiryTime < new Date().getTime()) : false; return this.clientModal.isEdit && this.client.expiryTime > 0 ? (this.client.expiryTime <
new Date().getTime()) : false;
}, },
get delayedStart() { get delayedStart() {
return this.clientModal.delayedStart; return this.clientModal.delayedStart;
@@ -150,8 +166,7 @@
return; return;
} }
document.getElementById("clientIPs").value = ""; document.getElementById("clientIPs").value = "";
} catch (error) { } catch (error) {}
}
}, },
resetClientTraffic(email, dbInboundId, iconElement) { resetClientTraffic(email, dbInboundId, iconElement) {
this.$confirm({ this.$confirm({
@@ -162,7 +177,8 @@
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: async () => { onOk: async () => {
iconElement.disabled = true; iconElement.disabled = true;
const msg = await HttpUtil.postWithModal('/panel/api/inbounds/' + dbInboundId + '/resetClientTraffic/' + email); const msg = await HttpUtil.postWithModal('/panel/api/inbounds/' +
dbInboundId + '/resetClientTraffic/' + email);
if (msg.success) { if (msg.success) {
this.clientModal.clientStats.up = 0; this.clientModal.clientStats.up = 0;
this.clientModal.clientStats.down = 0; this.clientModal.clientStats.down = 0;
@@ -173,6 +189,5 @@
}, },
}, },
}); });
</script> </script>
{{end}} {{end}}

View File

@@ -5,10 +5,12 @@
<a-list-item v-for="dns in dnsPresetsDatabase" :style="{ padding: '12px 16px' }"> <a-list-item v-for="dns in dnsPresetsDatabase" :style="{ padding: '12px 16px' }">
<div class="ant-dns-presets-line"> <div class="ant-dns-presets-line">
<a-space direction="horizontal" size="small" align="center"> <a-space direction="horizontal" size="small" align="center">
<a-tag :color="dns.family ? 'purple' : 'green'">[[ dns.family ? '{{ i18n "pages.xray.dns.dnsPresetFamily" }}' : 'DNS' ]]</a-tag> <a-tag :color="dns.family ? 'purple' : 'green'">[[ dns.family ? '{{ i18n "pages.xray.dns.dnsPresetFamily" }}'
: 'DNS' ]]</a-tag>
<span class="ant-dns-presets-list-name">[[ dns.name ]]</span> <span class="ant-dns-presets-list-name">[[ dns.name ]]</span>
</a-space> </a-space>
<a-button class="ant-dns-presets-install" type="primary" @click="dnsPresetsModal.install(dns.data)">{{ i18n "install" }}</a-button> <a-button class="ant-dns-presets-install" type="primary"
@click="dnsPresetsModal.install(dns.data)">{{ i18n "install" }}</a-button>
</div> </div>
</a-list-item> </a-list-item>
</a-list> </a-list>
@@ -36,8 +38,7 @@
</style> </style>
<script> <script>
const dnsPresetsDatabase = [ const dnsPresetsDatabase = [{
{
name: 'Google DNS', name: 'Google DNS',
family: false, family: false,
data: [ data: [
@@ -96,7 +97,11 @@
install(selectedPreset) { install(selectedPreset) {
return ObjectUtil.execute(dnsPresetsModal.selected, selectedPreset); return ObjectUtil.execute(dnsPresetsModal.selected, selectedPreset);
}, },
show({ title = '', selected = (selectedPreset) => { }, isEdit = false }) { show({
title = '',
selected = (selectedPreset) => {},
isEdit = false
}) {
this.title = title; this.title = title;
this.selected = selected; this.selected = selected;
this.visible = true; this.visible = true;

View File

@@ -521,7 +521,8 @@
@click="copy(infoModal.wireguardLinks[index])"></a-button> @click="copy(infoModal.wireguardLinks[index])"></a-button>
</a-tooltip> </a-tooltip>
</tr-info-title> </tr-info-title>
<code :style="{ display: 'block', whiteSpace: 'normal', wordBreak: 'break-all' }">[[ infoModal.wireguardLinks[index] ]]</code> <code :style="{ display: 'block', whiteSpace: 'normal', wordBreak: 'break-all' }">[[
infoModal.wireguardLinks[index] ]]</code>
</tr-info-row> </tr-info-row>
</td> </td>
</tr> </tr>
@@ -534,7 +535,10 @@
function refreshIPs(email) { function refreshIPs(email) {
return HttpUtil.post(`/panel/api/inbounds/clientIps/${email}`).then((msg) => { return HttpUtil.post(`/panel/api/inbounds/clientIps/${email}`).then((msg) => {
if (!msg.success) { if (!msg.success) {
return { text: 'No IP Record', array: [] }; return {
text: 'No IP Record',
array: []
};
} }
const formatIpRecord = (record) => { const formatIpRecord = (record) => {
@@ -574,7 +578,10 @@
try { try {
ips = JSON.parse(ips); ips = JSON.parse(ips);
} catch (e) { } catch (e) {
return { text: String(ips), array: [String(ips)] }; return {
text: String(ips),
array: [String(ips)]
};
} }
} }
@@ -586,20 +593,32 @@
// New format or object array // New format or object array
if (Array.isArray(ips) && ips.length > 0 && typeof ips[0] === 'object') { if (Array.isArray(ips) && ips.length > 0 && typeof ips[0] === 'object') {
const result = ips.map((item) => formatIpRecord(item)).filter(Boolean); const result = ips.map((item) => formatIpRecord(item)).filter(Boolean);
return { text: result.join(' | '), array: result }; return {
text: result.join(' | '),
array: result
};
} }
// Old format - simple array of IPs // Old format - simple array of IPs
if (Array.isArray(ips) && ips.length > 0) { if (Array.isArray(ips) && ips.length > 0) {
const result = ips.map((ip) => String(ip)); const result = ips.map((ip) => String(ip));
return { text: result.join(', '), array: result }; return {
text: result.join(', '),
array: result
};
} }
// Fallback for any other format // Fallback for any other format
return { text: String(ips), array: [String(ips)] }; return {
text: String(ips),
array: [String(ips)]
};
} catch (e) { } catch (e) {
return { text: 'Error loading IPs', array: [] }; return {
text: 'Error loading IPs',
array: []
};
} }
}); });
} }
@@ -626,7 +645,8 @@
this.dbInbound = new DBInbound(dbInbound); this.dbInbound = new DBInbound(dbInbound);
this.clientSettings = this.inbound.clients ? this.inbound.clients[index] : null; this.clientSettings = this.inbound.clients ? this.inbound.clients[index] : null;
this.isExpired = this.inbound.clients ? this.inbound.isExpiry(index) : this.dbInbound.isExpiry; this.isExpired = this.inbound.clients ? this.inbound.isExpiry(index) : this.dbInbound.isExpiry;
this.clientStats = this.inbound.clients ? (this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) || null) : null; this.clientStats = this.inbound.clients ? (this.dbInbound.clientStats.find(row => row.email === this
.clientSettings.email) || null) : null;
if ( if (
[ [
@@ -752,7 +772,8 @@
return ColorUtils.usageColor(stats.up + stats.down, app.trafficDiff, stats.total); return ColorUtils.usageColor(stats.up + stats.down, app.trafficDiff, stats.total);
}, },
getRemStats() { getRemStats() {
remained = this.infoModal.clientStats.total - this.infoModal.clientStats.up - this.infoModal.clientStats.down; remained = this.infoModal.clientStats.total - this.infoModal.clientStats.up - this.infoModal.clientStats
.down;
return remained > 0 ? SizeFormatter.sizeFormat(remained) : '-'; return remained > 0 ? SizeFormatter.sizeFormat(remained) : '-';
}, },
refreshIPs() { refreshIPs() {
@@ -775,7 +796,7 @@
this.infoModal.clientIps = 'No IP Record'; this.infoModal.clientIps = 'No IP Record';
this.infoModal.clientIpsArray = []; this.infoModal.clientIpsArray = [];
}) })
.catch(() => { }); .catch(() => {});
}, },
}, },
}); });

View File

@@ -23,7 +23,7 @@
okText = '{{ i18n "sure" }}', okText = '{{ i18n "sure" }}',
inbound = null, inbound = null,
dbInbound = null, dbInbound = null,
confirm = (inbound, dbInbound) => { }, confirm = (inbound, dbInbound) => {},
isEdit = false, isEdit = false,
}) { }) {
this.title = title; this.title = title;
@@ -127,17 +127,17 @@
get client() { get client() {
return inModal.inbound && return inModal.inbound &&
inModal.inbound.clients && inModal.inbound.clients &&
inModal.inbound.clients.length > 0 inModal.inbound.clients.length > 0 ?
? inModal.inbound.clients[0] inModal.inbound.clients[0] :
: null; null;
}, },
get datepicker() { get datepicker() {
return app.datepicker; return app.datepicker;
}, },
get delayedExpireDays() { get delayedExpireDays() {
return this.client && this.client.expiryTime < 0 return this.client && this.client.expiryTime < 0 ?
? this.client.expiryTime / -86400000 this.client.expiryTime / -86400000 :
: 0; 0;
}, },
set delayedExpireDays(days) { set delayedExpireDays(days) {
this.client.expiryTime = -86400000 * days; this.client.expiryTime = -86400000 * days;
@@ -147,14 +147,12 @@
}, },
set externalProxy(value) { set externalProxy(value) {
if (value) { if (value) {
inModal.inbound.stream.externalProxy = [ inModal.inbound.stream.externalProxy = [{
{ forceTls: "same",
forceTls: "same", dest: window.location.hostname,
dest: window.location.hostname, port: inModal.inbound.port,
port: inModal.inbound.port, remark: "",
remark: "", }, ];
},
];
} else { } else {
inModal.inbound.stream.externalProxy = []; inModal.inbound.stream.externalProxy = [];
} }
@@ -182,8 +180,8 @@
) { ) {
const hasVisionFlow = inModal.inbound.settings.vlesses.some( const hasVisionFlow = inModal.inbound.settings.vlesses.some(
(c) => (c) =>
c.flow === "xtls-rprx-vision" || c.flow === "xtls-rprx-vision" ||
c.flow === "xtls-rprx-vision-udp443", c.flow === "xtls-rprx-vision-udp443",
); );
if ( if (
hasVisionFlow && hasVisionFlow &&

View File

@@ -1,25 +1,31 @@
{{define "modals/nordModal"}} {{define "modals/nordModal"}}
<a-modal id="nord-modal" v-model="nordModal.visible" title="NordVPN NordLynx" <a-modal id="nord-modal" v-model="nordModal.visible" title="NordVPN NordLynx"
:confirm-loading="nordModal.confirmLoading" :closable="true" :mask-closable="true" :confirm-loading="nordModal.confirmLoading" :closable="true" :mask-closable="true" :footer="null"
:footer="null" :class="themeSwitcher.currentTheme"> :class="themeSwitcher.currentTheme">
<template v-if="nordModal.nordData == null"> <template v-if="nordModal.nordData == null">
<a-tabs default-active-key="token" :class="themeSwitcher.currentTheme"> <a-tabs default-active-key="token" :class="themeSwitcher.currentTheme">
<a-tab-pane key="token" tab='{{ i18n "pages.xray.outbound.accessToken" }}'> <a-tab-pane key="token" tab='{{ i18n "pages.xray.outbound.accessToken" }}'>
<a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:18} }" :style="{ marginTop: '20px' }"> <a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:18} }"
:style="{ marginTop: '20px' }">
<a-form-item label='{{ i18n "pages.xray.outbound.accessToken" }}'> <a-form-item label='{{ i18n "pages.xray.outbound.accessToken" }}'>
<a-input v-model="nordModal.token" placeholder='{{ i18n "pages.xray.outbound.accessToken" }}'></a-input> <a-input v-model="nordModal.token"
placeholder='{{ i18n "pages.xray.outbound.accessToken" }}'></a-input>
<div :style="{ marginTop: '10px' }"> <div :style="{ marginTop: '10px' }">
<a-button type="primary" icon="login" @click="login()" :loading="nordModal.confirmLoading">{{ i18n "login" }}</a-button> <a-button type="primary" icon="login" @click="login()"
:loading="nordModal.confirmLoading">{{ i18n "login" }}</a-button>
</div> </div>
</a-form-item> </a-form-item>
</a-form> </a-form>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="key" tab='{{ i18n "pages.xray.outbound.privateKey" }}'> <a-tab-pane key="key" tab='{{ i18n "pages.xray.outbound.privateKey" }}'>
<a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:18} }" :style="{ marginTop: '20px' }"> <a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:18} }"
:style="{ marginTop: '20px' }">
<a-form-item label='{{ i18n "pages.xray.outbound.privateKey" }}'> <a-form-item label='{{ i18n "pages.xray.outbound.privateKey" }}'>
<a-input v-model="nordModal.manualKey" placeholder='{{ i18n "pages.xray.outbound.privateKey" }}'></a-input> <a-input v-model="nordModal.manualKey"
placeholder='{{ i18n "pages.xray.outbound.privateKey" }}'></a-input>
<div :style="{ marginTop: '10px' }"> <div :style="{ marginTop: '10px' }">
<a-button type="primary" icon="save" @click="saveKey()" :loading="nordModal.confirmLoading">{{ i18n "save" }}</a-button> <a-button type="primary" icon="save" @click="saveKey()"
:loading="nordModal.confirmLoading">{{ i18n "save" }}</a-button>
</div> </div>
</a-form-item> </a-form-item>
</a-form> </a-form>
@@ -39,7 +45,8 @@
</table> </table>
<a-button @click="logout" :loading="nordModal.confirmLoading" type="danger">{{ i18n "logout" }}</a-button> <a-button @click="logout" :loading="nordModal.confirmLoading" type="danger">{{ i18n "logout" }}</a-button>
<a-divider :style="{ margin: '0' }">{{ i18n "pages.xray.outbound.settings" }}</a-divider> <a-divider :style="{ margin: '0' }">{{ i18n "pages.xray.outbound.settings" }}</a-divider>
<a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:18} }" :style="{ marginTop: '10px' }"> <a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:18} }"
:style="{ marginTop: '10px' }">
<a-form-item label='{{ i18n "pages.xray.outbound.country" }}'> <a-form-item label='{{ i18n "pages.xray.outbound.country" }}'>
<a-select v-model="nordModal.countryId" @change="fetchServers" show-search option-filter-prop="label"> <a-select v-model="nordModal.countryId" @change="fetchServers" show-search option-filter-prop="label">
<a-select-option v-for="c in nordModal.countries" :key="c.id" :value="c.id" :label="c.name"> <a-select-option v-for="c in nordModal.countries" :key="c.id" :value="c.id" :label="c.name">
@@ -69,11 +76,13 @@
<a-form :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<template v-if="nordOutboundIndex>=0"> <template v-if="nordOutboundIndex>=0">
<a-tag color="green" :style="{ lineHeight: '31px' }">{{ i18n "enabled" }}</a-tag> <a-tag color="green" :style="{ lineHeight: '31px' }">{{ i18n "enabled" }}</a-tag>
<a-button @click="resetOutbound" :loading="nordModal.confirmLoading" type="danger">{{ i18n "reset" }}</a-button> <a-button @click="resetOutbound" :loading="nordModal.confirmLoading"
type="danger">{{ i18n "reset" }}</a-button>
</template> </template>
<template v-else> <template v-else>
<a-tag color="orange" :style="{ lineHeight: '31px' }">{{ i18n "disabled" }}</a-tag> <a-tag color="orange" :style="{ lineHeight: '31px' }">{{ i18n "disabled" }}</a-tag>
<a-button @click="addOutbound" :disabled="!nordModal.serverId" :loading="nordModal.confirmLoading" type="primary">{{ i18n "pages.xray.outbound.addOutbound" }}</a-button> <a-button @click="addOutbound" :disabled="!nordModal.serverId" :loading="nordModal.confirmLoading"
type="primary">{{ i18n "pages.xray.outbound.addOutbound" }}</a-button>
</template> </template>
</a-form> </a-form>
</template> </template>
@@ -115,7 +124,9 @@
}, },
async login() { async login() {
this.loading(true); this.loading(true);
const msg = await HttpUtil.post('/panel/xray/nord/reg', { token: this.token }); const msg = await HttpUtil.post('/panel/xray/nord/reg', {
token: this.token
});
if (msg.success) { if (msg.success) {
this.nordData = JSON.parse(msg.obj); this.nordData = JSON.parse(msg.obj);
await this.fetchCountries(); await this.fetchCountries();
@@ -124,7 +135,9 @@
}, },
async saveKey() { async saveKey() {
this.loading(true); this.loading(true);
const msg = await HttpUtil.post('/panel/xray/nord/setKey', { key: this.manualKey }); const msg = await HttpUtil.post('/panel/xray/nord/setKey', {
key: this.manualKey
});
if (msg.success) { if (msg.success) {
this.nordData = JSON.parse(msg.obj); this.nordData = JSON.parse(msg.obj);
await this.fetchCountries(); await this.fetchCountries();
@@ -160,7 +173,9 @@
this.cities = []; this.cities = [];
this.serverId = null; this.serverId = null;
this.cityId = null; this.cityId = null;
const msg = await HttpUtil.post('/panel/xray/nord/servers', { countryId: this.countryId }); const msg = await HttpUtil.post('/panel/xray/nord/servers', {
countryId: this.countryId
});
if (msg.success) { if (msg.success) {
const data = JSON.parse(msg.obj); const data = JSON.parse(msg.obj);
const locations = data.locations || []; const locations = data.locations || [];
@@ -173,7 +188,7 @@
} }
}); });
this.cities = Array.from(citiesMap.values()).sort((a, b) => a.name.localeCompare(b.name)); this.cities = Array.from(citiesMap.values()).sort((a, b) => a.name.localeCompare(b.name));
this.servers = (data.servers || []).map(s => { this.servers = (data.servers || []).map(s => {
const firstLocId = (s.location_ids || [])[0]; const firstLocId = (s.location_ids || [])[0];
const city = locToCity[firstLocId]; const city = locToCity[firstLocId];
@@ -195,7 +210,7 @@
addOutbound() { addOutbound() {
const server = this.servers.find(s => s.id === this.serverId); const server = this.servers.find(s => s.id === this.serverId);
if (!server) return; if (!server) return;
const tech = server.technologies.find(t => t.id === 35); const tech = server.technologies.find(t => t.id === 35);
const publicKey = tech.metadata.find(m => m.name === 'public_key').value; const publicKey = tech.metadata.find(m => m.name === 'public_key').value;
@@ -221,7 +236,7 @@
resetOutbound(index) { resetOutbound(index) {
const server = this.servers.find(s => s.id === this.serverId); const server = this.servers.find(s => s.id === this.serverId);
if (!server || index === -1) return; if (!server || index === -1) return;
const tech = server.technologies.find(t => t.id === 35); const tech = server.technologies.find(t => t.id === 35);
const publicKey = tech.metadata.find(m => m.name === 'public_key').value; const publicKey = tech.metadata.find(m => m.name === 'public_key').value;
@@ -242,7 +257,7 @@
} }
}; };
app.templateSettings.outbounds[index] = outbound; app.templateSettings.outbounds[index] = outbound;
// Sync routing rules // Sync routing rules
app.templateSettings.routing.rules.forEach(r => { app.templateSettings.routing.rules.forEach(r => {
if (r.outboundTag === oldTag) { if (r.outboundTag === oldTag) {
@@ -262,7 +277,8 @@
}, },
delRouting() { delRouting() {
if (app.templateSettings && app.templateSettings.routing) { if (app.templateSettings && app.templateSettings.routing) {
app.templateSettings.routing.rules = app.templateSettings.routing.rules.filter(r => !r.outboundTag.startsWith("nord-")); app.templateSettings.routing.rules = app.templateSettings.routing.rules.filter(r => !r.outboundTag
.startsWith("nord-"));
} }
} }
}; };
@@ -276,10 +292,14 @@
methods: { methods: {
login: () => nordModal.login(), login: () => nordModal.login(),
saveKey: () => nordModal.saveKey(), saveKey: () => nordModal.saveKey(),
logout() { nordModal.logout(this.nordOutboundIndex) }, logout() {
nordModal.logout(this.nordOutboundIndex)
},
fetchServers: () => nordModal.fetchServers(), fetchServers: () => nordModal.fetchServers(),
addOutbound: () => nordModal.addOutbound(), addOutbound: () => nordModal.addOutbound(),
resetOutbound() { nordModal.resetOutbound(this.nordOutboundIndex) }, resetOutbound() {
nordModal.resetOutbound(this.nordOutboundIndex)
},
onCityChange() { onCityChange() {
if (this.filteredServers.length > 0) { if (this.filteredServers.length > 0) {
this.nordModal.serverId = this.filteredServers[0].id; this.nordModal.serverId = this.filteredServers[0].id;
@@ -290,8 +310,9 @@
}, },
computed: { computed: {
nordOutboundIndex: { nordOutboundIndex: {
get: function () { get: function() {
return app.templateSettings ? app.templateSettings.outbounds.findIndex((o) => o.tag.startsWith("nord-")) : -1; return app.templateSettings ? app.templateSettings.outbounds.findIndex((o) => o.tag
.startsWith("nord-")) : -1;
} }
}, },
filteredServers: function() { filteredServers: function() {
@@ -303,4 +324,4 @@
} }
}); });
</script> </script>
{{end}} {{end}}

View File

@@ -1,17 +1,13 @@
{{define "modals/promptModal"}} {{define "modals/promptModal"}}
<a-modal id="prompt-modal" v-model="promptModal.visible" :title="promptModal.title" <a-modal id="prompt-modal" v-model="promptModal.visible" :title="promptModal.title" :closable="true"
:closable="true" @ok="promptModal.ok" :mask-closable="false" @ok="promptModal.ok" :mask-closable="false" :confirm-loading="promptModal.confirmLoading"
:confirm-loading="promptModal.confirmLoading" :ok-text="promptModal.okText" cancel-text='{{ i18n "cancel" }}' :class="themeSwitcher.currentTheme">
:ok-text="promptModal.okText" cancel-text='{{ i18n "cancel" }}' :class="themeSwitcher.currentTheme"> <a-input id="prompt-modal-input" :type="promptModal.type" v-model="promptModal.value"
<a-input id="prompt-modal-input" :type="promptModal.type" :autosize="{minRows: 10, maxRows: 20}" @keydown.enter.native="promptModal.keyEnter"
v-model="promptModal.value" @keydown.ctrl.83="promptModal.ctrlS"></a-input>
:autosize="{minRows: 10, maxRows: 20}"
@keydown.enter.native="promptModal.keyEnter"
@keydown.ctrl.83="promptModal.ctrlS"></a-input>
</a-modal> </a-modal>
<script> <script>
const promptModal = { const promptModal = {
title: '', title: '',
type: '', type: '',
@@ -55,7 +51,7 @@
close() { close() {
this.visible = false; this.visible = false;
}, },
loading(loading=true) { loading(loading = true) {
this.confirmLoading = loading; this.confirmLoading = loading;
}, },
}; };
@@ -66,6 +62,5 @@
promptModal: promptModal, promptModal: promptModal,
}, },
}); });
</script> </script>
{{end}} {{end}}

View File

@@ -57,39 +57,44 @@
border-radius: 1rem; border-radius: 1rem;
overflow-x: hidden; overflow-x: hidden;
} }
/* QR code transition effects */ /* QR code transition effects */
.qr-cv { .qr-cv {
transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out;
} }
.qr-transition-enter-active, .qr-transition-leave-active { .qr-transition-enter-active,
.qr-transition-leave-active {
transition: opacity 0.3s, transform 0.3s; transition: opacity 0.3s, transform 0.3s;
} }
.qr-transition-enter, .qr-transition-leave-to { .qr-transition-enter,
.qr-transition-leave-to {
opacity: 0; opacity: 0;
transform: scale(0.9); transform: scale(0.9);
} }
.qr-transition-enter-to, .qr-transition-leave { .qr-transition-enter-to,
.qr-transition-leave {
opacity: 1; opacity: 1;
transform: scale(1); transform: scale(1);
} }
.qr-flash { .qr-flash {
animation: qr-flash-animation 0.6s; animation: qr-flash-animation 0.6s;
} }
@keyframes qr-flash-animation { @keyframes qr-flash-animation {
0% { 0% {
opacity: 1; opacity: 1;
transform: scale(1); transform: scale(1);
} }
50% { 50% {
opacity: 0.5; opacity: 0.5;
transform: scale(0.95); transform: scale(0.95);
} }
100% { 100% {
opacity: 1; opacity: 1;
transform: scale(1); transform: scale(1);
@@ -105,7 +110,7 @@
qrcodes: [], qrcodes: [],
visible: false, visible: false,
subId: '', subId: '',
show: function (title = '', dbInbound, client) { show: function(title = '', dbInbound, client) {
this.title = title; this.title = title;
this.dbInbound = dbInbound; this.dbInbound = dbInbound;
this.inbound = dbInbound.toInbound(); this.inbound = dbInbound.toInbound();
@@ -135,7 +140,7 @@
} }
this.visible = true; this.visible = true;
}, },
close: function () { close: function() {
this.visible = false; this.visible = false;
}, },
}; };
@@ -159,7 +164,7 @@
console.error("Failed to get status:", e); console.error("Failed to get status:", e);
} }
}, },
toggleIPv4(index) { toggleIPv4(index) {
const row = qrModal.qrcodes[index]; const row = qrModal.qrcodes[index];
row.useIPv4 = !row.useIPv4; row.useIPv4 = !row.useIPv4;
@@ -170,13 +175,13 @@
if (!this.serverStatus || !this.serverStatus.publicIP) { if (!this.serverStatus || !this.serverStatus.publicIP) {
return; return;
} }
if (row.useIPv4 && this.serverStatus.publicIP.ipv4) { if (row.useIPv4 && this.serverStatus.publicIP.ipv4) {
// Replace the hostname or IP in the link with the IPv4 address // Replace the hostname or IP in the link with the IPv4 address
const originalLink = row.originalLink; const originalLink = row.originalLink;
const url = new URL(originalLink); const url = new URL(originalLink);
const ipv4 = this.serverStatus.publicIP.ipv4; const ipv4 = this.serverStatus.publicIP.ipv4;
if (qrModal.inbound.protocol == Protocols.WIREGUARD) { if (qrModal.inbound.protocol == Protocols.WIREGUARD) {
// Special handling for WireGuard config // Special handling for WireGuard config
const endpointRegex = /Endpoint = ([^:]+):(\d+)/; const endpointRegex = /Endpoint = ([^:]+):(\d+)/;
@@ -196,19 +201,19 @@
// Restore original link // Restore original link
row.link = row.originalLink; row.link = row.originalLink;
} }
// Update QR code with transition effect // Update QR code with transition effect
const canvasElement = document.querySelector('#qrCode-' + index); const canvasElement = document.querySelector('#qrCode-' + index);
if (canvasElement) { if (canvasElement) {
// Add flash animation class // Add flash animation class
canvasElement.classList.add('qr-flash'); canvasElement.classList.add('qr-flash');
// Remove the class after animation completes // Remove the class after animation completes
setTimeout(() => { setTimeout(() => {
canvasElement.classList.remove('qr-flash'); canvasElement.classList.remove('qr-flash');
}, 600); }, 600);
} }
this.setQrCode("qrCode-" + index, row.link); this.setQrCode("qrCode-" + index, row.link);
}, },
copy(content) { copy(content) {

View File

@@ -21,13 +21,13 @@
fileName: '', fileName: '',
qrcode: null, qrcode: null,
visible: false, visible: false,
show: function (title = '', content = '', fileName = '') { show: function(title = '', content = '', fileName = '') {
this.title = title; this.title = title;
this.content = content; this.content = content;
this.fileName = fileName; this.fileName = fileName;
this.visible = true; this.visible = true;
}, },
copy: function (content = '') { copy: function(content = '') {
ClipboardManager ClipboardManager
.copyText(content) .copyText(content)
.then(() => { .then(() => {
@@ -35,7 +35,7 @@
this.close(); this.close();
}) })
}, },
close: function () { close: function() {
this.visible = false; this.visible = false;
}, },
}; };

View File

@@ -55,12 +55,12 @@
twoFactorModal.close() twoFactorModal.close()
}, },
show: function ({ show: function({
title = '', title = '',
description = '', description = '',
token = '', token = '',
type = 'set', type = 'set',
confirm = (success) => { } confirm = (success) => {}
}) { }) {
this.title = title; this.title = title;
this.description = description; this.description = description;
@@ -78,7 +78,7 @@
secret: twoFactorModal.token, secret: twoFactorModal.token,
}); });
}, },
close: function () { close: function() {
twoFactorModal.enteredCode = ""; twoFactorModal.enteredCode = "";
twoFactorModal.visible = false; twoFactorModal.visible = false;
}, },
@@ -91,34 +91,34 @@
twoFactorModal: twoFactorModal, twoFactorModal: twoFactorModal,
}, },
updated() { updated() {
if ( if (
this.twoFactorModal.visible && this.twoFactorModal.visible &&
this.twoFactorModal.type === 'set' && this.twoFactorModal.type === 'set' &&
document.getElementById('twofactor-qrcode') document.getElementById('twofactor-qrcode')
) { ) {
this.setQrCode('twofactor-qrcode', this.twoFactorModal.totpObject.toString()); this.setQrCode('twofactor-qrcode', this.twoFactorModal.totpObject.toString());
} }
}, },
methods: { methods: {
setQrCode(elementId, content) { setQrCode(elementId, content) {
new QRious({ new QRious({
element: document.getElementById(elementId), element: document.getElementById(elementId),
size: 200, size: 200,
value: content, value: content,
background: 'white', background: 'white',
backgroundAlpha: 0, backgroundAlpha: 0,
foreground: 'black', foreground: 'black',
padding: 2, padding: 2,
level: 'L' level: 'L'
}); });
}, },
copy(content) { copy(content) {
ClipboardManager ClipboardManager
.copyText(content) .copyText(content)
.then(() => { .then(() => {
app.$message.success('{{ i18n "copied" }}') app.$message.success('{{ i18n "copied" }}')
}) })
}, },
} }
}); });
</script> </script>

View File

@@ -1,7 +1,6 @@
{{define "modals/warpModal"}} {{define "modals/warpModal"}}
<a-modal id="warp-modal" v-model="warpModal.visible" title="Cloudflare WARP" <a-modal id="warp-modal" v-model="warpModal.visible" title="Cloudflare WARP" :confirm-loading="warpModal.confirmLoading"
:confirm-loading="warpModal.confirmLoading" :closable="true" :mask-closable="true" :closable="true" :mask-closable="true" :footer="null" :class="themeSwitcher.currentTheme">
:footer="null" :class="themeSwitcher.currentTheme">
<template v-if="ObjectUtil.isEmpty(warpModal.warpData)"> <template v-if="ObjectUtil.isEmpty(warpModal.warpData)">
<a-button icon="api" @click="register" :loading="warpModal.confirmLoading">{{ i18n "create" }}</a-button> <a-button icon="api" @click="register" :loading="warpModal.confirmLoading">{{ i18n "create" }}</a-button>
</template> </template>
@@ -81,11 +80,13 @@
<a-form :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<template v-if="warpOutboundIndex>=0"> <template v-if="warpOutboundIndex>=0">
<a-tag color="green" :style="{ lineHeight: '31px' }">{{ i18n "enabled" }}</a-tag> <a-tag color="green" :style="{ lineHeight: '31px' }">{{ i18n "enabled" }}</a-tag>
<a-button @click="resetOutbound" :loading="warpModal.confirmLoading" type="danger">{{ i18n "reset" }}</a-button> <a-button @click="resetOutbound" :loading="warpModal.confirmLoading"
type="danger">{{ i18n "reset" }}</a-button>
</template> </template>
<template v-else> <template v-else>
<a-tag color="orange" :style="{ lineHeight: '31px' }">{{ i18n "disabled" }}</a-tag> <a-tag color="orange" :style="{ lineHeight: '31px' }">{{ i18n "disabled" }}</a-tag>
<a-button @click="addOutbound" :loading="warpModal.confirmLoading" type="primary">{{ i18n "pages.xray.outbound.addOutbound" }}</a-button> <a-button @click="addOutbound" :loading="warpModal.confirmLoading"
type="primary">{{ i18n "pages.xray.outbound.addOutbound" }}</a-button>
</template> </template>
</a-form-item> </a-form-item>
</a-form> </a-form>
@@ -93,7 +94,6 @@
</template> </template>
</a-modal> </a-modal>
<script> <script>
const warpModal = { const warpModal = {
visible: false, visible: false,
confirmLoading: false, confirmLoading: false,
@@ -188,7 +188,9 @@
}, },
async updateLicense(l) { async updateLicense(l) {
warpModal.loading(true); warpModal.loading(true);
const msg = await HttpUtil.post('/panel/xray/warp/license', { license: l }); const msg = await HttpUtil.post('/panel/xray/warp/license', {
license: l
});
if (msg.success) { if (msg.success) {
warpModal.warpData = JSON.parse(msg.obj); warpModal.warpData = JSON.parse(msg.obj);
warpModal.warpConfig = null; warpModal.warpConfig = null;
@@ -235,12 +237,12 @@
}, },
computed: { computed: {
warpOutboundIndex: { warpOutboundIndex: {
get: function () { get: function() {
return app.templateSettings ? app.templateSettings.outbounds.findIndex((o) => o.tag == 'warp') : -1; return app.templateSettings ? app.templateSettings.outbounds.findIndex((o) => o.tag ==
'warp') : -1;
} }
} }
} }
}); });
</script> </script>
{{end}} {{end}}

View File

@@ -1,15 +1,7 @@
{{define "modals/balancerModal"}} {{define "modals/balancerModal"}}
<a-modal <a-modal id="balancer-modal" v-model="balancerModal.visible" :title="balancerModal.title" @ok="balancerModal.ok"
id="balancer-modal" :confirm-loading="balancerModal.confirmLoading" :ok-button-props="{ props: { disabled: !balancerModal.isValid } }"
v-model="balancerModal.visible" :closable="true" :mask-closable="false" :ok-text="balancerModal.okText" cancel-text='{{ i18n "close" }}'
:title="balancerModal.title"
@ok="balancerModal.ok"
:confirm-loading="balancerModal.confirmLoading"
:ok-button-props="{ props: { disabled: !balancerModal.isValid } }"
:closable="true"
:mask-closable="false"
:ok-text="balancerModal.okText"
cancel-text='{{ i18n "close" }}'
:class="themeSwitcher.currentTheme"> :class="themeSwitcher.currentTheme">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "pages.xray.balancer.tag" }}' has-feedback <a-form-item label='{{ i18n "pages.xray.balancer.tag" }}' has-feedback
@@ -35,7 +27,8 @@
<a-form-item label="Fallback"> <a-form-item label="Fallback">
<a-select v-model="balancerModal.balancer.fallbackTag" clearable <a-select v-model="balancerModal.balancer.fallbackTag" clearable
:dropdown-class-name="themeSwitcher.currentTheme"> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="tag in [ '', ...balancerModal.outboundTags]" :value="tag">[[ tag ]]</a-select-option> <a-select-option v-for="tag in [ '', ...balancerModal.outboundTags]" :value="tag">[[ tag
]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
</table> </table>
@@ -58,7 +51,7 @@
fallbackTag: '' fallbackTag: ''
}, },
outboundTags: [], outboundTags: [],
balancerTags:[], balancerTags: [],
ok() { ok() {
if (balancerModal.balancer.selector.length == 0) { if (balancerModal.balancer.selector.length == 0) {
balancerModal.emptySelector = true; balancerModal.emptySelector = true;
@@ -67,7 +60,14 @@
balancerModal.emptySelector = false; balancerModal.emptySelector = false;
ObjectUtil.execute(balancerModal.confirm, balancerModal.balancer); ObjectUtil.execute(balancerModal.confirm, balancerModal.balancer);
}, },
show({ title = '', okText = '{{ i18n "sure" }}', balancerTags = [], balancer, confirm = (balancer) => { }, isEdit = false }) { show({
title = '',
okText = '{{ i18n "sure" }}',
balancerTags = [],
balancer,
confirm = (balancer) => {},
isEdit = false
}) {
this.title = title; this.title = title;
this.okText = okText; this.okText = okText;
this.confirm = confirm; this.confirm = confirm;
@@ -83,7 +83,8 @@
}; };
} }
this.balancerTags = balancerTags.filter((tag) => tag != balancer.tag); this.balancerTags = balancerTags.filter((tag) => tag != balancer.tag);
this.outboundTags = app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag); this.outboundTags = app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj =>
obj.tag);
this.isEdit = isEdit; this.isEdit = isEdit;
this.check(); this.check();
this.checkSelector(); this.checkSelector();
@@ -92,7 +93,7 @@
this.visible = false; this.visible = false;
this.loading(false); this.loading(false);
}, },
loading(loading=true) { loading(loading = true) {
this.confirmLoading = loading; this.confirmLoading = loading;
}, },
check() { check() {
@@ -115,9 +116,7 @@
data: { data: {
balancerModal: balancerModal balancerModal: balancerModal
}, },
methods: { methods: {}
}
}); });
</script> </script>
{{end}} {{end}}

View File

@@ -75,15 +75,19 @@
okText: '{{ i18n "confirm" }}', okText: '{{ i18n "confirm" }}',
isEdit: false, isEdit: false,
confirm: null, confirm: null,
dnsServer: { ...defaultDnsObject }, dnsServer: {
...defaultDnsObject
},
ok() { ok() {
ObjectUtil.execute(dnsModal.confirm, { ...dnsModal.dnsServer }); ObjectUtil.execute(dnsModal.confirm, {
...dnsModal.dnsServer
});
}, },
show({ show({
title = '', title = '',
okText = '{{ i18n "confirm" }}', okText = '{{ i18n "confirm" }}',
dnsServer, dnsServer,
confirm = (dnsServer) => { }, confirm = (dnsServer) => {},
isEdit = false isEdit = false
}) { }) {
this.title = title; this.title = title;
@@ -95,7 +99,9 @@
if (isEdit) { if (isEdit) {
switch (typeof dnsServer) { switch (typeof dnsServer) {
case 'string': case 'string':
const dnsObj = { ...defaultDnsObject }; const dnsObj = {
...defaultDnsObject
};
dnsObj.address = dnsServer; dnsObj.address = dnsServer;
@@ -106,7 +112,9 @@
break; break;
} }
} else { } else {
this.dnsServer = { ...defaultDnsObject }; this.dnsServer = {
...defaultDnsObject
};
this.dnsServer.domains = []; this.dnsServer.domains = [];
this.dnsServer.expectIPs = []; this.dnsServer.expectIPs = [];

View File

@@ -23,11 +23,19 @@
okText: '{{ i18n "confirm" }}', okText: '{{ i18n "confirm" }}',
isEdit: false, isEdit: false,
confirm: null, confirm: null,
fakeDns: { ...fakednsDefaultData }, fakeDns: {
...fakednsDefaultData
},
ok() { ok() {
ObjectUtil.execute(fakednsModal.confirm, fakednsModal.fakeDns); ObjectUtil.execute(fakednsModal.confirm, fakednsModal.fakeDns);
}, },
show({ title = '', okText = '{{ i18n "confirm" }}', fakeDns, confirm = (fakeDns) => { }, isEdit = false }) { show({
title = '',
okText = '{{ i18n "confirm" }}',
fakeDns,
confirm = (fakeDns) => {},
isEdit = false
}) {
this.title = title; this.title = title;
this.okText = okText; this.okText = okText;
this.confirm = confirm; this.confirm = confirm;
@@ -35,7 +43,9 @@
if (isEdit) { if (isEdit) {
this.fakeDns = fakeDns; this.fakeDns = fakeDns;
} else { } else {
this.fakeDns = { ...fakednsDefaultData } this.fakeDns = {
...fakednsDefaultData
}
} }
this.isEdit = isEdit; this.isEdit = isEdit;
}, },
@@ -51,6 +61,5 @@
fakednsModal: fakednsModal, fakednsModal: fakednsModal,
} }
}); });
</script> </script>
{{end}} {{end}}

View File

@@ -1,12 +1,11 @@
{{define "modals/outModal"}} {{define "modals/outModal"}}
<a-modal id="out-modal" v-model="outModal.visible" :title="outModal.title" @ok="outModal.ok" <a-modal id="out-modal" v-model="outModal.visible" :title="outModal.title" @ok="outModal.ok"
:confirm-loading="outModal.confirmLoading" :closable="true" :mask-closable="false" :confirm-loading="outModal.confirmLoading" :closable="true" :mask-closable="false"
:ok-button-props="{ props: { disabled: !outModal.isValid } }" :style="{ overflow: 'hidden' }" :ok-button-props="{ props: { disabled: !outModal.isValid } }" :style="{ overflow: 'hidden' }"
:ok-text="outModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme"> :ok-text="outModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
{{template "form/outbound"}} {{template "form/outbound"}}
</a-modal> </a-modal>
<script> <script>
const outModal = { const outModal = {
title: '', title: '',
visible: false, visible: false,
@@ -25,7 +24,14 @@
ok() { ok() {
ObjectUtil.execute(outModal.confirm, outModal.outbound.toJson()); ObjectUtil.execute(outModal.confirm, outModal.outbound.toJson());
}, },
show({ title='', okText='{{ i18n "sure" }}', outbound, confirm=(outbound)=>{}, isEdit=false, tags=[] }) { show({
title = '',
okText = '{{ i18n "sure" }}',
outbound,
confirm = (outbound) => {},
isEdit = false,
tags = []
}) {
this.title = title; this.title = title;
this.okText = okText; this.okText = okText;
this.confirm = confirm; this.confirm = confirm;
@@ -42,11 +48,11 @@
outModal.visible = false; outModal.visible = false;
outModal.loading(false); outModal.loading(false);
}, },
loading(loading=true) { loading(loading = true) {
outModal.confirmLoading = loading; outModal.confirmLoading = loading;
}, },
check(){ check() {
if(outModal.outbound.tag == '' || outModal.tags.includes(outModal.outbound.tag)){ if (outModal.outbound.tag == '' || outModal.tags.includes(outModal.outbound.tag)) {
this.duplicateTag = true; this.duplicateTag = true;
this.isValid = false; this.isValid = false;
} else { } else {
@@ -56,25 +62,25 @@
}, },
toggleJson(jsonTab) { toggleJson(jsonTab) {
textAreaObj = document.getElementById('outboundJson'); textAreaObj = document.getElementById('outboundJson');
if(jsonTab){ if (jsonTab) {
if(this.cm != null) { if (this.cm != null) {
this.cm.toTextArea(); this.cm.toTextArea();
this.cm=null; this.cm = null;
} }
textAreaObj.value = JSON.stringify(this.outbound.toJson(), null, 2); textAreaObj.value = JSON.stringify(this.outbound.toJson(), null, 2);
this.cm = CodeMirror.fromTextArea(textAreaObj, app.cmOptions); this.cm = CodeMirror.fromTextArea(textAreaObj, app.cmOptions);
this.cm.on('change',editor => { this.cm.on('change', editor => {
value = editor.getValue(); value = editor.getValue();
if(this.isJsonString(value)){ if (this.isJsonString(value)) {
this.outbound = Outbound.fromJson(JSON.parse(value)); this.outbound = Outbound.fromJson(JSON.parse(value));
this.check(); this.check();
} }
}); });
this.activeKey = '2'; this.activeKey = '2';
} else { } else {
if(this.cm != null) { if (this.cm != null) {
this.cm.toTextArea(); this.cm.toTextArea();
this.cm=null; this.cm = null;
} }
this.activeKey = '1'; this.activeKey = '1';
} }
@@ -100,20 +106,21 @@
}, },
methods: { methods: {
streamNetworkChange() { streamNetworkChange() {
if (this.outModal.outbound.protocol == Protocols.VLESS && !outModal.outbound.canEnableTlsFlow()) { if (this.outModal.outbound.protocol == Protocols.VLESS && !outModal.outbound
.canEnableTlsFlow()) {
delete this.outModal.outbound.settings.flow; delete this.outModal.outbound.settings.flow;
} }
}, },
canEnableTls() { canEnableTls() {
return this.outModal.outbound.canEnableTls(); return this.outModal.outbound.canEnableTls();
}, },
convertLink(){ convertLink() {
newOutbound = Outbound.fromLink(outModal.link); newOutbound = Outbound.fromLink(outModal.link);
if(newOutbound){ if (newOutbound) {
this.outModal.outbound = newOutbound; this.outModal.outbound = newOutbound;
this.outModal.toggleJson(true); this.outModal.toggleJson(true);
this.outModal.check(); this.outModal.check();
this.$message.success('Link imported successfully...'); this.$message.success('Link imported successfully...');
outModal.link = ''; outModal.link = '';
} else { } else {
this.$message.error('Wrong Link!'); this.$message.error('Wrong Link!');
@@ -122,6 +129,5 @@
}, },
}, },
}); });
</script> </script>
{{end}} {{end}}

View File

@@ -1,7 +1,7 @@
{{define "modals/reverseModal"}} {{define "modals/reverseModal"}}
<a-modal id="reverse-modal" v-model="reverseModal.visible" :title="reverseModal.title" @ok="reverseModal.ok" <a-modal id="reverse-modal" v-model="reverseModal.visible" :title="reverseModal.title" @ok="reverseModal.ok"
:confirm-loading="reverseModal.confirmLoading" :closable="true" :mask-closable="false" :confirm-loading="reverseModal.confirmLoading" :closable="true" :mask-closable="false"
:ok-text="reverseModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme"> :ok-text="reverseModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "pages.xray.outbound.type" }}'> <a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
<a-select v-model="reverseModal.reverse.type" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="reverseModal.reverse.type" :dropdown-class-name="themeSwitcher.currentTheme">
@@ -15,26 +15,24 @@
<a-input v-model.trim="reverseModal.reverse.domain"></a-input> <a-input v-model.trim="reverseModal.reverse.domain"></a-input>
</a-form-item> </a-form-item>
<template v-if="reverseModal.reverse.type=='bridge'"> <template v-if="reverseModal.reverse.type=='bridge'">
<a-form-item label='{{ i18n "pages.xray.outbound.intercon" }}'> <a-form-item label='{{ i18n "pages.xray.outbound.intercon" }}'>
<a-select v-model="reverseModal.rules[0].outboundTag" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="reverseModal.rules[0].outboundTag" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="x in reverseModal.outboundTags" :value="x">[[ x ]]</a-select-option> <a-select-option v-for="x in reverseModal.outboundTags" :value="x">[[ x ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.xray.rules.outbound" }}'> <a-form-item label='{{ i18n "pages.xray.rules.outbound" }}'>
<a-select v-model="reverseModal.rules[1].outboundTag" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="reverseModal.rules[1].outboundTag" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="x in reverseModal.outboundTags" :value="x">[[ x ]]</a-select-option> <a-select-option v-for="x in reverseModal.outboundTags" :value="x">[[ x ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
</template> </template>
<template v-else> <template v-else>
<a-form-item label='{{ i18n "pages.xray.outbound.intercon" }}'> <a-form-item label='{{ i18n "pages.xray.outbound.intercon" }}'>
<a-checkbox-group <a-checkbox-group v-model="reverseModal.rules[0].inboundTag"
v-model="reverseModal.rules[0].inboundTag"
:options="reverseModal.inboundTags"></a-checkbox-group> :options="reverseModal.inboundTags"></a-checkbox-group>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.xray.rules.inbound" }}'> <a-form-item label='{{ i18n "pages.xray.rules.inbound" }}'>
<a-checkbox-group <a-checkbox-group v-model="reverseModal.rules[1].inboundTag"
v-model="reverseModal.rules[1].inboundTag"
:options="reverseModal.inboundTags"></a-checkbox-group> :options="reverseModal.inboundTags"></a-checkbox-group>
</a-form-item> </a-form-item>
</template> </template>
@@ -53,9 +51,14 @@
type: "", type: "",
domain: "" domain: ""
}, },
rules: [ rules: [{
{ outboundTag: '', inboundTag: []}, outboundTag: '',
{ outboundTag: '', inboundTag: []} inboundTag: []
},
{
outboundTag: '',
inboundTag: []
}
], ],
inboundTags: [], inboundTags: [],
outboundTags: [], outboundTags: [],
@@ -64,7 +67,7 @@
reverseModal.rules[0].type = 'field'; reverseModal.rules[0].type = 'field';
reverseModal.rules[1].type = 'field'; reverseModal.rules[1].type = 'field';
if(reverseModal.reverse.type == 'bridge'){ if (reverseModal.reverse.type == 'bridge') {
reverseModal.rules[0].inboundTag = [reverseModal.reverse.tag]; reverseModal.rules[0].inboundTag = [reverseModal.reverse.tag];
reverseModal.rules[1].inboundTag = [reverseModal.reverse.tag]; reverseModal.rules[1].inboundTag = [reverseModal.reverse.tag];
} else { } else {
@@ -73,22 +76,36 @@
} }
ObjectUtil.execute(reverseModal.confirm, reverseModal.reverse, reverseModal.rules); ObjectUtil.execute(reverseModal.confirm, reverseModal.reverse, reverseModal.rules);
}, },
show({ title='', okText='{{ i18n "sure" }}', reverse, rules, confirm=(reverse, rules)=>{}, isEdit=false }) { show({
title = '',
okText = '{{ i18n "sure" }}',
reverse,
rules,
confirm = (reverse, rules) => {},
isEdit = false
}) {
this.title = title; this.title = title;
this.okText = okText; this.okText = okText;
this.confirm = confirm; this.confirm = confirm;
this.visible = true; this.visible = true;
if(isEdit) { if (isEdit) {
this.reverse = { this.reverse = {
tag: reverse.tag, tag: reverse.tag,
type: reverse.type, type: reverse.type,
domain: reverse.domain, domain: reverse.domain,
}; };
reverse; reverse;
rules0 = rules.filter(r => r.domain != null); rules0 = rules.filter(r => r.domain != null);
if(rules0.length == 0) rules0 = [{ outboundTag: '', domain: ["full:" + this.reverse.domain], inboundTag: []}]; if (rules0.length == 0) rules0 = [{
outboundTag: '',
domain: ["full:" + this.reverse.domain],
inboundTag: []
}];
rules1 = rules.filter(r => r.domain == null); rules1 = rules.filter(r => r.domain == null);
if(rules1.length == 0) rules1 = [{ outboundTag: '', inboundTag: []}]; if (rules1.length == 0) rules1 = [{
outboundTag: '',
inboundTag: []
}];
this.rules = []; this.rules = [];
this.rules.push({ this.rules.push({
domain: rules0[0].domain, domain: rules0[0].domain,
@@ -105,22 +122,29 @@
type: "bridge", type: "bridge",
domain: "reverse.xui" domain: "reverse.xui"
} }
this.rules = [ this.rules = [{
{ outboundTag: '', inboundTag: []}, outboundTag: '',
{ outboundTag: '', inboundTag: []} inboundTag: []
},
{
outboundTag: '',
inboundTag: []
}
] ]
} }
this.isEdit = isEdit; this.isEdit = isEdit;
this.inboundTags = app.templateSettings.inbounds.filter((i) => !ObjectUtil.isEmpty(i.tag)).map(obj => obj.tag); this.inboundTags = app.templateSettings.inbounds.filter((i) => !ObjectUtil.isEmpty(i.tag)).map(obj =>
obj.tag);
this.inboundTags.push(...app.inboundTags); this.inboundTags.push(...app.inboundTags);
if (app.enableDNS && !ObjectUtil.isEmpty(app.dnsTag)) this.inboundTags.push(app.dnsTag) if (app.enableDNS && !ObjectUtil.isEmpty(app.dnsTag)) this.inboundTags.push(app.dnsTag)
this.outboundTags = app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag); this.outboundTags = app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj =>
obj.tag);
}, },
close() { close() {
reverseModal.visible = false; reverseModal.visible = false;
reverseModal.loading(false); reverseModal.loading(false);
}, },
loading(loading=true) { loading(loading = true) {
reverseModal.confirmLoading = loading; reverseModal.confirmLoading = loading;
}, },
}; };
@@ -130,9 +154,11 @@
el: '#reverse-modal', el: '#reverse-modal',
data: { data: {
reverseModal: reverseModal, reverseModal: reverseModal,
reverseTypes: { bridge: '{{ i18n "pages.xray.outbound.bridge" }}', portal:'{{ i18n "pages.xray.outbound.portal" }}'}, reverseTypes: {
bridge: '{{ i18n "pages.xray.outbound.bridge" }}',
portal: '{{ i18n "pages.xray.outbound.portal" }}'
},
}, },
}); });
</script> </script>
{{end}} {{end}}

View File

@@ -1,5 +1,7 @@
{{define "modals/ruleModal"}} {{define "modals/ruleModal"}}
<a-modal id="rule-modal" v-model="ruleModal.visible" :title="ruleModal.title" @ok="ruleModal.ok" :confirm-loading="ruleModal.confirmLoading" :closable="true" :mask-closable="false" :ok-text="ruleModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme"> <a-modal id="rule-modal" v-model="ruleModal.visible" :title="ruleModal.title" @ok="ruleModal.ok"
:confirm-loading="ruleModal.confirmLoading" :closable="true" :mask-closable="false" :ok-text="ruleModal.okText"
cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item> <a-form-item>
<template slot="label"> <template slot="label">
@@ -42,15 +44,19 @@
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label='Attributes'> <a-form-item label='Attributes'>
<a-button icon="plus" size="small" :style="{ marginLeft: '10px' }" @click="ruleModal.rule.attrs.push(['', ''])"></a-button> <a-button icon="plus" size="small" :style="{ marginLeft: '10px' }"
@click="ruleModal.rule.attrs.push(['', ''])"></a-button>
</a-form-item> </a-form-item>
<a-form-item :wrapper-col="{span: 24}"> <a-form-item :wrapper-col="{span: 24}">
<a-input-group compact v-for="(attr,index) in ruleModal.rule.attrs"> <a-input-group compact v-for="(attr,index) in ruleModal.rule.attrs">
<a-input :style="{ width: '50%' }" v-model="attr[0]" placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'> <a-input :style="{ width: '50%' }" v-model="attr[0]"
placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
<template slot="addonBefore" :style="{ margin: '0' }">[[ index+1 ]]</template> <template slot="addonBefore" :style="{ margin: '0' }">[[ index+1 ]]</template>
</a-input> </a-input>
<a-input :style="{ width: '50%' }" v-model="attr[1]" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'> <a-input :style="{ width: '50%' }" v-model="attr[1]"
<a-button icon="minus" slot="addonAfter" size="small" @click="ruleModal.rule.attrs.splice(index,1)"></a-button> placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
<a-button icon="minus" slot="addonAfter" size="small"
@click="ruleModal.rule.attrs.splice(index,1)"></a-button>
</a-input> </a-input>
</a-input-group> </a-input-group>
</a-form-item> </a-form-item>
@@ -196,16 +202,20 @@
this.inboundTags = app.templateSettings.inbounds.filter((i) => !ObjectUtil.isEmpty(i.tag)).map(obj => obj.tag); this.inboundTags = app.templateSettings.inbounds.filter((i) => !ObjectUtil.isEmpty(i.tag)).map(obj => obj.tag);
this.inboundTags.push(...app.inboundTags); this.inboundTags.push(...app.inboundTags);
if (app.enableDNS && !ObjectUtil.isEmpty(app.dnsTag)) this.inboundTags.push(app.dnsTag) if (app.enableDNS && !ObjectUtil.isEmpty(app.dnsTag)) this.inboundTags.push(app.dnsTag)
this.outboundTags = ["", ...app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag)]; this.outboundTags = ["", ...app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj =>
obj.tag)];
if (app.templateSettings.reverse) { if (app.templateSettings.reverse) {
if (app.templateSettings.reverse.bridges) { if (app.templateSettings.reverse.bridges) {
this.inboundTags.push(...app.templateSettings.reverse.bridges.map(b => b.tag)); this.inboundTags.push(...app.templateSettings.reverse.bridges.map(b => b.tag));
} }
if (app.templateSettings.reverse.portals) this.outboundTags.push(...app.templateSettings.reverse.portals.map(b => b.tag)); if (app.templateSettings.reverse.portals) this.outboundTags.push(...app.templateSettings.reverse.portals.map(
b => b.tag));
} }
this.balancerTags = [""]; this.balancerTags = [""];
if (app.templateSettings.routing && app.templateSettings.routing.balancers) { if (app.templateSettings.routing && app.templateSettings.routing.balancers) {
this.balancerTags = ["", ...app.templateSettings.routing.balancers.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag)]; this.balancerTags = ["", ...app.templateSettings.routing.balancers.filter((o) => !ObjectUtil.isEmpty(o.tag))
.map(obj => obj.tag)
];
} }
}, },
close() { close() {
@@ -234,7 +244,8 @@
rule.outboundTag = value.outboundTag == "" ? undefined : value.outboundTag; rule.outboundTag = value.outboundTag == "" ? undefined : value.outboundTag;
rule.balancerTag = value.balancerTag == "" ? undefined : value.balancerTag; rule.balancerTag = value.balancerTag == "" ? undefined : value.balancerTag;
for (const [key, value] of Object.entries(rule)) { for (const [key, value] of Object.entries(rule)) {
if (value !== null && value !== undefined && !(Array.isArray(value) && value.length === 0) && !(typeof value === 'object' && Object.keys(value).length === 0) && value !== '') { if (value !== null && value !== undefined && !(Array.isArray(value) && value.length === 0) && !(
typeof value === 'object' && Object.keys(value).length === 0) && value !== '') {
newRule[key] = value; newRule[key] = value;
} }
} }
@@ -249,4 +260,4 @@
} }
}); });
</script> </script>
{{end}} {{end}}

View File

@@ -79,7 +79,8 @@
</template> </template>
{{ template "settings/panel/subscription/general" . }} {{ template "settings/panel/subscription/general" . }}
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="5" v-if="allSetting.subJsonEnable || allSetting.subClashEnable" :style="{ paddingTop: '20px' }"> <a-tab-pane key="5" v-if="allSetting.subJsonEnable || allSetting.subClashEnable"
:style="{ paddingTop: '20px' }">
<template #tab> <template #tab>
<a-icon type="code"></a-icon> <a-icon type="code"></a-icon>
<span>{{ i18n "pages.settings.subSettings" }} (Formats)</span> <span>{{ i18n "pages.settings.subSettings" }} (Formats)</span>
@@ -124,9 +125,19 @@
user: {}, user: {},
lang: LanguageManager.getLanguage(), lang: LanguageManager.getLanguage(),
inboundOptions: [], inboundOptions: [],
remarkModels: { i: 'Inbound', e: 'Email', o: 'Other' }, remarkModels: {
i: 'Inbound',
e: 'Email',
o: 'Other'
},
remarkSeparators: [' ', '-', '_', '@', ':', '~', '|', ',', '.', '/'], remarkSeparators: [' ', '-', '_', '@', ':', '~', '|', ',', '.', '/'],
datepickerList: [{ name: 'Gregorian (Standard)', value: 'gregorian' }, { name: 'Jalalian (شمسی)', value: 'jalalian' }], datepickerList: [{
name: 'Gregorian (Standard)',
value: 'gregorian'
}, {
name: 'Jalalian (شمسی)',
value: 'jalalian'
}],
remarkSample: '', remarkSample: '',
defaultFragment: { defaultFragment: {
packets: "tlshello", packets: "tlshello",
@@ -134,17 +145,19 @@
interval: "10-20", interval: "10-20",
maxSplit: "300-400" maxSplit: "300-400"
}, },
defaultNoises: [ defaultNoises: [{
{ type: "rand", packet: "10-20", delay: "10-16", applyTo: "ip" } type: "rand",
], packet: "10-20",
delay: "10-16",
applyTo: "ip"
}],
defaultMux: { defaultMux: {
enabled: true, enabled: true,
concurrency: 8, concurrency: 8,
xudpConcurrency: 16, xudpConcurrency: 16,
xudpProxyUDP443: "reject" xudpProxyUDP443: "reject"
}, },
defaultRules: [ defaultRules: [{
{
type: "field", type: "field",
outboundTag: "direct", outboundTag: "direct",
domain: [ domain: [
@@ -160,26 +173,75 @@
] ]
}, },
], ],
directIPsOptions: [ directIPsOptions: [{
{ label: 'Private IP', value: 'geoip:private' }, label: 'Private IP',
{ label: '🇮🇷 Iran', value: 'geoip:ir' }, value: 'geoip:private'
{ label: '🇨🇳 China', value: 'geoip:cn' }, },
{ label: '🇷🇺 Russia', value: 'geoip:ru' }, {
{ label: '🇻🇳 Vietnam', value: 'geoip:vn' }, label: '🇮🇷 Iran',
{ label: '🇪🇸 Spain', value: 'geoip:es' }, value: 'geoip:ir'
{ label: '🇮🇩 Indonesia', value: 'geoip:id' }, },
{ label: '🇺🇦 Ukraine', value: 'geoip:ua' }, {
{ label: '🇹🇷 Türkiye', value: 'geoip:tr' }, label: '🇨🇳 China',
{ label: '🇧🇷 Brazil', value: 'geoip:br' }, value: 'geoip:cn'
},
{
label: '🇷🇺 Russia',
value: 'geoip:ru'
},
{
label: '🇻🇳 Vietnam',
value: 'geoip:vn'
},
{
label: '🇪🇸 Spain',
value: 'geoip:es'
},
{
label: '🇮🇩 Indonesia',
value: 'geoip:id'
},
{
label: '🇺🇦 Ukraine',
value: 'geoip:ua'
},
{
label: '🇹🇷 Türkiye',
value: 'geoip:tr'
},
{
label: '🇧🇷 Brazil',
value: 'geoip:br'
},
], ],
diretDomainsOptions: [ diretDomainsOptions: [{
{ label: 'Private DNS', value: 'geosite:private' }, label: 'Private DNS',
{ label: '🇮🇷 Iran', value: 'geosite:category-ir' }, value: 'geosite:private'
{ label: '🇨🇳 China', value: 'geosite:cn' }, },
{ label: '🇷🇺 Russia', value: 'geosite:category-ru' }, {
{ label: 'Apple', value: 'geosite:apple' }, label: '🇮🇷 Iran',
{ label: 'Meta', value: 'geosite:meta' }, value: 'geosite:category-ir'
{ label: 'Google', value: 'geosite:google' }, },
{
label: '🇨🇳 China',
value: 'geosite:cn'
},
{
label: '🇷🇺 Russia',
value: 'geosite:category-ru'
},
{
label: 'Apple',
value: 'geosite:apple'
},
{
label: 'Meta',
value: 'geosite:meta'
},
{
label: 'Google',
value: 'geosite:google'
},
], ],
get remarkModel() { get remarkModel() {
rm = this.allSetting.remarkModel; rm = this.allSetting.remarkModel;
@@ -317,7 +379,13 @@
this.loading(true); this.loading(true);
await PromiseUtil.sleep(5000); await PromiseUtil.sleep(5000);
const { webDomain, webPort, webBasePath, webCertFile, webKeyFile } = this.allSetting; const {
webDomain,
webPort,
webBasePath,
webCertFile,
webKeyFile
} = this.allSetting;
const newProtocol = (webCertFile || webKeyFile) ? "https:" : "http:"; const newProtocol = (webCertFile || webKeyFile) ? "https:" : "http:";
let base = webBasePath ? webBasePath.replace(/^\//, "") : ""; let base = webBasePath ? webBasePath.replace(/^\//, "") : "";
@@ -358,7 +426,8 @@
type: 'set', type: 'set',
confirm: (success) => { confirm: (success) => {
if (success) { if (success) {
Vue.prototype.$message['success']('{{ i18n "pages.settings.security.twoFactorModalSetSuccess" }}') Vue.prototype.$message['success'](
'{{ i18n "pages.settings.security.twoFactorModalSetSuccess" }}')
this.allSetting.twoFactorToken = newTwoFactorToken this.allSetting.twoFactorToken = newTwoFactorToken
} }
@@ -374,7 +443,8 @@
type: 'confirm', type: 'confirm',
confirm: (success) => { confirm: (success) => {
if (success) { if (success) {
Vue.prototype.$message['success']('{{ i18n "pages.settings.security.twoFactorModalDeleteSuccess" }}') Vue.prototype.$message['success'](
'{{ i18n "pages.settings.security.twoFactorModalDeleteSuccess" }}')
this.allSetting.twoFactorEnable = false this.allSetting.twoFactorEnable = false
this.allSetting.twoFactorToken = "" this.allSetting.twoFactorToken = ""
@@ -384,7 +454,12 @@
} }
}, },
addNoise() { addNoise() {
const newNoise = { type: "rand", packet: "10-20", delay: "10-16", applyTo: "ip" }; const newNoise = {
type: "rand",
packet: "10-20",
delay: "10-16",
applyTo: "ip"
};
this.noisesArray = [...this.noisesArray, newNoise]; this.noisesArray = [...this.noisesArray, newNoise];
}, },
removeNoise(index) { removeNoise(index) {
@@ -394,44 +469,60 @@
}, },
updateNoiseType(index, value) { updateNoiseType(index, value) {
const updatedNoises = [...this.noisesArray]; const updatedNoises = [...this.noisesArray];
updatedNoises[index] = { ...updatedNoises[index], type: value }; updatedNoises[index] = {
...updatedNoises[index],
type: value
};
this.noisesArray = updatedNoises; this.noisesArray = updatedNoises;
}, },
updateNoisePacket(index, value) { updateNoisePacket(index, value) {
const updatedNoises = [...this.noisesArray]; const updatedNoises = [...this.noisesArray];
updatedNoises[index] = { ...updatedNoises[index], packet: value }; updatedNoises[index] = {
...updatedNoises[index],
packet: value
};
this.noisesArray = updatedNoises; this.noisesArray = updatedNoises;
}, },
updateNoiseDelay(index, value) { updateNoiseDelay(index, value) {
const updatedNoises = [...this.noisesArray]; const updatedNoises = [...this.noisesArray];
updatedNoises[index] = { ...updatedNoises[index], delay: value }; updatedNoises[index] = {
...updatedNoises[index],
delay: value
};
this.noisesArray = updatedNoises; this.noisesArray = updatedNoises;
}, },
updateNoiseApplyTo(index, value) { updateNoiseApplyTo(index, value) {
const updatedNoises = [...this.noisesArray]; const updatedNoises = [...this.noisesArray];
updatedNoises[index] = { ...updatedNoises[index], applyTo: value }; updatedNoises[index] = {
...updatedNoises[index],
applyTo: value
};
this.noisesArray = updatedNoises; this.noisesArray = updatedNoises;
}, },
}, },
computed: { computed: {
ldapInboundTagList: { ldapInboundTagList: {
get: function () { get: function() {
const csv = this.allSetting.ldapInboundTags || ""; const csv = this.allSetting.ldapInboundTags || "";
return csv.length ? csv.split(',').map(s => s.trim()).filter(Boolean) : []; return csv.length ? csv.split(',').map(s => s.trim()).filter(Boolean) : [];
}, },
set: function (list) { set: function(list) {
this.allSetting.ldapInboundTags = Array.isArray(list) ? list.join(',') : ''; this.allSetting.ldapInboundTags = Array.isArray(list) ? list.join(',') : '';
} }
}, },
fragment: { fragment: {
get: function () { return this.allSetting?.subJsonFragment != ""; }, get: function() {
set: function (v) { return this.allSetting?.subJsonFragment != "";
},
set: function(v) {
this.allSetting.subJsonFragment = v ? JSON.stringify(this.defaultFragment) : ""; this.allSetting.subJsonFragment = v ? JSON.stringify(this.defaultFragment) : "";
} }
}, },
fragmentPackets: { fragmentPackets: {
get: function () { return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).packets : ""; }, get: function() {
set: function (v) { return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).packets : "";
},
set: function(v) {
if (v != "") { if (v != "") {
newFragment = JSON.parse(this.allSetting.subJsonFragment); newFragment = JSON.parse(this.allSetting.subJsonFragment);
newFragment.packets = v; newFragment.packets = v;
@@ -440,8 +531,10 @@
} }
}, },
fragmentLength: { fragmentLength: {
get: function () { return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).length : ""; }, get: function() {
set: function (v) { return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).length : "";
},
set: function(v) {
if (v != "") { if (v != "") {
newFragment = JSON.parse(this.allSetting.subJsonFragment); newFragment = JSON.parse(this.allSetting.subJsonFragment);
newFragment.length = v; newFragment.length = v;
@@ -450,8 +543,10 @@
} }
}, },
fragmentInterval: { fragmentInterval: {
get: function () { return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).interval : ""; }, get: function() {
set: function (v) { return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).interval : "";
},
set: function(v) {
if (v != "") { if (v != "") {
newFragment = JSON.parse(this.allSetting.subJsonFragment); newFragment = JSON.parse(this.allSetting.subJsonFragment);
newFragment.interval = v; newFragment.interval = v;
@@ -460,8 +555,10 @@
} }
}, },
fragmentMaxSplit: { fragmentMaxSplit: {
get: function () { return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).maxSplit : ""; }, get: function() {
set: function (v) { return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).maxSplit : "";
},
set: function(v) {
if (v != "") { if (v != "") {
newFragment = JSON.parse(this.allSetting.subJsonFragment); newFragment = JSON.parse(this.allSetting.subJsonFragment);
newFragment.maxSplit = v; newFragment.maxSplit = v;
@@ -492,50 +589,60 @@
} }
}, },
enableMux: { enableMux: {
get: function () { return this.allSetting?.subJsonMux != ""; }, get: function() {
set: function (v) { return this.allSetting?.subJsonMux != "";
},
set: function(v) {
this.allSetting.subJsonMux = v ? JSON.stringify(this.defaultMux) : ""; this.allSetting.subJsonMux = v ? JSON.stringify(this.defaultMux) : "";
} }
}, },
muxConcurrency: { muxConcurrency: {
get: function () { return this.enableMux ? JSON.parse(this.allSetting.subJsonMux).concurrency : -1; }, get: function() {
set: function (v) { return this.enableMux ? JSON.parse(this.allSetting.subJsonMux).concurrency : -1;
},
set: function(v) {
newMux = JSON.parse(this.allSetting.subJsonMux); newMux = JSON.parse(this.allSetting.subJsonMux);
newMux.concurrency = v; newMux.concurrency = v;
this.allSetting.subJsonMux = JSON.stringify(newMux); this.allSetting.subJsonMux = JSON.stringify(newMux);
} }
}, },
muxXudpConcurrency: { muxXudpConcurrency: {
get: function () { return this.enableMux ? JSON.parse(this.allSetting.subJsonMux).xudpConcurrency : -1; }, get: function() {
set: function (v) { return this.enableMux ? JSON.parse(this.allSetting.subJsonMux).xudpConcurrency : -1;
},
set: function(v) {
newMux = JSON.parse(this.allSetting.subJsonMux); newMux = JSON.parse(this.allSetting.subJsonMux);
newMux.xudpConcurrency = v; newMux.xudpConcurrency = v;
this.allSetting.subJsonMux = JSON.stringify(newMux); this.allSetting.subJsonMux = JSON.stringify(newMux);
} }
}, },
muxXudpProxyUDP443: { muxXudpProxyUDP443: {
get: function () { return this.enableMux ? JSON.parse(this.allSetting.subJsonMux).xudpProxyUDP443 : "reject"; }, get: function() {
set: function (v) { return this.enableMux ? JSON.parse(this.allSetting.subJsonMux).xudpProxyUDP443 : "reject";
},
set: function(v) {
newMux = JSON.parse(this.allSetting.subJsonMux); newMux = JSON.parse(this.allSetting.subJsonMux);
newMux.xudpProxyUDP443 = v; newMux.xudpProxyUDP443 = v;
this.allSetting.subJsonMux = JSON.stringify(newMux); this.allSetting.subJsonMux = JSON.stringify(newMux);
} }
}, },
enableDirect: { enableDirect: {
get: function () { return this.allSetting?.subJsonRules != ""; }, get: function() {
set: function (v) { return this.allSetting?.subJsonRules != "";
},
set: function(v) {
this.allSetting.subJsonRules = v ? JSON.stringify(this.defaultRules) : ""; this.allSetting.subJsonRules = v ? JSON.stringify(this.defaultRules) : "";
} }
}, },
directIPs: { directIPs: {
get: function () { get: function() {
if (!this.enableDirect) return []; if (!this.enableDirect) return [];
const rules = JSON.parse(this.allSetting.subJsonRules); const rules = JSON.parse(this.allSetting.subJsonRules);
if (!Array.isArray(rules)) return []; if (!Array.isArray(rules)) return [];
const ipRule = rules.find(r => r.ip); const ipRule = rules.find(r => r.ip);
return ipRule?.ip ?? []; return ipRule?.ip ?? [];
}, },
set: function (v) { set: function(v) {
let rules = JSON.parse(this.allSetting.subJsonRules); let rules = JSON.parse(this.allSetting.subJsonRules);
if (!Array.isArray(rules)) return; if (!Array.isArray(rules)) return;
@@ -554,14 +661,14 @@
} }
}, },
directDomains: { directDomains: {
get: function () { get: function() {
if (!this.enableDirect) return []; if (!this.enableDirect) return [];
const rules = JSON.parse(this.allSetting.subJsonRules); const rules = JSON.parse(this.allSetting.subJsonRules);
if (!Array.isArray(rules)) return []; if (!Array.isArray(rules)) return [];
const domainRule = rules.find(r => r.domain); const domainRule = rules.find(r => r.domain);
return domainRule?.domain ?? []; return domainRule?.domain ?? [];
}, },
set: function (v) { set: function(v) {
let rules = JSON.parse(this.allSetting.subJsonRules); let rules = JSON.parse(this.allSetting.subJsonRules);
if (!Array.isArray(rules)) return; if (!Array.isArray(rules)) return;
if (v.length == 0) { if (v.length == 0) {
@@ -576,7 +683,7 @@
} }
}, },
confAlerts: { confAlerts: {
get: function () { get: function() {
if (!this.allSetting) return []; if (!this.allSetting) return [];
var alerts = [] var alerts = []
if (window.location.protocol !== "https:") alerts.push('{{ i18n "secAlertSSL" }}'); if (window.location.protocol !== "https:") alerts.push('{{ i18n "secAlertSSL" }}');
@@ -584,11 +691,13 @@
panelPath = window.location.pathname.split('/').length < 4 panelPath = window.location.pathname.split('/').length < 4
if (panelPath && this.allSetting.webBasePath == '/') alerts.push('{{ i18n "secAlertPanelURI" }}'); if (panelPath && this.allSetting.webBasePath == '/') alerts.push('{{ i18n "secAlertPanelURI" }}');
if (this.allSetting.subEnable) { if (this.allSetting.subEnable) {
subPath = this.allSetting.subURI.length > 0 ? new URL(this.allSetting.subURI).pathname : this.allSetting.subPath; subPath = this.allSetting.subURI.length > 0 ? new URL(this.allSetting.subURI).pathname : this
.allSetting.subPath;
if (subPath == '/sub/') alerts.push('{{ i18n "secAlertSubURI" }}'); if (subPath == '/sub/') alerts.push('{{ i18n "secAlertSubURI" }}');
} }
if (this.allSetting.subJsonEnable) { if (this.allSetting.subJsonEnable) {
subJsonPath = this.allSetting.subJsonURI.length > 0 ? new URL(this.allSetting.subJsonURI).pathname : this.allSetting.subJsonPath; subJsonPath = this.allSetting.subJsonURI.length > 0 ? new URL(this.allSetting.subJsonURI).pathname :
this.allSetting.subJsonPath;
if (subJsonPath == '/json/') alerts.push('{{ i18n "secAlertSubJsonURI" }}'); if (subJsonPath == '/json/') alerts.push('{{ i18n "secAlertSubJsonURI" }}');
} }
return alerts return alerts

View File

@@ -162,7 +162,8 @@
<a-setting-list-item paddings="small"> <a-setting-list-item paddings="small">
<template #title>LDAP Port</template> <template #title>LDAP Port</template>
<template #control> <template #control>
<a-input-number :min="1" :max="65535" v-model="allSetting.ldapPort" :style="{ width: '100%' }"></a-input-number> <a-input-number :min="1" :max="65535" v-model="allSetting.ldapPort"
:style="{ width: '100%' }"></a-input-number>
</template> </template>
</a-setting-list-item> </a-setting-list-item>
<a-setting-list-item paddings="small"> <a-setting-list-item paddings="small">
@@ -239,10 +240,13 @@
<template #title>Inbound tags</template> <template #title>Inbound tags</template>
<template #description>Select inbounds to manage (auto create/delete)</template> <template #description>Select inbounds to manage (auto create/delete)</template>
<template #control> <template #control>
<a-select mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme" :style="{ width: '100%' }" v-model="ldapInboundTagList"> <a-select mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme" :style="{ width: '100%' }"
<a-select-option v-for="opt in inboundOptions" :key="opt.value" :value="opt.value">[[ opt.label ]]</a-select-option> v-model="ldapInboundTagList">
<a-select-option v-for="opt in inboundOptions" :key="opt.value" :value="opt.value">[[ opt.label
]]</a-select-option>
</a-select> </a-select>
<div v-if="inboundOptions.length==0" style="margin-top:6px;color:#999">No inbounds found. Please create one in Inbounds.</div> <div v-if="inboundOptions.length==0" style="margin-top:6px;color:#999">No inbounds found. Please create
one in Inbounds.</div>
</template> </template>
</a-setting-list-item> </a-setting-list-item>
<a-setting-list-item paddings="small"> <a-setting-list-item paddings="small">
@@ -260,19 +264,22 @@
<a-setting-list-item paddings="small"> <a-setting-list-item paddings="small">
<template #title>Default total (GB)</template> <template #title>Default total (GB)</template>
<template #control> <template #control>
<a-input-number :min="0" v-model="allSetting.ldapDefaultTotalGB" :style="{ width: '100%' }"></a-input-number> <a-input-number :min="0" v-model="allSetting.ldapDefaultTotalGB"
:style="{ width: '100%' }"></a-input-number>
</template> </template>
</a-setting-list-item> </a-setting-list-item>
<a-setting-list-item paddings="small"> <a-setting-list-item paddings="small">
<template #title>Default expiry (days)</template> <template #title>Default expiry (days)</template>
<template #control> <template #control>
<a-input-number :min="0" v-model="allSetting.ldapDefaultExpiryDays" :style="{ width: '100%' }"></a-input-number> <a-input-number :min="0" v-model="allSetting.ldapDefaultExpiryDays"
:style="{ width: '100%' }"></a-input-number>
</template> </template>
</a-setting-list-item> </a-setting-list-item>
<a-setting-list-item paddings="small"> <a-setting-list-item paddings="small">
<template #title>Default Limit IP</template> <template #title>Default Limit IP</template>
<template #control> <template #control>
<a-input-number :min="0" v-model="allSetting.ldapDefaultLimitIP" :style="{ width: '100%' }"></a-input-number> <a-input-number :min="0" v-model="allSetting.ldapDefaultLimitIP"
:style="{ width: '100%' }"></a-input-number>
</template> </template>
</a-setting-list-item> </a-setting-list-item>
</a-collapse-panel> </a-collapse-panel>

View File

@@ -46,8 +46,7 @@
<template #description>{{ i18n <template #description>{{ i18n
"pages.settings.subPortDesc"}}</template> "pages.settings.subPortDesc"}}</template>
<template #control> <template #control>
<a-input-number v-model="allSetting.subPort" :min="1" <a-input-number v-model="allSetting.subPort" :min="1" :min="65535"
:min="65535"
:style="{ width: '100%' }"></a-input-number> :style="{ width: '100%' }"></a-input-number>
</template> </template>
</a-setting-list-item> </a-setting-list-item>
@@ -67,8 +66,7 @@
<template #description>{{ i18n <template #description>{{ i18n
"pages.settings.subURIDesc"}}</template> "pages.settings.subURIDesc"}}</template>
<template #control> <template #control>
<a-input type="text" <a-input type="text" placeholder="(http|https)://domain[:port]/path/"
placeholder="(http|https)://domain[:port]/path/"
v-model="allSetting.subURI"></a-input> v-model="allSetting.subURI"></a-input>
</template> </template>
</a-setting-list-item> </a-setting-list-item>
@@ -104,8 +102,7 @@
<template #description>{{ i18n <template #description>{{ i18n
"pages.settings.subSupportUrlDesc"}}</template> "pages.settings.subSupportUrlDesc"}}</template>
<template #control> <template #control>
<a-input type="text" v-model="allSetting.subSupportUrl" <a-input type="text" v-model="allSetting.subSupportUrl" placeholder="https://example.com"></a-input>
placeholder="https://example.com"></a-input>
</template> </template>
</a-setting-list-item> </a-setting-list-item>
<a-setting-list-item paddings="small"> <a-setting-list-item paddings="small">
@@ -113,8 +110,7 @@
<template #description>{{ i18n <template #description>{{ i18n
"pages.settings.subProfileUrlDesc"}}</template> "pages.settings.subProfileUrlDesc"}}</template>
<template #control> <template #control>
<a-input type="text" v-model="allSetting.subProfileUrl" <a-input type="text" v-model="allSetting.subProfileUrl" placeholder="https://example.com"></a-input>
placeholder="https://example.com"></a-input>
</template> </template>
</a-setting-list-item> </a-setting-list-item>
<a-setting-list-item paddings="small"> <a-setting-list-item paddings="small">
@@ -141,8 +137,7 @@
<template #description>{{ i18n <template #description>{{ i18n
"pages.settings.subRoutingRulesDesc"}}</template> "pages.settings.subRoutingRulesDesc"}}</template>
<template #control> <template #control>
<a-textarea v-model="allSetting.subRoutingRules" <a-textarea v-model="allSetting.subRoutingRules" placeholder="happ://routing/add/..."></a-textarea>
placeholder="happ://routing/add/..."></a-textarea>
</template> </template>
</a-setting-list-item> </a-setting-list-item>
</a-collapse-panel> </a-collapse-panel>
@@ -170,8 +165,7 @@
<template #description>{{ i18n <template #description>{{ i18n
"pages.settings.subUpdatesDesc"}}</template> "pages.settings.subUpdatesDesc"}}</template>
<template #control> <template #control>
<a-input-number :min="1" v-model="allSetting.subUpdates" <a-input-number :min="1" v-model="allSetting.subUpdates" :style="{ width: '100%' }"></a-input-number>
:style="{ width: '100%' }"></a-input-number>
</template> </template>
</a-setting-list-item> </a-setting-list-item>
</a-collapse-panel> </a-collapse-panel>

View File

@@ -100,7 +100,8 @@
<a-form-item> <a-form-item>
<a-space direction="vertical" align="center"> <a-space direction="vertical" align="center">
<a-row type="flex" :gutter="[8,8]" justify="center" style="width:100%"> <a-row type="flex" :gutter="[8,8]" justify="center" style="width:100%">
<a-col :xs="24" :sm="app.subJsonUrl || app.subClashUrl ? 12 : 24" style="text-align:center;"> <a-col :xs="24" :sm="app.subJsonUrl || app.subClashUrl ? 12 : 24"
style="text-align:center;">
<tr-qr-box class="qr-box"> <tr-qr-box class="qr-box">
<a-tag color="purple" class="qr-tag"> <a-tag color="purple" class="qr-tag">
<span>{{ i18n <span>{{ i18n
@@ -270,11 +271,11 @@
</a-layout> </a-layout>
<!-- Bootstrap data for external JS --> <!-- Bootstrap data for external JS -->
<template id="subscription-data" data-sid="{{ .sId }}" data-sub-url="{{ .subUrl }}" data-subjson-url="{{ .subJsonUrl }}" data-subclash-url="{{ .subClashUrl }}" <template id="subscription-data" data-sid="{{ .sId }}" data-sub-url="{{ .subUrl }}" data-subjson-url="{{ .subJsonUrl }}"
data-download="{{ .download }}" data-upload="{{ .upload }}" data-used="{{ .used }}" data-total="{{ .total }}" data-subclash-url="{{ .subClashUrl }}" data-download="{{ .download }}" data-upload="{{ .upload }}"
data-remained="{{ .remained }}" data-expire="{{ .expire }}" data-lastonline="{{ .lastOnline }}" data-used="{{ .used }}" data-total="{{ .total }}" data-remained="{{ .remained }}" data-expire="{{ .expire }}"
data-downloadbyte="{{ .downloadByte }}" data-uploadbyte="{{ .uploadByte }}" data-totalbyte="{{ .totalByte }}" data-lastonline="{{ .lastOnline }}" data-downloadbyte="{{ .downloadByte }}" data-uploadbyte="{{ .uploadByte }}"
data-datepicker="{{ .datepicker }}"></template> data-totalbyte="{{ .totalByte }}" data-datepicker="{{ .datepicker }}"></template>
<textarea id="subscription-links" style="display:none">{{ range .result }}{{ . }} <textarea id="subscription-links" style="display:none">{{ range .result }}{{ . }}
{{ end }}</textarea> {{ end }}</textarea>

View File

@@ -5,7 +5,8 @@
<span>{{ i18n "pages.xray.balancer.addBalancer"}}</span> <span>{{ i18n "pages.xray.balancer.addBalancer"}}</span>
</a-button> </a-button>
<a-table :columns="balancerColumns" bordered :row-key="r => r.key" :data-source="balancersData" <a-table :columns="balancerColumns" bordered :row-key="r => r.key" :data-source="balancersData"
:scroll="isMobile ? {} : { x: 200 }" :pagination="false" :indent-size="0" :locale='{ filterConfirm: `{{ i18n "confirm" }}`, filterReset: `{{ i18n "reset" }}` }'> :scroll="isMobile ? {} : { x: 200 }" :pagination="false" :indent-size="0"
:locale='{ filterConfirm: `{{ i18n "confirm" }}`, filterReset: `{{ i18n "reset" }}` }'>
<template slot="action" slot-scope="text, balancer, index"> <template slot="action" slot-scope="text, balancer, index">
<span>[[ index+1 ]]</span> <span>[[ index+1 ]]</span>
<a-dropdown :trigger="['click']"> <a-dropdown :trigger="['click']">
@@ -18,7 +19,7 @@
</a-menu-item> </a-menu-item>
<a-menu-item @click="deleteBalancer(index)"> <a-menu-item @click="deleteBalancer(index)">
<span :style="{ color: '#FF4D4F' }"> <span :style="{ color: '#FF4D4F' }">
<a-icon type="delete"></a-icon> <a-icon type="delete"></a-icon>
<span>{{ i18n "delete"}}</span> <span>{{ i18n "delete"}}</span>
</span> </span>
</a-menu-item> </a-menu-item>
@@ -32,7 +33,8 @@
<a-tag :style="{ margin: '0' }" v-if="balancer.strategy=='leastPing'" color="green">Least Ping</a-tag> <a-tag :style="{ margin: '0' }" v-if="balancer.strategy=='leastPing'" color="green">Least Ping</a-tag>
</template> </template>
<template slot="selector" slot-scope="text, balancer, index"> <template slot="selector" slot-scope="text, balancer, index">
<a-tag class="info-large-tag" :style="{ margin: '1' }" v-for="sel in balancer.selector">[[ sel ]]</a-tag> <a-tag class="info-large-tag" :style="{ margin: '1' }" v-for="sel in balancer.selector">[[ sel
]]</a-tag>
</template> </template>
</a-table> </a-table>
<a-radio-group v-if="observatoryEnable || burstObservatoryEnable" v-model="obsSettings" @change="changeObsCode" <a-radio-group v-if="observatoryEnable || burstObservatoryEnable" v-model="obsSettings" @change="changeObsCode"

View File

@@ -4,8 +4,7 @@
<a-row :xs="24" :sm="24" :lg="12"> <a-row :xs="24" :sm="24" :lg="12">
<a-alert type="warning" :style="{ textAlign: 'center' }"> <a-alert type="warning" :style="{ textAlign: 'center' }">
<template slot="message"> <template slot="message">
<a-icon type="exclamation-circle" theme="filled" <a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
:style="{ color: '#FFA031' }"></a-icon>
<span>{{ i18n "pages.xray.generalConfigsDesc" }}</span> <span>{{ i18n "pages.xray.generalConfigsDesc" }}</span>
</template> </template>
</a-alert> </a-alert>
@@ -15,11 +14,9 @@
<template #description>{{ i18n "pages.xray.FreedomStrategyDesc" <template #description>{{ i18n "pages.xray.FreedomStrategyDesc"
}}</template> }}</template>
<template #control> <template #control>
<a-select v-model="freedomStrategy" <a-select v-model="freedomStrategy" :dropdown-class-name="themeSwitcher.currentTheme"
:dropdown-class-name="themeSwitcher.currentTheme"
:style="{ width: '100%' }"> :style="{ width: '100%' }">
<a-select-option v-for="s in OutboundDomainStrategies" <a-select-option v-for="s in OutboundDomainStrategies" :value="s">
:value="s">
<span>[[ s ]]</span> <span>[[ s ]]</span>
</a-select-option> </a-select-option>
</a-select> </a-select>
@@ -30,11 +27,9 @@
<template #description>{{ i18n "pages.xray.RoutingStrategyDesc" <template #description>{{ i18n "pages.xray.RoutingStrategyDesc"
}}</template> }}</template>
<template #control> <template #control>
<a-select v-model="routingStrategy" <a-select v-model="routingStrategy" :dropdown-class-name="themeSwitcher.currentTheme"
:dropdown-class-name="themeSwitcher.currentTheme"
:style="{ width: '100%' }"> :style="{ width: '100%' }">
<a-select-option v-for="s in routingDomainStrategies" <a-select-option v-for="s in routingDomainStrategies" :value="s">
:value="s">
<span>[[ s ]]</span> <span>[[ s ]]</span>
</a-select-option> </a-select-option>
</a-select> </a-select>
@@ -45,8 +40,7 @@
<template #description>{{ i18n "pages.xray.outboundTestUrlDesc" <template #description>{{ i18n "pages.xray.outboundTestUrlDesc"
}}</template> }}</template>
<template #control> <template #control>
<a-input v-model="outboundTestUrl" <a-input v-model="outboundTestUrl" :placeholder="'https://www.google.com/generate_204'"
:placeholder="'https://www.google.com/generate_204'"
:style="{ width: '100%' }"></a-input> :style="{ width: '100%' }"></a-input>
</template> </template>
</a-setting-list-item> </a-setting-list-item>
@@ -93,8 +87,7 @@
<a-row :xs="24" :sm="24" :lg="12"> <a-row :xs="24" :sm="24" :lg="12">
<a-alert type="warning" :style="{ textAlign: 'center' }"> <a-alert type="warning" :style="{ textAlign: 'center' }">
<template slot="message"> <template slot="message">
<a-icon type="exclamation-circle" theme="filled" <a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
:style="{ color: '#FFA031' }"></a-icon>
<span>{{ i18n "pages.xray.logConfigsDesc" }}</span> <span>{{ i18n "pages.xray.logConfigsDesc" }}</span>
</template> </template>
</a-alert> </a-alert>
@@ -104,8 +97,7 @@
<template #description>{{ i18n "pages.xray.logLevelDesc" <template #description>{{ i18n "pages.xray.logLevelDesc"
}}</template> }}</template>
<template #control> <template #control>
<a-select v-model="logLevel" <a-select v-model="logLevel" :dropdown-class-name="themeSwitcher.currentTheme"
:dropdown-class-name="themeSwitcher.currentTheme"
:style="{ width: '100%' }"> :style="{ width: '100%' }">
<a-select-option v-for="s in log.loglevel" :value="s"> <a-select-option v-for="s in log.loglevel" :value="s">
<span>[[ s ]]</span> <span>[[ s ]]</span>
@@ -118,8 +110,7 @@
<template #description>{{ i18n "pages.xray.accessLogDesc" <template #description>{{ i18n "pages.xray.accessLogDesc"
}}</template> }}</template>
<template #control> <template #control>
<a-select v-model="accessLog" <a-select v-model="accessLog" :dropdown-class-name="themeSwitcher.currentTheme"
:dropdown-class-name="themeSwitcher.currentTheme"
:style="{ width: '100%' }"> :style="{ width: '100%' }">
<a-select-option value> <a-select-option value>
<span>Empty</span> <span>Empty</span>
@@ -135,8 +126,7 @@
<template #description>{{ i18n "pages.xray.errorLogDesc" <template #description>{{ i18n "pages.xray.errorLogDesc"
}}</template> }}</template>
<template #control> <template #control>
<a-select v-model="errorLog" <a-select v-model="errorLog" :dropdown-class-name="themeSwitcher.currentTheme"
:dropdown-class-name="themeSwitcher.currentTheme"
:style="{ width: '100%' }"> :style="{ width: '100%' }">
<a-select-option value> <a-select-option value>
<span>Empty</span> <span>Empty</span>
@@ -152,8 +142,7 @@
<template #description>{{ i18n "pages.xray.maskAddressDesc" <template #description>{{ i18n "pages.xray.maskAddressDesc"
}}</template> }}</template>
<template #control> <template #control>
<a-select v-model="maskAddressLog" <a-select v-model="maskAddressLog" :dropdown-class-name="themeSwitcher.currentTheme"
:dropdown-class-name="themeSwitcher.currentTheme"
:style="{ width: '100%' }"> :style="{ width: '100%' }">
<a-select-option value> <a-select-option value>
<span>Empty</span> <span>Empty</span>
@@ -176,8 +165,7 @@
<a-row :xs="24" :sm="24" :lg="12"> <a-row :xs="24" :sm="24" :lg="12">
<a-alert type="warning" :style="{ textAlign: 'center' }"> <a-alert type="warning" :style="{ textAlign: 'center' }">
<template slot="message"> <template slot="message">
<a-icon type="exclamation-circle" theme="filled" <a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
:style="{ color: '#FFA031' }"></a-icon>
<span>{{ i18n "pages.xray.blockConfigsDesc" }}</span> <span>{{ i18n "pages.xray.blockConfigsDesc" }}</span>
</template> </template>
</a-alert> </a-alert>
@@ -191,8 +179,7 @@
<a-row :xs="24" :sm="24" :lg="12"> <a-row :xs="24" :sm="24" :lg="12">
<a-alert type="warning" :style="{ textAlign: 'center' }"> <a-alert type="warning" :style="{ textAlign: 'center' }">
<template slot="message"> <template slot="message">
<a-icon type="exclamation-circle" theme="filled" <a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
:style="{ color: '#FFA031' }"></a-icon>
<span>{{ i18n "pages.xray.blockConnectionsConfigsDesc" <span>{{ i18n "pages.xray.blockConnectionsConfigsDesc"
}}</span> }}</span>
</template> </template>
@@ -201,11 +188,9 @@
<a-setting-list-item paddings="small"> <a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.xray.blockips" }}</template> <template #title>{{ i18n "pages.xray.blockips" }}</template>
<template #control> <template #control>
<a-select mode="tags" v-model="blockedIPs" <a-select mode="tags" v-model="blockedIPs" :style="{ width: '100%' }"
:style="{ width: '100%' }"
:dropdown-class-name="themeSwitcher.currentTheme"> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="p.value" :label="p.label" <a-select-option :value="p.value" :label="p.label" v-for="p in settingsData.IPsOptions">
v-for="p in settingsData.IPsOptions">
<span>[[ p.label ]]</span> <span>[[ p.label ]]</span>
</a-select-option> </a-select-option>
</a-select> </a-select>
@@ -214,22 +199,18 @@
<a-setting-list-item paddings="small"> <a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.xray.blockdomains" }}</template> <template #title>{{ i18n "pages.xray.blockdomains" }}</template>
<template #control> <template #control>
<a-select mode="tags" v-model="blockedDomains" <a-select mode="tags" v-model="blockedDomains" :style="{ width: '100%' }"
:style="{ width: '100%' }"
:dropdown-class-name="themeSwitcher.currentTheme"> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="p.value" :label="p.label" <a-select-option :value="p.value" :label="p.label" v-for="p in settingsData.BlockDomainsOptions">
v-for="p in settingsData.BlockDomainsOptions">
<span>[[ p.label ]]</span> <span>[[ p.label ]]</span>
</a-select-option> </a-select-option>
</a-select> </a-select>
</template> </template>
</a-setting-list-item> </a-setting-list-item>
<a-row :xs="24" :sm="24" :lg="12"> <a-row :xs="24" :sm="24" :lg="12">
<a-alert type="warning" <a-alert type="warning" :style="{ textAlign: 'center', marginTop: '20px' }">
:style="{ textAlign: 'center', marginTop: '20px' }">
<template slot="message"> <template slot="message">
<a-icon type="exclamation-circle" theme="filled" <a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
:style="{ color: '#FFA031' }"></a-icon>
<span>{{ i18n "pages.xray.directConnectionsConfigsDesc" <span>{{ i18n "pages.xray.directConnectionsConfigsDesc"
}}</span> }}</span>
</template> </template>
@@ -238,11 +219,9 @@
<a-setting-list-item paddings="small"> <a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.xray.directips" }}</template> <template #title>{{ i18n "pages.xray.directips" }}</template>
<template #control> <template #control>
<a-select mode="tags" :style="{ width: '100%' }" <a-select mode="tags" :style="{ width: '100%' }" v-model="directIPs"
v-model="directIPs"
:dropdown-class-name="themeSwitcher.currentTheme"> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="p.value" :label="p.label" <a-select-option :value="p.value" :label="p.label" v-for="p in settingsData.IPsOptions">
v-for="p in settingsData.IPsOptions">
<span>[[ p.label ]]</span> <span>[[ p.label ]]</span>
</a-select-option> </a-select-option>
</a-select> </a-select>
@@ -251,22 +230,18 @@
<a-setting-list-item paddings="small"> <a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.xray.directdomains" }}</template> <template #title>{{ i18n "pages.xray.directdomains" }}</template>
<template #control> <template #control>
<a-select mode="tags" :style="{ width: '100%' }" <a-select mode="tags" :style="{ width: '100%' }" v-model="directDomains"
v-model="directDomains"
:dropdown-class-name="themeSwitcher.currentTheme"> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="p.value" :label="p.label" <a-select-option :value="p.value" :label="p.label" v-for="p in settingsData.DomainsOptions">
v-for="p in settingsData.DomainsOptions">
<span>[[ p.label ]]</span> <span>[[ p.label ]]</span>
</a-select-option> </a-select-option>
</a-select> </a-select>
</template> </template>
</a-setting-list-item> </a-setting-list-item>
<a-row :xs="24" :sm="24" :lg="12"> <a-row :xs="24" :sm="24" :lg="12">
<a-alert type="warning" <a-alert type="warning" :style="{ textAlign: 'center', marginTop: '20px' }">
:style="{ textAlign: 'center', marginTop: '20px' }">
<template slot="message"> <template slot="message">
<a-icon type="exclamation-circle" theme="filled" <a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
:style="{ color: '#FFA031' }"></a-icon>
<span>{{ i18n "pages.xray.ipv4RoutingDesc" }}</span> <span>{{ i18n "pages.xray.ipv4RoutingDesc" }}</span>
</template> </template>
</a-alert> </a-alert>
@@ -274,22 +249,18 @@
<a-setting-list-item paddings="small"> <a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.xray.ipv4Routing" }}</template> <template #title>{{ i18n "pages.xray.ipv4Routing" }}</template>
<template #control> <template #control>
<a-select mode="tags" :style="{ width: '100%' }" <a-select mode="tags" :style="{ width: '100%' }" v-model="ipv4Domains"
v-model="ipv4Domains"
:dropdown-class-name="themeSwitcher.currentTheme"> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="p.value" :label="p.label" <a-select-option :value="p.value" :label="p.label" v-for="p in settingsData.ServicesOptions">
v-for="p in settingsData.ServicesOptions">
<span>[[ p.label ]]</span> <span>[[ p.label ]]</span>
</a-select-option> </a-select-option>
</a-select> </a-select>
</template> </template>
</a-setting-list-item> </a-setting-list-item>
<a-row :xs="24" :sm="24" :lg="12"> <a-row :xs="24" :sm="24" :lg="12">
<a-alert type="warning" <a-alert type="warning" :style="{ textAlign: 'center', marginTop: '20px' }">
:style="{ textAlign: 'center', marginTop: '20px' }">
<template slot="message"> <template slot="message">
<a-icon type="exclamation-circle" theme="filled" <a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
:style="{ color: '#FFA031' }"></a-icon>
{{ i18n "pages.xray.warpRoutingDesc" }} {{ i18n "pages.xray.warpRoutingDesc" }}
</template> </template>
</a-alert> </a-alert>
@@ -298,18 +269,15 @@
<template #title>{{ i18n "pages.xray.warpRouting" }}</template> <template #title>{{ i18n "pages.xray.warpRouting" }}</template>
<template #control> <template #control>
<template v-if="WarpExist"> <template v-if="WarpExist">
<a-select mode="tags" :style="{ width: '100%' }" <a-select mode="tags" :style="{ width: '100%' }" v-model="warpDomains"
v-model="warpDomains"
:dropdown-class-name="themeSwitcher.currentTheme"> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="p.value" :label="p.label" <a-select-option :value="p.value" :label="p.label" v-for="p in settingsData.ServicesOptions">
v-for="p in settingsData.ServicesOptions">
<span>[[ p.label ]]</span> <span>[[ p.label ]]</span>
</a-select-option> </a-select-option>
</a-select> </a-select>
</template> </template>
<template v-else> <template v-else>
<a-button type="primary" icon="cloud" <a-button type="primary" icon="cloud" @click="showWarp()">WARP</a-button>
@click="showWarp()">WARP</a-button>
</template> </template>
</template> </template>
</a-setting-list-item> </a-setting-list-item>
@@ -317,11 +285,9 @@
<template #title>{{ i18n "pages.xray.nordRouting" }}</template> <template #title>{{ i18n "pages.xray.nordRouting" }}</template>
<template #control> <template #control>
<template v-if="NordExist"> <template v-if="NordExist">
<a-select mode="tags" :style="{ width: '100%' }" <a-select mode="tags" :style="{ width: '100%' }" v-model="nordDomains"
v-model="nordDomains"
:dropdown-class-name="themeSwitcher.currentTheme"> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="p.value" :label="p.label" <a-select-option :value="p.value" :label="p.label" v-for="p in settingsData.ServicesOptions">
v-for="p in settingsData.ServicesOptions">
<span>[[ p.label ]]</span> <span>[[ p.label ]]</span>
</a-select-option> </a-select-option>
</a-select> </a-select>
@@ -333,8 +299,7 @@
</template> </template>
</a-setting-list-item> </a-setting-list-item>
</a-collapse-panel> </a-collapse-panel>
<a-collapse-panel key="6" <a-collapse-panel key="6" header='{{ i18n "pages.settings.resetDefaultConfig"}}'>
header='{{ i18n "pages.settings.resetDefaultConfig"}}'>
<a-space direction="horizontal" :style="{ padding: '0 20px' }"> <a-space direction="horizontal" :style="{ padding: '0 20px' }">
<a-button type="danger" @click="resetXrayConfigToDefault"> <a-button type="danger" @click="resetXrayConfigToDefault">
<span>{{ i18n "pages.settings.resetDefaultConfig" }}</span> <span>{{ i18n "pages.settings.resetDefaultConfig" }}</span>

View File

@@ -29,7 +29,8 @@
<template #control> <template #control>
<a-select v-model="dnsStrategy" :style="{ width: '100%' }" <a-select v-model="dnsStrategy" :style="{ width: '100%' }"
:dropdown-class-name="themeSwitcher.currentTheme"> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="l" :label="l" v-for="l in ['UseSystem', 'UseIP', 'UseIPv4', 'UseIPv6']"> <a-select-option :value="l" :label="l"
v-for="l in ['UseSystem', 'UseIP', 'UseIPv4', 'UseIPv6']">
<span>[[ l ]]</span> <span>[[ l ]]</span>
</a-select-option> </a-select-option>
</a-select> </a-select>

View File

@@ -7,21 +7,16 @@
<span v-if="!isMobile">{{ i18n <span v-if="!isMobile">{{ i18n
"pages.xray.outbound.addOutbound" }}</span> "pages.xray.outbound.addOutbound" }}</span>
</a-button> </a-button>
<a-button type="primary" icon="cloud" <a-button type="primary" icon="cloud" @click="showWarp()">WARP</a-button>
@click="showWarp()">WARP</a-button> <a-button type="primary" icon="api" @click="showNord()">NordVPN</a-button>
<a-button type="primary" icon="api"
@click="showNord()">NordVPN</a-button>
</a-space> </a-space>
</a-col> </a-col>
<a-col :xs="12" :sm="12" :lg="12" :style="{ textAlign: 'right' }"> <a-col :xs="12" :sm="12" :lg="12" :style="{ textAlign: 'right' }">
<a-button-group> <a-button-group>
<a-button icon="sync" @click="refreshOutboundTraffic()" <a-button icon="sync" @click="refreshOutboundTraffic()" :loading="refreshing"></a-button>
:loading="refreshing"></a-button> <a-popconfirm placement="topRight" @confirm="resetOutboundTraffic(-1)"
<a-popconfirm placement="topRight"
@confirm="resetOutboundTraffic(-1)"
title='{{ i18n "pages.inbounds.resetTrafficContent"}}' title='{{ i18n "pages.inbounds.resetTrafficContent"}}'
:overlay-class-name="themeSwitcher.currentTheme" :overlay-class-name="themeSwitcher.currentTheme" ok-text='{{ i18n "reset"}}'
ok-text='{{ i18n "reset"}}'
cancel-text='{{ i18n "cancel"}}'> cancel-text='{{ i18n "cancel"}}'>
<a-icon slot="icon" type="question-circle-o" <a-icon slot="icon" type="question-circle-o"
:style="{ color: themeSwitcher.isDarkTheme ? '#008771' : '#008771' }"></a-icon> :style="{ color: themeSwitcher.isDarkTheme ? '#008771' : '#008771' }"></a-icon>
@@ -30,10 +25,8 @@
</a-button-group> </a-button-group>
</a-col> </a-col>
</a-row> </a-row>
<a-table :columns="outboundColumns" bordered :row-key="r => r.key" <a-table :columns="outboundColumns" bordered :row-key="r => r.key" :data-source="outboundData"
:data-source="outboundData" :scroll="isMobile ? {} : { x: 800 }" :pagination="false" :indent-size="0"
:scroll="isMobile ? {} : { x: 800 }" :pagination="false"
:indent-size="0"
:locale='{ filterConfirm: `{{ i18n "confirm" }}`, filterReset: `{{ i18n "reset" }}` }'> :locale='{ filterConfirm: `{{ i18n "confirm" }}`, filterReset: `{{ i18n "reset" }}` }'>
<template slot="action" slot-scope="text, outbound, index"> <template slot="action" slot-scope="text, outbound, index">
<span>[[ index+1 ]]</span> <span>[[ index+1 ]]</span>
@@ -41,8 +34,7 @@
<a-icon @click="e => e.preventDefault()" type="more" <a-icon @click="e => e.preventDefault()" type="more"
:style="{ fontSize: '16px', textDecoration: 'bold' }"></a-icon> :style="{ fontSize: '16px', textDecoration: 'bold' }"></a-icon>
<a-menu slot="overlay" :theme="themeSwitcher.currentTheme"> <a-menu slot="overlay" :theme="themeSwitcher.currentTheme">
<a-menu-item v-if="index>0" <a-menu-item v-if="index>0" @click="setFirstOutbound(index)">
@click="setFirstOutbound(index)">
<a-icon type="vertical-align-top"></a-icon> <a-icon type="vertical-align-top"></a-icon>
<span>{{ i18n "pages.xray.rules.first"}}</span> <span>{{ i18n "pages.xray.rules.first"}}</span>
</a-menu-item> </a-menu-item>
@@ -66,8 +58,7 @@
</a-dropdown> </a-dropdown>
</template> </template>
<template slot="address" slot-scope="text, outbound, index"> <template slot="address" slot-scope="text, outbound, index">
<p :style="{ margin: '0 5px' }" <p :style="{ margin: '0 5px' }" v-for="addr in findOutboundAddress(outbound)">[[ addr ]]</p>
v-for="addr in findOutboundAddress(outbound)">[[ addr ]]</p>
</template> </template>
<template slot="protocol" slot-scope="text, outbound, index"> <template slot="protocol" slot-scope="text, outbound, index">
<a-tag :style="{ margin: '0' }" color="purple">[[ outbound.protocol <a-tag :style="{ margin: '0' }" color="purple">[[ outbound.protocol
@@ -76,11 +67,8 @@
v-if="[Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(outbound.protocol)"> v-if="[Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(outbound.protocol)">
<a-tag :style="{ margin: '0' }" color="blue">[[ <a-tag :style="{ margin: '0' }" color="blue">[[
outbound.streamSettings.network ]]</a-tag> outbound.streamSettings.network ]]</a-tag>
<a-tag :style="{ margin: '0' }" <a-tag :style="{ margin: '0' }" v-if="outbound.streamSettings.security=='tls'" color="green">tls</a-tag>
v-if="outbound.streamSettings.security=='tls'" <a-tag :style="{ margin: '0' }" v-if="outbound.streamSettings.security=='reality'"
color="green">tls</a-tag>
<a-tag :style="{ margin: '0' }"
v-if="outbound.streamSettings.security=='reality'"
color="green">reality</a-tag> color="green">reality</a-tag>
</template> </template>
</template> </template>
@@ -91,10 +79,7 @@
<a-tooltip> <a-tooltip>
<template slot="title">{{ i18n "pages.xray.outbound.test" <template slot="title">{{ i18n "pages.xray.outbound.test"
}}</template> }}</template>
<a-button <a-button type="primary" shape="circle" icon="thunderbolt"
type="primary"
shape="circle"
icon="thunderbolt"
:loading="outboundTestStates[index] && outboundTestStates[index].testing" :loading="outboundTestStates[index] && outboundTestStates[index].testing"
@click="testOutbound(index)" @click="testOutbound(index)"
:disabled="(outbound.protocol === 'blackhole' || outbound.tag === 'blocked') || (outboundTestStates[index] && outboundTestStates[index].testing)"> :disabled="(outbound.protocol === 'blackhole' || outbound.tag === 'blocked') || (outboundTestStates[index] && outboundTestStates[index].testing)">
@@ -102,24 +87,20 @@
</a-tooltip> </a-tooltip>
</template> </template>
<template slot="testResult" slot-scope="text, outbound, index"> <template slot="testResult" slot-scope="text, outbound, index">
<div <div v-if="outboundTestStates[index] && outboundTestStates[index].result">
v-if="outboundTestStates[index] && outboundTestStates[index].result"> <a-tag v-if="outboundTestStates[index].result.success" color="green">
<a-tag v-if="outboundTestStates[index].result.success"
color="green">
[[ outboundTestStates[index].result.delay ]]ms [[ outboundTestStates[index].result.delay ]]ms
<span v-if="outboundTestStates[index].result.statusCode"> <span v-if="outboundTestStates[index].result.statusCode">
([[ outboundTestStates[index].result.statusCode ([[ outboundTestStates[index].result.statusCode
]])</span> ]])</span>
</a-tag> </a-tag>
<a-tooltip v-else <a-tooltip v-else :title="outboundTestStates[index].result.error">
:title="outboundTestStates[index].result.error">
<a-tag color="red"> <a-tag color="red">
Failed Failed
</a-tag> </a-tag>
</a-tooltip> </a-tooltip>
</div> </div>
<span <span v-else-if="outboundTestStates[index] && outboundTestStates[index].testing">
v-else-if="outboundTestStates[index] && outboundTestStates[index].testing">
<a-icon type="loading" /> <a-icon type="loading" />
</span> </span>
<span v-else>-</span> <span v-else>-</span>

File diff suppressed because it is too large Load Diff

1427
x-ui.sh

File diff suppressed because it is too large Load Diff