Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a6bca794f | ||
|
|
4e20bb5f02 | ||
|
|
44b7eb881c | ||
|
|
9b1146780c | ||
|
|
866f7bd042 | ||
|
|
9454af46b7 | ||
|
|
9d18e925cc | ||
|
|
21d988133e | ||
|
|
0633a25d29 | ||
|
|
47243f0374 | ||
|
|
ede67f2d6c | ||
|
|
d759e09569 | ||
|
|
684e777628 | ||
|
|
1e902c2511 | ||
|
|
1a0cdfbfbc | ||
|
|
53e7608ddf | ||
|
|
43ec88bb24 | ||
|
|
c5b30d6c6c | ||
|
|
7fad81edab | ||
|
|
b9a5fd2c09 | ||
|
|
0bec29f2ba | ||
|
|
a7418d9708 | ||
|
|
abd69a253a | ||
|
|
2b420bd517 | ||
|
|
14d04e8ec9 | ||
|
|
1e2be10429 | ||
|
|
569c9428fb | ||
|
|
97489e743a | ||
|
|
c74efa1d43 | ||
|
|
18af7047f8 | ||
|
|
e7ae846823 | ||
|
|
b042f01e58 | ||
|
|
e43601ac08 | ||
|
|
bc29d7a252 | ||
|
|
dd21bb2db7 | ||
|
|
4d07b99fe7 | ||
|
|
8b5fe0b018 | ||
|
|
fc23af5db6 | ||
|
|
10f54cd937 | ||
|
|
b21758e6a6 | ||
|
|
903db95783 | ||
|
|
6fdc07a2d0 | ||
|
|
c9f245cb25 | ||
|
|
5dc95f8556 | ||
|
|
d9d8545cca | ||
|
|
4593147b65 | ||
|
|
020d1adc55 | ||
|
|
389b5408b1 | ||
|
|
037dbb5670 | ||
|
|
6af0d55ca9 | ||
|
|
0917eb2742 | ||
|
|
f8be7a7649 | ||
|
|
5b87b12535 | ||
|
|
8908e8b16a | ||
|
|
4f7af1e57a | ||
|
|
3af55cc5b4 | ||
|
|
ac5d8af4f9 | ||
|
|
01cd7539f9 | ||
|
|
102864525c | ||
|
|
e6254e23f2 | ||
|
|
5f3c2f781e | ||
|
|
6c73791cab | ||
|
|
f76e3ff94b | ||
|
|
7f0fc1b8ef | ||
|
|
d18eb7e4e4 | ||
|
|
d3377cd45e | ||
|
|
64a5a9f1bc | ||
|
|
32afd7200a | ||
|
|
ffa531f9ca | ||
|
|
05deb89451 | ||
|
|
6f807823b9 | ||
|
|
2f594ca7c9 | ||
|
|
ecae6e5ead | ||
|
|
34ab6ed7ee | ||
|
|
5ba9d6e118 | ||
|
|
c47a67975f | ||
|
|
6563d23f38 | ||
|
|
3a46c3302b | ||
|
|
1c97593360 | ||
|
|
95416cd553 |
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
@@ -17,6 +17,7 @@ jobs:
|
||||
- armv6
|
||||
- 386
|
||||
- armv5
|
||||
- s390x
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -40,6 +41,8 @@ jobs:
|
||||
sudo apt install gcc-i686-linux-gnu
|
||||
elif [ "${{ matrix.platform }}" == "armv5" ]; then
|
||||
sudo apt install gcc-arm-linux-gnueabi
|
||||
elif [ "${{ matrix.platform }}" == "s390x" ]; then
|
||||
sudo apt install gcc-s390x-linux-gnu
|
||||
fi
|
||||
|
||||
- name: Build x-ui
|
||||
@@ -65,6 +68,9 @@ jobs:
|
||||
export GOARCH=arm
|
||||
export GOARM=5
|
||||
export CC=arm-linux-gnueabi-gcc
|
||||
elif [ "${{ matrix.platform }}" == "s390x" ]; then
|
||||
export GOARCH=s390x
|
||||
export CC=s390x-linux-gnu-gcc
|
||||
fi
|
||||
go build -o xui-release -v main.go
|
||||
|
||||
@@ -77,7 +83,7 @@ jobs:
|
||||
cd x-ui/bin
|
||||
|
||||
# Download dependencies
|
||||
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v1.8.8/"
|
||||
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v1.8.10/"
|
||||
if [ "${{ matrix.platform }}" == "amd64" ]; then
|
||||
wget ${Xray_URL}Xray-linux-64.zip
|
||||
unzip Xray-linux-64.zip
|
||||
@@ -102,6 +108,10 @@ jobs:
|
||||
wget ${Xray_URL}Xray-linux-arm32-v5.zip
|
||||
unzip Xray-linux-arm32-v5.zip
|
||||
rm -f Xray-linux-arm32-v5.zip
|
||||
elif [ "${{ matrix.platform }}" == "s390x" ]; then
|
||||
wget ${Xray_URL}Xray-linux-s390x.zip
|
||||
unzip Xray-linux-s390x.zip
|
||||
rm -f Xray-linux-s390x.zip
|
||||
fi
|
||||
rm -f geoip.dat geosite.dat
|
||||
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,6 +3,7 @@
|
||||
.cache
|
||||
.sync*
|
||||
*.tar.gz
|
||||
*.log
|
||||
access.log
|
||||
error.log
|
||||
tmp
|
||||
|
||||
@@ -27,7 +27,7 @@ case $1 in
|
||||
esac
|
||||
mkdir -p build/bin
|
||||
cd build/bin
|
||||
wget "https://github.com/XTLS/Xray-core/releases/download/v1.8.8/Xray-linux-${ARCH}.zip"
|
||||
wget "https://github.com/XTLS/Xray-core/releases/download/v1.8.10/Xray-linux-${ARCH}.zip"
|
||||
unzip "Xray-linux-${ARCH}.zip"
|
||||
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat
|
||||
mv xray "xray-linux-${FNAME}"
|
||||
|
||||
474
README.es_ES.md
Normal file
474
README.es_ES.md
Normal file
@@ -0,0 +1,474 @@
|
||||
# 3X-UI
|
||||
|
||||
[Español](/README.es_ES.md) | [English](/README.md) | [Chinese](/README.zh.md)
|
||||
|
||||
<p align="center"><a href="#"><img src="./media/3X-UI.png" alt="Image"></a></p>
|
||||
|
||||
**Un Panel Web Avanzado • Construido sobre Xray Core**
|
||||
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
[](#)
|
||||
[](#)
|
||||
[](#)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
> **Descargo de responsabilidad:** Este proyecto es solo para aprendizaje personal y comunicación, por favor no lo uses con fines ilegales, por favor no lo uses en un entorno de producción
|
||||
|
||||
**Si este proyecto te es útil, podrías considerar darle una**:star2:
|
||||
|
||||
<p align="left"><a href="#"><img width="125" src="https://github.com/MHSanaei/3x-ui/assets/115543613/7aa895dd-048a-42e7-989b-afd41a74e2e1" alt="Image"></a></p>
|
||||
|
||||
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
|
||||
|
||||
## Instalar y Actualizar
|
||||
|
||||
```
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
|
||||
```
|
||||
|
||||
## Instalar una Versión Personalizada
|
||||
|
||||
Para instalar la versión deseada, agrega la versión al final del comando de instalación. Por ejemplo, ver `v2.2.6`:
|
||||
|
||||
```
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v2.2.6
|
||||
```
|
||||
|
||||
## Certificado SSL
|
||||
|
||||
<details>
|
||||
<summary>Haz clic para el Certificado SSL</summary>
|
||||
|
||||
### Cloudflare
|
||||
|
||||
El script de gestión tiene una aplicación de certificado SSL incorporada para Cloudflare. Para usar este script para colocar un certificado, necesitas lo siguiente:
|
||||
|
||||
- Correo electrónico registrado en Cloudflare
|
||||
- Clave Global de API de Cloudflare
|
||||
- El nombre de dominio se ha resuelto en el servidor actual a través de Cloudflare
|
||||
|
||||
**1:** Ejecuta el comando`x-ui`en la terminal, luego elige `Certificado SSL de Cloudflare`.
|
||||
|
||||
|
||||
### Certbot
|
||||
```
|
||||
apt-get install certbot -y
|
||||
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com
|
||||
certbot renew --dry-run
|
||||
```
|
||||
|
||||
***Consejo:*** *Certbot también está integrado en el script de gestión. Puedes ejecutar el comando `x-ui` , luego elegir `Gestión de Certificados SSL`.*
|
||||
|
||||
</details>
|
||||
|
||||
## Instalación y Actualización Manual
|
||||
|
||||
<details>
|
||||
<summary>Haz clic para más detalles de la instalación manual</summary>
|
||||
|
||||
#### Uso
|
||||
|
||||
1. Para descargar la última versión del paquete comprimido directamente en tu servidor, ejecuta el siguiente comando:
|
||||
|
||||
```sh
|
||||
ARCH=$(uname -m)
|
||||
case "${ARCH}" in
|
||||
x86_64 | x64 | amd64) XUI_ARCH="amd64" ;;
|
||||
i*86 | x86) XUI_ARCH="386" ;;
|
||||
armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;;
|
||||
armv7* | armv7) XUI_ARCH="armv7" ;;
|
||||
armv6* | armv6) XUI_ARCH="armv6" ;;
|
||||
armv5* | armv5) XUI_ARCH="armv5" ;;
|
||||
*) XUI_ARCH="amd64" ;;
|
||||
esac
|
||||
|
||||
|
||||
wget https://github.com/MHSanaei/3x-ui/releases/latest/download/x-ui-linux-${XUI_ARCH}.tar.gz
|
||||
```
|
||||
|
||||
2. Una vez que se haya descargado el paquete comprimido, ejecuta los siguientes comandos para instalar o actualizar x-ui:
|
||||
|
||||
```sh
|
||||
ARCH=$(uname -m)
|
||||
case "${ARCH}" in
|
||||
x86_64 | x64 | amd64) XUI_ARCH="amd64" ;;
|
||||
i*86 | x86) XUI_ARCH="386" ;;
|
||||
armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;;
|
||||
armv7* | armv7) XUI_ARCH="armv7" ;;
|
||||
armv6* | armv6) XUI_ARCH="armv6" ;;
|
||||
armv5* | armv5) XUI_ARCH="armv5" ;;
|
||||
*) XUI_ARCH="amd64" ;;
|
||||
esac
|
||||
|
||||
cd /root/
|
||||
rm -rf x-ui/ /usr/local/x-ui/ /usr/bin/x-ui
|
||||
tar zxvf x-ui-linux-${XUI_ARCH}.tar.gz
|
||||
chmod +x x-ui/x-ui x-ui/bin/xray-linux-* x-ui/x-ui.sh
|
||||
cp x-ui/x-ui.sh /usr/bin/x-ui
|
||||
cp -f x-ui/x-ui.service /etc/systemd/system/
|
||||
mv x-ui/ /usr/local/
|
||||
systemctl daemon-reload
|
||||
systemctl enable x-ui
|
||||
systemctl restart x-ui
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Instalar con Docker
|
||||
|
||||
<details>
|
||||
<summary>Haz clic para más detalles del Docker</summary>
|
||||
|
||||
#### Uso
|
||||
|
||||
1. Instala Docker:
|
||||
|
||||
```sh
|
||||
bash <(curl -sSL https://get.docker.com)
|
||||
```
|
||||
|
||||
2. Clona el Repositorio del Proyecto:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/MHSanaei/3x-ui.git
|
||||
cd 3x-ui
|
||||
```
|
||||
|
||||
3. Inicia el Servicio
|
||||
|
||||
```sh
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
O tambien
|
||||
|
||||
```sh
|
||||
docker run -itd \
|
||||
-e XRAY_VMESS_AEAD_FORCED=false \
|
||||
-v $PWD/db/:/etc/x-ui/ \
|
||||
-v $PWD/cert/:/root/cert/ \
|
||||
--network=host \
|
||||
--restart=unless-stopped \
|
||||
--name 3x-ui \
|
||||
ghcr.io/mhsanaei/3x-ui:latest
|
||||
```
|
||||
|
||||
actualizar a la última versión
|
||||
|
||||
```sh
|
||||
cd 3x-ui
|
||||
docker compose down
|
||||
docker compose pull 3x-ui
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
eliminar 3x-ui de docker
|
||||
|
||||
```sh
|
||||
docker stop 3x-ui
|
||||
docker rm 3x-ui
|
||||
cd --
|
||||
rm -r 3x-ui
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
## SO Recomendados
|
||||
|
||||
- Ubuntu 20.04+
|
||||
- Debian 11+
|
||||
- CentOS 8+
|
||||
- Fedora 36+
|
||||
- Arch Linux
|
||||
- Manjaro
|
||||
- Armbian
|
||||
- AlmaLinux 9+
|
||||
- Rockylinux 9+
|
||||
|
||||
## Arquitecturas y Dispositivos Compatibles
|
||||
|
||||
<details>
|
||||
<summary>Haz clic para detalles de arquitecturas y dispositivos compatibles</summary>
|
||||
|
||||
Nuestra plataforma ofrece compatibilidad con una amplia gama de arquitecturas y dispositivos, garantizando flexibilidad en diversos entornos informáticos. A continuación se presentan las principales arquitecturas que admitimos:
|
||||
|
||||
- **amd64**: Esta arquitectura predominante es la estándar para computadoras personales y servidores, y admite la mayoría de los sistemas operativos modernos sin problemas.
|
||||
|
||||
- **x86 / i386**: Ampliamente adoptada en computadoras de escritorio y portátiles, esta arquitectura cuenta con un amplio soporte de numerosos sistemas operativos y aplicaciones, incluidos, entre otros, Windows, macOS y sistemas Linux.
|
||||
|
||||
- **armv8 / arm64 / aarch64**: Diseñada para dispositivos móviles y embebidos contemporáneos, como teléfonos inteligentes y tabletas, esta arquitectura está ejemplificada por dispositivos como Raspberry Pi 4, Raspberry Pi 3, Raspberry Pi Zero 2/Zero 2 W, Orange Pi 3 LTS, entre otros.
|
||||
|
||||
- **armv7 / arm / arm32**: Sirve como arquitectura para dispositivos móviles y embebidos más antiguos, y sigue siendo ampliamente utilizada en dispositivos como Orange Pi Zero LTS, Orange Pi PC Plus, Raspberry Pi 2, entre otros.
|
||||
|
||||
- **armv6 / arm / arm32**: Orientada a dispositivos embebidos muy antiguos, esta arquitectura, aunque menos común, todavía se utiliza. Dispositivos como Raspberry Pi 1, Raspberry Pi Zero/Zero W, dependen de esta arquitectura.
|
||||
|
||||
- **armv5 / arm / arm32**: Una arquitectura más antigua asociada principalmente con sistemas embebidos tempranos, es menos común hoy en día pero aún puede encontrarse en dispositivos heredados como versiones antiguas de Raspberry Pi y algunos teléfonos inteligentes más antiguos.
|
||||
</details>
|
||||
|
||||
## Idiomas
|
||||
|
||||
- Inglés
|
||||
- Farsi
|
||||
- Chino
|
||||
- Ruso
|
||||
- Vietnamita
|
||||
- Español
|
||||
- Indonesio
|
||||
- Ucraniano
|
||||
|
||||
|
||||
## Características
|
||||
|
||||
- Monitoreo del Estado del Sistema
|
||||
- Búsqueda dentro de todas las reglas de entrada y clientes
|
||||
- Tema Oscuro/Claro
|
||||
- Soporta multiusuario y multiprotocolo
|
||||
- Soporta protocolos, incluyendo VMess, VLESS, Trojan, Shadowsocks, Dokodemo-door, Socks, HTTP, wireguard
|
||||
- Soporta Protocolos nativos XTLS, incluyendo RPRX-Direct, Visión, REALITY
|
||||
- Estadísticas de tráfico, límite de tráfico, límite de tiempo de vencimiento
|
||||
- Plantillas de configuración de Xray personalizables
|
||||
- Soporta acceso HTTPS al panel (dominio proporcionado por uno mismo + certificado SSL)
|
||||
- Soporta la solicitud y renovación automática de certificados SSL con un clic
|
||||
- Para elementos de configuración más avanzados, consulta el panel
|
||||
- Corrige rutas de API (la configuración del usuario se creará con la API)
|
||||
- Soporta cambiar las configuraciones por diferentes elementos proporcionados en el panel.
|
||||
- Soporta exportar/importar base de datos desde el panel
|
||||
|
||||
|
||||
## Configuraciones por Defecto
|
||||
|
||||
<details>
|
||||
<summary>Haz clic para detalles de las configuraciones por defecto</summary>
|
||||
|
||||
### Información
|
||||
|
||||
- **Puerto:** 2053
|
||||
- **Usuario y Contraseña:** Se generarán aleatoriamente si omites la modificación.
|
||||
- **Ruta de la Base de Datos:**
|
||||
- /etc/x-ui/x-ui.db
|
||||
- **Ruta de Configuración de Xray:**
|
||||
- /usr/local/x-ui/bin/config.json
|
||||
- **Ruta del Panel Web sin Implementar SSL:**
|
||||
- http://ip:2053/panel
|
||||
- http://domain:2053/panel
|
||||
- **Ruta del Panel Web con Implementación de SSL:**
|
||||
- https://domain:2053/panel
|
||||
|
||||
</details>
|
||||
|
||||
## [Configuración WARP](https://gitlab.com/fscarmen/warp)
|
||||
|
||||
<details>
|
||||
<summary>Haz clic para detalles de la configuración WARP</summary>
|
||||
|
||||
#### Uso
|
||||
|
||||
Si deseas usar enrutamiento a WARP antes de la versión v2.1.0, sigue los pasos a continuación:
|
||||
|
||||
**1.** Instala WARP en **Modo de Proxy SOCKS**:
|
||||
|
||||
```sh
|
||||
bash <(curl -sSL https://raw.githubusercontent.com/hamid-gh98/x-ui-scripts/main/install_warp_proxy.sh)
|
||||
```
|
||||
|
||||
**2.** Si ya instalaste warp, puedes desinstalarlo usando el siguiente comando:
|
||||
|
||||
```sh
|
||||
warp u
|
||||
```
|
||||
|
||||
**3.** Activa la configuración que necesites en el panel
|
||||
|
||||
Características de Configuración:
|
||||
|
||||
- Bloquear Anuncios
|
||||
- Enrutar Google + Netflix + Spotify + OpenAI (ChatGPT) a WARP
|
||||
- Corregir error 403 de Google
|
||||
|
||||
</details>
|
||||
|
||||
## Límite de IP
|
||||
|
||||
<details>
|
||||
<summary>Haz clic para más detalles del límite de IP</summary>
|
||||
|
||||
#### Uso
|
||||
|
||||
**Nota:** El Límite de IP no funcionará correctamente cuando se use IP Tunnel
|
||||
|
||||
- Para versiones hasta `v1.6.1`:
|
||||
|
||||
- El límite de IP está integrado en el panel.
|
||||
|
||||
- Para versiones `v1.7.0` y posteriores:
|
||||
|
||||
- Para que el Límite de IP funcione correctamente, necesitas instalar fail2ban y sus archivos requeridos siguiendo estos pasos:
|
||||
|
||||
1. Usa el comando `x-ui` dentro de la terminal.
|
||||
2. Selecciona `Gestión de Límite de IP`.
|
||||
3. Elige las opciones apropiadas según tus necesidades.
|
||||
|
||||
- asegúrate de tener ./access.log en tu Configuración de Xray después de la v2.1.3 tenemos una opción para ello
|
||||
|
||||
```sh
|
||||
"log": {
|
||||
"access": "./access.log",
|
||||
"dnsLog": false,
|
||||
"loglevel": "warning"
|
||||
},
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Bot de Telegram
|
||||
|
||||
<details>
|
||||
<summary>Haz clic para más detalles del bot de Telegram</summary>
|
||||
|
||||
#### Uso
|
||||
|
||||
El panel web admite tráfico diario, inicio de sesión en el panel, copia de seguridad de la base de datos, estado del sistema, información del cliente y otras notificaciones y funciones a través del Bot de Telegram. Para usar el bot, debes establecer los parámetros relacionados con el bot en el panel, que incluyen:
|
||||
|
||||
- Token de Telegram
|
||||
- ID de chat de administrador(es)
|
||||
- Hora de Notificación (en sintaxis cron)
|
||||
- Notificación de Fecha de Caducidad
|
||||
- Notificación de Capacidad de Tráfico
|
||||
- Copia de seguridad de la base de datos
|
||||
- Notificación de Carga de CPU
|
||||
|
||||
|
||||
**Sintaxis de referencia:**
|
||||
|
||||
- `30 \* \* \* \* \*` - Notifica a los 30s de cada punto
|
||||
- `0 \*/10 \* \* \* \*` - Notifica en el primer segundo de cada 10 minutos
|
||||
- `@hourly` - Notificación por hora
|
||||
- `@daily` - Notificación diaria (00:00 de la mañana)
|
||||
- `@weekly` - Notificación semanal
|
||||
- `@every 8h` - Notifica cada 8 horas
|
||||
|
||||
### Funcionalidades del Bot de Telegram
|
||||
|
||||
- Reporte periódico
|
||||
- Notificación de inicio de sesión
|
||||
- Notificación de umbral de CPU
|
||||
- Umbral de Notificación para Fecha de Caducidad y Tráfico para informar con anticipación
|
||||
- Soporte para menú de reporte de cliente si el nombre de usuario de Telegram del cliente se agrega a las configuraciones de usuario
|
||||
- Soporte para reporte de tráfico de Telegram buscado con UUID (VMESS/VLESS) o Contraseña (TROJAN) - anónimamente
|
||||
- Bot basado en menú
|
||||
- Buscar cliente por correo electrónico (solo administrador)
|
||||
- Ver todas las Entradas
|
||||
- Ver estado del servidor
|
||||
- Ver clientes agotados
|
||||
- Recibir copia de seguridad bajo demanda y en informes periódicos
|
||||
- Bot multilingüe
|
||||
|
||||
### Configuración del Bot de Telegram
|
||||
|
||||
- Inicia [Botfather](https://t.me/BotFather) en tu cuenta de Telegram:
|
||||

|
||||
|
||||
- Crea un nuevo bot usando el comando /newbot: Te hará 2 preguntas, Un nombre y un nombre de usuario para tu bot. Ten en cuenta que el nombre de usuario debe terminar con la palabra "bot".
|
||||

|
||||
|
||||
- Inicia el bot que acabas de crear. Puedes encontrar el enlace a tu bot aquí.
|
||||

|
||||
|
||||
- Ingresa a tu panel y configura los ajustes del bot de Telegram como se muestra a continuación:
|
||||

|
||||
|
||||
Ingresa el token de tu bot en el campo de entrada número 3.
|
||||
Ingresa el ID de chat de usuario en el campo de entrada número 4. Las cuentas de Telegram con esta ID serán los administradores del bot. (Puedes ingresar más de uno, solo sepáralos con ,)
|
||||
|
||||
- ¿Cómo obtener el ID de chat de Telegram? Usa este [bot](https://t.me/useridinfobot), Inicia el bot y te dará el ID de chat del usuario de Telegram.
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
## Rutas de API
|
||||
|
||||
<details>
|
||||
<summary>Haz clic para más detalles de las rutas de API</summary>
|
||||
|
||||
#### Uso
|
||||
|
||||
- `/login` con `POST` datos de usuario: `{username: '', password: ''}` para iniciar sesión
|
||||
- `/panel/api/inbounds` base para las siguientes acciones:
|
||||
|
||||
| Método | Ruta | Acción |
|
||||
| :----: | ---------------------------------- | -------------------------------------------------------- |
|
||||
| `GET` | `"/list"` | Obtener todas los Entradas |
|
||||
| `GET` | `"/get/:id"` | Obtener Entrada con inbound.id |
|
||||
| `GET` | `"/getClientTraffics/:email"` | Obtener Tráficos del Cliente con email |
|
||||
| `GET` | `"/createbackup"` | El bot de Telegram envía copia de seguridad a los admins |
|
||||
| `POST` | `"/add"` | Agregar Entrada |
|
||||
| `POST` | `"/del/:id"` | Eliminar Entrada |
|
||||
| `POST` | `"/update/:id"` | Actualizar Entrada |
|
||||
| `POST` | `"/clientIps/:email"` | Dirección IP del Cliente |
|
||||
| `POST` | `"/clearClientIps/:email"` | Borrar Dirección IP del Cliente |
|
||||
| `POST` | `"/addClient"` | Agregar Cliente a la Entrada |
|
||||
| `POST` | `"/:id/delClient/:clientId"` | Eliminar Cliente por clientId\* |
|
||||
| `POST` | `"/updateClient/:clientId"` | Actualizar Cliente por clientId\* |
|
||||
| `POST` | `"/:id/resetClientTraffic/:email"` | Restablecer Tráfico del Cliente |
|
||||
| `POST` | `"/resetAllTraffics"` | Restablecer tráfico de todos las Entradas |
|
||||
| `POST` | `"/resetAllClientTraffics/:id"` | Restablecer tráfico de todos los clientes en una Entrada |
|
||||
| `POST` | `"/delDepletedClients/:id"` | Eliminar clientes agotados de la entrada (-1: todos) |
|
||||
| `POST` | `"/onlines"` | Obtener usuarios en línea (lista de correos electrónicos)|
|
||||
|
||||
\*- El campo `clientId` debe llenarse por:
|
||||
|
||||
- `client.id` para VMESS y VLESS
|
||||
- `client.password` para TROJAN
|
||||
- `client.email` para Shadowsocks
|
||||
|
||||
|
||||
- [Documentación de API](https://documenter.getpostman.com/view/16802678/2s9YkgD5jm)
|
||||
- [<img src="https://run.pstmn.io/button.svg" alt="Run In Postman" style="width: 128px; height: 32px;">](https://app.getpostman.com/run-collection/16802678-1a4c9270-ac77-40ed-959a-7aa56dc4a415?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D16802678-1a4c9270-ac77-40ed-959a-7aa56dc4a415%26entityType%3Dcollection%26workspaceId%3D2cd38c01-c851-4a15-a972-f181c23359d9)
|
||||
</details>
|
||||
|
||||
## Variables de Entorno
|
||||
|
||||
<details>
|
||||
<summary>Haz clic para más detalles de las variables de entorno</summary>
|
||||
|
||||
#### Uso
|
||||
|
||||
| Variable | Tipo | Predeterminado|
|
||||
| -------------- | :--------------------------------------------: | :------------ |
|
||||
| XUI_LOG_LEVEL | `"debug"` \| `"info"` \| `"warn"` \| `"error"` | `"info"` |
|
||||
| XUI_DEBUG | `boolean` | `false` |
|
||||
| XUI_BIN_FOLDER | `string` | `"bin"` |
|
||||
| XUI_DB_FOLDER | `string` | `"/etc/x-ui"` |
|
||||
| XUI_LOG_FOLDER | `string` | `"/var/log"` |
|
||||
|
||||
Ejemplo:
|
||||
|
||||
```sh
|
||||
XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Vista previa
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## Un agradecimiento especial a
|
||||
|
||||
- [alireza0](https://github.com/alireza0/)
|
||||
|
||||
## Reconocimientos
|
||||
|
||||
- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (Licencia: **GPL-3.0**): _Reglas de enrutamiento mejoradas de v2ray/xray y v2ray/xray-clients con dominios iraníes integrados y un enfoque en seguridad y bloqueo de anuncios._
|
||||
- [Vietnam Adblock rules](https://github.com/vuong2023/vn-v2ray-rules) (License: **GPL-3.0**): _Un dominio alojado en Vietnam y una lista de bloqueo con la máxima eficiencia para vietnamitas._
|
||||
|
||||
## Estrellas a lo largo del tiempo
|
||||
|
||||
[](https://starchart.cc/MHSanaei/3x-ui)
|
||||
19
README.md
19
README.md
@@ -1,5 +1,7 @@
|
||||
# 3X-UI
|
||||
|
||||
[English](/README.md) | [Chinese](/README.zh.md) | [Español](/README.es_ES.md)
|
||||
|
||||
<p align="center"><a href="#"><img src="./media/3X-UI.png" alt="Image"></a></p>
|
||||
|
||||
**An Advanced Web Panel • Built on Xray Core**
|
||||
@@ -26,10 +28,10 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
|
||||
|
||||
## Install Custom Version
|
||||
|
||||
To install your desired version, add the version to the end of the installation command. e.g., ver `v2.2.1`:
|
||||
To install your desired version, add the version to the end of the installation command. e.g., ver `v2.2.7`:
|
||||
|
||||
```
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v2.2.1
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v2.2.7
|
||||
```
|
||||
|
||||
## SSL Certificate
|
||||
@@ -77,6 +79,7 @@ case "${ARCH}" in
|
||||
armv7* | armv7) XUI_ARCH="armv7" ;;
|
||||
armv6* | armv6) XUI_ARCH="armv6" ;;
|
||||
armv5* | armv5) XUI_ARCH="armv5" ;;
|
||||
s390x) echo 's390x' ;;
|
||||
*) XUI_ARCH="amd64" ;;
|
||||
esac
|
||||
|
||||
@@ -95,6 +98,7 @@ case "${ARCH}" in
|
||||
armv7* | armv7) XUI_ARCH="armv7" ;;
|
||||
armv6* | armv6) XUI_ARCH="armv6" ;;
|
||||
armv5* | armv5) XUI_ARCH="armv5" ;;
|
||||
s390x) echo 's390x' ;;
|
||||
*) XUI_ARCH="amd64" ;;
|
||||
esac
|
||||
|
||||
@@ -182,7 +186,8 @@ remove 3x-ui from docker
|
||||
- Manjaro
|
||||
- Armbian
|
||||
- AlmaLinux 9+
|
||||
- Rockylinux 9+
|
||||
- Rocky Linux 9+
|
||||
- Oracle Linux 8+
|
||||
|
||||
## Supported Architectures and Devices
|
||||
|
||||
@@ -202,6 +207,8 @@ Our platform offers compatibility with a diverse range of architectures and devi
|
||||
- **armv6 / arm / arm32**: Geared towards very old embedded devices, this architecture, while less prevalent, is still in use. Devices such as Raspberry Pi 1, Raspberry Pi Zero/Zero W, rely on this architecture.
|
||||
|
||||
- **armv5 / arm / arm32**: An older architecture primarily associated with early embedded systems, it is less common today but may still be found in legacy devices like early Raspberry Pi versions and some older smartphones.
|
||||
|
||||
- **s390x**: This architecture is commonly used in IBM mainframe computers and offers high performance and reliability for enterprise workloads.
|
||||
</details>
|
||||
|
||||
## Languages
|
||||
@@ -311,9 +318,9 @@ If you want to use routing to WARP before v2.1.0 follow steps as below:
|
||||
|
||||
```sh
|
||||
"log": {
|
||||
"access": "./access.log",
|
||||
"dnsLog": false,
|
||||
"loglevel": "warning"
|
||||
"access": "./access.log",
|
||||
"dnsLog": false,
|
||||
"loglevel": "warning"
|
||||
},
|
||||
```
|
||||
|
||||
|
||||
473
README.zh.md
Normal file
473
README.zh.md
Normal file
@@ -0,0 +1,473 @@
|
||||
# 3X-UI
|
||||
|
||||
[English](/README.md) | [Chinese](/README.zh.md)
|
||||
|
||||
<p align="center"><a href="#"><img src="./media/3X-UI.png" alt="Image"></a></p>
|
||||
|
||||
**一个更好的面板 • 基于Xray Core构建**
|
||||
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
[](#)
|
||||
[](#)
|
||||
[](#)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
> **Disclaimer:** 此项目仅供个人学习交流,请不要用于非法目的,请不要在生产环境中使用。
|
||||
|
||||
**如果此项目对你有用,请给一个**:star2:
|
||||
|
||||
<p align="left"><a href="#"><img width="125" src="https://github.com/MHSanaei/3x-ui/assets/115543613/7aa895dd-048a-42e7-989b-afd41a74e2e1" alt="Image"></a></p>
|
||||
|
||||
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
|
||||
|
||||
## 安装 & 升级
|
||||
|
||||
```
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
|
||||
```
|
||||
|
||||
## 安装指定版本
|
||||
|
||||
要安装所需的版本,请将该版本添加到安装命令的末尾。 e.g., ver `v2.2.6`:
|
||||
|
||||
```
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v2.2.6
|
||||
```
|
||||
|
||||
## SSL 认证
|
||||
|
||||
<details>
|
||||
<summary>点击查看 SSL 认证</summary>
|
||||
|
||||
### Cloudflare
|
||||
|
||||
管理脚本具有用于 Cloudflare 的内置 SSL 证书应用程序。若要使用此脚本申请证书,需要满足以下条件:
|
||||
|
||||
- Cloudflare 邮箱地址
|
||||
- Cloudflare Global API Key
|
||||
- 域名已通过 cloudflare 解析到当前服务器
|
||||
|
||||
**1:** 在终端中运行`x-ui`, 选择 `Cloudflare SSL Certificate`.
|
||||
|
||||
|
||||
### Certbot
|
||||
```
|
||||
apt-get install certbot -y
|
||||
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com
|
||||
certbot renew --dry-run
|
||||
```
|
||||
|
||||
***Tip:*** *管理脚本具有 Certbot 。使用 `x-ui` 命令, 选择 `SSL Certificate Management`.*
|
||||
|
||||
</details>
|
||||
|
||||
## 手动安装 & 升级
|
||||
|
||||
<details>
|
||||
<summary>点击查看 手动安装 & 升级</summary>
|
||||
|
||||
#### 使用
|
||||
|
||||
1. 若要将最新版本的压缩包直接下载到服务器,请运行以下命令:
|
||||
|
||||
```sh
|
||||
ARCH=$(uname -m)
|
||||
case "${ARCH}" in
|
||||
x86_64 | x64 | amd64) XUI_ARCH="amd64" ;;
|
||||
i*86 | x86) XUI_ARCH="386" ;;
|
||||
armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;;
|
||||
armv7* | armv7) XUI_ARCH="armv7" ;;
|
||||
armv6* | armv6) XUI_ARCH="armv6" ;;
|
||||
armv5* | armv5) XUI_ARCH="armv5" ;;
|
||||
*) XUI_ARCH="amd64" ;;
|
||||
esac
|
||||
|
||||
|
||||
wget https://github.com/MHSanaei/3x-ui/releases/latest/download/x-ui-linux-${XUI_ARCH}.tar.gz
|
||||
```
|
||||
|
||||
2. 下载压缩包后,执行以下命令安装或升级 x-ui:
|
||||
|
||||
```sh
|
||||
ARCH=$(uname -m)
|
||||
case "${ARCH}" in
|
||||
x86_64 | x64 | amd64) XUI_ARCH="amd64" ;;
|
||||
i*86 | x86) XUI_ARCH="386" ;;
|
||||
armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;;
|
||||
armv7* | armv7) XUI_ARCH="armv7" ;;
|
||||
armv6* | armv6) XUI_ARCH="armv6" ;;
|
||||
armv5* | armv5) XUI_ARCH="armv5" ;;
|
||||
*) XUI_ARCH="amd64" ;;
|
||||
esac
|
||||
|
||||
cd /root/
|
||||
rm -rf x-ui/ /usr/local/x-ui/ /usr/bin/x-ui
|
||||
tar zxvf x-ui-linux-${XUI_ARCH}.tar.gz
|
||||
chmod +x x-ui/x-ui x-ui/bin/xray-linux-* x-ui/x-ui.sh
|
||||
cp x-ui/x-ui.sh /usr/bin/x-ui
|
||||
cp -f x-ui/x-ui.service /etc/systemd/system/
|
||||
mv x-ui/ /usr/local/
|
||||
systemctl daemon-reload
|
||||
systemctl enable x-ui
|
||||
systemctl restart x-ui
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## 通过Docker安装
|
||||
|
||||
<details>
|
||||
<summary>点击查看 通过Docker安装</summary>
|
||||
|
||||
#### 使用
|
||||
|
||||
1. 安装Docker:
|
||||
|
||||
```sh
|
||||
bash <(curl -sSL https://get.docker.com)
|
||||
```
|
||||
|
||||
2. 克隆仓库:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/MHSanaei/3x-ui.git
|
||||
cd 3x-ui
|
||||
```
|
||||
|
||||
3. 运行服务:
|
||||
|
||||
```sh
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
或
|
||||
|
||||
```sh
|
||||
docker run -itd \
|
||||
-e XRAY_VMESS_AEAD_FORCED=false \
|
||||
-v $PWD/db/:/etc/x-ui/ \
|
||||
-v $PWD/cert/:/root/cert/ \
|
||||
--network=host \
|
||||
--restart=unless-stopped \
|
||||
--name 3x-ui \
|
||||
ghcr.io/mhsanaei/3x-ui:latest
|
||||
```
|
||||
|
||||
更新至最新版本
|
||||
|
||||
```sh
|
||||
cd 3x-ui
|
||||
docker compose down
|
||||
docker compose pull 3x-ui
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
从Docker中删除3x-ui
|
||||
|
||||
```sh
|
||||
docker stop 3x-ui
|
||||
docker rm 3x-ui
|
||||
cd --
|
||||
rm -r 3x-ui
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
## 建议使用的操作系统
|
||||
|
||||
- Ubuntu 20.04+
|
||||
- Debian 11+
|
||||
- CentOS 8+
|
||||
- Fedora 36+
|
||||
- Arch Linux
|
||||
- Manjaro
|
||||
- Armbian
|
||||
- AlmaLinux 9+
|
||||
- Rockylinux 9+
|
||||
|
||||
## 支持的架构和设备
|
||||
<details>
|
||||
<summary>点击查看 支持的架构和设备</summary>
|
||||
|
||||
我们的平台提供与各种架构和设备的兼容性,确保在各种计算环境中的灵活性。以下是我们支持的关键架构:
|
||||
|
||||
- **amd64**: 这种流行的架构是个人计算机和服务器的标准,可以无缝地适应大多数现代操作系统。
|
||||
|
||||
- **x86 / i386**: 这种架构在台式机和笔记本电脑中被广泛采用,得到了众多操作系统和应用程序的广泛支持,包括但不限于 Windows、macOS 和 Linux 系统。
|
||||
|
||||
- **armv8 / arm64 / aarch64**: 这种架构专为智能手机和平板电脑等当代移动和嵌入式设备量身定制,以 Raspberry Pi 4、Raspberry Pi 3、Raspberry Pi Zero 2/Zero 2 W、Orange Pi 3 LTS 等设备为例。
|
||||
|
||||
- **armv7 / arm / arm32**: 作为较旧的移动和嵌入式设备的架构,它仍然广泛用于Orange Pi Zero LTS、Orange Pi PC Plus、Raspberry Pi 2等设备。
|
||||
|
||||
- **armv6 / arm / arm32**: 这种架构面向非常老旧的嵌入式设备,虽然不太普遍,但仍在使用中。Raspberry Pi 1、Raspberry Pi Zero/Zero W 等设备都依赖于这种架构。
|
||||
|
||||
- **armv5 / arm / arm32**: 它是一种主要与早期嵌入式系统相关的旧架构,目前不太常见,但仍可能出现在早期 Raspberry Pi 版本和一些旧智能手机等传统设备中。
|
||||
</details>
|
||||
|
||||
## Languages
|
||||
|
||||
- English(英语)
|
||||
- Farsi(伊朗语)
|
||||
- Chinese(中文)
|
||||
- Russian(俄语)
|
||||
- Vietnamese(越南语)
|
||||
- Spanish(西班牙语)
|
||||
- Indonesian (印度尼西亚语)
|
||||
- Ukrainian(乌克兰语)
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
- 系统状态监控
|
||||
- 在所有入站和客户端中搜索
|
||||
- 深色/浅色主题
|
||||
- 支持多用户和多协议
|
||||
- 支持多种协议,包括 VMess、VLESS、Trojan、Shadowsocks、Dokodemo-door、Socks、HTTP、wireguard
|
||||
- 支持 XTLS 原生协议,包括 RPRX-Direct、Vision、REALITY
|
||||
- 流量统计、流量限制、过期时间限制
|
||||
- 可自定义的 Xray配置模板
|
||||
- 支持HTTPS访问面板(自建域名+SSL证书)
|
||||
- 支持一键式SSL证书申请和自动续费
|
||||
- 更多高级配置项目请参考面板
|
||||
- 修复了 API 路由(用户设置将使用 API 创建)
|
||||
- 支持通过面板中提供的不同项目更改配置。
|
||||
- 支持从面板导出/导入数据库
|
||||
|
||||
|
||||
## 默认设置
|
||||
|
||||
<details>
|
||||
<summary>点击查看 默认设置</summary>
|
||||
|
||||
### 信息
|
||||
|
||||
- **端口:** 2053
|
||||
- **用户名 & 密码:** 当您跳过设置时,此项会随机生成。
|
||||
- **数据库路径:**
|
||||
- /etc/x-ui/x-ui.db
|
||||
- **Xray 配置路径:**
|
||||
- /usr/local/x-ui/bin/config.json
|
||||
- **面板链接(无SSL):**
|
||||
- http://ip:2053/panel
|
||||
- http://domain:2053/panel
|
||||
- **面板链接(有SSL):**
|
||||
- https://domain:2053/panel
|
||||
|
||||
</details>
|
||||
|
||||
## [WARP 配置](https://gitlab.com/fscarmen/warp)
|
||||
|
||||
<details>
|
||||
<summary>点击查看 WARP 配置</summary>
|
||||
|
||||
#### 使用
|
||||
|
||||
如果要在 v2.1.0 之前使用 WARP 路由,请按照以下步骤操作:
|
||||
|
||||
**1.** 在 **SOCKS Proxy Mode** 模式中安装Wrap
|
||||
|
||||
```sh
|
||||
bash <(curl -sSL https://raw.githubusercontent.com/hamid-gh98/x-ui-scripts/main/install_warp_proxy.sh)
|
||||
```
|
||||
|
||||
**2.** 如果您已经安装了 warp,您可以使用以下命令卸载:
|
||||
|
||||
```sh
|
||||
warp u
|
||||
```
|
||||
|
||||
**3.** 在面板中打开您需要的配置
|
||||
|
||||
配置:
|
||||
|
||||
- Block Ads
|
||||
- Route Google + Netflix + Spotify + OpenAI (ChatGPT) to WARP
|
||||
- Fix Google 403 error
|
||||
|
||||
</details>
|
||||
|
||||
## IP 限制
|
||||
|
||||
<details>
|
||||
<summary>点击查看 IP 限制</summary>
|
||||
|
||||
#### 使用
|
||||
|
||||
**注意:** 使用 IP 隧道时,IP 限制无法正常工作。
|
||||
|
||||
- 适用于最高 `v1.6.1` :
|
||||
|
||||
- IP 限制 已被集成在面板中。
|
||||
|
||||
- 适用于 `v1.7.0` 以及更新的版本:
|
||||
|
||||
- 要使 IP 限制正常工作,您需要按照以下步骤安装 fail2ban 及其所需的文件:
|
||||
|
||||
1. 使用面板内置的 `x-ui` 指令
|
||||
2. 选择 `IP Limit Management`.
|
||||
3. 根据您的需要选择合适的选项。
|
||||
|
||||
- 确保您的 Xray 配置上有 ./access.log 。在 v2.1.3 之后,我们有一个选项。
|
||||
|
||||
```sh
|
||||
"log": {
|
||||
"access": "./access.log",
|
||||
"dnsLog": false,
|
||||
"loglevel": "warning"
|
||||
},
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Telegram 机器人
|
||||
|
||||
<details>
|
||||
<summary>点击查看 Telegram 机器人</summary>
|
||||
|
||||
#### 使用
|
||||
|
||||
Web 面板通过 Telegram Bot 支持每日流量、面板登录、数据库备份、系统状态、客户端信息等通知和功能。要使用机器人,您需要在面板中设置机器人相关参数,包括:
|
||||
|
||||
- 电报令牌
|
||||
- 管理员聊天 ID
|
||||
- 通知时间(cron 语法)
|
||||
- 到期日期通知
|
||||
- 流量上限通知
|
||||
- 数据库备份
|
||||
- CPU 负载通知
|
||||
|
||||
|
||||
**参考:**
|
||||
|
||||
- `30 \* \* \* \* \*` - 在每个点的 30 秒处通知
|
||||
- `0 \*/10 \* \* \* \*` - 每 10 分钟的第一秒通知
|
||||
- `@hourly` - 每小时通知
|
||||
- `@daily` - 每天通知 (00:00)
|
||||
- `@weekly` - 每周通知
|
||||
- `@every 8h` - 每8小时通知
|
||||
|
||||
### Telegram Bot 功能
|
||||
|
||||
- 定期报告
|
||||
- 登录通知
|
||||
- CPU 阈值通知
|
||||
- 提前报告的过期时间和流量阈值
|
||||
- 如果将客户的电报用户名添加到用户的配置中,则支持客户端报告菜单
|
||||
- 支持使用UUID(VMESS/VLESS)或密码(TROJAN)搜索报文流量报告 - 匿名
|
||||
- 基于菜单的机器人
|
||||
- 通过电子邮件搜索客户端(仅限管理员)
|
||||
- 检查所有入库
|
||||
- 检查服务器状态
|
||||
- 检查耗尽的用户
|
||||
- 根据请求和定期报告接收备份
|
||||
- 多语言机器人
|
||||
|
||||
### 注册 Telegram bot
|
||||
|
||||
- 与 [Botfather](https://t.me/BotFather) 对话:
|
||||

|
||||
|
||||
- 使用 /newbot 创建新机器人:你需要提供机器人名称以及用户名,注意名称中末尾要包含“bot”
|
||||

|
||||
|
||||
- 启动您刚刚创建的机器人。可以在此处找到机器人的链接。
|
||||

|
||||
|
||||
- 输入您的面板并配置 Telegram 机器人设置,如下所示:
|
||||

|
||||
|
||||
在输入字段编号 3 中输入机器人令牌。
|
||||
在输入字段编号 4 中输入用户 ID。具有此 id 的 Telegram 帐户将是机器人管理员。 (您可以输入多个,只需将它们用“ ,”分开即可)
|
||||
|
||||
- 如何获取TG ID? 使用 [bot](https://t.me/useridinfobot), 启动机器人,它会给你 Telegram 用户 ID。
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
## API 路由
|
||||
|
||||
<details>
|
||||
<summary>点击查看 API 路由</summary>
|
||||
|
||||
#### 使用
|
||||
|
||||
- `/login` 使用 `POST` 用户名称 & 密码: `{username: '', password: ''}` 登录
|
||||
- `/panel/api/inbounds` 以下操作的基础:
|
||||
|
||||
| 方法 | 路径 | 操作 |
|
||||
| :----: | ---------------------------------- | ------------------------------------------- |
|
||||
| `GET` | `"/list"` | 获取所有入站 |
|
||||
| `GET` | `"/get/:id"` | 获取所有入站以及inbound.id |
|
||||
| `GET` | `"/getClientTraffics/:email"` | 通过电子邮件获取客户端流量 |
|
||||
| `GET` | `"/createbackup"` | Telegram 机器人向管理员发送备份 |
|
||||
| `POST` | `"/add"` | 添加入站 |
|
||||
| `POST` | `"/del/:id"` | 删除入站 |
|
||||
| `POST` | `"/update/:id"` | 更新入站 |
|
||||
| `POST` | `"/clientIps/:email"` | 客户端 IP 地址 |
|
||||
| `POST` | `"/clearClientIps/:email"` | 清除客户端 IP 地址 |
|
||||
| `POST` | `"/addClient"` | 将客户端添加到入站 |
|
||||
| `POST` | `"/:id/delClient/:clientId"` | 通过 clientId\* 删除客户端 |
|
||||
| `POST` | `"/updateClient/:clientId"` | 通过 clientId\* 更新客户端 |
|
||||
| `POST` | `"/:id/resetClientTraffic/:email"` | 重置客户端的流量 |
|
||||
| `POST` | `"/resetAllTraffics"` | 重置所有入站的流量 |
|
||||
| `POST` | `"/resetAllClientTraffics/:id"` | 重置入站中所有客户端的流量 |
|
||||
| `POST` | `"/delDepletedClients/:id"` | 删除入站耗尽的客户端 (-1: all) |
|
||||
| `POST` | `"/onlines"` | 获取在线用户 ( 电子邮件列表 ) |
|
||||
|
||||
\*- `clientId` 项应该使用下列数据
|
||||
|
||||
- `client.id` VMESS and VLESS
|
||||
- `client.password` TROJAN
|
||||
- `client.email` Shadowsocks
|
||||
|
||||
|
||||
- [API 文档](https://documenter.getpostman.com/view/16802678/2s9YkgD5jm)
|
||||
- [<img src="https://run.pstmn.io/button.svg" alt="Run In Postman" style="width: 128px; height: 32px;">](https://app.getpostman.com/run-collection/16802678-1a4c9270-ac77-40ed-959a-7aa56dc4a415?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D16802678-1a4c9270-ac77-40ed-959a-7aa56dc4a415%26entityType%3Dcollection%26workspaceId%3D2cd38c01-c851-4a15-a972-f181c23359d9)
|
||||
</details>
|
||||
|
||||
## 环境变量
|
||||
|
||||
<details>
|
||||
<summary>点击查看 环境变量</summary>
|
||||
|
||||
#### Usage
|
||||
|
||||
| 变量 | Type | 默认 |
|
||||
| -------------- | :--------------------------------------------: | :------------ |
|
||||
| XUI_LOG_LEVEL | `"debug"` \| `"info"` \| `"warn"` \| `"error"` | `"info"` |
|
||||
| XUI_DEBUG | `boolean` | `false` |
|
||||
| XUI_BIN_FOLDER | `string` | `"bin"` |
|
||||
| XUI_DB_FOLDER | `string` | `"/etc/x-ui"` |
|
||||
| XUI_LOG_FOLDER | `string` | `"/var/log"` |
|
||||
|
||||
例子:
|
||||
|
||||
```sh
|
||||
XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## 预览
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## 特别感谢
|
||||
|
||||
- [alireza0](https://github.com/alireza0/)
|
||||
|
||||
## 致谢
|
||||
|
||||
- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (License: **GPL-3.0**): _Enhanced v2ray/xray and v2ray/xray-clients routing rules with built-in Iranian domains and a focus on security and adblocking._
|
||||
- [Vietnam Adblock rules](https://github.com/vuong2023/vn-v2ray-rules) (License: **GPL-3.0**): _A hosted domain hosted in Vietnam and blocklist with the most efficiency for Vietnamese._
|
||||
|
||||
## Star趋势
|
||||
|
||||
[](https://starchart.cc/MHSanaei/3x-ui)
|
||||
@@ -1 +1 @@
|
||||
2.2.1
|
||||
2.2.7
|
||||
@@ -2,6 +2,7 @@ package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"x-ui/util/json_util"
|
||||
"x-ui/xray"
|
||||
)
|
||||
|
||||
56
go.mod
56
go.mod
@@ -4,43 +4,43 @@ go 1.22.0
|
||||
|
||||
require (
|
||||
github.com/Calidity/gin-sessions v1.3.1
|
||||
github.com/gin-contrib/gzip v0.0.6
|
||||
github.com/gin-contrib/gzip v1.0.0
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/goccy/go-json v0.10.2
|
||||
github.com/mymmrac/telego v0.29.1
|
||||
github.com/nicksnyder/go-i18n/v2 v2.4.0
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||
github.com/pelletier/go-toml/v2 v2.1.1
|
||||
github.com/pelletier/go-toml/v2 v2.2.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/shirou/gopsutil/v3 v3.24.1
|
||||
github.com/shirou/gopsutil/v3 v3.24.2
|
||||
github.com/valyala/fasthttp v1.52.0
|
||||
github.com/xtls/xray-core v1.8.8
|
||||
github.com/xtls/xray-core v1.8.10
|
||||
go.uber.org/atomic v1.11.0
|
||||
golang.org/x/text v0.14.0
|
||||
google.golang.org/grpc v1.62.0
|
||||
google.golang.org/grpc v1.62.1
|
||||
gorm.io/driver/sqlite v1.5.5
|
||||
gorm.io/gorm v1.25.7
|
||||
gorm.io/gorm v1.25.9
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/bytedance/sonic v1.10.2 // indirect
|
||||
github.com/bytedance/sonic v1.11.3 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
||||
github.com/chenzhuoyu/iasm v0.9.1 // indirect
|
||||
github.com/cloudflare/circl v1.3.7 // indirect
|
||||
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect
|
||||
github.com/fasthttp/router v1.4.22 // indirect
|
||||
github.com/fasthttp/router v1.5.0 // indirect
|
||||
github.com/francoispqt/gojay v1.2.13 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.17.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.19.0 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/btree v1.1.2 // indirect
|
||||
github.com/google/pprof v0.0.0-20240225044709-fd706174c886 // indirect
|
||||
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 // indirect
|
||||
github.com/gorilla/context v1.1.2 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/gorilla/sessions v1.2.2 // indirect
|
||||
@@ -51,49 +51,49 @@ require (
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.17.7 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20231016141302-07b5767bb0ed // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.19 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.15.0 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.16.0 // indirect
|
||||
github.com/pires/go-proxyproto v0.7.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect
|
||||
github.com/quic-go/quic-go v0.41.0 // indirect
|
||||
github.com/quic-go/quic-go v0.42.0 // indirect
|
||||
github.com/refraction-networking/utls v1.6.3 // indirect
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
||||
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
||||
github.com/sagernet/sing v0.3.2 // indirect
|
||||
github.com/sagernet/sing v0.3.8 // indirect
|
||||
github.com/sagernet/sing-shadowsocks v0.2.6 // indirect
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
|
||||
github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511 // indirect
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fastjson v1.6.4 // indirect
|
||||
github.com/vishvananda/netlink v1.2.1-beta.2.0.20230316163032-ced5aaba43e3 // indirect
|
||||
github.com/vishvananda/netns v0.0.4 // indirect
|
||||
github.com/xtls/reality v0.0.0-20231112171332-de1173cf2b19 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.3 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.uber.org/mock v0.4.0 // indirect
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||
golang.org/x/arch v0.6.0 // indirect
|
||||
golang.org/x/crypto v0.19.0 // indirect
|
||||
golang.org/x/arch v0.7.0 // indirect
|
||||
golang.org/x/crypto v0.21.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
|
||||
golang.org/x/mod v0.15.0 // indirect
|
||||
golang.org/x/net v0.21.0 // indirect
|
||||
golang.org/x/sys v0.17.0 // indirect
|
||||
golang.org/x/mod v0.16.0 // indirect
|
||||
golang.org/x/net v0.22.0 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.18.0 // indirect
|
||||
golang.org/x/tools v0.19.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c // indirect
|
||||
google.golang.org/protobuf v1.32.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240308144416-29370a3891b7 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20231104011432-48a6d7d5bd0b // indirect
|
||||
lukechampine.com/blake3 v1.2.1 // indirect
|
||||
|
||||
159
go.sum
159
go.sum
@@ -20,8 +20,8 @@ github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBT
|
||||
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
|
||||
github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE=
|
||||
github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
|
||||
github.com/bytedance/sonic v1.11.3 h1:jRN+yEjakWh8aK5FzrciUHG8OFXK+4/KrAX/ysEtHAA=
|
||||
github.com/bytedance/sonic v1.11.3/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
|
||||
@@ -33,7 +33,6 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
|
||||
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
|
||||
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
|
||||
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -41,8 +40,8 @@ github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165/go.mod h1:c9O8+fp
|
||||
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 h1:y7y0Oa6UawqTFPCDw9JG6pdKt4F9pAhHv0B7FMGaGD0=
|
||||
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/fasthttp/router v1.4.22 h1:qwWcYBbndVDwts4dKaz+A2ehsnbKilmiP6pUhXBfYKo=
|
||||
github.com/fasthttp/router v1.4.22/go.mod h1:KeMvHLqhlB9vyDWD5TSvTccl9qeWrjSSiTJrJALHKV0=
|
||||
github.com/fasthttp/router v1.5.0 h1:3Qbbo27HAPzwbpRzgiV5V9+2faPkPt3eNuRaDV6LYDA=
|
||||
github.com/fasthttp/router v1.5.0/go.mod h1:FddcKNXFZg1imHcy+uKB0oo/o6yE9zD3wNguqlhWDak=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
|
||||
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
|
||||
@@ -52,35 +51,29 @@ github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcP
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
|
||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
|
||||
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
||||
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
|
||||
github.com/gin-contrib/gzip v1.0.0 h1:UKN586Po/92IDX6ie5CWLgMI81obiIp5nSP85T3wlTk=
|
||||
github.com/gin-contrib/gzip v1.0.0/go.mod h1:CtG7tQrPB3vIBo6Gat9FVUsis+1emjvQqd66ME5TdnE=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
|
||||
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
||||
github.com/go-playground/validator/v10 v10.17.0 h1:SmVVlfAOtlZncTxRuinDPomC2DkXJ4E5T9gDA0AIH74=
|
||||
github.com/go-playground/validator/v10 v10.17.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4=
|
||||
github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
@@ -92,14 +85,12 @@ github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/
|
||||
github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
|
||||
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
@@ -111,8 +102,8 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20240225044709-fd706174c886 h1:JSJUTZTQT1Gzb2ROdAKOY3HwzBYcclS2GgumhMfHqjw=
|
||||
github.com/google/pprof v0.0.0-20240225044709-fd706174c886/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
|
||||
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 h1:y3N7Bm7Y9/CtpiVkw/ZWj6lSlDF3F74SfKwfTCer72Q=
|
||||
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
|
||||
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
||||
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
@@ -145,28 +136,23 @@ github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuV
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/lufia/plan9stats v0.0.0-20231016141302-07b5767bb0ed h1:036IscGBfJsFIgJQzlui7nK1Ncm0tp2ktmPj8xO4N/0=
|
||||
github.com/lufia/plan9stats v0.0.0-20231016141302-07b5767bb0ed/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
|
||||
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI=
|
||||
github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
||||
github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
|
||||
@@ -183,8 +169,8 @@ github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJE
|
||||
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4=
|
||||
github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY=
|
||||
github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM=
|
||||
github.com/onsi/ginkgo/v2 v2.16.0 h1:7q1w9frJDzninhXxjZd+Y/x54XNjG/UlRLIYPZafsPM=
|
||||
github.com/onsi/ginkgo/v2 v2.16.0/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs=
|
||||
github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8=
|
||||
github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
|
||||
@@ -192,12 +178,10 @@ github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWEr
|
||||
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
|
||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
|
||||
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
|
||||
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo=
|
||||
github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
|
||||
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
@@ -208,30 +192,28 @@ github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/quic-go/quic-go v0.41.0 h1:aD8MmHfgqTURWNJy48IYFg2OnxwHT3JL7ahGs73lb4k=
|
||||
github.com/quic-go/quic-go v0.41.0/go.mod h1:qCkNjqczPEvgsOnxZ0eCD14lv+B2LHlFAB++CNOh9hA=
|
||||
github.com/quic-go/quic-go v0.42.0 h1:uSfdap0eveIl8KXnipv9K7nlwZ5IqLlYOpJ58u5utpM=
|
||||
github.com/quic-go/quic-go v0.42.0/go.mod h1:132kz4kL3F9vxhW3CtQJLDVwcFe5wdWeJXXijhsO57M=
|
||||
github.com/refraction-networking/utls v1.6.3 h1:MFOfRN35sSx6K5AZNIoESsBuBxS2LCgRilRIdHb6fDc=
|
||||
github.com/refraction-networking/utls v1.6.3/go.mod h1:yil9+7qSl+gBwJqztoQseO6Pr3h62pQoY1lXiNR/FPs=
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/sagernet/sing v0.3.2 h1:CwWcxUBPkMvwgfe2/zUgY5oHG9qOL8Aob/evIFYK9jo=
|
||||
github.com/sagernet/sing v0.3.2/go.mod h1:qHySJ7u8po9DABtMYEkNBcOumx7ZZJf/fbv2sfTkNHE=
|
||||
github.com/sagernet/sing v0.3.8 h1:gm4JKalPhydMYX2zFOTnnd4TXtM/16WFRqSjMepYQQk=
|
||||
github.com/sagernet/sing v0.3.8/go.mod h1:+60H3Cm91RnL9dpVGWDPHt0zTQImO9Vfqt9a4rSambI=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.6 h1:xr7ylAS/q1cQYS8oxKKajhuQcchd5VJJ4K4UZrrpp0s=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.6/go.mod h1:j2YZBIpWIuElPFL/5sJAj470bcn/3QQ5lxZUNKLDNAM=
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
|
||||
github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511 h1:KanIMPX0QdEdB4R3CiimCAbxFrhB3j7h0/OvpYGVQa8=
|
||||
github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg=
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U=
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shirou/gopsutil/v3 v3.24.1 h1:R3t6ondCEvmARp3wxODhXMTLC/klMa87h2PHUw5m7QI=
|
||||
github.com/shirou/gopsutil/v3 v3.24.1/go.mod h1:UU7a2MSBQa+kW1uuDq8DeEBS8kmrnQwsv2b5O513rwU=
|
||||
github.com/shirou/gopsutil/v3 v3.24.2 h1:kcR0erMbLg5/3LcInpw0X/rrPSqq4CDPyI6A6ZRC18Y=
|
||||
github.com/shirou/gopsutil/v3 v3.24.2/go.mod h1:tSg/594BcA+8UdQU2XcW803GWYgdtauFFPgJCJKZlVk=
|
||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
||||
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
||||
@@ -263,6 +245,7 @@ github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
@@ -270,9 +253,9 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
@@ -280,10 +263,8 @@ github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+F
|
||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI=
|
||||
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
@@ -301,10 +282,10 @@ github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1Y
|
||||
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
github.com/xtls/reality v0.0.0-20231112171332-de1173cf2b19 h1:capMfFYRgH9BCLd6A3Er/cH3A9Nz3CU2KwxwOQZIePI=
|
||||
github.com/xtls/reality v0.0.0-20231112171332-de1173cf2b19/go.mod h1:dm4y/1QwzjGaK17ofi0Vs6NpKAHegZky8qk6J2JJZAE=
|
||||
github.com/xtls/xray-core v1.8.8 h1:6ApBa5pNkPZ+I7jyJDZod3v5sadizS/BZr0pW7zcs8o=
|
||||
github.com/xtls/xray-core v1.8.8/go.mod h1:Zp33A8cxnhP5Kt6nguQrMgNH4A/tgq7LE8cBedeNje8=
|
||||
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
|
||||
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
github.com/xtls/xray-core v1.8.10 h1:qxae6gSteonpPI7EZyOyqw5HmRVRzmU07qs0l1GNqz4=
|
||||
github.com/xtls/xray-core v1.8.10/go.mod h1:Mc1t+kLBPE5a1EpsUNKjMLviGz3Y0XywxeEraJZAMlI=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
@@ -314,23 +295,22 @@ go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.6.0 h1:S0JTfE48HbRj80+4tbvZDYsJ3tGv6BUU3XxyZ7CirAc=
|
||||
golang.org/x/arch v0.6.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
|
||||
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
|
||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
|
||||
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -339,9 +319,8 @@ golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73r
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
@@ -360,25 +339,18 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220804214406-8e32c043e418/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@@ -386,12 +358,11 @@ golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxb
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
|
||||
golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
|
||||
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
|
||||
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||
@@ -409,24 +380,19 @@ google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoA
|
||||
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
|
||||
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c h1:NUsgEN92SQQqzfA+YtqYNqYmB3DMMYLlIwUZAQFVFbo=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240308144416-29370a3891b7 h1:em/y72n4XlYRtayY/cVj6pnVzHa//BDA1BdoO+z9mdE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240308144416-29370a3891b7/go.mod h1:UCOku4NytXMJuLQE5VuqA5lX3PcHCBo8pxNyvkf4xBs=
|
||||
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.62.0 h1:HQKZ/fa1bXkX1oFOvSjmZEUL8wLSaZTjCcLAlmZRtdk=
|
||||
google.golang.org/grpc v1.62.0/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
||||
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk=
|
||||
google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
@@ -434,13 +400,12 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E=
|
||||
gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE=
|
||||
gorm.io/gorm v1.25.7 h1:VsD6acwRjz2zFxGO50gPO6AkNs7KKnvfzUjHQhZDz/A=
|
||||
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
gorm.io/gorm v1.25.9 h1:wct0gxZIELDk8+ZqF/MVnHLkA1rvYlBWUMv2EdsK1g8=
|
||||
gorm.io/gorm v1.25.9/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
|
||||
gvisor.dev/gvisor v0.0.0-20231104011432-48a6d7d5bd0b h1:yqkg3pTifuKukuWanp8spDsL4irJkHF5WI0J47hU87o=
|
||||
gvisor.dev/gvisor v0.0.0-20231104011432-48a6d7d5bd0b/go.mod h1:10sU+Uh5KKNv1+2x2A0Gvzt8FjD3ASIhorV3YsauXhk=
|
||||
|
||||
63
install.sh
63
install.sh
@@ -23,7 +23,7 @@ else
|
||||
fi
|
||||
echo "The OS release is: $release"
|
||||
|
||||
arch3xui() {
|
||||
arch() {
|
||||
case "$(uname -m)" in
|
||||
x86_64 | x64 | amd64) echo 'amd64' ;;
|
||||
i*86 | x86) echo '386' ;;
|
||||
@@ -31,16 +31,23 @@ arch3xui() {
|
||||
armv7* | armv7 | arm) echo 'armv7' ;;
|
||||
armv6* | armv6) echo 'armv6' ;;
|
||||
armv5* | armv5) echo 'armv5' ;;
|
||||
s390x) echo 's390x' ;;
|
||||
*) echo -e "${green}Unsupported CPU architecture! ${plain}" && rm -f install.sh && exit 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
echo "arch: $(arch3xui)"
|
||||
echo "arch: $(arch)"
|
||||
|
||||
os_version=""
|
||||
os_version=$(grep -i version_id /etc/os-release | cut -d \" -f2 | cut -d . -f1)
|
||||
|
||||
if [[ "${release}" == "centos" ]]; then
|
||||
if [[ "${release}" == "arch" ]]; then
|
||||
echo "Your OS is Arch Linux"
|
||||
elif [[ "${release}" == "manjaro" ]]; then
|
||||
echo "Your OS is Manjaro"
|
||||
elif [[ "${release}" == "armbian" ]]; then
|
||||
echo "Your OS is Armbian"
|
||||
elif [[ "${release}" == "centos" ]]; then
|
||||
if [[ ${os_version} -lt 8 ]]; then
|
||||
echo -e "${red} Please use CentOS 8 or higher ${plain}\n" && exit 1
|
||||
fi
|
||||
@@ -48,40 +55,46 @@ elif [[ "${release}" == "ubuntu" ]]; then
|
||||
if [[ ${os_version} -lt 20 ]]; then
|
||||
echo -e "${red} Please use Ubuntu 20 or higher version!${plain}\n" && exit 1
|
||||
fi
|
||||
|
||||
elif [[ "${release}" == "fedora" ]]; then
|
||||
if [[ ${os_version} -lt 36 ]]; then
|
||||
echo -e "${red} Please use Fedora 36 or higher version!${plain}\n" && exit 1
|
||||
fi
|
||||
|
||||
elif [[ "${release}" == "debian" ]]; then
|
||||
if [[ ${os_version} -lt 11 ]]; then
|
||||
echo -e "${red} Please use Debian 11 or higher ${plain}\n" && exit 1
|
||||
fi
|
||||
|
||||
elif [[ "${release}" == "almalinux" ]]; then
|
||||
if [[ ${os_version} -lt 9 ]]; then
|
||||
echo -e "${red} Please use AlmaLinux 9 or higher ${plain}\n" && exit 1
|
||||
fi
|
||||
|
||||
elif [[ "${release}" == "rocky" ]]; then
|
||||
if [[ ${os_version} -lt 9 ]]; then
|
||||
echo -e "${red} Please use RockyLinux 9 or higher ${plain}\n" && exit 1
|
||||
echo -e "${red} Please use Rocky Linux 9 or higher ${plain}\n" && exit 1
|
||||
fi
|
||||
elif [[ "${release}" == "oracle" ]]; then
|
||||
if [[ ${os_version} -lt 8 ]]; then
|
||||
echo -e "${red} Please use Oracle Linux 8 or higher ${plain}\n" && exit 1
|
||||
fi
|
||||
elif [[ "${release}" == "arch" ]]; then
|
||||
echo "Your OS is ArchLinux"
|
||||
elif [[ "${release}" == "manjaro" ]]; then
|
||||
echo "Your OS is Manjaro"
|
||||
elif [[ "${release}" == "armbian" ]]; then
|
||||
echo "Your OS is Armbian"
|
||||
|
||||
else
|
||||
echo -e "${red}Failed to check the OS version, please contact the author!${plain}" && exit 1
|
||||
echo -e "${red}Your operating system is not supported by this script.${plain}\n"
|
||||
echo "Please ensure you are using one of the following supported operating systems:"
|
||||
echo "- Ubuntu 20.04+"
|
||||
echo "- Debian 11+"
|
||||
echo "- CentOS 8+"
|
||||
echo "- Fedora 36+"
|
||||
echo "- Arch Linux"
|
||||
echo "- Manjaro"
|
||||
echo "- Armbian"
|
||||
echo "- AlmaLinux 9+"
|
||||
echo "- Rocky Linux 9+"
|
||||
echo "- Oracle Linux 8+"
|
||||
exit 1
|
||||
|
||||
fi
|
||||
|
||||
install_base() {
|
||||
case "${release}" in
|
||||
centos | almalinux | rocky)
|
||||
centos | almalinux | rocky | oracle)
|
||||
yum -y update && yum install -y -q wget curl tar tzdata
|
||||
;;
|
||||
fedora)
|
||||
@@ -141,16 +154,16 @@ install_x-ui() {
|
||||
exit 1
|
||||
fi
|
||||
echo -e "Got x-ui latest version: ${last_version}, beginning the installation..."
|
||||
wget -N --no-check-certificate -O /usr/local/x-ui-linux-$(arch3xui).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${last_version}/x-ui-linux-$(arch3xui).tar.gz
|
||||
wget -N --no-check-certificate -O /usr/local/x-ui-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${last_version}/x-ui-linux-$(arch).tar.gz
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo -e "${red}Downloading x-ui failed, please be sure that your server can access Github ${plain}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
last_version=$1
|
||||
url="https://github.com/MHSanaei/3x-ui/releases/download/${last_version}/x-ui-linux-$(arch3xui).tar.gz"
|
||||
url="https://github.com/MHSanaei/3x-ui/releases/download/${last_version}/x-ui-linux-$(arch).tar.gz"
|
||||
echo -e "Beginning to install x-ui $1"
|
||||
wget -N --no-check-certificate -O /usr/local/x-ui-linux-$(arch3xui).tar.gz ${url}
|
||||
wget -N --no-check-certificate -O /usr/local/x-ui-linux-$(arch).tar.gz ${url}
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo -e "${red}Download x-ui $1 failed,please check the version exists ${plain}"
|
||||
exit 1
|
||||
@@ -162,18 +175,18 @@ install_x-ui() {
|
||||
rm /usr/local/x-ui/ -rf
|
||||
fi
|
||||
|
||||
tar zxvf x-ui-linux-$(arch3xui).tar.gz
|
||||
rm x-ui-linux-$(arch3xui).tar.gz -f
|
||||
tar zxvf x-ui-linux-$(arch).tar.gz
|
||||
rm x-ui-linux-$(arch).tar.gz -f
|
||||
cd x-ui
|
||||
chmod +x x-ui
|
||||
|
||||
# Check the system's architecture and rename the file accordingly
|
||||
if [[ $(arch3xui) == "armv5" || $(arch3xui) == "armv6" || $(arch3xui) == "armv7" ]]; then
|
||||
mv bin/xray-linux-$(arch3xui) bin/xray-linux-arm
|
||||
if [[ $(arch) == "armv5" || $(arch) == "armv6" || $(arch) == "armv7" ]]; then
|
||||
mv bin/xray-linux-$(arch) bin/xray-linux-arm
|
||||
chmod +x bin/xray-linux-arm
|
||||
fi
|
||||
|
||||
chmod +x x-ui bin/xray-linux-$(arch3xui)
|
||||
chmod +x x-ui bin/xray-linux-$(arch)
|
||||
cp -f x-ui.service /etc/systemd/system/
|
||||
wget --no-check-certificate -O /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh
|
||||
chmod +x /usr/local/x-ui/x-ui.sh
|
||||
|
||||
@@ -8,12 +8,14 @@ import (
|
||||
"github.com/op/go-logging"
|
||||
)
|
||||
|
||||
var logger *logging.Logger
|
||||
var logBuffer []struct {
|
||||
time string
|
||||
level logging.Level
|
||||
log string
|
||||
}
|
||||
var (
|
||||
logger *logging.Logger
|
||||
logBuffer []struct {
|
||||
time string
|
||||
level logging.Level
|
||||
log string
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
InitLogger(logging.INFO)
|
||||
|
||||
26
main.go
26
main.go
@@ -8,6 +8,7 @@ import (
|
||||
"os/signal"
|
||||
"syscall"
|
||||
_ "unsafe"
|
||||
|
||||
"x-ui/config"
|
||||
"x-ui/database"
|
||||
"x-ui/logger"
|
||||
@@ -243,21 +244,33 @@ func migrateDb() {
|
||||
}
|
||||
|
||||
func removeSecret() {
|
||||
err := database.InitDB(config.GetDBPath())
|
||||
userService := service.UserService{}
|
||||
|
||||
secretExists, err := userService.CheckSecretExistence()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Println("Error checking secret existence:", err)
|
||||
return
|
||||
}
|
||||
userService := service.UserService{}
|
||||
|
||||
if !secretExists {
|
||||
fmt.Println("No secret exists to remove.")
|
||||
return
|
||||
}
|
||||
|
||||
err = userService.RemoveUserSecret()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Println("Error removing secret:", err)
|
||||
return
|
||||
}
|
||||
|
||||
settingService := service.SettingService{}
|
||||
err = settingService.SetSecretStatus(false)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Println("Error updating secret status:", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Secret removed successfully.")
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -284,6 +297,7 @@ func main() {
|
||||
var remove_secret bool
|
||||
settingCmd.BoolVar(&reset, "reset", false, "reset all settings")
|
||||
settingCmd.BoolVar(&show, "show", false, "show current settings")
|
||||
settingCmd.BoolVar(&remove_secret, "remove_secret", false, "remove secret")
|
||||
settingCmd.IntVar(&port, "port", 0, "set panel port")
|
||||
settingCmd.StringVar(&username, "username", "", "set login username")
|
||||
settingCmd.StringVar(&password, "password", "", "set login password")
|
||||
@@ -342,7 +356,7 @@ func main() {
|
||||
updateTgbotEnableSts(enabletgbot)
|
||||
}
|
||||
default:
|
||||
fmt.Println("except 'run' or 'setting' subcommands")
|
||||
fmt.Println("Invalid subcommands")
|
||||
fmt.Println()
|
||||
runCmd.Usage()
|
||||
fmt.Println()
|
||||
|
||||
BIN
media/3X-UI.png
BIN
media/3X-UI.png
Binary file not shown.
|
Before Width: | Height: | Size: 198 KiB After Width: | Height: | Size: 226 KiB |
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"remarks": "",
|
||||
"dns": {
|
||||
"tag": "dns_out",
|
||||
"queryStrategy": "UseIP",
|
||||
@@ -78,28 +79,9 @@
|
||||
{
|
||||
"type": "field",
|
||||
"network": "tcp,udp",
|
||||
"balancerTag": "all"
|
||||
}
|
||||
],
|
||||
"balancers": [
|
||||
{
|
||||
"tag": "all",
|
||||
"selector": [
|
||||
"proxy"
|
||||
],
|
||||
"strategy": {
|
||||
"type": "leastPing"
|
||||
}
|
||||
"outboundTag": "proxy"
|
||||
}
|
||||
]
|
||||
},
|
||||
"observatory": {
|
||||
"probeInterval": "5m",
|
||||
"probeURL": "https://api.github.com/_private/browser/stats",
|
||||
"subjectSelector": [
|
||||
"proxy"
|
||||
],
|
||||
"EnableConcurrency": true
|
||||
},
|
||||
"stats": {}
|
||||
}
|
||||
41
sub/sub.go
41
sub/sub.go
@@ -7,6 +7,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"x-ui/config"
|
||||
"x-ui/logger"
|
||||
"x-ui/util/common"
|
||||
@@ -91,15 +92,27 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
SubJsonFragment = ""
|
||||
}
|
||||
|
||||
SubJsonMux, err := s.settingService.GetSubJsonMux()
|
||||
if err != nil {
|
||||
SubJsonMux = ""
|
||||
}
|
||||
|
||||
SubJsonRules, err := s.settingService.GetSubJsonRules()
|
||||
if err != nil {
|
||||
SubJsonRules = ""
|
||||
}
|
||||
|
||||
g := engine.Group("/")
|
||||
|
||||
s.sub = NewSUBController(g, LinksPath, JsonPath, Encrypt, ShowInfo, RemarkModel, SubUpdates, SubJsonFragment)
|
||||
s.sub = NewSUBController(
|
||||
g, LinksPath, JsonPath, Encrypt, ShowInfo, RemarkModel, SubUpdates,
|
||||
SubJsonFragment, SubJsonMux, SubJsonRules)
|
||||
|
||||
return engine, nil
|
||||
}
|
||||
|
||||
func (s *Server) Start() (err error) {
|
||||
//This is an anonymous function, no function name
|
||||
// This is an anonymous function, no function name
|
||||
defer func() {
|
||||
if err != nil {
|
||||
s.Stop()
|
||||
@@ -144,21 +157,19 @@ func (s *Server) Start() (err error) {
|
||||
|
||||
if certFile != "" || keyFile != "" {
|
||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
listener.Close()
|
||||
return err
|
||||
if err == nil {
|
||||
c := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
listener = network.NewAutoHttpsListener(listener)
|
||||
listener = tls.NewListener(listener, c)
|
||||
logger.Info("sub server run https on", listener.Addr())
|
||||
} else {
|
||||
logger.Error("error in loading certificates: ", err)
|
||||
logger.Info("sub server run http on", listener.Addr())
|
||||
}
|
||||
c := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
listener = network.NewAutoHttpsListener(listener)
|
||||
listener = tls.NewListener(listener, c)
|
||||
}
|
||||
|
||||
if certFile != "" || keyFile != "" {
|
||||
logger.Info("Sub server run https on", listener.Addr())
|
||||
} else {
|
||||
logger.Info("Sub server run http on", listener.Addr())
|
||||
logger.Info("sub server run http on", listener.Addr())
|
||||
}
|
||||
s.listener = listener
|
||||
|
||||
|
||||
@@ -25,16 +25,19 @@ func NewSUBController(
|
||||
showInfo bool,
|
||||
rModel string,
|
||||
update string,
|
||||
jsonFragment string) *SUBController {
|
||||
|
||||
jsonFragment string,
|
||||
jsonMux string,
|
||||
jsonRules string,
|
||||
) *SUBController {
|
||||
sub := NewSubService(showInfo, rModel)
|
||||
a := &SUBController{
|
||||
subPath: subPath,
|
||||
subJsonPath: jsonPath,
|
||||
subEncrypt: encrypt,
|
||||
updateInterval: update,
|
||||
|
||||
subService: NewSubService(showInfo, rModel),
|
||||
subJsonService: NewSubJsonService(jsonFragment),
|
||||
subService: sub,
|
||||
subJsonService: NewSubJsonService(jsonFragment, jsonMux, jsonRules, sub),
|
||||
}
|
||||
a.initRouter(g)
|
||||
return a
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"x-ui/database/model"
|
||||
"x-ui/logger"
|
||||
"x-ui/util/json_util"
|
||||
@@ -17,15 +18,46 @@ import (
|
||||
var defaultJson string
|
||||
|
||||
type SubJsonService struct {
|
||||
fragmanet string
|
||||
configJson map[string]interface{}
|
||||
defaultOutbounds []json_util.RawMessage
|
||||
fragment string
|
||||
mux string
|
||||
|
||||
inboundService service.InboundService
|
||||
SubService
|
||||
SubService *SubService
|
||||
}
|
||||
|
||||
func NewSubJsonService(fragment string) *SubJsonService {
|
||||
func NewSubJsonService(fragment string, mux string, rules string, subService *SubService) *SubJsonService {
|
||||
var configJson map[string]interface{}
|
||||
var defaultOutbounds []json_util.RawMessage
|
||||
json.Unmarshal([]byte(defaultJson), &configJson)
|
||||
if outboundSlices, ok := configJson["outbounds"].([]interface{}); ok {
|
||||
for _, defaultOutbound := range outboundSlices {
|
||||
jsonBytes, _ := json.Marshal(defaultOutbound)
|
||||
defaultOutbounds = append(defaultOutbounds, jsonBytes)
|
||||
}
|
||||
}
|
||||
|
||||
if rules != "" {
|
||||
var newRules []interface{}
|
||||
routing, _ := configJson["routing"].(map[string]interface{})
|
||||
defaultRules, _ := routing["rules"].([]interface{})
|
||||
json.Unmarshal([]byte(rules), &newRules)
|
||||
defaultRules = append(newRules, defaultRules...)
|
||||
routing["rules"] = defaultRules
|
||||
configJson["routing"] = routing
|
||||
}
|
||||
|
||||
if fragment != "" {
|
||||
defaultOutbounds = append(defaultOutbounds, json_util.RawMessage(fragment))
|
||||
}
|
||||
|
||||
return &SubJsonService{
|
||||
fragmanet: fragment,
|
||||
configJson: configJson,
|
||||
defaultOutbounds: defaultOutbounds,
|
||||
fragment: fragment,
|
||||
mux: mux,
|
||||
SubService: subService,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,19 +70,8 @@ func (s *SubJsonService) GetJson(subId string, host string) (string, string, err
|
||||
var header string
|
||||
var traffic xray.ClientTraffic
|
||||
var clientTraffics []xray.ClientTraffic
|
||||
var configJson map[string]interface{}
|
||||
var defaultOutbounds []json_util.RawMessage
|
||||
var configArray []json_util.RawMessage
|
||||
|
||||
json.Unmarshal([]byte(defaultJson), &configJson)
|
||||
if outboundSlices, ok := configJson["outbounds"].([]interface{}); ok {
|
||||
for _, defaultOutbound := range outboundSlices {
|
||||
jsonBytes, _ := json.Marshal(defaultOutbound)
|
||||
defaultOutbounds = append(defaultOutbounds, jsonBytes)
|
||||
}
|
||||
}
|
||||
|
||||
outbounds := []json_util.RawMessage{}
|
||||
startIndex := 0
|
||||
// Prepare Inbounds
|
||||
for _, inbound := range inbounds {
|
||||
clients, err := s.inboundService.GetClients(inbound)
|
||||
@@ -61,7 +82,7 @@ func (s *SubJsonService) GetJson(subId string, host string) (string, string, err
|
||||
continue
|
||||
}
|
||||
if len(inbound.Listen) > 0 && inbound.Listen[0] == '@' {
|
||||
listen, port, streamSettings, err := s.getFallbackMaster(inbound.Listen, inbound.StreamSettings)
|
||||
listen, port, streamSettings, err := s.SubService.getFallbackMaster(inbound.Listen, inbound.StreamSettings)
|
||||
if err == nil {
|
||||
inbound.Listen = listen
|
||||
inbound.Port = port
|
||||
@@ -69,22 +90,16 @@ func (s *SubJsonService) GetJson(subId string, host string) (string, string, err
|
||||
}
|
||||
}
|
||||
|
||||
var subClients []model.Client
|
||||
for _, client := range clients {
|
||||
if client.Enable && client.SubID == subId {
|
||||
subClients = append(subClients, client)
|
||||
clientTraffics = append(clientTraffics, s.SubService.getClientTraffics(inbound.ClientStats, client.Email))
|
||||
newConfigs := s.getConfig(inbound, client, host)
|
||||
configArray = append(configArray, newConfigs...)
|
||||
}
|
||||
}
|
||||
|
||||
outbound := s.getOutbound(inbound, subClients, host, startIndex)
|
||||
if outbound != nil {
|
||||
outbounds = append(outbounds, outbound...)
|
||||
startIndex += len(outbound)
|
||||
}
|
||||
}
|
||||
|
||||
if len(outbounds) == 0 {
|
||||
if len(configArray) == 0 {
|
||||
return "", "", nil
|
||||
}
|
||||
|
||||
@@ -111,21 +126,20 @@ func (s *SubJsonService) GetJson(subId string, host string) (string, string, err
|
||||
}
|
||||
}
|
||||
|
||||
if s.fragmanet != "" {
|
||||
outbounds = append(outbounds, json_util.RawMessage(s.fragmanet))
|
||||
}
|
||||
|
||||
// Combile outbounds
|
||||
outbounds = append(outbounds, defaultOutbounds...)
|
||||
configJson["outbounds"] = outbounds
|
||||
finalJson, _ := json.MarshalIndent(configJson, "", " ")
|
||||
var finalJson []byte
|
||||
if len(configArray) == 1 {
|
||||
finalJson, _ = json.MarshalIndent(configArray[0], "", " ")
|
||||
} else {
|
||||
finalJson, _ = json.MarshalIndent(configArray, "", " ")
|
||||
}
|
||||
|
||||
header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
|
||||
return string(finalJson), header, nil
|
||||
}
|
||||
|
||||
func (s *SubJsonService) getOutbound(inbound *model.Inbound, clients []model.Client, host string, startIndex int) []json_util.RawMessage {
|
||||
var newOutbounds []json_util.RawMessage
|
||||
func (s *SubJsonService) getConfig(inbound *model.Inbound, client model.Client, host string) []json_util.RawMessage {
|
||||
var newJsonArray []json_util.RawMessage
|
||||
stream := s.streamData(inbound.StreamSettings)
|
||||
|
||||
externalProxies, ok := stream["externalProxy"].([]interface{})
|
||||
@@ -135,13 +149,13 @@ func (s *SubJsonService) getOutbound(inbound *model.Inbound, clients []model.Cli
|
||||
"forceTls": "same",
|
||||
"dest": host,
|
||||
"port": float64(inbound.Port),
|
||||
"remark": "",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
delete(stream, "externalProxy")
|
||||
|
||||
config_index := startIndex
|
||||
for _, ep := range externalProxies {
|
||||
extPrxy := ep.(map[string]interface{})
|
||||
inbound.Listen = extPrxy["dest"].(string)
|
||||
@@ -160,21 +174,29 @@ func (s *SubJsonService) getOutbound(inbound *model.Inbound, clients []model.Cli
|
||||
}
|
||||
}
|
||||
streamSettings, _ := json.MarshalIndent(newStream, "", " ")
|
||||
inbound.StreamSettings = string(streamSettings)
|
||||
|
||||
for _, client := range clients {
|
||||
inbound.Tag = fmt.Sprintf("proxy_%d", config_index)
|
||||
switch inbound.Protocol {
|
||||
case "vmess", "vless":
|
||||
newOutbounds = append(newOutbounds, s.genVnext(inbound, client))
|
||||
case "trojan", "shadowsocks":
|
||||
newOutbounds = append(newOutbounds, s.genServer(inbound, client))
|
||||
}
|
||||
config_index += 1
|
||||
var newOutbounds []json_util.RawMessage
|
||||
|
||||
switch inbound.Protocol {
|
||||
case "vmess", "vless":
|
||||
newOutbounds = append(newOutbounds, s.genVnext(inbound, streamSettings, client))
|
||||
case "trojan", "shadowsocks":
|
||||
newOutbounds = append(newOutbounds, s.genServer(inbound, streamSettings, client))
|
||||
}
|
||||
|
||||
newOutbounds = append(newOutbounds, s.defaultOutbounds...)
|
||||
newConfigJson := make(map[string]interface{})
|
||||
for key, value := range s.configJson {
|
||||
newConfigJson[key] = value
|
||||
}
|
||||
newConfigJson["outbounds"] = newOutbounds
|
||||
newConfigJson["remarks"] = s.SubService.genRemark(inbound, client.Email, extPrxy["remark"].(string))
|
||||
|
||||
newConfig, _ := json.MarshalIndent(newConfigJson, "", " ")
|
||||
newJsonArray = append(newJsonArray, newConfig)
|
||||
}
|
||||
|
||||
return newOutbounds
|
||||
return newJsonArray
|
||||
}
|
||||
|
||||
func (s *SubJsonService) streamData(stream string) map[string]interface{} {
|
||||
@@ -188,7 +210,7 @@ func (s *SubJsonService) streamData(stream string) map[string]interface{} {
|
||||
}
|
||||
delete(streamSettings, "sockopt")
|
||||
|
||||
if s.fragmanet != "" {
|
||||
if s.fragment != "" {
|
||||
streamSettings["sockopt"] = json_util.RawMessage(`{"dialerProxy": "fragment", "tcpKeepAliveIdle": 100, "tcpNoDelay": true}`)
|
||||
}
|
||||
|
||||
@@ -199,6 +221,8 @@ func (s *SubJsonService) streamData(stream string) map[string]interface{} {
|
||||
streamSettings["tcpSettings"] = s.removeAcceptProxy(streamSettings["tcpSettings"])
|
||||
case "ws":
|
||||
streamSettings["wsSettings"] = s.removeAcceptProxy(streamSettings["wsSettings"])
|
||||
case "httpupgrade":
|
||||
streamSettings["httpupgradeSettings"] = s.removeAcceptProxy(streamSettings["httpupgradeSettings"])
|
||||
}
|
||||
|
||||
return streamSettings
|
||||
@@ -214,7 +238,7 @@ func (s *SubJsonService) removeAcceptProxy(setting interface{}) map[string]inter
|
||||
|
||||
func (s *SubJsonService) tlsData(tData map[string]interface{}) map[string]interface{} {
|
||||
tlsData := make(map[string]interface{}, 1)
|
||||
tlsClientSettings := tData["settings"].(map[string]interface{})
|
||||
tlsClientSettings, _ := tData["settings"].(map[string]interface{})
|
||||
|
||||
tlsData["serverName"] = tData["serverName"]
|
||||
tlsData["alpn"] = tData["alpn"]
|
||||
@@ -229,7 +253,7 @@ func (s *SubJsonService) tlsData(tData map[string]interface{}) map[string]interf
|
||||
|
||||
func (s *SubJsonService) realityData(rData map[string]interface{}) map[string]interface{} {
|
||||
rltyData := make(map[string]interface{}, 1)
|
||||
rltyClientSettings := rData["settings"].(map[string]interface{})
|
||||
rltyClientSettings, _ := rData["settings"].(map[string]interface{})
|
||||
|
||||
rltyData["show"] = false
|
||||
rltyData["publicKey"] = rltyClientSettings["publicKey"]
|
||||
@@ -253,7 +277,7 @@ func (s *SubJsonService) realityData(rData map[string]interface{}) map[string]in
|
||||
return rltyData
|
||||
}
|
||||
|
||||
func (s *SubJsonService) genVnext(inbound *model.Inbound, client model.Client) json_util.RawMessage {
|
||||
func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client) json_util.RawMessage {
|
||||
outbound := Outbound{}
|
||||
usersData := make([]UserVnext, 1)
|
||||
|
||||
@@ -272,8 +296,11 @@ func (s *SubJsonService) genVnext(inbound *model.Inbound, client model.Client) j
|
||||
}
|
||||
|
||||
outbound.Protocol = string(inbound.Protocol)
|
||||
outbound.Tag = inbound.Tag
|
||||
outbound.StreamSettings = json_util.RawMessage(inbound.StreamSettings)
|
||||
outbound.Tag = "proxy"
|
||||
if s.mux != "" {
|
||||
outbound.Mux = json_util.RawMessage(s.mux)
|
||||
}
|
||||
outbound.StreamSettings = streamSettings
|
||||
outbound.Settings = OutboundSettings{
|
||||
Vnext: vnextData,
|
||||
}
|
||||
@@ -282,7 +309,7 @@ func (s *SubJsonService) genVnext(inbound *model.Inbound, client model.Client) j
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *SubJsonService) genServer(inbound *model.Inbound, client model.Client) json_util.RawMessage {
|
||||
func (s *SubJsonService) genServer(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client) json_util.RawMessage {
|
||||
outbound := Outbound{}
|
||||
|
||||
serverData := make([]ServerSetting, 1)
|
||||
@@ -308,8 +335,11 @@ func (s *SubJsonService) genServer(inbound *model.Inbound, client model.Client)
|
||||
}
|
||||
|
||||
outbound.Protocol = string(inbound.Protocol)
|
||||
outbound.Tag = inbound.Tag
|
||||
outbound.StreamSettings = json_util.RawMessage(inbound.StreamSettings)
|
||||
outbound.Tag = "proxy"
|
||||
if s.mux != "" {
|
||||
outbound.Mux = json_util.RawMessage(s.mux)
|
||||
}
|
||||
outbound.StreamSettings = streamSettings
|
||||
outbound.Settings = OutboundSettings{
|
||||
Servers: serverData,
|
||||
}
|
||||
@@ -322,7 +352,7 @@ type Outbound struct {
|
||||
Protocol string `json:"protocol"`
|
||||
Tag string `json:"tag"`
|
||||
StreamSettings json_util.RawMessage `json:"streamSettings"`
|
||||
Mux map[string]interface{} `json:"mux,omitempty"`
|
||||
Mux json_util.RawMessage `json:"mux,omitempty"`
|
||||
ProxySettings map[string]interface{} `json:"proxySettings,omitempty"`
|
||||
Settings OutboundSettings `json:"settings,omitempty"`
|
||||
}
|
||||
|
||||
@@ -6,10 +6,12 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"x-ui/database"
|
||||
"x-ui/database/model"
|
||||
"x-ui/logger"
|
||||
"x-ui/util/common"
|
||||
"x-ui/util/random"
|
||||
"x-ui/web/service"
|
||||
"x-ui/xray"
|
||||
|
||||
@@ -43,6 +45,10 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, string, error
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
if len(inbounds) == 0 {
|
||||
return nil, "", common.NewError("No inbounds found with ", subId)
|
||||
}
|
||||
|
||||
s.datepicker, err = s.settingService.GetDatepicker()
|
||||
if err != nil {
|
||||
s.datepicker = "gregorian"
|
||||
@@ -196,8 +202,13 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
||||
case "ws":
|
||||
ws, _ := stream["wsSettings"].(map[string]interface{})
|
||||
obj["path"] = ws["path"].(string)
|
||||
headers, _ := ws["headers"].(map[string]interface{})
|
||||
obj["host"] = searchHost(headers)
|
||||
obj["host"] = ws["host"].(string)
|
||||
if headers, ok := ws["headers"].(map[string]interface{}); ok {
|
||||
hostFromHeaders := searchHost(headers)
|
||||
if hostFromHeaders != "" {
|
||||
obj["host"] = hostFromHeaders
|
||||
}
|
||||
}
|
||||
case "http":
|
||||
obj["net"] = "h2"
|
||||
http, _ := stream["httpSettings"].(map[string]interface{})
|
||||
@@ -212,9 +223,14 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
||||
case "grpc":
|
||||
grpc, _ := stream["grpcSettings"].(map[string]interface{})
|
||||
obj["path"] = grpc["serviceName"].(string)
|
||||
obj["authority"] = grpc["authority"].(string)
|
||||
if grpc["multiMode"].(bool) {
|
||||
obj["type"] = "multi"
|
||||
}
|
||||
case "httpupgrade":
|
||||
httpupgrade, _ := stream["httpupgradeSettings"].(map[string]interface{})
|
||||
obj["path"] = httpupgrade["path"].(string)
|
||||
obj["host"] = httpupgrade["host"].(string)
|
||||
}
|
||||
|
||||
security, _ := stream["security"].(string)
|
||||
@@ -332,7 +348,13 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||
ws, _ := stream["wsSettings"].(map[string]interface{})
|
||||
params["path"] = ws["path"].(string)
|
||||
headers, _ := ws["headers"].(map[string]interface{})
|
||||
params["host"] = searchHost(headers)
|
||||
params["host"] = ws["host"].(string)
|
||||
if headers != nil {
|
||||
hostFromHeaders := searchHost(headers)
|
||||
if hostFromHeaders != "" {
|
||||
params["host"] = hostFromHeaders
|
||||
}
|
||||
}
|
||||
case "http":
|
||||
http, _ := stream["httpSettings"].(map[string]interface{})
|
||||
params["path"] = http["path"].(string)
|
||||
@@ -346,9 +368,14 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||
case "grpc":
|
||||
grpc, _ := stream["grpcSettings"].(map[string]interface{})
|
||||
params["serviceName"] = grpc["serviceName"].(string)
|
||||
params["authority"], _ = grpc["authority"].(string)
|
||||
if grpc["multiMode"].(bool) {
|
||||
params["mode"] = "multi"
|
||||
}
|
||||
case "httpupgrade":
|
||||
httpupgrade, _ := stream["httpupgradeSettings"].(map[string]interface{})
|
||||
params["path"] = httpupgrade["path"].(string)
|
||||
params["host"] = httpupgrade["host"].(string)
|
||||
}
|
||||
|
||||
security, _ := stream["security"].(string)
|
||||
@@ -391,25 +418,21 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||
if realitySetting != nil {
|
||||
if sniValue, ok := searchKey(realitySetting, "serverNames"); ok {
|
||||
sNames, _ := sniValue.([]interface{})
|
||||
params["sni"], _ = sNames[0].(string)
|
||||
params["sni"] = sNames[random.Num(len(sNames))].(string)
|
||||
}
|
||||
if pbkValue, ok := searchKey(realitySettings, "publicKey"); ok {
|
||||
params["pbk"], _ = pbkValue.(string)
|
||||
}
|
||||
if sidValue, ok := searchKey(realitySetting, "shortIds"); ok {
|
||||
shortIds, _ := sidValue.([]interface{})
|
||||
params["sid"], _ = shortIds[0].(string)
|
||||
params["sid"] = shortIds[random.Num(len(shortIds))].(string)
|
||||
}
|
||||
if fpValue, ok := searchKey(realitySettings, "fingerprint"); ok {
|
||||
if fp, ok := fpValue.(string); ok && len(fp) > 0 {
|
||||
params["fp"] = fp
|
||||
}
|
||||
}
|
||||
if spxValue, ok := searchKey(realitySettings, "spiderX"); ok {
|
||||
if spx, ok := spxValue.(string); ok && len(spx) > 0 {
|
||||
params["spx"] = spx
|
||||
}
|
||||
}
|
||||
params["spx"] = "/" + random.Seq(15)
|
||||
}
|
||||
|
||||
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
|
||||
@@ -548,7 +571,13 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
|
||||
ws, _ := stream["wsSettings"].(map[string]interface{})
|
||||
params["path"] = ws["path"].(string)
|
||||
headers, _ := ws["headers"].(map[string]interface{})
|
||||
params["host"] = searchHost(headers)
|
||||
params["host"] = ws["host"].(string)
|
||||
if headers != nil {
|
||||
hostFromHeaders := searchHost(headers)
|
||||
if hostFromHeaders != "" {
|
||||
params["host"] = hostFromHeaders
|
||||
}
|
||||
}
|
||||
case "http":
|
||||
http, _ := stream["httpSettings"].(map[string]interface{})
|
||||
params["path"] = http["path"].(string)
|
||||
@@ -562,9 +591,14 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
|
||||
case "grpc":
|
||||
grpc, _ := stream["grpcSettings"].(map[string]interface{})
|
||||
params["serviceName"] = grpc["serviceName"].(string)
|
||||
params["authority"], _ = grpc["authority"].(string)
|
||||
if grpc["multiMode"].(bool) {
|
||||
params["mode"] = "multi"
|
||||
}
|
||||
case "httpupgrade":
|
||||
httpupgrade, _ := stream["httpupgradeSettings"].(map[string]interface{})
|
||||
params["path"] = httpupgrade["path"].(string)
|
||||
params["host"] = httpupgrade["host"].(string)
|
||||
}
|
||||
|
||||
security, _ := stream["security"].(string)
|
||||
@@ -603,25 +637,21 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
|
||||
if realitySetting != nil {
|
||||
if sniValue, ok := searchKey(realitySetting, "serverNames"); ok {
|
||||
sNames, _ := sniValue.([]interface{})
|
||||
params["sni"], _ = sNames[0].(string)
|
||||
params["sni"] = sNames[random.Num(len(sNames))].(string)
|
||||
}
|
||||
if pbkValue, ok := searchKey(realitySettings, "publicKey"); ok {
|
||||
params["pbk"], _ = pbkValue.(string)
|
||||
}
|
||||
if sidValue, ok := searchKey(realitySetting, "shortIds"); ok {
|
||||
shortIds, _ := sidValue.([]interface{})
|
||||
params["sid"], _ = shortIds[0].(string)
|
||||
params["sid"] = shortIds[random.Num(len(shortIds))].(string)
|
||||
}
|
||||
if fpValue, ok := searchKey(realitySettings, "fingerprint"); ok {
|
||||
if fp, ok := fpValue.(string); ok && len(fp) > 0 {
|
||||
params["fp"] = fp
|
||||
}
|
||||
}
|
||||
if spxValue, ok := searchKey(realitySettings, "spiderX"); ok {
|
||||
if spx, ok := spxValue.(string); ok && len(spx) > 0 {
|
||||
params["spx"] = spx
|
||||
}
|
||||
}
|
||||
params["spx"] = "/" + random.Seq(15)
|
||||
}
|
||||
|
||||
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
|
||||
@@ -765,7 +795,13 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
|
||||
ws, _ := stream["wsSettings"].(map[string]interface{})
|
||||
params["path"] = ws["path"].(string)
|
||||
headers, _ := ws["headers"].(map[string]interface{})
|
||||
params["host"] = searchHost(headers)
|
||||
params["host"] = ws["host"].(string)
|
||||
if headers != nil {
|
||||
hostFromHeaders := searchHost(headers)
|
||||
if hostFromHeaders != "" {
|
||||
params["host"] = hostFromHeaders
|
||||
}
|
||||
}
|
||||
case "http":
|
||||
http, _ := stream["httpSettings"].(map[string]interface{})
|
||||
params["path"] = http["path"].(string)
|
||||
@@ -779,9 +815,14 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
|
||||
case "grpc":
|
||||
grpc, _ := stream["grpcSettings"].(map[string]interface{})
|
||||
params["serviceName"] = grpc["serviceName"].(string)
|
||||
params["authority"], _ = grpc["authority"].(string)
|
||||
if grpc["multiMode"].(bool) {
|
||||
params["mode"] = "multi"
|
||||
}
|
||||
case "httpupgrade":
|
||||
httpupgrade, _ := stream["httpupgradeSettings"].(map[string]interface{})
|
||||
params["path"] = httpupgrade["path"].(string)
|
||||
params["host"] = httpupgrade["host"].(string)
|
||||
}
|
||||
|
||||
security, _ := stream["security"].(string)
|
||||
|
||||
@@ -3,6 +3,7 @@ package common
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"x-ui/logger"
|
||||
)
|
||||
|
||||
|
||||
@@ -4,12 +4,14 @@ import (
|
||||
"math/rand"
|
||||
)
|
||||
|
||||
var numSeq [10]rune
|
||||
var lowerSeq [26]rune
|
||||
var upperSeq [26]rune
|
||||
var numLowerSeq [36]rune
|
||||
var numUpperSeq [36]rune
|
||||
var allSeq [62]rune
|
||||
var (
|
||||
numSeq [10]rune
|
||||
lowerSeq [26]rune
|
||||
upperSeq [26]rune
|
||||
numLowerSeq [36]rune
|
||||
numUpperSeq [36]rune
|
||||
allSeq [62]rune
|
||||
)
|
||||
|
||||
func init() {
|
||||
for i := 0; i < 10; i++ {
|
||||
|
||||
@@ -36,6 +36,43 @@
|
||||
--dark-color-codemirror-line-selection: rgba(0, 135, 113, 0.3);
|
||||
--dark-color-login-background: var(--dark-color-background);
|
||||
--dark-color-login-wave: var(--dark-color-surface-200);
|
||||
--dark-color-tooltip: rgba(61, 76, 104, 0.9);
|
||||
}
|
||||
|
||||
html[data-theme-animations='off'] {
|
||||
.ant-menu,
|
||||
.ant-layout-sider,
|
||||
.ant-card,
|
||||
.ant-tag,
|
||||
.ant-progress-circle > *,
|
||||
.ant-input,
|
||||
.ant-table-row-expand-icon,
|
||||
.ant-switch,
|
||||
.ant-table-thead > tr > th,
|
||||
.ant-select-selection,
|
||||
.ant-btn,
|
||||
.ant-input-number,
|
||||
.ant-input-group-addon,
|
||||
.ant-checkbox-inner,
|
||||
.ant-progress-bg, .ant-progress-success-bg,
|
||||
.ant-radio-button-wrapper:not(:first-child):before,
|
||||
.ant-radio-button-wrapper,
|
||||
#login {
|
||||
transition: border 0s, background 0s !important;
|
||||
}
|
||||
|
||||
.ant-menu-item, .ant-menu-submenu-title,
|
||||
.ant-alert-close-icon .anticon-close,
|
||||
.ant-tabs-nav .ant-tabs-tab,
|
||||
.ant-input-number-input,
|
||||
.ant-collapse>.ant-collapse-item>.ant-collapse-header,
|
||||
.Line-Hover {
|
||||
transition: color 0s !important;
|
||||
}
|
||||
|
||||
.wave-btn-bg {
|
||||
transition: width 0s !important;
|
||||
}
|
||||
}
|
||||
|
||||
html[data-theme='ultra-dark'] {
|
||||
@@ -69,6 +106,7 @@ html[data-theme='ultra-dark'] {
|
||||
--dark-color-codemirror-line-selection: rgba(85, 85, 85, 0.4);
|
||||
--dark-color-login-background: #0a2227;
|
||||
--dark-color-login-wave: #0f2d32;
|
||||
--dark-color-tooltip: rgba(88, 93, 100, 0.9);
|
||||
.ant-dropdown-menu-dark {
|
||||
background-color: var(--dark-color-surface-500);
|
||||
}
|
||||
@@ -80,6 +118,11 @@ html[data-theme='ultra-dark'] {
|
||||
.dark .waves-header {
|
||||
background-color: #0a2227;
|
||||
}
|
||||
.dark .ant-calendar-year-panel-year:hover,
|
||||
.dark .ant-calendar-month-panel-month:hover,
|
||||
.dark .ant-calendar-decade-panel-decade:hover {
|
||||
background-color: var(--dark-color-surface-600);
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
@@ -138,10 +181,13 @@ html {
|
||||
style attribute {
|
||||
text-align: center;
|
||||
}
|
||||
.ant-table-tbody > tr > td,
|
||||
|
||||
.ant-table-thead > tr > th {
|
||||
padding: 16px 8px;
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr > td {
|
||||
padding: 12px 8px;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
.ant-table-thead > tr > th {
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
@@ -150,10 +196,6 @@ style attribute {
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
.ant-table-row-cell-break-word {
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.ant-table table {
|
||||
width: 100%;
|
||||
@@ -175,7 +217,7 @@ style attribute {
|
||||
position: relative;
|
||||
clear: both;
|
||||
}
|
||||
.ant-table-body {
|
||||
.ant-table-wrapper > div > div > div > div > div > div {
|
||||
overflow-x: auto !important;
|
||||
}
|
||||
.ant-card-hoverable {
|
||||
@@ -644,11 +686,17 @@ style attribute {
|
||||
.dark .ant-modal-footer,
|
||||
.dark .ant-collapse-content,
|
||||
.dark .ant-calendar-footer,
|
||||
.dark .ant-divider-horizontal.ant-divider-with-text-left:before,
|
||||
.dark .ant-divider-horizontal.ant-divider-with-text-left:after,
|
||||
.dark .ant-divider-horizontal.ant-divider-with-text-center:before,
|
||||
.dark .ant-divider-horizontal.ant-divider-with-text-center:after {
|
||||
border-top-color: var(--dark-color-surface-300);
|
||||
}
|
||||
|
||||
.ant-divider-horizontal.ant-divider-with-text-left:before {
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
.dark .ant-progress-text,
|
||||
.dark .ant-card-head,
|
||||
.dark .ant-form,
|
||||
@@ -707,7 +755,6 @@ style attribute {
|
||||
.dark .ant-select-dropdown,
|
||||
.dark .ant-select-dropdown li,
|
||||
.dark .ant-select-dropdown-menu-item,
|
||||
.dark .ant-divider:not(.ant-divider-with-text-center),
|
||||
.dark .client-table-header,
|
||||
.dark .ant-select-selection--multiple .ant-select-selection__choice,
|
||||
.dark .ant-calendar-time-picker-inner {
|
||||
@@ -750,8 +797,8 @@ style attribute {
|
||||
.dark .ant-btn-danger[disabled],
|
||||
.dark .ant-calendar-ok-btn-disabled {
|
||||
color: rgb(255 255 255 / 35%);
|
||||
background-color: var(--dark-color-surface-300);
|
||||
border-color: #42516c;
|
||||
background-color: var(--dark-color-surface-200);
|
||||
border-color: var(--dark-color-surface-300);
|
||||
}
|
||||
|
||||
.dark
|
||||
@@ -791,9 +838,17 @@ style attribute {
|
||||
border-color: var(--dark-color-surface-500);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dark .ant-popover-inner {
|
||||
background-color: var(--dark-color-surface-200);
|
||||
}
|
||||
.dark > .ant-popover-content > .ant-popover-arrow {
|
||||
border-color: var(--dark-color-surface-200);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-dropdown-menu-dark .ant-dropdown-menu-item:hover,
|
||||
.dark .ant-select-dropdown-menu-item-selected,
|
||||
.dark .ant-select-dropdown-menu-item:hover,
|
||||
.dark .ant-calendar-time-picker-select-option-selected {
|
||||
background-color: var(--dark-color-surface-600);
|
||||
}
|
||||
@@ -935,10 +990,15 @@ style attribute {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.ant-checkbox-wrapper,
|
||||
.ant-input-group-addon,
|
||||
.ant-tabs-tab,
|
||||
.ant-input::placeholder,
|
||||
.ant-collapse-header,
|
||||
.ant-menu,
|
||||
.ant-radio-button-wrapper {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.ant-calendar-date,
|
||||
@@ -1052,6 +1112,7 @@ li.ant-select-dropdown-menu-item:empty:after {
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
.dark .ant-divider:not(.ant-divider-with-text-center, .ant-divider-with-text-left, .ant-divider-with-text-right),
|
||||
.ant-dropdown-menu-dark,
|
||||
.dark .ant-calendar-year-panel-year:hover,
|
||||
.dark .ant-calendar-month-panel-month:hover,
|
||||
@@ -1203,15 +1264,39 @@ li.ant-select-dropdown-menu-item:empty:after {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.qr-bg {
|
||||
.qr-cv {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
opacity: 0.8;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.qr-cv:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.qr-cv:active {
|
||||
transform: scale(0.98);
|
||||
transition: all 0.1s;
|
||||
}
|
||||
|
||||
.dark .qr-cv {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.qr-bg {
|
||||
background-color: #ffffff;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
padding: 0.5rem;
|
||||
padding: 0.8rem;
|
||||
border-radius: 1rem;
|
||||
border: solid 1px #e8e8e8;
|
||||
}
|
||||
|
||||
.dark .qr-bg {
|
||||
background-color: var(--dark-color-surface-700);
|
||||
border-color: var(--dark-color-surface-300);
|
||||
}
|
||||
|
||||
.ant-input-group-addon:not(:first-child):not(:last-child) {
|
||||
@@ -1249,3 +1334,126 @@ b, strong {
|
||||
.dark .ant-alert-close-icon .anticon-close:hover {
|
||||
color: rgb(255 255 255);
|
||||
}
|
||||
|
||||
.ant-empty-small {
|
||||
margin: 4px 0;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.ant-empty-small .ant-empty-image {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.ant-menu-theme-switch:hover {
|
||||
background-color: transparent !important;
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
.dark .ant-tooltip-inner,
|
||||
.dark .ant-tooltip-arrow:before {
|
||||
background-color: var(--dark-color-tooltip);
|
||||
}
|
||||
|
||||
.ant-select-sm .ant-select-selection__rendered {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.ant-collapse {
|
||||
-moz-animation: collfade 0.3s ease;
|
||||
-webkit-animation: 0.3s collfade 0.3s ease;
|
||||
animation: collfade 0.3s ease;
|
||||
}
|
||||
|
||||
@-webkit-keyframes collfade {
|
||||
0% {
|
||||
transform: scaleY(.8);
|
||||
transform-origin: 0% 0%;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scaleY(1);
|
||||
transform-origin: 0% 0%;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes collfade {
|
||||
0% {
|
||||
transform: scaleY(.8);
|
||||
transform-origin: 0% 0%;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scaleY(1);
|
||||
transform-origin: 0% 0%;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-tbody>tr>td {
|
||||
border-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.ant-table-row-expand-icon {
|
||||
vertical-align: middle;
|
||||
margin-inline-end: 8px;
|
||||
position: relative;
|
||||
transform: scale(0.9411764705882353);
|
||||
}
|
||||
|
||||
.ant-table-row-collapsed::before {
|
||||
transform: rotate(-180deg);
|
||||
top: 7px;
|
||||
inset-inline-end: 3px;
|
||||
inset-inline-start: 3px;
|
||||
height: 1px;
|
||||
position: absolute;
|
||||
background: currentcolor;
|
||||
transition: transform 0.3s ease-out;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.ant-table-row-collapsed::after {
|
||||
transform: rotate(0deg);
|
||||
top: 3px;
|
||||
bottom: 3px;
|
||||
inset-inline-start: 7px;
|
||||
width: 1px;
|
||||
position: absolute;
|
||||
background: currentcolor;
|
||||
transition: transform 0.3s ease-out;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.ant-table-row-expanded::before {
|
||||
top: 7px;
|
||||
inset-inline-end: 3px;
|
||||
inset-inline-start: 3px;
|
||||
height: 1px;
|
||||
position: absolute;
|
||||
background: currentcolor;
|
||||
transition: transform 0.3s ease-out;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.ant-table-row-expanded::after {
|
||||
top: 3px;
|
||||
bottom: 3px;
|
||||
inset-inline-start: 7px;
|
||||
width: 1px;
|
||||
transform: rotate(90deg);
|
||||
position: absolute;
|
||||
background: currentcolor;
|
||||
transition: transform 0.3s ease-out;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.ant-menu-item:active, .ant-menu-submenu-title:active {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.ant-menu-theme-switch.ant-menu-item .ant-switch:not(.ant-switch-disabled):active:after, .ant-switch:not(.ant-switch-disabled):active:before {
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
@@ -14,3 +14,17 @@ axios.interceptors.request.use(
|
||||
},
|
||||
(error) => Promise.reject(error),
|
||||
);
|
||||
|
||||
axios.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response) {
|
||||
const statusCode = error.response.status;
|
||||
// Check the status code
|
||||
if (statusCode === 401) { // Unauthorized
|
||||
return window.location.reload();
|
||||
}
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -51,7 +51,14 @@ const OutboundDomainStrategies = [
|
||||
"AsIs",
|
||||
"UseIP",
|
||||
"UseIPv4",
|
||||
"UseIPv6"
|
||||
"UseIPv6",
|
||||
"UseIPv6v4",
|
||||
"UseIPv4v6",
|
||||
"ForceIP",
|
||||
"ForceIPv6v4",
|
||||
"ForceIPv6",
|
||||
"ForceIPv4v6",
|
||||
"ForceIPv4"
|
||||
];
|
||||
|
||||
const WireguardDomainStrategy = [
|
||||
@@ -187,6 +194,7 @@ class WsStreamSettings extends CommonClass {
|
||||
static fromJson(json={}) {
|
||||
return new WsStreamSettings(
|
||||
json.path,
|
||||
json.host,
|
||||
json.headers && !ObjectUtil.isEmpty(json.headers.Host) ? json.headers.Host : '',
|
||||
);
|
||||
}
|
||||
@@ -194,6 +202,7 @@ class WsStreamSettings extends CommonClass {
|
||||
toJson() {
|
||||
return {
|
||||
path: this.path,
|
||||
host: this.host,
|
||||
headers: ObjectUtil.isEmpty(this.host) ? undefined : {Host: this.host},
|
||||
};
|
||||
}
|
||||
@@ -250,24 +259,48 @@ class QuicStreamSettings extends CommonClass {
|
||||
}
|
||||
|
||||
class GrpcStreamSettings extends CommonClass {
|
||||
constructor(serviceName="", multiMode=false) {
|
||||
constructor(serviceName="", multiMode=false, authority="") {
|
||||
super();
|
||||
this.serviceName = serviceName;
|
||||
this.multiMode = multiMode;
|
||||
this.authority = authority;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
return new GrpcStreamSettings(json.serviceName, json.multiMode);
|
||||
return new GrpcStreamSettings(json.serviceName, json.multiMode,json.authority);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
serviceName: this.serviceName,
|
||||
multiMode: this.multiMode,
|
||||
authority: this.authority
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class HttpUpgradeStreamSettings extends CommonClass {
|
||||
constructor(path='/', host='') {
|
||||
super();
|
||||
this.path = path;
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
return new HttpUpgradeStreamSettings(
|
||||
json.path,
|
||||
json.Host,
|
||||
);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
path: this.path,
|
||||
host: this.host,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class TlsStreamSettings extends CommonClass {
|
||||
constructor(serverName='',
|
||||
alpn=[],
|
||||
@@ -327,6 +360,34 @@ class RealityStreamSettings extends CommonClass {
|
||||
};
|
||||
}
|
||||
};
|
||||
class SockoptStreamSettings extends CommonClass {
|
||||
constructor(dialerProxy = "", tcpFastOpen = false, tcpKeepAliveInterval = 0, tcpNoDelay = false) {
|
||||
super();
|
||||
this.dialerProxy = dialerProxy;
|
||||
this.tcpFastOpen = tcpFastOpen;
|
||||
this.tcpKeepAliveInterval = tcpKeepAliveInterval;
|
||||
this.tcpNoDelay = tcpNoDelay;
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
if (Object.keys(json).length === 0) return undefined;
|
||||
return new SockoptStreamSettings(
|
||||
json.dialerProxy,
|
||||
json.tcpFastOpen,
|
||||
json.tcpKeepAliveInterval,
|
||||
json.tcpNoDelay,
|
||||
);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
dialerProxy: this.dialerProxy,
|
||||
tcpFastOpen: this.tcpFastOpen,
|
||||
tcpKeepAliveInterval: this.tcpKeepAliveInterval,
|
||||
tcpNoDelay: this.tcpNoDelay,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class StreamSettings extends CommonClass {
|
||||
constructor(network='tcp',
|
||||
@@ -339,6 +400,8 @@ class StreamSettings extends CommonClass {
|
||||
httpSettings=new HttpStreamSettings(),
|
||||
quicSettings=new QuicStreamSettings(),
|
||||
grpcSettings=new GrpcStreamSettings(),
|
||||
httpupgradeSettings=new HttpUpgradeStreamSettings(),
|
||||
sockopt = undefined,
|
||||
) {
|
||||
super();
|
||||
this.network = network;
|
||||
@@ -351,6 +414,8 @@ class StreamSettings extends CommonClass {
|
||||
this.http = httpSettings;
|
||||
this.quic = quicSettings;
|
||||
this.grpc = grpcSettings;
|
||||
this.httpupgrade = httpupgradeSettings;
|
||||
this.sockopt = sockopt;
|
||||
}
|
||||
|
||||
get isTls() {
|
||||
@@ -361,6 +426,14 @@ class StreamSettings extends CommonClass {
|
||||
return this.security === "reality";
|
||||
}
|
||||
|
||||
get sockoptSwitch() {
|
||||
return this.sockopt != undefined;
|
||||
}
|
||||
|
||||
set sockoptSwitch(value) {
|
||||
this.sockopt = value ? new SockoptStreamSettings() : undefined;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
return new StreamSettings(
|
||||
json.network,
|
||||
@@ -373,6 +446,8 @@ class StreamSettings extends CommonClass {
|
||||
HttpStreamSettings.fromJson(json.httpSettings),
|
||||
QuicStreamSettings.fromJson(json.quicSettings),
|
||||
GrpcStreamSettings.fromJson(json.grpcSettings),
|
||||
HttpUpgradeStreamSettings.fromJson(json.httpupgradeSettings),
|
||||
SockoptStreamSettings.fromJson(json.sockopt),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -389,6 +464,37 @@ class StreamSettings extends CommonClass {
|
||||
httpSettings: network === 'http' ? this.http.toJson() : undefined,
|
||||
quicSettings: network === 'quic' ? this.quic.toJson() : undefined,
|
||||
grpcSettings: network === 'grpc' ? this.grpc.toJson() : undefined,
|
||||
httpupgradeSettings: network === 'httpupgrade' ? this.httpupgrade.toJson() : undefined,
|
||||
sockopt: this.sockopt != undefined ? this.sockopt.toJson() : undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class Mux extends CommonClass {
|
||||
constructor(enabled = false, concurrency = 8, xudpConcurrency = 16, xudpProxyUDP443 = "reject") {
|
||||
super();
|
||||
this.enabled = enabled;
|
||||
this.concurrency = concurrency;
|
||||
this.xudpConcurrency = xudpConcurrency;
|
||||
this.xudpProxyUDP443 = xudpProxyUDP443;
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
if (Object.keys(json).length === 0) return undefined;
|
||||
return new Mux(
|
||||
json.enabled,
|
||||
json.concurrency,
|
||||
json.xudpConcurrency,
|
||||
json.xudpProxyUDP443,
|
||||
);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
enabled: this.enabled,
|
||||
concurrency: this.concurrency,
|
||||
xudpConcurrency: this.xudpConcurrency,
|
||||
xudpProxyUDP443: this.xudpProxyUDP443,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -399,12 +505,16 @@ class Outbound extends CommonClass {
|
||||
protocol=Protocols.VMess,
|
||||
settings=null,
|
||||
streamSettings = new StreamSettings(),
|
||||
sendThrough,
|
||||
mux = new Mux(),
|
||||
) {
|
||||
super();
|
||||
this.tag = tag;
|
||||
this._protocol = protocol;
|
||||
this.settings = settings == null ? Outbound.Settings.getSettings(protocol) : settings;
|
||||
this.stream = streamSettings;
|
||||
this.sendThrough = sendThrough;
|
||||
this.mux = mux;
|
||||
}
|
||||
|
||||
get protocol() {
|
||||
@@ -419,7 +529,7 @@ class Outbound extends CommonClass {
|
||||
|
||||
canEnableTls() {
|
||||
if (![Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(this.protocol)) return false;
|
||||
return ["tcp", "ws", "http", "quic", "grpc"].includes(this.stream.network);
|
||||
return ["tcp", "ws", "http", "quic", "grpc", "httpupgrade"].includes(this.stream.network);
|
||||
}
|
||||
|
||||
//this is used for xtls-rprx-vision
|
||||
@@ -439,6 +549,10 @@ class Outbound extends CommonClass {
|
||||
return [Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(this.protocol);
|
||||
}
|
||||
|
||||
canEnableMux() {
|
||||
return [Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks, Protocols.HTTP, Protocols.Socks].includes(this.protocol);
|
||||
}
|
||||
|
||||
hasVnext() {
|
||||
return [Protocols.VMess, Protocols.VLESS].includes(this.protocol);
|
||||
}
|
||||
@@ -469,15 +583,26 @@ class Outbound extends CommonClass {
|
||||
json.protocol,
|
||||
Outbound.Settings.fromJson(json.protocol, json.settings),
|
||||
StreamSettings.fromJson(json.streamSettings),
|
||||
json.sendThrough,
|
||||
Mux.fromJson(json.mux),
|
||||
)
|
||||
}
|
||||
|
||||
toJson() {
|
||||
var stream;
|
||||
if (this.canEnableStream()) {
|
||||
stream = this.stream.toJson();
|
||||
} else {
|
||||
if (this.stream?.sockopt)
|
||||
stream = { sockopt: this.stream.sockopt.toJson() };
|
||||
}
|
||||
return {
|
||||
tag: this.tag == '' ? undefined : this.tag,
|
||||
protocol: this.protocol,
|
||||
settings: this.settings instanceof CommonClass ? this.settings.toJson() : this.settings,
|
||||
streamSettings: this.canEnableStream() ? this.stream.toJson() : undefined,
|
||||
streamSettings: stream,
|
||||
sendThrough: this.sendThrough != "" ? this.sendThrough : undefined,
|
||||
mux: this.mux?.enabled ? this.mux : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -522,7 +647,9 @@ class Outbound extends CommonClass {
|
||||
json.path,
|
||||
json.type ? json.type : 'none');
|
||||
} else if (network === 'grpc') {
|
||||
stream.grpc = new GrpcStreamSettings(json.path, json.type == 'multi');
|
||||
stream.grpc = new GrpcStreamSettings(json.path, json.authority, json.type == 'multi');
|
||||
} else if (network === 'httpupgrade') {
|
||||
stream.httpupgrade = new HttpUpgradeStreamSettings(json.path,json.host);
|
||||
}
|
||||
|
||||
if(json.tls && json.tls == 'tls'){
|
||||
@@ -533,21 +660,22 @@ class Outbound extends CommonClass {
|
||||
json.allowInsecure);
|
||||
}
|
||||
|
||||
const port = json.port * 1;
|
||||
|
||||
return new Outbound(json.ps, Protocols.VMess, new Outbound.VmessSettings(json.add, json.port, json.id), stream);
|
||||
return new Outbound(json.ps, Protocols.VMess, new Outbound.VmessSettings(json.add, port, json.id), stream);
|
||||
}
|
||||
|
||||
static fromParamLink(link){
|
||||
const url = new URL(link);
|
||||
let type = url.searchParams.get('type');
|
||||
let type = url.searchParams.get('type') ?? 'tcp';
|
||||
let security = url.searchParams.get('security') ?? 'none';
|
||||
let stream = new StreamSettings(type, security);
|
||||
|
||||
let headerType = url.searchParams.get('headerType');
|
||||
let host = url.searchParams.get('host');
|
||||
let path = url.searchParams.get('path');
|
||||
let headerType = url.searchParams.get('headerType') ?? undefined;
|
||||
let host = url.searchParams.get('host') ?? undefined;
|
||||
let path = url.searchParams.get('path') ?? undefined;
|
||||
|
||||
if (type === 'tcp') {
|
||||
if (type === 'tcp' || type === 'none') {
|
||||
stream.tcp = new TcpStreamSettings(headerType ?? 'none', host, path);
|
||||
} else if (type === 'kcp') {
|
||||
stream.kcp = new KcpStreamSettings();
|
||||
@@ -562,8 +690,13 @@ class Outbound extends CommonClass {
|
||||
url.searchParams.get('quicSecurity') ?? 'none',
|
||||
url.searchParams.get('key') ?? '',
|
||||
headerType ?? 'none');
|
||||
} else if (type === 'grpc') {
|
||||
stream.grpc = new GrpcStreamSettings(url.searchParams.get('serviceName') ?? '', url.searchParams.get('mode') == 'multi');
|
||||
} else if (type === 'grpc') {
|
||||
stream.grpc = new GrpcStreamSettings(
|
||||
url.searchParams.get('serviceName') ?? '',
|
||||
url.searchParams.get('authority') ?? '',
|
||||
url.searchParams.get('mode') == 'multi');
|
||||
} else if (type === 'httpupgrade') {
|
||||
stream.httpupgrade = new HttpUpgradeStreamSettings(path,host);
|
||||
}
|
||||
|
||||
if(security == 'tls'){
|
||||
@@ -580,13 +713,10 @@ class Outbound extends CommonClass {
|
||||
let sni=url.searchParams.get('sni') ?? '';
|
||||
let sid=url.searchParams.get('sid') ?? '';
|
||||
let spx=url.searchParams.get('spx') ?? '';
|
||||
stream.reality = new RealityStreamSettings(pbk, fp, sni, sid, spx);
|
||||
stream.reality = new RealityStreamSettings(pbk, fp, sni, sid, spx);
|
||||
}
|
||||
|
||||
let data = link.split('?');
|
||||
if(data.length != 2) return null;
|
||||
|
||||
const regex = /([^@]+):\/\/([^@]+)@([^:]+):(\d+)\?(.*)$/;
|
||||
const regex = /([^@]+):\/\/([^@]+)@(.+):(\d+)(.*)$/;
|
||||
const match = link.match(regex);
|
||||
|
||||
if (!match) return null;
|
||||
|
||||
@@ -35,9 +35,11 @@ class AllSetting {
|
||||
this.subUpdates = 0;
|
||||
this.subEncrypt = true;
|
||||
this.subShowInfo = false;
|
||||
this.subURI = '';
|
||||
this.subJsonURI = '';
|
||||
this.subJsonFragment = '';
|
||||
this.subURI = "";
|
||||
this.subJsonURI = "";
|
||||
this.subJsonFragment = "";
|
||||
this.subJsonMux = "";
|
||||
this.subJsonRules = "";
|
||||
|
||||
this.timeLocation = "Asia/Tehran";
|
||||
|
||||
|
||||
@@ -338,10 +338,11 @@ class KcpStreamSettings extends XrayCommonClass {
|
||||
}
|
||||
|
||||
class WsStreamSettings extends XrayCommonClass {
|
||||
constructor(acceptProxyProtocol=false, path='/', headers=[]) {
|
||||
constructor(acceptProxyProtocol=false, path='/', host='', headers=[]) {
|
||||
super();
|
||||
this.acceptProxyProtocol = acceptProxyProtocol;
|
||||
this.path = path;
|
||||
this.host = host;
|
||||
this.headers = headers;
|
||||
}
|
||||
|
||||
@@ -366,6 +367,7 @@ class WsStreamSettings extends XrayCommonClass {
|
||||
return new WsStreamSettings(
|
||||
json.acceptProxyProtocol,
|
||||
json.path,
|
||||
json.host,
|
||||
XrayCommonClass.toHeaders(json.headers),
|
||||
);
|
||||
}
|
||||
@@ -374,6 +376,7 @@ class WsStreamSettings extends XrayCommonClass {
|
||||
return {
|
||||
acceptProxyProtocol: this.acceptProxyProtocol,
|
||||
path: this.path,
|
||||
host: this.host,
|
||||
headers: XrayCommonClass.toV2Headers(this.headers, false),
|
||||
};
|
||||
}
|
||||
@@ -446,16 +449,19 @@ class QuicStreamSettings extends XrayCommonClass {
|
||||
class GrpcStreamSettings extends XrayCommonClass {
|
||||
constructor(
|
||||
serviceName="",
|
||||
authority="",
|
||||
multiMode=false,
|
||||
) {
|
||||
super();
|
||||
this.serviceName = serviceName;
|
||||
this.authority = authority;
|
||||
this.multiMode = multiMode;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
return new GrpcStreamSettings(
|
||||
json.serviceName,
|
||||
json.authority,
|
||||
json.multiMode
|
||||
);
|
||||
}
|
||||
@@ -463,11 +469,36 @@ class GrpcStreamSettings extends XrayCommonClass {
|
||||
toJson() {
|
||||
return {
|
||||
serviceName: this.serviceName,
|
||||
authority: this.authority,
|
||||
multiMode: this.multiMode,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class HttpUpgradeStreamSettings extends XrayCommonClass {
|
||||
constructor(acceptProxyProtocol=false, path='/', host='') {
|
||||
super();
|
||||
this.acceptProxyProtocol = acceptProxyProtocol;
|
||||
this.path = path;
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
return new HttpUpgradeStreamSettings(
|
||||
json.acceptProxyProtocol,
|
||||
json.path,
|
||||
json.host,
|
||||
);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
acceptProxyProtocol: this.acceptProxyProtocol,
|
||||
path: this.path,
|
||||
host: this.host,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class TlsStreamSettings extends XrayCommonClass {
|
||||
constructor(serverName='',
|
||||
@@ -833,6 +864,7 @@ class StreamSettings extends XrayCommonClass {
|
||||
httpSettings=new HttpStreamSettings(),
|
||||
quicSettings=new QuicStreamSettings(),
|
||||
grpcSettings=new GrpcStreamSettings(),
|
||||
httpupgradeSettings=new HttpUpgradeStreamSettings(),
|
||||
sockopt = undefined,
|
||||
) {
|
||||
super();
|
||||
@@ -848,6 +880,7 @@ class StreamSettings extends XrayCommonClass {
|
||||
this.http = httpSettings;
|
||||
this.quic = quicSettings;
|
||||
this.grpc = grpcSettings;
|
||||
this.httpupgrade = httpupgradeSettings;
|
||||
this.sockopt = sockopt;
|
||||
}
|
||||
|
||||
@@ -910,6 +943,7 @@ class StreamSettings extends XrayCommonClass {
|
||||
HttpStreamSettings.fromJson(json.httpSettings),
|
||||
QuicStreamSettings.fromJson(json.quicSettings),
|
||||
GrpcStreamSettings.fromJson(json.grpcSettings),
|
||||
HttpUpgradeStreamSettings.fromJson(json.httpupgradeSettings),
|
||||
SockoptStreamSettings.fromJson(json.sockopt),
|
||||
);
|
||||
}
|
||||
@@ -929,6 +963,7 @@ class StreamSettings extends XrayCommonClass {
|
||||
httpSettings: network === 'http' ? this.http.toJson() : undefined,
|
||||
quicSettings: network === 'quic' ? this.quic.toJson() : undefined,
|
||||
grpcSettings: network === 'grpc' ? this.grpc.toJson() : undefined,
|
||||
httpupgradeSettings: network === 'httpupgrade' ? this.httpupgrade.toJson() : undefined,
|
||||
sockopt: this.sockopt != undefined ? this.sockopt.toJson() : undefined,
|
||||
};
|
||||
}
|
||||
@@ -1045,6 +1080,10 @@ class Inbound extends XrayCommonClass {
|
||||
return this.network === "http";
|
||||
}
|
||||
|
||||
get isHttpupgrade() {
|
||||
return this.network === "httpupgrade";
|
||||
}
|
||||
|
||||
// Shadowsocks
|
||||
get method() {
|
||||
switch (this.protocol) {
|
||||
@@ -1072,9 +1111,16 @@ class Inbound extends XrayCommonClass {
|
||||
if (this.isTcp) {
|
||||
return this.stream.tcp.request.getHeader("Host");
|
||||
} else if (this.isWs) {
|
||||
return this.stream.ws.getHeader("Host");
|
||||
const hostHeader = this.stream.ws.getHeader("Host");
|
||||
if (hostHeader !== null) {
|
||||
return hostHeader;
|
||||
} else {
|
||||
return this.stream.ws.host;
|
||||
}
|
||||
} else if (this.isH2) {
|
||||
return this.stream.http.host[0];
|
||||
} else if (this.isHttpupgrade) {
|
||||
return this.stream.httpupgrade.host;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -1086,6 +1132,8 @@ class Inbound extends XrayCommonClass {
|
||||
return this.stream.ws.path;
|
||||
} else if (this.isH2) {
|
||||
return this.stream.http.path;
|
||||
} else if (this.isHttpupgrade) {
|
||||
return this.stream.httpupgrade.path;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -1121,7 +1169,7 @@ class Inbound extends XrayCommonClass {
|
||||
|
||||
canEnableTls() {
|
||||
if(![Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(this.protocol)) return false;
|
||||
return ["tcp", "ws", "http", "quic", "grpc"].includes(this.network);
|
||||
return ["tcp", "ws", "http", "quic", "grpc", "httpupgrade"].includes(this.network);
|
||||
}
|
||||
|
||||
//this is used for xtls-rprx-vision
|
||||
@@ -1190,6 +1238,7 @@ class Inbound extends XrayCommonClass {
|
||||
} else if (network === 'ws') {
|
||||
let ws = this.stream.ws;
|
||||
obj.path = ws.path;
|
||||
obj.host = ws.host;
|
||||
let index = ws.headers.findIndex(header => header.name.toLowerCase() === 'host');
|
||||
if (index >= 0) {
|
||||
obj.host = ws.headers[index].value;
|
||||
@@ -1204,9 +1253,14 @@ class Inbound extends XrayCommonClass {
|
||||
obj.path = this.stream.quic.key;
|
||||
} else if (network === 'grpc') {
|
||||
obj.path = this.stream.grpc.serviceName;
|
||||
obj.authority = this.stream.grpc.authority;
|
||||
if (this.stream.grpc.multiMode){
|
||||
obj.type = 'multi'
|
||||
}
|
||||
} else if (network === 'httpupgrade') {
|
||||
let httpupgrade = this.stream.httpupgrade;
|
||||
obj.path = httpupgrade.path;
|
||||
obj.host = httpupgrade.host;
|
||||
}
|
||||
|
||||
if (security === 'tls') {
|
||||
@@ -1255,6 +1309,7 @@ class Inbound extends XrayCommonClass {
|
||||
case "ws":
|
||||
const ws = this.stream.ws;
|
||||
params.set("path", ws.path);
|
||||
params.set("host", ws.host);
|
||||
const index = ws.headers.findIndex(header => header.name.toLowerCase() === 'host');
|
||||
if (index >= 0) {
|
||||
const host = ws.headers[index].value;
|
||||
@@ -1275,10 +1330,16 @@ class Inbound extends XrayCommonClass {
|
||||
case "grpc":
|
||||
const grpc = this.stream.grpc;
|
||||
params.set("serviceName", grpc.serviceName);
|
||||
params.set("authority", grpc.authority);
|
||||
if(grpc.multiMode){
|
||||
params.set("mode", "multi");
|
||||
}
|
||||
break;
|
||||
case "httpupgrade":
|
||||
const httpupgrade = this.stream.httpupgrade;
|
||||
params.set("path", httpupgrade.path);
|
||||
params.set("host", httpupgrade.host);
|
||||
break;
|
||||
}
|
||||
|
||||
if (security === 'tls') {
|
||||
@@ -1369,6 +1430,7 @@ class Inbound extends XrayCommonClass {
|
||||
case "ws":
|
||||
const ws = this.stream.ws;
|
||||
params.set("path", ws.path);
|
||||
params.set("host", ws.host);
|
||||
const index = ws.headers.findIndex(header => header.name.toLowerCase() === 'host');
|
||||
if (index >= 0) {
|
||||
const host = ws.headers[index].value;
|
||||
@@ -1389,10 +1451,16 @@ class Inbound extends XrayCommonClass {
|
||||
case "grpc":
|
||||
const grpc = this.stream.grpc;
|
||||
params.set("serviceName", grpc.serviceName);
|
||||
params.set("authority", grpc.authority);
|
||||
if(grpc.multiMode){
|
||||
params.set("mode", "multi");
|
||||
}
|
||||
break;
|
||||
case "httpupgrade":
|
||||
const httpupgrade = this.stream.httpupgrade;
|
||||
params.set("path", httpupgrade.path);
|
||||
params.set("host", httpupgrade.host);
|
||||
break;
|
||||
}
|
||||
|
||||
if (security === 'tls') {
|
||||
@@ -1450,6 +1518,7 @@ class Inbound extends XrayCommonClass {
|
||||
case "ws":
|
||||
const ws = this.stream.ws;
|
||||
params.set("path", ws.path);
|
||||
params.set("host", ws.host);
|
||||
const index = ws.headers.findIndex(header => header.name.toLowerCase() === 'host');
|
||||
if (index >= 0) {
|
||||
const host = ws.headers[index].value;
|
||||
@@ -1470,10 +1539,16 @@ class Inbound extends XrayCommonClass {
|
||||
case "grpc":
|
||||
const grpc = this.stream.grpc;
|
||||
params.set("serviceName", grpc.serviceName);
|
||||
params.set("authority", grpc.authority);
|
||||
if(grpc.multiMode){
|
||||
params.set("mode", "multi");
|
||||
}
|
||||
break;
|
||||
case "httpupgrade":
|
||||
const httpupgrade = this.stream.httpupgrade;
|
||||
params.set("path", httpupgrade.path);
|
||||
params.set("host", httpupgrade.host);
|
||||
break;
|
||||
}
|
||||
|
||||
if (security === 'tls') {
|
||||
|
||||
@@ -131,11 +131,11 @@ class RandomUtil {
|
||||
static randomUUID() {
|
||||
const template = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx';
|
||||
return template.replace(/[xy]/g, function (c) {
|
||||
const randomValues = new Uint8Array(1);
|
||||
crypto.getRandomValues(randomValues);
|
||||
let randomValue = randomValues[0] % 16;
|
||||
let calculatedValue = (c === 'x') ? randomValue : (randomValue & 0x3 | 0x8);
|
||||
return calculatedValue.toString(16);
|
||||
const randomValues = new Uint8Array(1);
|
||||
crypto.getRandomValues(randomValues);
|
||||
let randomValue = randomValues[0] % 16;
|
||||
let calculatedValue = (c === 'x') ? randomValue : (randomValue & 0x3 | 0x8);
|
||||
return calculatedValue.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
5
web/assets/qrcode/qrious.min.js
vendored
5
web/assets/qrcode/qrious.min.js
vendored
File diff suppressed because one or more lines are too long
5
web/assets/qrcode/qrious2.min.js
vendored
Normal file
5
web/assets/qrcode/qrious2.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -22,91 +22,37 @@ func (a *APIController) initRouter(g *gin.RouterGroup) {
|
||||
g = g.Group("/panel/api/inbounds")
|
||||
g.Use(a.checkLogin)
|
||||
|
||||
g.GET("/list", a.getAllInbounds)
|
||||
g.GET("/get/:id", a.getSingleInbound)
|
||||
g.GET("/getClientTraffics/:email", a.getClientTraffics)
|
||||
g.POST("/add", a.addInbound)
|
||||
g.POST("/del/:id", a.delInbound)
|
||||
g.POST("/update/:id", a.updateInbound)
|
||||
g.POST("/clientIps/:email", a.getClientIps)
|
||||
g.POST("/clearClientIps/:email", a.clearClientIps)
|
||||
g.POST("/addClient", a.addInboundClient)
|
||||
g.POST("/:id/delClient/:clientId", a.delInboundClient)
|
||||
g.POST("/updateClient/:clientId", a.updateInboundClient)
|
||||
g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
|
||||
g.POST("/resetAllTraffics", a.resetAllTraffics)
|
||||
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
|
||||
g.POST("/delDepletedClients/:id", a.delDepletedClients)
|
||||
g.GET("/createbackup", a.createBackup)
|
||||
g.POST("/onlines", a.onlines)
|
||||
|
||||
a.inboundController = NewInboundController(g)
|
||||
}
|
||||
|
||||
func (a *APIController) getAllInbounds(c *gin.Context) {
|
||||
a.inboundController.getInbounds(c)
|
||||
}
|
||||
inboundRoutes := []struct {
|
||||
Method string
|
||||
Path string
|
||||
Handler gin.HandlerFunc
|
||||
}{
|
||||
{"GET", "/createbackup", a.createBackup},
|
||||
{"GET", "/list", a.inboundController.getInbounds},
|
||||
{"GET", "/get/:id", a.inboundController.getInbound},
|
||||
{"GET", "/getClientTraffics/:email", a.inboundController.getClientTraffics},
|
||||
{"POST", "/add", a.inboundController.addInbound},
|
||||
{"POST", "/del/:id", a.inboundController.delInbound},
|
||||
{"POST", "/update/:id", a.inboundController.updateInbound},
|
||||
{"POST", "/clientIps/:email", a.inboundController.getClientIps},
|
||||
{"POST", "/clearClientIps/:email", a.inboundController.clearClientIps},
|
||||
{"POST", "/addClient", a.inboundController.addInboundClient},
|
||||
{"POST", "/:id/delClient/:clientId", a.inboundController.delInboundClient},
|
||||
{"POST", "/updateClient/:clientId", a.inboundController.updateInboundClient},
|
||||
{"POST", "/:id/resetClientTraffic/:email", a.inboundController.resetClientTraffic},
|
||||
{"POST", "/resetAllTraffics", a.inboundController.resetAllTraffics},
|
||||
{"POST", "/resetAllClientTraffics/:id", a.inboundController.resetAllClientTraffics},
|
||||
{"POST", "/delDepletedClients/:id", a.inboundController.delDepletedClients},
|
||||
{"POST", "/onlines", a.inboundController.onlines},
|
||||
}
|
||||
|
||||
func (a *APIController) getSingleInbound(c *gin.Context) {
|
||||
a.inboundController.getInbound(c)
|
||||
}
|
||||
|
||||
func (a *APIController) getClientTraffics(c *gin.Context) {
|
||||
a.inboundController.getClientTraffics(c)
|
||||
}
|
||||
|
||||
func (a *APIController) addInbound(c *gin.Context) {
|
||||
a.inboundController.addInbound(c)
|
||||
}
|
||||
|
||||
func (a *APIController) delInbound(c *gin.Context) {
|
||||
a.inboundController.delInbound(c)
|
||||
}
|
||||
|
||||
func (a *APIController) updateInbound(c *gin.Context) {
|
||||
a.inboundController.updateInbound(c)
|
||||
}
|
||||
|
||||
func (a *APIController) getClientIps(c *gin.Context) {
|
||||
a.inboundController.getClientIps(c)
|
||||
}
|
||||
|
||||
func (a *APIController) clearClientIps(c *gin.Context) {
|
||||
a.inboundController.clearClientIps(c)
|
||||
}
|
||||
|
||||
func (a *APIController) addInboundClient(c *gin.Context) {
|
||||
a.inboundController.addInboundClient(c)
|
||||
}
|
||||
|
||||
func (a *APIController) delInboundClient(c *gin.Context) {
|
||||
a.inboundController.delInboundClient(c)
|
||||
}
|
||||
|
||||
func (a *APIController) updateInboundClient(c *gin.Context) {
|
||||
a.inboundController.updateInboundClient(c)
|
||||
}
|
||||
|
||||
func (a *APIController) resetClientTraffic(c *gin.Context) {
|
||||
a.inboundController.resetClientTraffic(c)
|
||||
}
|
||||
|
||||
func (a *APIController) resetAllTraffics(c *gin.Context) {
|
||||
a.inboundController.resetAllTraffics(c)
|
||||
}
|
||||
|
||||
func (a *APIController) resetAllClientTraffics(c *gin.Context) {
|
||||
a.inboundController.resetAllClientTraffics(c)
|
||||
}
|
||||
|
||||
func (a *APIController) delDepletedClients(c *gin.Context) {
|
||||
a.inboundController.delDepletedClients(c)
|
||||
for _, route := range inboundRoutes {
|
||||
g.Handle(route.Method, route.Path, route.Handler)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *APIController) createBackup(c *gin.Context) {
|
||||
a.Tgbot.SendBackupToAdmins()
|
||||
}
|
||||
|
||||
func (a *APIController) onlines(c *gin.Context) {
|
||||
a.inboundController.onlines(c)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"x-ui/logger"
|
||||
"x-ui/web/locale"
|
||||
"x-ui/web/session"
|
||||
@@ -9,13 +10,12 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type BaseController struct {
|
||||
}
|
||||
type BaseController struct{}
|
||||
|
||||
func (a *BaseController) checkLogin(c *gin.Context) {
|
||||
if !session.IsLogin(c) {
|
||||
if isAjax(c) {
|
||||
pureJsonMsg(c, false, I18nWeb(c, "pages.login.loginAgain"))
|
||||
pureJsonMsg(c, http.StatusUnauthorized, false, I18nWeb(c, "pages.login.loginAgain"))
|
||||
} else {
|
||||
c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"x-ui/database/model"
|
||||
"x-ui/web/service"
|
||||
"x-ui/web/session"
|
||||
@@ -174,7 +175,7 @@ func (a *InboundController) addInboundClient(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
jsonMsg(c, "Client(s) added", nil)
|
||||
if err == nil && needRestart {
|
||||
if needRestart {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
@@ -195,7 +196,7 @@ func (a *InboundController) delInboundClient(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
jsonMsg(c, "Client deleted", nil)
|
||||
if err == nil && needRestart {
|
||||
if needRestart {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
@@ -218,7 +219,7 @@ func (a *InboundController) updateInboundClient(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
jsonMsg(c, "Client updated", nil)
|
||||
if err == nil && needRestart {
|
||||
if needRestart {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
@@ -239,7 +240,7 @@ func (a *InboundController) resetClientTraffic(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
jsonMsg(c, "traffic reseted", nil)
|
||||
if err == nil && needRestart {
|
||||
if needRestart {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package controller
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"x-ui/logger"
|
||||
"x-ui/web/service"
|
||||
"x-ui/web/session"
|
||||
@@ -49,15 +50,15 @@ func (a *IndexController) login(c *gin.Context) {
|
||||
var form LoginForm
|
||||
err := c.ShouldBind(&form)
|
||||
if err != nil {
|
||||
pureJsonMsg(c, false, I18nWeb(c, "pages.login.toasts.invalidFormData"))
|
||||
pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.invalidFormData"))
|
||||
return
|
||||
}
|
||||
if form.Username == "" {
|
||||
pureJsonMsg(c, false, I18nWeb(c, "pages.login.toasts.emptyUsername"))
|
||||
pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.emptyUsername"))
|
||||
return
|
||||
}
|
||||
if form.Password == "" {
|
||||
pureJsonMsg(c, false, I18nWeb(c, "pages.login.toasts.emptyPassword"))
|
||||
pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.emptyPassword"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -66,7 +67,7 @@ func (a *IndexController) login(c *gin.Context) {
|
||||
if user == nil {
|
||||
logger.Warningf("wrong username or password: \"%s\" \"%s\"", form.Username, form.Password)
|
||||
a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0)
|
||||
pureJsonMsg(c, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword"))
|
||||
pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword"))
|
||||
return
|
||||
} else {
|
||||
logger.Infof("%s login success, Ip Address: %s\n", form.Username, getRemoteIp(c))
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"x-ui/web/global"
|
||||
"x-ui/web/service"
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package controller
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"x-ui/web/entity"
|
||||
"x-ui/web/service"
|
||||
"x-ui/web/session"
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"x-ui/config"
|
||||
"x-ui/logger"
|
||||
"x-ui/web/entity"
|
||||
@@ -48,18 +49,11 @@ func jsonMsgObj(c *gin.Context, msg string, obj interface{}, err error) {
|
||||
c.JSON(http.StatusOK, m)
|
||||
}
|
||||
|
||||
func pureJsonMsg(c *gin.Context, success bool, msg string) {
|
||||
if success {
|
||||
c.JSON(http.StatusOK, entity.Msg{
|
||||
Success: true,
|
||||
Msg: msg,
|
||||
})
|
||||
} else {
|
||||
c.JSON(http.StatusOK, entity.Msg{
|
||||
Success: false,
|
||||
Msg: msg,
|
||||
})
|
||||
}
|
||||
func pureJsonMsg(c *gin.Context, statusCode int, success bool, msg string) {
|
||||
c.JSON(statusCode, entity.Msg{
|
||||
Success: success,
|
||||
Msg: msg,
|
||||
})
|
||||
}
|
||||
|
||||
func html(c *gin.Context, name string, title string, data gin.H) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"x-ui/util/common"
|
||||
)
|
||||
|
||||
@@ -51,6 +52,8 @@ type AllSetting struct {
|
||||
SubJsonPath string `json:"subJsonPath" form:"subJsonPath"`
|
||||
SubJsonURI string `json:"subJsonURI" form:"subJsonURI"`
|
||||
SubJsonFragment string `json:"subJsonFragment" form:"subJsonFragment"`
|
||||
SubJsonMux string `json:"subJsonMux" form:"subJsonMux"`
|
||||
SubJsonRules string `json:"subJsonRules" form:"subJsonRules"`
|
||||
Datepicker string `json:"datepicker" form:"datepicker"`
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,10 @@ import (
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
var webServer WebServer
|
||||
var subServer SubServer
|
||||
var (
|
||||
webServer WebServer
|
||||
subServer SubServer
|
||||
)
|
||||
|
||||
type WebServer interface {
|
||||
GetCron() *cron.Cron
|
||||
|
||||
@@ -1,31 +1,23 @@
|
||||
{{define "qrcodeModal"}}
|
||||
<a-modal id="qrcode-modal" v-model="qrModal.visible" :title="qrModal.title"
|
||||
:closable="true"
|
||||
:class="themeSwitcher.currentTheme"
|
||||
:footer="null"
|
||||
width="300px">
|
||||
<a-tag color="green" style="margin-bottom: 10px;display: block;text-align: center;">
|
||||
{{ i18n "pages.inbounds.clickOnQRcode" }}
|
||||
</a-tag>
|
||||
<template v-if="app.subSettings.enable && qrModal.subId">
|
||||
<a-divider>{{ i18n "pages.settings.subSettings"}}</a-divider>
|
||||
<canvas @click="copyToClipboard('qrCode-sub',genSubLink(qrModal.client.subId))"
|
||||
id="qrCode-sub"
|
||||
class="qr-bg">
|
||||
</canvas>
|
||||
<a-divider>{{ i18n "pages.settings.subSettings"}} Json</a-divider>
|
||||
<canvas @click="copyToClipboard('qrCode-subJson',genSubJsonLink(qrModal.client.subId))"
|
||||
id="qrCode-subJson"
|
||||
style="width: 100%; height: 100%; display: flex; border-radius: 1rem;">
|
||||
</canvas>
|
||||
</template>
|
||||
<a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
|
||||
<template v-for="(row, index) in qrModal.qrcodes">
|
||||
<a-tag color="green" style="margin: 10px 0; display: block; text-align: center;">[[ row.remark ]]</a-tag>
|
||||
<canvas @click="copyToClipboard('qrCode-'+index, row.link)"
|
||||
:id="'qrCode-'+index"
|
||||
class="qr-bg"></canvas>
|
||||
</template>
|
||||
:dialog-style="{ top: '20px' }"
|
||||
:closable="true"
|
||||
:class="themeSwitcher.currentTheme"
|
||||
:footer="null" width="300px">
|
||||
<a-tag color="green" style="margin-bottom: 10px;display: block;text-align: center;">
|
||||
{{ i18n "pages.inbounds.clickOnQRcode" }}
|
||||
</a-tag>
|
||||
<template v-if="app.subSettings.enable && qrModal.subId">
|
||||
<a-divider>{{ i18n "pages.settings.subSettings"}}</a-divider>
|
||||
<div class="qr-bg"><canvas @click="copyToClipboard('qrCode-sub',genSubLink(qrModal.client.subId))" id="qrCode-sub" class="qr-cv"></canvas></div>
|
||||
<a-divider>{{ i18n "pages.settings.subSettings"}} Json</a-divider>
|
||||
<div class="qr-bg"><canvas @click="copyToClipboard('qrCode-subJson',genSubJsonLink(qrModal.client.subId))" id="qrCode-subJson" class="qr-cv"></canvas></div>
|
||||
</template>
|
||||
<a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
|
||||
<template v-for="(row, index) in qrModal.qrcodes">
|
||||
<a-tag color="green" style="margin: 10px 0; display: block; text-align: center;">[[ row.remark ]]</a-tag>
|
||||
<div class="qr-bg"><canvas @click="copyToClipboard('qrCode-'+index, row.link)" :id="'qrCode-'+index" class="qr-cv"></canvas></div>
|
||||
</template>
|
||||
</a-modal>
|
||||
|
||||
<script>
|
||||
@@ -86,8 +78,13 @@
|
||||
setQrCode(elmentId, content) {
|
||||
new QRious({
|
||||
element: document.querySelector('#' + elmentId),
|
||||
size: 260,
|
||||
size: 400,
|
||||
value: content,
|
||||
background: 'white',
|
||||
backgroundAlpha: 0,
|
||||
foreground: 'black',
|
||||
padding: 2,
|
||||
level: 'L'
|
||||
});
|
||||
},
|
||||
genSubLink(subID) {
|
||||
|
||||
@@ -374,6 +374,12 @@
|
||||
transform: translateZ(-100px);
|
||||
}
|
||||
}
|
||||
.ant-menu-item .anticon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
.ant-menu-inline .ant-menu-item {
|
||||
padding: 0 16px !important;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
|
||||
@@ -410,19 +416,19 @@
|
||||
<a-col span="24">
|
||||
<a-form>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="user.username" placeholder='{{ i18n "username" }}'
|
||||
<a-input autocomplete="username" v-model.trim="user.username" placeholder='{{ i18n "username" }}'
|
||||
@keydown.enter.native="login" autofocus>
|
||||
<a-icon slot="prefix" type="user" style="font-size: 16px;"></a-icon>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<password-input icon="lock" v-model.trim="user.password"
|
||||
<password-input autocomplete="current-password" icon="lock" v-model.trim="user.password"
|
||||
placeholder='{{ i18n "password" }}'
|
||||
@keydown.enter.native="login">
|
||||
</password-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="secretEnable">
|
||||
<password-input icon="key" v-model.trim="user.loginSecret"
|
||||
<password-input autocomplete="secret" icon="key" v-model.trim="user.loginSecret"
|
||||
placeholder='{{ i18n "secretToken" }}'
|
||||
@keydown.enter.native="login">
|
||||
</password-input>
|
||||
@@ -455,17 +461,7 @@
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-row justify="center" class="centered">
|
||||
<a-col>
|
||||
<a-icon type="bulb" :theme="themeSwitcher.isDarkTheme ? 'filled' : 'outlined'"></a-icon>
|
||||
</a-col>
|
||||
<a-col>
|
||||
<theme-switch></theme-switch>
|
||||
<a-checkbox v-if="themeSwitcher.isDarkTheme" style="padding-left: 1rem; vertical-align: middle;"
|
||||
:checked="themeSwitcher.isUltra"
|
||||
@click="themeSwitcher.toggleUltra()">
|
||||
Ultra
|
||||
</a-checkbox>
|
||||
</a-col>
|
||||
<theme-switch></theme-switch>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
@@ -24,18 +24,7 @@
|
||||
|
||||
{{define "commonSider"}}
|
||||
<a-layout-sider :theme="themeSwitcher.currentTheme" id="sider" collapsible breakpoint="md" collapsed-width="0">
|
||||
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" selected-keys="">
|
||||
<a-menu-item mode="inline">
|
||||
<a-icon type="bulb" :theme="themeSwitcher.isDarkTheme ? 'filled' : 'outlined'"></a-icon>
|
||||
<theme-switch>
|
||||
</theme-switch>
|
||||
<a-checkbox v-if="themeSwitcher.isDarkTheme" style="padding-left: 1rem; vertical-align: middle;"
|
||||
:checked="themeSwitcher.isUltra"
|
||||
@click="themeSwitcher.toggleUltra()">
|
||||
Ultra
|
||||
</a-checkbox>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
<theme-switch></theme-switch>
|
||||
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" :selected-keys="['{{ .request_uri }}']"
|
||||
@click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key">
|
||||
{{template "menuItems" .}}
|
||||
@@ -49,18 +38,7 @@
|
||||
<div class="drawer-handle" @click="siderDrawer.change()" slot="handle">
|
||||
<a-icon :type="siderDrawer.visible ? 'close' : 'menu-fold'"></a-icon>
|
||||
</div>
|
||||
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" selected-keys="">
|
||||
<a-menu-item mode="inline">
|
||||
<a-icon type="bulb" :theme="themeSwitcher.isDarkTheme ? 'filled' : 'outlined'"></a-icon>
|
||||
<theme-switch>
|
||||
</theme-switch>
|
||||
<a-checkbox v-if="themeSwitcher.isDarkTheme" style="padding-left: 1rem; vertical-align: middle;"
|
||||
:checked="themeSwitcher.isUltra"
|
||||
@click="themeSwitcher.toggleUltra()">
|
||||
Ultra
|
||||
</a-checkbox>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
<theme-switch></theme-switch>
|
||||
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" :selected-keys="['{{ .request_uri }}']"
|
||||
@click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key">
|
||||
{{template "menuItems" .}}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<a-input :value="value" @input="$emit('input', $event.target.value)" :placeholder="placeholder"></a-input>
|
||||
</template>
|
||||
<template v-else-if="type === 'number'">
|
||||
<a-input-number :value="value" :step="step" @change="value => $emit('input', value)" :min="min" style="width: 100%;"></a-input-number>
|
||||
<a-input-number :value="value" :step="step" @change="value => $emit('input', value)" :min="min" :max="max" style="width: 100%;"></a-input-number>
|
||||
</template>
|
||||
<template v-else-if="type === 'switch'">
|
||||
<a-switch :checked="value" @change="value => $emit('input', value)"></a-switch>
|
||||
@@ -29,7 +29,7 @@
|
||||
{{define "component/setting"}}
|
||||
<script>
|
||||
Vue.component('setting-list-item', {
|
||||
props: ["type", "title", "desc", "value", "min", "step", "placeholder"],
|
||||
props: ["type", "title", "desc", "value", "min", "max" , "step", "placeholder"],
|
||||
template: `{{template "component/settingListItem"}}`,
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
},
|
||||
props: ['data-source', 'customRow'],
|
||||
inheritAttrs: false,
|
||||
provide() {
|
||||
provide() {
|
||||
const sortable = {}
|
||||
|
||||
Object.defineProperty(sortable, "setSortableIndex", {
|
||||
@@ -50,25 +50,21 @@
|
||||
}
|
||||
},
|
||||
render: function (createElement) {
|
||||
return createElement(
|
||||
'a-table',
|
||||
{
|
||||
class: {
|
||||
'ant-table-is-sorting': this.isDragging(),
|
||||
},
|
||||
props: {
|
||||
...this.$attrs,
|
||||
'data-source': this.records,
|
||||
customRow: (record, index) => this.customRowRender(record, index),
|
||||
},
|
||||
on: this.$listeners,
|
||||
nativeOn: {
|
||||
drop: (e) => this.dropHandler(e),
|
||||
},
|
||||
scopedSlots: this.$scopedSlots,
|
||||
return createElement('a-table', {
|
||||
class: {
|
||||
'ant-table-is-sorting': this.isDragging(),
|
||||
},
|
||||
this.$slots.default,
|
||||
)
|
||||
props: {
|
||||
...this.$attrs,
|
||||
'data-source': this.records,
|
||||
customRow: (record, index) => this.customRowRender(record, index),
|
||||
},
|
||||
on: this.$listeners,
|
||||
nativeOn: {
|
||||
drop: (e) => this.dropHandler(e),
|
||||
},
|
||||
scopedSlots: this.$scopedSlots,
|
||||
}, this.$slots.default, )
|
||||
},
|
||||
created() {
|
||||
this.$memoSort = {};
|
||||
@@ -91,8 +87,14 @@
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
const hideDragImage = this.$el.cloneNode(true);
|
||||
hideDragImage.id = "hideDragImage-hide";
|
||||
hideDragImage.style.opacity = 0;
|
||||
e.dataTransfer.setDragImage(hideDragImage, 0, 0);
|
||||
},
|
||||
dragStopHandler(e, index) {
|
||||
const hideDragImage = document.getElementById('hideDragImage-hide');
|
||||
if (hideDragImage) hideDragImage.remove();
|
||||
this.resetSortableIndex(e, index);
|
||||
},
|
||||
dragOverHandler(e, index) {
|
||||
@@ -209,16 +211,26 @@
|
||||
}
|
||||
}
|
||||
.ant-table-is-sorting .draggable-row td {
|
||||
background-color: white !important;
|
||||
background-color: #ffffff !important;
|
||||
}
|
||||
.dark .ant-table-is-sorting .draggable-row td {
|
||||
background-color: var(--dark-color-surface-100) !important;
|
||||
}
|
||||
.ant-table-is-sorting .dragging td {
|
||||
background-color: rgb(232 244 242) !important;
|
||||
color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.dark .ant-table-is-sorting .dragging td {
|
||||
background-color: var(--dark-color-table-hover) !important;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
.ant-table-is-sorting .dragging {
|
||||
opacity: 0.5;
|
||||
opacity: 1;
|
||||
box-shadow: 1px -2px 2px #008771;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.ant-table-is-sorting .dragging .ant-table-row-index {
|
||||
opacity: 0;
|
||||
opacity: 0.3;
|
||||
}
|
||||
</style>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
{{define "component/themeSwitchTemplate"}}
|
||||
<template>
|
||||
<a-switch size="small" :default-checked="themeSwitcher.isDarkTheme"
|
||||
@change="themeSwitcher.toggleTheme()">
|
||||
</a-switch>
|
||||
<a-menu class="change-theme" :theme="themeSwitcher.currentTheme" mode="inline" selected-keys="">
|
||||
<a-menu-item mode="inline" class="ant-menu-theme-switch">
|
||||
<a-icon type="bulb" :theme="themeSwitcher.isDarkTheme ? 'filled' : 'outlined'"></a-icon>
|
||||
<a-switch size="small" :default-checked="themeSwitcher.isDarkTheme" @change="themeSwitcher.toggleTheme()"></a-switch>
|
||||
<template v-if="themeSwitcher.isDarkTheme">
|
||||
<a-checkbox style="margin-left: 1rem; vertical-align: middle;" :checked="themeSwitcher.isUltra" @click="themeSwitcher.toggleUltra()">Ultra</a-checkbox>
|
||||
</template>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
{{end}}
|
||||
|
||||
@@ -51,6 +57,13 @@
|
||||
getContainer: () => document.getElementById('message')
|
||||
});
|
||||
document.getElementById('message').className = themeSwitcher.currentTheme;
|
||||
const themeAnimations = document.querySelector('.change-theme');
|
||||
themeAnimations.addEventListener('mousedown', () => {
|
||||
document.documentElement.setAttribute('data-theme-animations', 'off');
|
||||
});
|
||||
themeAnimations.addEventListener('mouseleave', () => {
|
||||
document.documentElement.removeAttribute('data-theme-animations');
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
</template>
|
||||
<a-input v-model.trim="client.tgId"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-form-item v-if="app.ipLimitEnable">
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
@@ -75,13 +75,13 @@
|
||||
</template>
|
||||
<a-input-number v-model="client.limitIp" min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="client.email && client.limitIp > 0 && isEdit">
|
||||
<a-form-item v-if="client.limitIp > 0 && client.email && isEdit">
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.IPLimitlogDesc" }}</span>
|
||||
</template>
|
||||
<span>{{ i18n "pages.inbounds.IPLimitlog" }}</span>
|
||||
<span>{{ i18n "pages.inbounds.IPLimitlog" }} </span>
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label='{{ i18n "pages.inbounds.port" }}'>
|
||||
<a-input-number v-model.number="inbound.port"></a-input-number>
|
||||
<a-input-number v-model.number="inbound.port" :min="1" :max="65531"></a-input-number>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
<a-form-item label='{{ i18n "pages.xray.outbound.tag" }}' has-feedback :validate-status="outModal.duplicateTag? 'warning' : 'success'">
|
||||
<a-input v-model.trim="outbound.tag" @change="outModal.check()" placeholder='{{ i18n "pages.xray.outbound.tagDesc" }}'></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.outbound.sendThrough" }}'>
|
||||
<a-input v-model="outbound.sendThrough"></a-input>
|
||||
</a-form-item>
|
||||
|
||||
<!-- freedom settings-->
|
||||
<template v-if="outbound.protocol === Protocols.Freedom">
|
||||
@@ -214,34 +217,34 @@
|
||||
|
||||
<!-- stream settings -->
|
||||
<template v-if="outbound.canEnableStream()">
|
||||
<a-form-item label='{{ i18n "transmission" }}'>
|
||||
<a-select v-model="outbound.stream.network" @change="streamNetworkChange"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="tcp">TCP</a-select-option>
|
||||
<a-select-option value="kcp">mKCP</a-select-option>
|
||||
<a-select-option value="ws">WS</a-select-option>
|
||||
<a-select-option value="http">H2</a-select-option>
|
||||
<a-select-option value="quic">QUIC</a-select-option>
|
||||
<a-select-option value="grpc">gRPC</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "transmission" }}'>
|
||||
<a-select v-model="outbound.stream.network" @change="streamNetworkChange"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="tcp">TCP</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="http">H2</a-select-option>
|
||||
<a-select-option value="quic">QUIC</a-select-option>
|
||||
<a-select-option value="grpc">gRPC</a-select-option>
|
||||
<a-select-option value="httpupgrade">HTTPUpgrade</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<template v-if="outbound.stream.network === 'tcp'">
|
||||
<a-form-item label='HTTP {{ i18n "camouflage" }}'>
|
||||
<a-switch
|
||||
:checked="outbound.stream.tcp.type === 'http'"
|
||||
<a-switch :checked="outbound.stream.tcp.type === 'http'"
|
||||
@change="checked => outbound.stream.tcp.type = checked ? 'http' : 'none'">
|
||||
</a-switch>
|
||||
</a-form-item>
|
||||
<template v-if="outbound.stream.tcp.type == 'http'">
|
||||
<a-form-item label='{{ i18n "host" }}'>
|
||||
<a-input v-model.trim="outbound.stream.tcp.host"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
<a-input v-model.trim="outbound.stream.tcp.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "host" }}'>
|
||||
<a-input v-model.trim="outbound.stream.tcp.host"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
<a-input v-model.trim="outbound.stream.tcp.path"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
|
||||
<!-- kcp -->
|
||||
<template v-if="outbound.stream.network === 'kcp'">
|
||||
<a-form-item label='{{ i18n "camouflage" }}'>
|
||||
@@ -252,6 +255,7 @@
|
||||
<a-select-option value="wechat-video">WeChat</a-select-option>
|
||||
<a-select-option value="dtls">DTLS 1.2</a-select-option>
|
||||
<a-select-option value="wireguard">WireGuard</a-select-option>
|
||||
<a-select-option value="dns">DNS</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
@@ -265,7 +269,7 @@
|
||||
</a-form-item>
|
||||
<a-form-item label='Uplink (MB/s)'>
|
||||
<a-input-number v-model.number="outbound.stream.kcp.upCap"></a-input-number>
|
||||
</a-form-item>
|
||||
</a-form-item>
|
||||
<a-form-item label='Downlink (MB/s)'>
|
||||
<a-input-number v-model.number="outbound.stream.kcp.downCap"></a-input-number>
|
||||
</a-form-item>
|
||||
@@ -279,7 +283,7 @@
|
||||
<a-input-number v-model.number="outbound.stream.kcp.writeBuffer"></a-input-number>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
|
||||
<!-- ws -->
|
||||
<template v-if="outbound.stream.network === 'ws'">
|
||||
<a-form-item label='{{ i18n "host" }}'>
|
||||
@@ -287,9 +291,9 @@
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
<a-form-item><a-input v-model.trim="outbound.stream.ws.path"></a-input>
|
||||
</a-form-item>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
|
||||
<!-- http -->
|
||||
<template v-if="outbound.stream.network === 'http'">
|
||||
<a-form-item label='{{ i18n "host" }}'>
|
||||
@@ -299,7 +303,7 @@
|
||||
<a-input v-model.trim="outbound.stream.http.path"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
|
||||
<!-- quic -->
|
||||
<template v-if="outbound.stream.network === 'quic'">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.quic.encryption" }}'>
|
||||
@@ -311,7 +315,7 @@
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
<a-input v-model.trim="outbound.stream.quic.key"></a-input>
|
||||
</a-form-item>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "camouflage" }}'>
|
||||
<a-select v-model="outbound.stream.quic.type" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="none">None</a-select-option>
|
||||
@@ -323,16 +327,29 @@
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
|
||||
<!-- grpc -->
|
||||
<template v-if="outbound.stream.network === 'grpc'">
|
||||
<a-form-item label='Service Name'>
|
||||
<a-input v-model.trim="outbound.stream.grpc.serviceName"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Authority">
|
||||
<a-input v-model.trim="outbound.stream.grpc.authority"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Multi Mode'>
|
||||
<a-switch v-model="outbound.stream.grpc.multiMode"></a-switch>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- httpupgrade -->
|
||||
<template v-if="outbound.stream.network === 'httpupgrade'">
|
||||
<a-form-item label='{{ i18n "host" }}'>
|
||||
<a-input v-model="outbound.stream.httpupgrade.host"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
<a-form-item><a-input v-model.trim="outbound.stream.httpupgrade.path"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- tls settings -->
|
||||
@@ -341,7 +358,7 @@
|
||||
<a-radio-group v-model="outbound.stream.security" button-style="solid">
|
||||
<a-radio-button value="none">{{ i18n "none" }}</a-radio-button>
|
||||
<a-radio-button value="tls">TLS</a-radio-button>
|
||||
<a-radio-button v-if="outbound.canEnableReality()" value="reality">REALITY</a-radio-button>
|
||||
<a-radio-button v-if="outbound.canEnableReality()" value="reality">Reality</a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<template v-if="outbound.stream.isTls">
|
||||
@@ -389,6 +406,48 @@
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- sockopt settings -->
|
||||
<a-form-item label="Sockopts">
|
||||
<a-switch v-model="outbound.stream.sockoptSwitch"></a-switch>
|
||||
</a-form-item>
|
||||
<template v-if="outbound.stream.sockoptSwitch">
|
||||
<a-form-item label="Dialer Proxy">
|
||||
<a-select v-model="outbound.stream.sockopt.dialerProxy" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="tag in ['', ...outModal.tags]" :value="tag">[[ tag ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="TCP Fast Open">
|
||||
<a-switch v-model="outbound.stream.sockopt.tcpFastOpen"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="Keep Alive Interval">
|
||||
<a-input-number v-model="outbound.stream.sockopt.tcpKeepAliveInterval" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="TCP No-Delay">
|
||||
<a-switch v-model="outbound.stream.sockopt.tcpNoDelay"></a-switch>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- mux settings -->
|
||||
<template v-if="outbound.canEnableMux()">
|
||||
<a-form-item label="Mux">
|
||||
<a-switch v-model="outbound.mux.enabled"></a-switch>
|
||||
</a-form-item>
|
||||
<template v-if="outbound.mux.enabled">
|
||||
<a-form-item label="Concurrency">
|
||||
<a-input-number v-model="outbound.mux.concurrency" :min="-1" :max="1024"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="xudp Concurrency">
|
||||
<a-input-number v-model="outbound.mux.xudpConcurrency" :min="-1" :max="1024"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="xudp UDP 443">
|
||||
<a-select v-model="outbound.mux.xudpProxyUDP443" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="c in ['reject', 'allow', 'skip']" :value="c">[[ c ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2" tab="JSON" force-render="true">
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
<a-form-item label="Service Name">
|
||||
<a-input v-model.trim="inbound.stream.grpc.serviceName"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Authority">
|
||||
<a-input v-model.trim="inbound.stream.grpc.authority"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Multi Mode">
|
||||
<a-switch v-model="inbound.stream.grpc.multiMode"></a-switch>
|
||||
</a-form-item>
|
||||
|
||||
13
web/html/xui/form/stream/stream_httpupgrade.html
Normal file
13
web/html/xui/form/stream/stream_httpupgrade.html
Normal file
@@ -0,0 +1,13 @@
|
||||
{{define "form/streamHTTPUpgrade"}}
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="PROXY Protocol">
|
||||
<a-switch v-model="inbound.stream.httpupgrade.acceptProxyProtocol"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "host" }}'>
|
||||
<a-input v-model.trim="inbound.stream.httpupgrade.host"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
<a-input v-model.trim="inbound.stream.httpupgrade.path"></a-input>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
@@ -23,25 +23,25 @@
|
||||
<a-input v-model.trim="inbound.stream.kcp.seed"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='MTU'>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.mtu"></a-input-number>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.mtu" :min="576" :max="1460"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='TTI (ms)'>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.tti"></a-input-number>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.tti" :min="10" :max="100"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Uplink (MB/s)'>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.upCap"></a-input-number>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.upCap" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Downlink (MB/s)'>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.downCap"></a-input-number>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.downCap" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Congestion'>
|
||||
<a-switch v-model="inbound.stream.kcp.congestion"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='Read Buffer (MB)'>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.readBuffer"></a-input-number>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.readBuffer" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Write Buffer (MB)'>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.writeBuffer"></a-input-number>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.writeBuffer" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
<!-- select stream network -->
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "transmission" }}'>
|
||||
<a-select v-model="inbound.stream.network" style="width: 50%" @change="streamNetworkChange"
|
||||
<a-select v-model="inbound.stream.network" style="width: 75%" @change="streamNetworkChange"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="tcp">TCP</a-select-option>
|
||||
<a-select-option value="kcp">mKCP</a-select-option>
|
||||
<a-select-option value="ws">WS</a-select-option>
|
||||
<a-select-option value="ws">WebSocket</a-select-option>
|
||||
<a-select-option value="http">H2</a-select-option>
|
||||
<a-select-option value="quic">QUIC</a-select-option>
|
||||
<a-select-option value="grpc">gRPC</a-select-option>
|
||||
<a-select-option value="httpupgrade">HTTPUpgrade</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
@@ -43,6 +44,12 @@
|
||||
<template v-if="inbound.stream.network === 'grpc'">
|
||||
{{template "form/streamGRPC"}}
|
||||
</template>
|
||||
|
||||
<!-- httpupgrade -->
|
||||
<template v-if="inbound.stream.network === 'httpupgrade'">
|
||||
{{template "form/streamHTTPUpgrade"}}
|
||||
</template>
|
||||
|
||||
<!-- sockopt -->
|
||||
<template>
|
||||
{{template "form/streamSockopt"}}
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
<a-form-item label="PROXY Protocol">
|
||||
<a-switch v-model="inbound.stream.ws.acceptProxyProtocol"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "host" }}'>
|
||||
<a-input v-model.trim="inbound.stream.ws.host"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
<a-input v-model.trim="inbound.stream.ws.path"></a-input>
|
||||
</a-form-item>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
:overlay-class-name="themeSwitcher.currentTheme"
|
||||
ok-text='{{ i18n "reset"}}'
|
||||
cancel-text='{{ i18n "cancel"}}'>
|
||||
<a-icon slot="icon" type="question-circle-o" :style="themeSwitcher.isDarkTheme ? 'color: #008771' : 'color: #008771'"></a-icon>
|
||||
<a-icon slot="icon" type="question-circle-o" :style="themeSwitcher.isDarkTheme ? 'color: var(--color-primary-100)' : 'color: var(--color-primary-100)'"></a-icon>
|
||||
<a-icon style="font-size: 24px; cursor: pointer;" class="normal-icon" type="retweet" v-if="client.email.length > 0"></a-icon>
|
||||
</a-popconfirm>
|
||||
</a-tooltip>
|
||||
@@ -86,13 +86,12 @@
|
||||
<td width="120px" v-else-if="client.totalGB > 0">
|
||||
<a-progress :stroke-color="clientStatsColor(record, client.email)"
|
||||
:show-info="false"
|
||||
:status="isClientOnline(client.email)? 'active' : isClientEnabled(record, client.email)? 'exception' : ''"
|
||||
:status="isClientEnabled(record, client.email)? 'exception' : ''"
|
||||
:percent="statsProgress(record, client.email)"/>
|
||||
</td>
|
||||
<td width="120px" v-else class="infinite-bar">
|
||||
<a-progress
|
||||
:show-info="false"
|
||||
:status="isClientOnline(client.email)? 'active' : ''"
|
||||
:percent="100"></a-progress>
|
||||
</td>
|
||||
<td width="60px">
|
||||
@@ -117,7 +116,7 @@
|
||||
</td>
|
||||
<td width="120px" class="infinite-bar">
|
||||
<a-progress :show-info="false"
|
||||
:status="isClientOnline(client.email)? 'active' : isClientEnabled(record, client.email)? 'exception' : ''"
|
||||
:status="isClientEnabled(record, client.email)? 'exception' : ''"
|
||||
:percent="expireProgress(client.expiryTime, client.reset)"/>
|
||||
</td>
|
||||
<td width="60px">[[ client.reset + "d" ]]</td>
|
||||
@@ -202,14 +201,13 @@
|
||||
</template>
|
||||
<a-progress :stroke-color="clientStatsColor(record, client.email)"
|
||||
:show-info="false"
|
||||
:status="isClientOnline(client.email)? 'active' : isClientEnabled(record, client.email)? 'exception' : ''"
|
||||
:status="isClientEnabled(record, client.email)? 'exception' : ''"
|
||||
:percent="statsProgress(record, client.email)"/>
|
||||
</a-popover>
|
||||
</td>
|
||||
<td width="120px" v-else class="infinite-bar">
|
||||
<a-progress :stroke-color="themeSwitcher.isDarkTheme ? '#2c1e32':'#F2EAF1'"
|
||||
:show-info="false"
|
||||
:status="isClientOnline(client.email)? 'active' : ''"
|
||||
:percent="100"></a-progress>
|
||||
</td>
|
||||
<td width="80px">
|
||||
@@ -235,7 +233,7 @@
|
||||
<span v-else>[[ DateUtil.formatMillis(client._expiryTime) ]]</span>
|
||||
</template>
|
||||
<a-progress :show-info="false"
|
||||
:status="isClientOnline(client.email)? 'active' : isClientEnabled(record, client.email)? 'exception' : ''"
|
||||
:status="isClientEnabled(record, client.email)? 'exception' : ''"
|
||||
:percent="expireProgress(client.expiryTime, client.reset)"/>
|
||||
</a-popover>
|
||||
</td>
|
||||
@@ -260,7 +258,7 @@
|
||||
</table>
|
||||
</template>
|
||||
<a-badge>
|
||||
<a-icon v-if="!client.enable" slot="count" type="pause-circle" :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="font-size: 14px; padding: 0 10px;"><a-icon type="solution"></a-icon></a-button>
|
||||
</a-badge>
|
||||
</a-popover>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<tr>
|
||||
<td>{{ i18n "transmission" }}</td><td><a-tag color="green">[[ inbound.network ]]</a-tag></td>
|
||||
</tr>
|
||||
<template v-if="inbound.isTcp || inbound.isWs || inbound.isH2">
|
||||
<template v-if="inbound.isTcp || inbound.isWs || inbound.isH2 || inbound.isHttpupgrade ">
|
||||
<tr>
|
||||
<td>{{ i18n "host" }}</td>
|
||||
<td v-if="inbound.host">
|
||||
@@ -143,7 +143,7 @@
|
||||
<tr>
|
||||
<td>
|
||||
<a-tag v-if="infoModal.clientStats && infoModal.clientSettings.totalGB > 0" :color="statsColor(infoModal.clientStats)">
|
||||
[[ sizeFormat(infoModal.clientSettings.totalGB - infoModal.clientStats.up - infoModal.clientStats.down) ]]
|
||||
[[ getRemStats() ]]
|
||||
</a-tag>
|
||||
</td>
|
||||
<td>
|
||||
@@ -423,7 +423,11 @@
|
||||
},
|
||||
statsColor(stats) {
|
||||
return usageColor(stats.up + stats.down, app.trafficDiff, stats.total);
|
||||
}
|
||||
},
|
||||
getRemStats() {
|
||||
remained = this.infoModal.clientStats.total - this.infoModal.clientStats.up - this.infoModal.clientStats.down;
|
||||
return remained>0 ? sizeFormat(remained) : '-';
|
||||
},
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
{{define "inboundModal"}}
|
||||
<a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title" @ok="inModal.ok"
|
||||
:confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false"
|
||||
:class="themeSwitcher.currentTheme"
|
||||
:ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
|
||||
<a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title"
|
||||
:dialog-style="{ top: '20px' }" @ok="inModal.ok"
|
||||
:confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false"
|
||||
:class="themeSwitcher.currentTheme"
|
||||
:ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
|
||||
{{template "form/inbound"}}
|
||||
</a-modal>
|
||||
<script>
|
||||
|
||||
@@ -2,457 +2,463 @@
|
||||
<html lang="en">
|
||||
{{template "head" .}}
|
||||
<style>
|
||||
@media (min-width: 769px) {
|
||||
.ant-layout-content {
|
||||
margin: 24px 16px;
|
||||
}
|
||||
@media (min-width: 769px) {
|
||||
.ant-layout-content {
|
||||
margin: 24px 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.ant-card-body {
|
||||
padding: .5rem;
|
||||
}
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.ant-card-body {
|
||||
padding: .5rem;
|
||||
}
|
||||
|
||||
.ant-col-sm-24 {
|
||||
margin: 0.5rem -2rem 0.5rem 2rem;
|
||||
}
|
||||
.ant-col-sm-24 {
|
||||
margin: 0.5rem -2rem 0.5rem 2rem;
|
||||
}
|
||||
tr.hideExpandIcon .ant-table-row-expand-icon {
|
||||
display: none;
|
||||
}
|
||||
.infinite-tag {
|
||||
padding: 0 5px;
|
||||
border-radius: 2rem;
|
||||
min-width: 50px;
|
||||
}
|
||||
.infinite-bar .ant-progress-inner .ant-progress-bg {
|
||||
background-color: #F2EAF1;
|
||||
border: #D5BED2 solid 1px;
|
||||
}
|
||||
.dark .infinite-bar .ant-progress-inner .ant-progress-bg {
|
||||
background-color: #7a316f;
|
||||
border: #7a316f solid 1px;
|
||||
}
|
||||
.ant-collapse {
|
||||
margin: 5px 0;
|
||||
}
|
||||
.info-large-tag {
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.online-animation .ant-badge-status-dot {
|
||||
animation: onlineAnimation 1.2s linear infinite;
|
||||
}
|
||||
@keyframes onlineAnimation {
|
||||
0%,
|
||||
50%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
tr.hideExpandIcon .ant-table-row-expand-icon {
|
||||
display: none;
|
||||
}
|
||||
.infinite-tag {
|
||||
padding: 0 5px;
|
||||
border-radius: 2rem;
|
||||
min-width: 50px;
|
||||
}
|
||||
.infinite-bar .ant-progress-inner .ant-progress-bg {
|
||||
background-color: #F2EAF1;
|
||||
border: #D5BED2 solid 1px;
|
||||
}
|
||||
.dark .infinite-bar .ant-progress-inner .ant-progress-bg {
|
||||
background-color: #3c1536;
|
||||
border: #7a316f solid 1px;
|
||||
}
|
||||
.ant-collapse {
|
||||
margin: 5px 0;
|
||||
}
|
||||
.online-animation .ant-badge-status-dot {
|
||||
animation: 1.2s ease infinite normal none running onlineAnimation;
|
||||
}
|
||||
@keyframes onlineAnimation {
|
||||
0%, 50%, 100% { transform: scale(1); opacity: 1; }
|
||||
10% { transform: scale(1.5); opacity: .2; }
|
||||
}
|
||||
.info-large-tag {
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
10% {
|
||||
transform: scale(1.5);
|
||||
opacity: .2;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<body>
|
||||
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
|
||||
{{ template "commonSider" . }}
|
||||
<a-layout id="content-layout">
|
||||
<a-layout-content>
|
||||
<a-spin :spinning="spinning" :delay="500" tip='{{ i18n "loading"}}'>
|
||||
<transition name="list" appear>
|
||||
<a-alert type="error" v-if="showAlert" style="margin-bottom: 10px"
|
||||
message='{{ i18n "secAlertTitle" }}'
|
||||
color="red"
|
||||
description='{{ i18n "secAlertSsl" }}'
|
||||
show-icon closable
|
||||
>
|
||||
</a-alert>
|
||||
</transition>
|
||||
<transition name="list" appear>
|
||||
<a-card hoverable>
|
||||
<a-row>
|
||||
<a-col :xs="24" :sm="24" :lg="12">
|
||||
{{ i18n "pages.inbounds.totalDownUp" }}:
|
||||
<a-tag color="green">[[ sizeFormat(total.up) ]] / [[ sizeFormat(total.down) ]]</a-tag>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="24" :lg="12">
|
||||
{{ i18n "pages.inbounds.totalUsage" }}:
|
||||
<a-tag color="green">[[ sizeFormat(total.up + total.down) ]]</a-tag>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="24" :lg="12">
|
||||
{{ i18n "pages.inbounds.inboundCount" }}:
|
||||
<a-tag color="green">[[ dbInbounds.length ]]</a-tag>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="24" :lg="12">
|
||||
<template>
|
||||
<div>
|
||||
<a-back-top :target="() => document.getElementById('content-layout')" visibility-height="200">
|
||||
</a-back-top>
|
||||
{{ i18n "clients" }}:
|
||||
<a-tag color="green">[[ total.clients ]]</a-tag>
|
||||
<a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<p v-for="clientEmail in total.deactive">[[ clientEmail ]]</p>
|
||||
</template>
|
||||
<a-tag v-if="total.deactive.length">[[ total.deactive.length ]]</a-tag>
|
||||
</a-popover>
|
||||
<a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<p v-for="clientEmail in total.depleted">[[ clientEmail ]]</p>
|
||||
</template>
|
||||
<a-tag color="red" v-if="total.depleted.length">[[ total.depleted.length ]]</a-tag>
|
||||
</a-popover>
|
||||
<a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<p v-for="clientEmail in total.expiring">[[ clientEmail ]]</p>
|
||||
</template>
|
||||
<a-tag color="orange" v-if="total.expiring.length">[[ total.expiring.length ]]</a-tag>
|
||||
</a-popover>
|
||||
<a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<p v-for="clientEmail in onlineClients">[[ clientEmail ]]</p>
|
||||
</template>
|
||||
<a-tag color="blue" v-if="onlineClients.length">[[ onlineClients.length ]]</a-tag>
|
||||
</a-popover>
|
||||
</div>
|
||||
</template>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</transition>
|
||||
<transition name="list" appear>
|
||||
<a-card hoverable>
|
||||
<div slot="title">
|
||||
<a-row>
|
||||
<a-col :xs="12" :sm="12" :lg="12">
|
||||
<a-button type="primary" icon="plus" @click="openAddInbound">
|
||||
<template v-if="!isMobile">{{ i18n "pages.inbounds.addInbound" }}</template>
|
||||
</a-button>
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-button type="primary" icon="menu">
|
||||
<template v-if="!isMobile">{{ i18n "pages.inbounds.generalActions" }}</template>
|
||||
</a-button>
|
||||
<a-menu slot="overlay" @click="a => generalActions(a)" :theme="themeSwitcher.currentTheme">
|
||||
<a-menu-item key="import">
|
||||
<a-icon type="import"></a-icon>
|
||||
{{ i18n "pages.inbounds.importInbound" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="export">
|
||||
<a-icon type="export"></a-icon>
|
||||
{{ i18n "pages.inbounds.export" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="subs" v-if="subSettings.enable">
|
||||
<a-icon type="export"></a-icon>
|
||||
{{ i18n "pages.inbounds.export" }} - {{ i18n "pages.settings.subSettings" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="resetInbounds">
|
||||
<a-icon type="reload"></a-icon>
|
||||
{{ i18n "pages.inbounds.resetAllTraffic" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="resetClients">
|
||||
<a-icon type="file-done"></a-icon>
|
||||
{{ i18n "pages.inbounds.resetAllClientTraffics" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="delDepletedClients" style="color: #FF4D4F;">
|
||||
<a-icon type="rest"></a-icon>
|
||||
{{ i18n "pages.inbounds.delDepletedClients" }}
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-dropdown>
|
||||
</a-col>
|
||||
<a-col :xs="12" :sm="12" :lg="12" style="text-align: right;">
|
||||
<a-select v-model="refreshInterval"
|
||||
style="width: 65px;"
|
||||
v-if="isRefreshEnabled"
|
||||
@change="changeRefreshInterval"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in [5,10,30,60]" :value="key*1000">[[ key ]]s</a-select-option>
|
||||
</a-select>
|
||||
<a-icon type="sync" :spin="refreshing" @click="manualRefresh" style="margin: 0 5px;"></a-icon>
|
||||
<a-switch v-model="isRefreshEnabled" @change="toggleRefresh"></a-switch>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
<div :style="isMobile ? '' : 'display: flex; align-items: center; justify-content: flex-start;'">
|
||||
<a-switch v-model="enableFilter"
|
||||
:style="isMobile ? 'margin-bottom: .5rem; display: flex;' : 'margin-right: .5rem;'"
|
||||
@change="toggleFilter">
|
||||
<a-icon slot="checkedChildren" type="search"></a-icon>
|
||||
<a-icon slot="unCheckedChildren" type="filter"></a-icon>
|
||||
</a-switch>
|
||||
<a-input v-if="!enableFilter" v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus style="max-width: 300px" :size="isMobile ? 'small' : ''"></a-input>
|
||||
<a-radio-group v-if="enableFilter" v-model="filterBy" @change="filterInbounds" button-style="solid" :size="isMobile ? 'small' : ''">
|
||||
<a-radio-button value="">{{ i18n "none" }}</a-radio-button>
|
||||
<a-radio-button value="deactive">{{ i18n "disabled" }}</a-radio-button>
|
||||
<a-radio-button value="depleted">{{ i18n "depleted" }}</a-radio-button>
|
||||
<a-radio-button value="expiring">{{ i18n "depletingSoon" }}</a-radio-button>
|
||||
<a-radio-button value="online">{{ i18n "online" }}</a-radio-button>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<a-back-top></a-back-top>
|
||||
<a-table :columns="isMobile ? mobileColumns : columns" :row-key="dbInbound => dbInbound.id"
|
||||
:data-source="searchedInbounds"
|
||||
:scroll="isMobile ? {} : { x: 1000 }"
|
||||
:pagination=pagination(searchedInbounds)
|
||||
:expand-icon-as-cell="false"
|
||||
:expand-row-by-click="false"
|
||||
:expand-icon-column-index="0"
|
||||
:indent-size="0"
|
||||
:row-class-name="dbInbound => (dbInbound.isMultiUser() ? '' : 'hideExpandIcon')"
|
||||
style="margin-top: 10px">
|
||||
<template slot="action" slot-scope="text, dbInbound">
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-icon @click="e => e.preventDefault()" type="more" style="font-size: 20px; text-decoration: solid;"></a-icon>
|
||||
<a-menu slot="overlay" @click="a => clickAction(a, dbInbound)" :theme="themeSwitcher.currentTheme">
|
||||
<a-menu-item key="edit">
|
||||
<a-icon type="edit"></a-icon>
|
||||
{{ i18n "edit" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="qrcode" v-if="(dbInbound.isSS && !dbInbound.toInbound().isSSMultiUser) || dbInbound.isWireguard">
|
||||
<a-icon type="qrcode"></a-icon>
|
||||
{{ i18n "qrCode" }}
|
||||
</a-menu-item>
|
||||
<template v-if="dbInbound.isMultiUser()">
|
||||
<a-menu-item key="addClient">
|
||||
<a-icon type="user-add"></a-icon>
|
||||
{{ i18n "pages.client.add"}}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="addBulkClient">
|
||||
<a-icon type="usergroup-add"></a-icon>
|
||||
{{ i18n "pages.client.bulk"}}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="resetClients">
|
||||
<a-icon type="file-done"></a-icon>
|
||||
{{ i18n "pages.inbounds.resetInboundClientTraffics"}}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="export">
|
||||
<a-icon type="export"></a-icon>
|
||||
{{ i18n "pages.inbounds.export"}}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="subs" v-if="subSettings.enable">
|
||||
<a-icon type="export"></a-icon>
|
||||
{{ i18n "pages.inbounds.export"}} - {{ i18n "pages.settings.subSettings" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="delDepletedClients" style="color: #FF4D4F;">
|
||||
<a-icon type="rest"></a-icon>
|
||||
{{ i18n "pages.inbounds.delDepletedClients" }}
|
||||
</a-menu-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-menu-item key="showInfo">
|
||||
<a-icon type="info-circle"></a-icon>
|
||||
{{ i18n "info"}}
|
||||
</a-menu-item>
|
||||
</template>
|
||||
<a-menu-item key="clipboard">
|
||||
<a-icon type="copy"></a-icon>
|
||||
{{ i18n "pages.inbounds.exportInbound" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="resetTraffic">
|
||||
<a-icon type="retweet"></a-icon> {{ i18n "pages.inbounds.resetTraffic" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="clone">
|
||||
<a-icon type="block"></a-icon> {{ i18n "pages.inbounds.clone"}}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="delete">
|
||||
<span style="color: #FF4D4F">
|
||||
<a-icon type="delete"></a-icon> {{ i18n "delete"}}
|
||||
</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item v-if="isMobile">
|
||||
<a-switch size="small" v-model="dbInbound.enable" @change="switchEnable(dbInbound.id,dbInbound.enable)"></a-switch>
|
||||
{{ i18n "pages.inbounds.enable" }}
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-dropdown>
|
||||
{{ template "commonSider" . }}
|
||||
<a-layout id="content-layout">
|
||||
<a-layout-content>
|
||||
<a-spin :spinning="spinning" :delay="500" tip='{{ i18n "loading"}}'>
|
||||
<transition name="list" appear>
|
||||
<a-alert type="error" v-if="showAlert" style="margin-bottom: 10px"
|
||||
message='{{ i18n "secAlertTitle" }}'
|
||||
color="red"
|
||||
description='{{ i18n "secAlertSsl" }}'
|
||||
show-icon closable>
|
||||
</a-alert>
|
||||
</transition>
|
||||
<transition name="list" appear>
|
||||
<a-card hoverable>
|
||||
<a-row>
|
||||
<a-col :xs="24" :sm="24" :lg="12">
|
||||
{{ i18n "pages.inbounds.totalDownUp" }}:
|
||||
<a-tag color="green">[[ sizeFormat(total.up) ]] / [[ sizeFormat(total.down) ]]</a-tag>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="24" :lg="12">
|
||||
{{ i18n "pages.inbounds.totalUsage" }}:
|
||||
<a-tag color="green">[[ sizeFormat(total.up + total.down) ]]</a-tag>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="24" :lg="12">
|
||||
{{ i18n "pages.inbounds.inboundCount" }}:
|
||||
<a-tag color="green">[[ dbInbounds.length ]]</a-tag>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="24" :lg="12">
|
||||
<template>
|
||||
<div>
|
||||
<a-back-top :target="() => document.getElementById('content-layout')" visibility-height="200"></a-back-top>
|
||||
{{ i18n "clients" }}:
|
||||
<a-tag color="green">[[ total.clients ]]</a-tag>
|
||||
<a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<p v-for="clientEmail in total.deactive">[[ clientEmail ]]</p>
|
||||
</template>
|
||||
<a-tag v-if="total.deactive.length">[[ total.deactive.length ]]</a-tag>
|
||||
</a-popover>
|
||||
<a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<p v-for="clientEmail in total.depleted">[[ clientEmail ]]</p>
|
||||
</template>
|
||||
<a-tag color="red" v-if="total.depleted.length">[[ total.depleted.length ]]</a-tag>
|
||||
</a-popover>
|
||||
<a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<p v-for="clientEmail in total.expiring">[[ clientEmail ]]</p>
|
||||
</template>
|
||||
<a-tag color="orange" v-if="total.expiring.length">[[ total.expiring.length ]]</a-tag>
|
||||
</a-popover>
|
||||
<a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<p v-for="clientEmail in onlineClients">[[ clientEmail ]]</p>
|
||||
</template>
|
||||
<a-tag color="blue" v-if="onlineClients.length">[[ onlineClients.length ]]</a-tag>
|
||||
</a-popover>
|
||||
</div>
|
||||
</template>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</transition>
|
||||
<transition name="list" appear>
|
||||
<a-card hoverable>
|
||||
<div slot="title">
|
||||
<a-row>
|
||||
<a-col :xs="12" :sm="12" :lg="12">
|
||||
<a-button type="primary" icon="plus" @click="openAddInbound">
|
||||
<template v-if="!isMobile">{{ i18n "pages.inbounds.addInbound" }}</template>
|
||||
</a-button>
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-button type="primary" icon="menu">
|
||||
<template v-if="!isMobile">{{ i18n "pages.inbounds.generalActions" }}</template>
|
||||
</a-button>
|
||||
<a-menu slot="overlay" @click="a => generalActions(a)" :theme="themeSwitcher.currentTheme">
|
||||
<a-menu-item key="import">
|
||||
<a-icon type="import"></a-icon>
|
||||
{{ i18n "pages.inbounds.importInbound" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="export">
|
||||
<a-icon type="export"></a-icon>
|
||||
{{ i18n "pages.inbounds.export" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="subs" v-if="subSettings.enable">
|
||||
<a-icon type="export"></a-icon>
|
||||
{{ i18n "pages.inbounds.export" }} - {{ i18n "pages.settings.subSettings" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="resetInbounds">
|
||||
<a-icon type="reload"></a-icon>
|
||||
{{ i18n "pages.inbounds.resetAllTraffic" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="resetClients">
|
||||
<a-icon type="file-done"></a-icon>
|
||||
{{ i18n "pages.inbounds.resetAllClientTraffics" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="delDepletedClients" style="color: #FF4D4F;">
|
||||
<a-icon type="rest"></a-icon>
|
||||
{{ i18n "pages.inbounds.delDepletedClients" }}
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-dropdown>
|
||||
</a-col>
|
||||
<a-col :xs="12" :sm="12" :lg="12" style="text-align: right;">
|
||||
<a-select v-model="refreshInterval"
|
||||
style="width: 65px;"
|
||||
v-if="isRefreshEnabled"
|
||||
@change="changeRefreshInterval"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in [5,10,30,60]" :value="key*1000">[[ key ]]s</a-select-option>
|
||||
</a-select>
|
||||
<a-icon type="sync" :spin="refreshing" @click="manualRefresh" style="margin: 0 5px;"></a-icon>
|
||||
<a-switch v-model="isRefreshEnabled" @change="toggleRefresh"></a-switch>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
<div :style="isMobile ? '' : 'display: flex; align-items: center; justify-content: flex-start;'">
|
||||
<a-switch v-model="enableFilter"
|
||||
:style="isMobile ? 'margin-bottom: .5rem; display: flex;' : 'margin-right: .5rem;'"
|
||||
@change="toggleFilter">
|
||||
<a-icon slot="checkedChildren" type="search"></a-icon>
|
||||
<a-icon slot="unCheckedChildren" type="filter"></a-icon>
|
||||
</a-switch>
|
||||
<a-input v-if="!enableFilter" v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus style="max-width: 300px" :size="isMobile ? 'small' : ''"></a-input>
|
||||
<a-radio-group v-if="enableFilter" v-model="filterBy" @change="filterInbounds" button-style="solid" :size="isMobile ? 'small' : ''">
|
||||
<a-radio-button value="">{{ i18n "none" }}</a-radio-button>
|
||||
<a-radio-button value="deactive">{{ i18n "disabled" }}</a-radio-button>
|
||||
<a-radio-button value="depleted">{{ i18n "depleted" }}</a-radio-button>
|
||||
<a-radio-button value="expiring">{{ i18n "depletingSoon" }}</a-radio-button>
|
||||
<a-radio-button value="online">{{ i18n "online" }}</a-radio-button>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<a-back-top></a-back-top>
|
||||
<a-table :columns="isMobile ? mobileColumns : columns" :row-key="dbInbound => dbInbound.id"
|
||||
:data-source="searchedInbounds"
|
||||
:scroll="isMobile ? {} : { x: 1000 }"
|
||||
:pagination=pagination(searchedInbounds)
|
||||
:expand-icon-as-cell="false"
|
||||
:expand-row-by-click="false"
|
||||
:expand-icon-column-index="0"
|
||||
:indent-size="0"
|
||||
:row-class-name="dbInbound => (dbInbound.isMultiUser() ? '' : 'hideExpandIcon')"
|
||||
style="margin-top: 10px">
|
||||
<template slot="action" slot-scope="text, dbInbound">
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-icon @click="e => e.preventDefault()" type="more" style="font-size: 20px; text-decoration: solid;"></a-icon>
|
||||
<a-menu slot="overlay" @click="a => clickAction(a, dbInbound)" :theme="themeSwitcher.currentTheme">
|
||||
<a-menu-item key="edit">
|
||||
<a-icon type="edit"></a-icon>
|
||||
{{ i18n "edit" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="qrcode" v-if="(dbInbound.isSS && !dbInbound.toInbound().isSSMultiUser) || dbInbound.isWireguard">
|
||||
<a-icon type="qrcode"></a-icon>
|
||||
{{ i18n "qrCode" }}
|
||||
</a-menu-item>
|
||||
<template v-if="dbInbound.isMultiUser()">
|
||||
<a-menu-item key="addClient">
|
||||
<a-icon type="user-add"></a-icon>
|
||||
{{ i18n "pages.client.add"}}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="addBulkClient">
|
||||
<a-icon type="usergroup-add"></a-icon>
|
||||
{{ i18n "pages.client.bulk"}}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="resetClients">
|
||||
<a-icon type="file-done"></a-icon>
|
||||
{{ i18n "pages.inbounds.resetInboundClientTraffics"}}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="export">
|
||||
<a-icon type="export"></a-icon>
|
||||
{{ i18n "pages.inbounds.export"}}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="subs" v-if="subSettings.enable">
|
||||
<a-icon type="export"></a-icon>
|
||||
{{ i18n "pages.inbounds.export"}} - {{ i18n "pages.settings.subSettings" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="delDepletedClients" style="color: #FF4D4F;">
|
||||
<a-icon type="rest"></a-icon>
|
||||
{{ i18n "pages.inbounds.delDepletedClients" }}
|
||||
</a-menu-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-menu-item key="showInfo">
|
||||
<a-icon type="info-circle"></a-icon>
|
||||
{{ i18n "info"}}
|
||||
</a-menu-item>
|
||||
</template>
|
||||
<a-menu-item key="clipboard">
|
||||
<a-icon type="copy"></a-icon>
|
||||
{{ i18n "pages.inbounds.exportInbound" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="resetTraffic">
|
||||
<a-icon type="retweet"></a-icon> {{ i18n "pages.inbounds.resetTraffic" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="clone">
|
||||
<a-icon type="block"></a-icon> {{ i18n "pages.inbounds.clone"}}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="delete">
|
||||
<span style="color: #FF4D4F">
|
||||
<a-icon type="delete"></a-icon> {{ i18n "delete"}}
|
||||
</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item v-if="isMobile">
|
||||
<a-switch size="small" v-model="dbInbound.enable" @change="switchEnable(dbInbound.id,dbInbound.enable)"></a-switch>
|
||||
{{ i18n "pages.inbounds.enable" }}
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
<template slot="protocol" slot-scope="text, dbInbound">
|
||||
<a-tag style="margin:0;" color="purple">[[ dbInbound.protocol ]]</a-tag>
|
||||
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
|
||||
<a-tag style="margin:0;" color="green">[[ dbInbound.toInbound().stream.network ]]</a-tag>
|
||||
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isTls" color="blue">TLS</a-tag>
|
||||
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isXtls" color="blue">XTLS</a-tag>
|
||||
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isReality" color="blue">Reality</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
<template slot="clients" slot-scope="text, dbInbound">
|
||||
<template v-if="clientCount[dbInbound.id]">
|
||||
<a-tag style="margin:0;" color="green">[[ clientCount[dbInbound.id].clients ]]</a-tag>
|
||||
<a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<p v-for="clientEmail in clientCount[dbInbound.id].deactive">[[ clientEmail ]]</p>
|
||||
</template>
|
||||
<a-tag style="margin:0; padding: 0 2px;" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag>
|
||||
</a-popover>
|
||||
<a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<p v-for="clientEmail in clientCount[dbInbound.id].depleted">[[ clientEmail ]]</p>
|
||||
</template>
|
||||
<a-tag style="margin:0; padding: 0 2px;" color="red" v-if="clientCount[dbInbound.id].depleted.length">[[ clientCount[dbInbound.id].depleted.length ]]</a-tag>
|
||||
</a-popover>
|
||||
<a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<p v-for="clientEmail in clientCount[dbInbound.id].expiring">[[ clientEmail ]]</p>
|
||||
</template>
|
||||
<a-tag style="margin:0; padding: 0 2px;" color="orange" v-if="clientCount[dbInbound.id].expiring.length">[[ clientCount[dbInbound.id].expiring.length ]]</a-tag>
|
||||
</a-popover>
|
||||
<a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<p v-for="clientEmail in clientCount[dbInbound.id].online">[[ clientEmail ]]</p>
|
||||
</template>
|
||||
<a-tag style="margin:0; padding: 0 2px;" color="blue" v-if="clientCount[dbInbound.id].online.length">[[ clientCount[dbInbound.id].online.length ]]</a-tag>
|
||||
</a-popover>
|
||||
</template>
|
||||
</template>
|
||||
<template slot="traffic" slot-scope="text, dbInbound">
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<table cellpadding="2" width="100%">
|
||||
<tr>
|
||||
<td>↑[[ sizeFormat(dbInbound.up) ]]</td>
|
||||
<td>↓[[ sizeFormat(dbInbound.down) ]]</td>
|
||||
</tr>
|
||||
<tr v-if="dbInbound.total > 0 && dbInbound.up + dbInbound.down < dbInbound.total">
|
||||
<td>{{ i18n "remained" }}</td>
|
||||
<td>[[ sizeFormat(dbInbound.total - dbInbound.up - dbInbound.down) ]]</td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
<a-tag :color="usageColor(dbInbound.up + dbInbound.down, app.trafficDiff, dbInbound.total)">
|
||||
[[ sizeFormat(dbInbound.up + dbInbound.down) ]] /
|
||||
<template v-if="dbInbound.total > 0">
|
||||
[[ sizeFormat(dbInbound.total) ]]
|
||||
</template>
|
||||
<template v-else>
|
||||
<svg style="fill: currentColor; height: 10px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512">
|
||||
<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" />
|
||||
</svg>
|
||||
</template>
|
||||
</a-tag>
|
||||
</a-popover>
|
||||
</template>
|
||||
<template slot="enable" slot-scope="text, dbInbound">
|
||||
<a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound.id,dbInbound.enable)"></a-switch>
|
||||
</template>
|
||||
<template slot="expiryTime" slot-scope="text, dbInbound">
|
||||
<a-popover v-if="dbInbound.expiryTime > 0" :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
[[ DateUtil.formatMillis(dbInbound.expiryTime) ]]
|
||||
</template>
|
||||
<a-tag style="min-width: 50px;" :color="usageColor(new Date().getTime(), app.expireDiff, dbInbound._expiryTime)">
|
||||
[[ remainedDays(dbInbound._expiryTime) ]]
|
||||
</a-tag>
|
||||
</a-popover>
|
||||
<a-tag v-else color="purple" class="infinite-tag">∞</a-tag>
|
||||
</template>
|
||||
<template slot="info" slot-scope="text, dbInbound">
|
||||
<a-popover placement="bottomRight" :overlay-class-name="themeSwitcher.currentTheme" trigger="click">
|
||||
<template slot="content">
|
||||
<table cellpadding="2">
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.protocol" }}</td>
|
||||
<td>
|
||||
<a-tag style="margin:0;" color="purple">[[ dbInbound.protocol ]]</a-tag>
|
||||
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
|
||||
<a-tag style="margin:0;" color="blue">[[ dbInbound.toInbound().stream.network ]]</a-tag>
|
||||
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isTls" color="green">tls</a-tag>
|
||||
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isReality" color="green">reality</a-tag>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.port" }}</td>
|
||||
<td><a-tag>[[ dbInbound.port ]]</a-tag></td>
|
||||
</tr>
|
||||
<tr v-if="clientCount[dbInbound.id]">
|
||||
<td>{{ i18n "clients" }}</td>
|
||||
<td>
|
||||
<a-tag style="margin:0;" color="blue">[[ clientCount[dbInbound.id].clients ]]</a-tag>
|
||||
<a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<p v-for="clientEmail in clientCount[dbInbound.id].deactive">[[ clientEmail ]]</p>
|
||||
</template>
|
||||
<template slot="protocol" slot-scope="text, dbInbound">
|
||||
<a-tag style="margin:0;" color="purple">[[ dbInbound.protocol ]]</a-tag>
|
||||
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
|
||||
<a-tag style="margin:0;" color="green">[[ dbInbound.toInbound().stream.network ]]</a-tag>
|
||||
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isTls" color="blue">TLS</a-tag>
|
||||
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isXtls" color="blue">XTLS</a-tag>
|
||||
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isReality" color="blue">Reality</a-tag>
|
||||
<a-tag style="margin:0; padding: 0 2px;" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag>
|
||||
</a-popover>
|
||||
<a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<p v-for="clientEmail in clientCount[dbInbound.id].depleted">[[ clientEmail ]]</p>
|
||||
</template>
|
||||
<a-tag style="margin:0; padding: 0 2px;" color="red" v-if="clientCount[dbInbound.id].depleted.length">[[ clientCount[dbInbound.id].depleted.length ]]</a-tag>
|
||||
</a-popover>
|
||||
<a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<p v-for="clientEmail in clientCount[dbInbound.id].expiring">[[ clientEmail ]]</p>
|
||||
</template>
|
||||
<a-tag style="margin:0; padding: 0 2px;" color="orange" v-if="clientCount[dbInbound.id].expiring.length">[[ clientCount[dbInbound.id].expiring.length ]]</a-tag>
|
||||
</a-popover>
|
||||
<a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<p v-for="clientEmail in clientCount[dbInbound.id].online">[[ clientEmail ]]</p>
|
||||
</template>
|
||||
<a-tag style="margin:0; padding: 0 2px;" color="green" v-if="clientCount[dbInbound.id].online.length">[[ clientCount[dbInbound.id].online.length ]]</a-tag>
|
||||
</a-popover>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.traffic" }}</td>
|
||||
<td>
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<table cellpadding="2" width="100%">
|
||||
<tr>
|
||||
<td>↑[[ sizeFormat(dbInbound.up) ]]</td>
|
||||
<td>↓[[ sizeFormat(dbInbound.down) ]]</td>
|
||||
</tr>
|
||||
<tr v-if="dbInbound.total > 0 && dbInbound.up + dbInbound.down < dbInbound.total">
|
||||
<td>{{ i18n "remained" }}</td>
|
||||
<td>[[ sizeFormat(dbInbound.total - dbInbound.up - dbInbound.down) ]]</td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
<a-tag :color="usageColor(dbInbound.up + dbInbound.down, app.trafficDiff, dbInbound.total)">
|
||||
[[ sizeFormat(dbInbound.up + dbInbound.down) ]] /
|
||||
<template v-if="dbInbound.total > 0">
|
||||
[[ sizeFormat(dbInbound.total) ]]
|
||||
</template>
|
||||
</template>
|
||||
<template slot="clients" slot-scope="text, dbInbound">
|
||||
<template v-if="clientCount[dbInbound.id]">
|
||||
<a-tag style="margin:0;" color="green">[[ clientCount[dbInbound.id].clients ]]</a-tag>
|
||||
<a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<p v-for="clientEmail in clientCount[dbInbound.id].deactive">[[ clientEmail ]]</p>
|
||||
</template>
|
||||
<a-tag style="margin:0; padding: 0 2px;" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag>
|
||||
</a-popover>
|
||||
<a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<p v-for="clientEmail in clientCount[dbInbound.id].depleted">[[ clientEmail ]]</p>
|
||||
</template>
|
||||
<a-tag style="margin:0; padding: 0 2px;" color="red" v-if="clientCount[dbInbound.id].depleted.length">[[ clientCount[dbInbound.id].depleted.length ]]</a-tag>
|
||||
</a-popover>
|
||||
<a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<p v-for="clientEmail in clientCount[dbInbound.id].expiring">[[ clientEmail ]]</p>
|
||||
</template>
|
||||
<a-tag style="margin:0; padding: 0 2px;" color="orange" v-if="clientCount[dbInbound.id].expiring.length">[[ clientCount[dbInbound.id].expiring.length ]]</a-tag>
|
||||
</a-popover>
|
||||
<a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<p v-for="clientEmail in clientCount[dbInbound.id].online">[[ clientEmail ]]</p>
|
||||
</template>
|
||||
<a-tag style="margin:0; padding: 0 2px;" color="blue" v-if="clientCount[dbInbound.id].online.length">[[ clientCount[dbInbound.id].online.length ]]</a-tag>
|
||||
</a-popover>
|
||||
</template>
|
||||
</template>
|
||||
<template slot="traffic" slot-scope="text, dbInbound">
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<table cellpadding="2" width="100%">
|
||||
<tr>
|
||||
<td>↑[[ sizeFormat(dbInbound.up) ]]</td>
|
||||
<td>↓[[ sizeFormat(dbInbound.down) ]]</td>
|
||||
</tr>
|
||||
<tr v-if="dbInbound.total > 0 && dbInbound.up + dbInbound.down < dbInbound.total">
|
||||
<td>{{ i18n "remained" }}</td>
|
||||
<td>[[ sizeFormat(dbInbound.total - dbInbound.up - dbInbound.down) ]]</td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
<a-tag :color="usageColor(dbInbound.up + dbInbound.down, app.trafficDiff, dbInbound.total)">
|
||||
[[ sizeFormat(dbInbound.up + dbInbound.down) ]] /
|
||||
<template v-if="dbInbound.total > 0">
|
||||
[[ sizeFormat(dbInbound.total) ]]
|
||||
</template>
|
||||
<template v-else>
|
||||
<svg style="fill: currentColor; height: 10px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><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"/></svg>
|
||||
</template>
|
||||
</a-tag>
|
||||
</a-popover>
|
||||
</template>
|
||||
<template slot="enable" slot-scope="text, dbInbound">
|
||||
<a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound.id,dbInbound.enable)"></a-switch>
|
||||
</template>
|
||||
<template slot="expiryTime" slot-scope="text, dbInbound">
|
||||
<a-popover v-if="dbInbound.expiryTime > 0" :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
[[ DateUtil.formatMillis(dbInbound.expiryTime) ]]
|
||||
</template>
|
||||
<a-tag style="min-width: 50px;" :color="usageColor(new Date().getTime(), app.expireDiff, dbInbound._expiryTime)">
|
||||
[[ remainedDays(dbInbound._expiryTime) ]]
|
||||
</a-tag>
|
||||
</a-popover>
|
||||
<a-tag v-else color="purple" class="infinite-tag">∞</a-tag>
|
||||
</template>
|
||||
<template slot="info" slot-scope="text, dbInbound">
|
||||
<a-popover placement="bottomRight" :overlay-class-name="themeSwitcher.currentTheme" trigger="click">
|
||||
<template slot="content">
|
||||
<table cellpadding="2">
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.protocol" }}</td>
|
||||
<td>
|
||||
<a-tag style="margin:0;" color="purple">[[ dbInbound.protocol ]]</a-tag>
|
||||
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
|
||||
<a-tag style="margin:0;" color="blue">[[ dbInbound.toInbound().stream.network ]]</a-tag>
|
||||
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isTls" color="green">tls</a-tag>
|
||||
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isReality" color="green">reality</a-tag>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.port" }}</td>
|
||||
<td><a-tag>[[ dbInbound.port ]]</a-tag></td>
|
||||
</tr>
|
||||
<tr v-if="clientCount[dbInbound.id]">
|
||||
<td>{{ i18n "clients" }}</td>
|
||||
<td>
|
||||
<a-tag style="margin:0;" color="blue">[[ clientCount[dbInbound.id].clients ]]</a-tag>
|
||||
<a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<p v-for="clientEmail in clientCount[dbInbound.id].deactive">[[ clientEmail ]]</p>
|
||||
</template>
|
||||
<a-tag style="margin:0; padding: 0 2px;" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag>
|
||||
</a-popover>
|
||||
<a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<p v-for="clientEmail in clientCount[dbInbound.id].depleted">[[ clientEmail ]]</p>
|
||||
</template>
|
||||
<a-tag style="margin:0; padding: 0 2px;" color="red" v-if="clientCount[dbInbound.id].depleted.length">[[ clientCount[dbInbound.id].depleted.length ]]</a-tag>
|
||||
</a-popover>
|
||||
<a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<p v-for="clientEmail in clientCount[dbInbound.id].expiring">[[ clientEmail ]]</p>
|
||||
</template>
|
||||
<a-tag style="margin:0; padding: 0 2px;" color="orange" v-if="clientCount[dbInbound.id].expiring.length">[[ clientCount[dbInbound.id].expiring.length ]]</a-tag>
|
||||
</a-popover>
|
||||
<a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<p v-for="clientEmail in clientCount[dbInbound.id].online">[[ clientEmail ]]</p>
|
||||
</template>
|
||||
<a-tag style="margin:0; padding: 0 2px;" color="green" v-if="clientCount[dbInbound.id].online.length">[[ clientCount[dbInbound.id].online.length ]]</a-tag>
|
||||
</a-popover>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.traffic" }}</td>
|
||||
<td>
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<table cellpadding="2" width="100%">
|
||||
<tr>
|
||||
<td>↑[[ sizeFormat(dbInbound.up) ]]</td>
|
||||
<td>↓[[ sizeFormat(dbInbound.down) ]]</td>
|
||||
</tr>
|
||||
<tr v-if="dbInbound.total > 0 && dbInbound.up + dbInbound.down < dbInbound.total">
|
||||
<td>{{ i18n "remained" }}</td>
|
||||
<td>[[ sizeFormat(dbInbound.total - dbInbound.up - dbInbound.down) ]]</td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
<a-tag :color="usageColor(dbInbound.up + dbInbound.down, app.trafficDiff, dbInbound.total)">
|
||||
[[ sizeFormat(dbInbound.up + dbInbound.down) ]] /
|
||||
<template v-if="dbInbound.total > 0">
|
||||
[[ sizeFormat(dbInbound.total) ]]
|
||||
</template>
|
||||
<template v-else>∞</template>
|
||||
</a-tag>
|
||||
</a-popover>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.expireDate" }}</td>
|
||||
<td>
|
||||
<a-tag style="min-width: 50px; text-align: center;" v-if="dbInbound.expiryTime > 0" :color="dbInbound.isExpiry? 'red': 'blue'">
|
||||
[[ DateUtil.formatMillis(dbInbound.expiryTime) ]]
|
||||
</a-tag>
|
||||
<a-tag v-else style="text-align: center;" color="purple" class="infinite-tag">∞</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
<a-badge>
|
||||
<a-icon v-if="!dbInbound.enable" slot="count" type="pause-circle" :style="'color: ' + themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'"></a-icon>
|
||||
<a-button shape="round" size="small" style="font-size: 14px; padding: 0 10px;">
|
||||
<a-icon type="info"></a-icon>
|
||||
</a-button>
|
||||
</a-badge>
|
||||
</a-popover>
|
||||
</template>
|
||||
<template slot="expandedRowRender" slot-scope="record">
|
||||
<a-table
|
||||
:row-key="client => client.id"
|
||||
:columns="isMobile ? innerMobileColumns : innerColumns"
|
||||
:data-source="getInboundClients(record)"
|
||||
:pagination=pagination(getInboundClients(record))
|
||||
:style="isMobile ? 'margin: -12px 2px -13px;' : 'margin: -12px 22px -13px;'">
|
||||
{{template "client_table"}}
|
||||
</a-table>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</transition>
|
||||
</a-spin>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
<template v-else>∞</template>
|
||||
</a-tag>
|
||||
</a-popover>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.expireDate" }}</td>
|
||||
<td>
|
||||
<a-tag style="min-width: 50px; text-align: center;" v-if="dbInbound.expiryTime > 0" :color="dbInbound.isExpiry? 'red': 'blue'">
|
||||
[[ DateUtil.formatMillis(dbInbound.expiryTime) ]]
|
||||
</a-tag>
|
||||
<a-tag v-else style="text-align: center;" color="purple" class="infinite-tag">∞</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
<a-badge>
|
||||
<a-icon v-if="!dbInbound.enable" slot="count" type="pause-circle" :style="'color: ' + themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'"></a-icon>
|
||||
<a-button shape="round" size="small" style="font-size: 14px; padding: 0 10px;">
|
||||
<a-icon type="info"></a-icon>
|
||||
</a-button>
|
||||
</a-badge>
|
||||
</a-popover>
|
||||
</template>
|
||||
<template slot="expandedRowRender" slot-scope="record">
|
||||
<a-table
|
||||
:row-key="client => client.id"
|
||||
:columns="isMobile ? innerMobileColumns : innerColumns"
|
||||
:data-source="getInboundClients(record)"
|
||||
:pagination=pagination(getInboundClients(record))
|
||||
:style="isMobile ? 'margin: -12px 2px -13px;' : 'margin: -12px 22px -13px;'">
|
||||
{{template "client_table"}}
|
||||
</a-table>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</transition>
|
||||
</a-spin>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
{{template "js" .}}
|
||||
<script src="{{ .base_path }}assets/base64/base64.min.js"></script>
|
||||
<script src="{{ .base_path }}assets/qrcode/qrious.min.js"></script>
|
||||
<script src="{{ .base_path }}assets/qrcode/qrious2.min.js"></script>
|
||||
<script src="{{ .base_path }}assets/clipboard/clipboard.min.js"></script>
|
||||
<script src="{{ .base_path }}assets/uri/URI.min.js"></script>
|
||||
<script src="{{ .base_path }}assets/js/model/xray.js?{{ .cur_ver }}"></script>
|
||||
@@ -537,7 +543,7 @@
|
||||
{ title: '{{ i18n "online" }}', width: 30, scopedSlots: { customRender: 'online' } },
|
||||
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
|
||||
{ title: '{{ i18n "pages.inbounds.traffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'traffic' } },
|
||||
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 80, align: 'center', scopedSlots: { customRender: 'expiryTime' } },
|
||||
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 100, align: 'center', scopedSlots: { customRender: 'expiryTime' } },
|
||||
];
|
||||
|
||||
const innerMobileColumns = [
|
||||
@@ -578,6 +584,7 @@
|
||||
datepicker: 'gregorian',
|
||||
tgBotEnable: false,
|
||||
showAlert: false,
|
||||
ipLimitEnable: false,
|
||||
pageSize: 0,
|
||||
isMobile: window.innerWidth <= 768,
|
||||
},
|
||||
@@ -625,6 +632,7 @@
|
||||
this.pageSize = pageSize;
|
||||
this.remarkModel = remarkModel;
|
||||
this.datepicker = datepicker;
|
||||
this.ipLimitEnable = ipLimitEnable;
|
||||
}
|
||||
},
|
||||
setInbounds(dbInbounds) {
|
||||
|
||||
@@ -19,331 +19,313 @@
|
||||
</style>
|
||||
|
||||
<body>
|
||||
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
|
||||
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
|
||||
{{ template "commonSider" . }}
|
||||
<a-layout id="content-layout">
|
||||
<a-layout-content>
|
||||
<a-spin :spinning="spinning" :delay="200" :tip="loadingTip"/>
|
||||
<transition name="list" appear>
|
||||
<a-alert type="error" v-if="showAlert" style="margin-bottom: 10px"
|
||||
message='{{ i18n "secAlertTitle" }}'
|
||||
color="red"
|
||||
description='{{ i18n "secAlertSsl" }}'
|
||||
show-icon closable
|
||||
>
|
||||
</a-alert>
|
||||
</transition>
|
||||
<transition name="list" appear>
|
||||
<a-layout-content>
|
||||
<a-spin :spinning="spinning" :delay="200" :tip="loadingTip"></a-spin>
|
||||
<transition name="list" appear>
|
||||
<a-alert type="error" v-if="showAlert" style="margin-bottom: 10px"
|
||||
message='{{ i18n "secAlertTitle" }}'
|
||||
color="red"
|
||||
description='{{ i18n "secAlertSsl" }}'
|
||||
show-icon closable>
|
||||
</a-alert>
|
||||
</transition>
|
||||
<transition name="list" appear>
|
||||
<a-row>
|
||||
<a-card hoverable>
|
||||
<a-row>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-row>
|
||||
<a-col :span="12" style="text-align: center">
|
||||
<a-progress type="dashboard" status="normal"
|
||||
:stroke-color="status.cpu.color"
|
||||
:percent="status.cpu.percent"></a-progress>
|
||||
<div><b>CPU:</b> [[ cpuCoreFormat(status.cpuCores) ]]</div>
|
||||
<div><b>Speed:</b> [[ cpuSpeedFormat(status.cpuSpeedMhz) ]]</div>
|
||||
</a-col>
|
||||
<a-col :span="12" style="text-align: center">
|
||||
<a-progress type="dashboard" status="normal"
|
||||
:stroke-color="status.mem.color"
|
||||
:percent="status.mem.percent"></a-progress>
|
||||
<div>
|
||||
<b>{{ i18n "pages.index.memory"}}:</b> [[ sizeFormat(status.mem.current) ]] / [[ sizeFormat(status.mem.total) ]]
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-row>
|
||||
<a-col :span="12" style="text-align: center">
|
||||
<a-progress type="dashboard" status="normal"
|
||||
:stroke-color="status.swap.color"
|
||||
:percent="status.swap.percent"></a-progress>
|
||||
<div>
|
||||
<b>Swap:</b> [[ sizeFormat(status.swap.current) ]] / [[ sizeFormat(status.swap.total) ]]
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="12" style="text-align: center">
|
||||
<a-progress type="dashboard" status="normal"
|
||||
:stroke-color="status.disk.color"
|
||||
:percent="status.disk.percent"></a-progress>
|
||||
<div>
|
||||
<b>{{ i18n "pages.index.hard"}}:</b> [[ sizeFormat(status.disk.current) ]] / [[ sizeFormat(status.disk.total) ]]
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</a-row>
|
||||
</transition>
|
||||
<transition name="list" appear>
|
||||
<a-row>
|
||||
<a-col :sm="24" :lg="12">
|
||||
<a-card hoverable>
|
||||
<b>3X-UI:</b>
|
||||
<a rel="noopener" href="https://github.com/MHSanaei/3x-ui/releases" target="_blank"><a-tag color="green">v{{ .cur_ver }}</a-tag></a>
|
||||
<a rel="noopener" href="https://t.me/XrayUI" target="_blank"><a-tag color="green">@XrayUI</a-tag></a>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :lg="12">
|
||||
<a-card hoverable>
|
||||
<b>{{ i18n "pages.index.operationHours" }}:</b>
|
||||
<a-tag :color="status.xray.color">Xray: [[ formatSecond(status.appStats.uptime) ]]</a-tag>
|
||||
<a-tag color="green">OS: [[ formatSecond(status.uptime) ]]</a-tag>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :lg="12">
|
||||
<a-card hoverable>
|
||||
<b>{{ i18n "pages.index.xrayStatus" }}:</b>
|
||||
<a-tag style="text-transform: capitalize;" :color="status.xray.color">[[ status.xray.state ]] </a-tag>
|
||||
<a-popover v-if="status.xray.state === State.Error" :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<span slot="title" style="font-size: 12pt">An error occurred while running Xray
|
||||
<a-tag color="purple" style="cursor: pointer; float: right;" @click="openLogs()">{{ i18n "pages.index.logs" }}</a-tag>
|
||||
</span>
|
||||
<template slot="content">
|
||||
<p style="max-width: 400px" v-for="line in status.xray.errorMsg.split('\n')">[[ line ]]</p>
|
||||
</template>
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-popover>
|
||||
<a-tag color="purple" style="cursor: pointer;" @click="stopXrayService">{{ i18n "pages.index.stopXray" }}</a-tag>
|
||||
<a-tag color="purple" style="cursor: pointer;" @click="restartXrayService">{{ i18n "pages.index.restartXray" }}</a-tag>
|
||||
<a-tag color="purple" style="cursor: pointer;" @click="openSelectV2rayVersion">v[[ status.xray.version ]]</a-tag>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :lg="12">
|
||||
<a-card hoverable>
|
||||
<b>{{ i18n "menu.link" }}:</b>
|
||||
<a-tag color="purple" style="cursor: pointer;" @click="openLogs()">{{ i18n "pages.index.logs" }}</a-tag>
|
||||
<a-tag color="purple" style="cursor: pointer;" @click="openConfig">{{ i18n "pages.index.config" }}</a-tag>
|
||||
<a-tag color="purple" style="cursor: pointer;" @click="openBackup">{{ i18n "pages.index.backup" }}</a-tag>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :lg="12">
|
||||
<a-card hoverable>
|
||||
<b>{{ i18n "pages.index.systemLoad" }}:</b>
|
||||
<a-tag color="green">
|
||||
<a-tooltip>
|
||||
[[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.systemLoadDesc" }}
|
||||
</template>
|
||||
</a-tooltip>
|
||||
</a-tag>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :lg="12">
|
||||
<a-card hoverable>
|
||||
<b>{{ i18n "usage"}}:</b>
|
||||
<a-tag color="green"> RAM: [[ sizeFormat(status.appStats.mem) ]] </a-tag>
|
||||
<a-tag color="green"> Threads: [[ status.appStats.threads ]] </a-tag>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :lg="12">
|
||||
<a-card hoverable>
|
||||
<a-row>
|
||||
<a-card hoverable>
|
||||
<a-row>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-row>
|
||||
<a-col :span="12" style="text-align: center">
|
||||
<a-progress type="dashboard" status="normal"
|
||||
:stroke-color="status.cpu.color"
|
||||
:percent="status.cpu.percent"></a-progress>
|
||||
<div><b>CPU:</b> [[ cpuCoreFormat(status.cpuCores) ]]</div>
|
||||
<div><b>Speed:</b> [[ cpuSpeedFormat(status.cpuSpeedMhz) ]]</div>
|
||||
</a-col>
|
||||
<a-col :span="12" style="text-align: center">
|
||||
<a-progress type="dashboard" status="normal"
|
||||
:stroke-color="status.mem.color"
|
||||
:percent="status.mem.percent"></a-progress>
|
||||
<div>
|
||||
<b>{{ i18n "pages.index.memory"}}:</b> [[ sizeFormat(status.mem.current) ]] / [[ sizeFormat(status.mem.total) ]]
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-row>
|
||||
<a-col :span="12" style="text-align: center">
|
||||
<a-progress type="dashboard" status="normal"
|
||||
:stroke-color="status.swap.color"
|
||||
:percent="status.swap.percent"></a-progress>
|
||||
<div>
|
||||
<b>Swap:</b> [[ sizeFormat(status.swap.current) ]] / [[ sizeFormat(status.swap.total) ]]
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="12" style="text-align: center">
|
||||
<a-progress type="dashboard" status="normal"
|
||||
:stroke-color="status.disk.color"
|
||||
:percent="status.disk.percent"></a-progress>
|
||||
<div>
|
||||
<b>{{ i18n "pages.index.hard"}}:</b> [[ sizeFormat(status.disk.current) ]] / [[ sizeFormat(status.disk.total) ]]
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
<a-col :span="12">
|
||||
<a-tag>
|
||||
<a-tooltip>
|
||||
<a-icon type="global"></a-icon> IPv4
|
||||
<template slot="title">
|
||||
[[ status.publicIP.ipv4 ]]
|
||||
</template>
|
||||
</a-tooltip>
|
||||
</a-tag>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-tag>
|
||||
<a-tooltip>
|
||||
<a-icon type="global"></a-icon> IPv6
|
||||
<template slot="title">
|
||||
[[ status.publicIP.ipv6 ]]
|
||||
</template>
|
||||
</a-tooltip>
|
||||
</a-tag>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</transition>
|
||||
<transition name="list" appear>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :lg="12">
|
||||
<a-card hoverable>
|
||||
<a-row>
|
||||
<a-col :sm="24" :lg="12">
|
||||
<a-card hoverable>
|
||||
<b>3X-UI:</b>
|
||||
<a rel="noopener" href="https://github.com/MHSanaei/3x-ui/releases" target="_blank"><a-tag color="green">v{{ .cur_ver }}</a-tag></a>
|
||||
<a rel="noopener" href="https://t.me/panel3xui" target="_blank"><a-tag color="green">@Panel3xui</a-tag></a>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :lg="12">
|
||||
<a-card hoverable>
|
||||
<b>{{ i18n "pages.index.operationHours" }}:</b>
|
||||
<a-tag :color="status.xray.color">Xray: [[ formatSecond(status.appStats.uptime) ]]</a-tag>
|
||||
<a-tag color="green">OS: [[ formatSecond(status.uptime) ]]</a-tag>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :lg="12">
|
||||
<a-card hoverable>
|
||||
<b>{{ i18n "pages.index.xrayStatus" }}:</b>
|
||||
<a-tag style="text-transform: capitalize;" :color="status.xray.color">[[ status.xray.state ]]
|
||||
</a-tag>
|
||||
<a-popover v-if="status.xray.state === State.Error"
|
||||
:overlay-class-name="themeSwitcher.currentTheme">
|
||||
<span slot="title" style="font-size: 12pt">An error occurred while running Xray
|
||||
<a-tag color="purple" style="cursor: pointer; float: right;" @click="openLogs()">{{ i18n "pages.index.logs" }}</a-tag>
|
||||
</span>
|
||||
<template slot="content">
|
||||
<p style="max-width: 400px" v-for="line in status.xray.errorMsg.split('\n')">[[ line ]]</p>
|
||||
</template>
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-popover>
|
||||
<a-tag color="purple" style="cursor: pointer;" @click="stopXrayService">{{ i18n "pages.index.stopXray" }}</a-tag>
|
||||
<a-tag color="purple" style="cursor: pointer;" @click="restartXrayService">{{ i18n "pages.index.restartXray" }}</a-tag>
|
||||
<a-tag color="purple" style="cursor: pointer;" @click="openSelectV2rayVersion">v[[ status.xray.version ]]</a-tag>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :lg="12">
|
||||
<a-card hoverable>
|
||||
<b>{{ i18n "menu.link" }}:</b>
|
||||
<a-tag color="purple" style="cursor: pointer;" @click="openLogs()">{{ i18n "pages.index.logs" }}</a-tag>
|
||||
<a-tag color="purple" style="cursor: pointer;" @click="openConfig">{{ i18n "pages.index.config" }}</a-tag>
|
||||
<a-tag color="purple" style="cursor: pointer;" @click="openBackup">{{ i18n "pages.index.backup" }}</a-tag>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :lg="12">
|
||||
<a-card hoverable>
|
||||
<b>{{ i18n "pages.index.systemLoad" }}:</b>
|
||||
<a-tag color="green">
|
||||
<a-tooltip>
|
||||
[[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.systemLoadDesc" }}
|
||||
</template>
|
||||
</a-tooltip>
|
||||
</a-tag>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :lg="12">
|
||||
<a-card hoverable>
|
||||
<b>{{ i18n "usage"}}:</b>
|
||||
<a-tag color="green">
|
||||
RAM: [[ sizeFormat(status.appStats.mem) ]]
|
||||
</a-tag>
|
||||
<a-tag color="green">
|
||||
Threads: [[ status.appStats.threads ]]
|
||||
</a-tag>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :lg="12">
|
||||
<a-card hoverable>
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
<a-tag>
|
||||
<a-tooltip>
|
||||
<a-icon type="global"></a-icon> IPv4
|
||||
<template slot="title">
|
||||
[[ status.publicIP.ipv4 ]]
|
||||
</template>
|
||||
</a-tooltip>
|
||||
</a-tag>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-tag>
|
||||
<a-tooltip>
|
||||
<a-icon type="global"></a-icon> IPv6
|
||||
<template slot="title">
|
||||
[[ status.publicIP.ipv6 ]]
|
||||
</template>
|
||||
</a-tooltip>
|
||||
</a-tag>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :lg="12">
|
||||
<a-card hoverable>
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
<a-tag>
|
||||
<a-tooltip>
|
||||
<a-icon type="swap"></a-icon> TCP: [[ status.tcpCount ]]
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.connectionTcpCountDesc" }}
|
||||
</template>
|
||||
</a-tooltip>
|
||||
</a-tag>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-tag>
|
||||
<a-tooltip>
|
||||
<a-icon type="swap"></a-icon> UDP: [[ status.udpCount ]]
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.connectionUdpCountDesc" }}
|
||||
</template>
|
||||
</a-tooltip>
|
||||
</a-tag>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :lg="12">
|
||||
<a-card hoverable>
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
<a-tag>
|
||||
<a-tooltip>
|
||||
<a-icon type="arrow-up"></a-icon>
|
||||
Up: [[ sizeFormat(status.netIO.up) ]]/s
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.upSpeed" }}
|
||||
</template>
|
||||
</a-tooltip>
|
||||
</a-tag>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-tag>
|
||||
<a-tooltip>
|
||||
<a-icon type="arrow-down"></a-icon>
|
||||
Down: [[ sizeFormat(status.netIO.down) ]]/s
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.downSpeed" }}
|
||||
</template>
|
||||
</a-tooltip>
|
||||
</a-tag>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :lg="12">
|
||||
<a-card hoverable>
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
<a-tag>
|
||||
<a-tooltip>
|
||||
<a-icon type="cloud-upload"></a-icon>
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.totalSent" }}
|
||||
</template> Out: [[ sizeFormat(status.netTraffic.sent) ]]
|
||||
</a-tooltip>
|
||||
</a-tag>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-tag>
|
||||
<a-tooltip>
|
||||
<a-icon type="cloud-download"></a-icon>
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.totalReceive" }}
|
||||
</template> In: [[ sizeFormat(status.netTraffic.recv) ]]
|
||||
</a-tooltip>
|
||||
</a-tag>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-tag>
|
||||
<a-tooltip>
|
||||
<a-icon type="swap"></a-icon> TCP: [[ status.tcpCount ]]
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.connectionTcpCountDesc" }}
|
||||
</template>
|
||||
</a-tooltip>
|
||||
</a-tag>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-tag>
|
||||
<a-tooltip>
|
||||
<a-icon type="swap"></a-icon> UDP: [[ status.udpCount ]]
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.connectionUdpCountDesc" }}
|
||||
</template>
|
||||
</a-tooltip>
|
||||
</a-tag>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</transition>
|
||||
</a-layout-content>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :lg="12">
|
||||
<a-card hoverable>
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
<a-tag>
|
||||
<a-tooltip>
|
||||
<a-icon type="arrow-up"></a-icon> Up: [[ sizeFormat(status.netIO.up) ]]/s
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.upSpeed" }}
|
||||
</template>
|
||||
</a-tooltip>
|
||||
</a-tag>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-tag>
|
||||
<a-tooltip>
|
||||
<a-icon type="arrow-down"></a-icon> Down: [[ sizeFormat(status.netIO.down) ]]/s
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.downSpeed" }}
|
||||
</template>
|
||||
</a-tooltip>
|
||||
</a-tag>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :lg="12">
|
||||
<a-card hoverable>
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
<a-tag>
|
||||
<a-tooltip>
|
||||
<a-icon type="cloud-upload"></a-icon>
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.totalSent" }}
|
||||
</template> Out: [[ sizeFormat(status.netTraffic.sent) ]]
|
||||
</a-tooltip>
|
||||
</a-tag>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-tag>
|
||||
<a-tooltip>
|
||||
<a-icon type="cloud-download"></a-icon>
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.totalReceive" }}
|
||||
</template> In: [[ sizeFormat(status.netTraffic.recv) ]]
|
||||
</a-tooltip>
|
||||
</a-tag>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</transition>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
|
||||
<a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}'
|
||||
:closable="true" @ok="() => versionModal.visible = false"
|
||||
:class="themeSwitcher.currentTheme"
|
||||
footer="">
|
||||
<a-alert type="warning" style="margin-bottom: 12px; width: fit-content"
|
||||
message='{{ i18n "pages.index.xraySwitchClickDesk" }}'
|
||||
show-icon
|
||||
></a-alert>
|
||||
<template v-for="version, index in versionModal.versions">
|
||||
<a-tag :color="index % 2 == 0 ? 'purple' : 'green'"
|
||||
style="margin-right: 10px" @click="switchV2rayVersion(version)">
|
||||
[[ version ]]
|
||||
</a-tag>
|
||||
</template>
|
||||
<a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}' :closable="true"
|
||||
@ok="() => versionModal.visible = false" :class="themeSwitcher.currentTheme" footer="">
|
||||
<a-alert type="warning" style="margin-bottom: 12px; width: fit-content"
|
||||
message='{{ i18n "pages.index.xraySwitchClickDesk" }}' show-icon></a-alert>
|
||||
<template v-for="version, index in versionModal.versions">
|
||||
<a-tag :color="index % 2 == 0 ? 'purple' : 'green'" style="margin-right: 12px; margin-bottom: 12px"
|
||||
@click="switchV2rayVersion(version)">
|
||||
[[ version ]]
|
||||
</a-tag>
|
||||
</template>
|
||||
</a-modal>
|
||||
|
||||
<a-modal id="log-modal" v-model="logModal.visible"
|
||||
:closable="true" @cancel="() => logModal.visible = false"
|
||||
:class="themeSwitcher.currentTheme"
|
||||
width="800px" footer="">
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.logs" }}
|
||||
<a-icon :spin="logModal.loading"
|
||||
type="sync"
|
||||
style="vertical-align: middle; margin-left: 10px;"
|
||||
:disabled="logModal.loading"
|
||||
@click="openLogs()">
|
||||
</a-icon>
|
||||
</template>
|
||||
<a-form layout="inline">
|
||||
<a-form-item>
|
||||
<a-input-group compact>
|
||||
<a-select v-model="logModal.rows" style="width:70px;"
|
||||
@change="openLogs()" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="10">10</a-select-option>
|
||||
<a-select-option value="20">20</a-select-option>
|
||||
<a-select-option value="50">50</a-select-option>
|
||||
<a-select-option value="100">100</a-select-option>
|
||||
</a-select>
|
||||
<a-select v-model="logModal.level" style="width:100px;"
|
||||
@change="openLogs()" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="debug">Debug</a-select-option>
|
||||
<a-select-option value="info">Info</a-select-option>
|
||||
<a-select-option value="notice">Notice</a-select-option>
|
||||
<a-select-option value="warning">Warning</a-select-option>
|
||||
<a-select-option value="err">Error</a-select-option>
|
||||
</a-select>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-checkbox v-model="logModal.syslog" @change="openLogs()">SysLog</a-checkbox>
|
||||
</a-form-item>
|
||||
<a-form-item style="float: right;">
|
||||
<a-button type="primary" icon="download"
|
||||
:href="'data:application/text;charset=utf-8,' + encodeURIComponent(logModal.logs.join('\n'))" download="x-ui.log">
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<div class="ant-input" style="height: auto; max-height: 500px; overflow: auto;" v-html="logModal.formattedLogs"></div>
|
||||
:closable="true" @cancel="() => logModal.visible = false"
|
||||
:class="themeSwitcher.currentTheme"
|
||||
width="800px" footer="">
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.logs" }}
|
||||
<a-icon :spin="logModal.loading"
|
||||
type="sync"
|
||||
style="vertical-align: middle; margin-left: 10px;"
|
||||
:disabled="logModal.loading"
|
||||
@click="openLogs()">
|
||||
</a-icon>
|
||||
</template>
|
||||
<a-form layout="inline">
|
||||
<a-form-item style="margin-right: 0.5rem;">
|
||||
<a-input-group compact>
|
||||
<a-select size="small" v-model="logModal.rows" style="width:70px;"
|
||||
@change="openLogs()" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="10">10</a-select-option>
|
||||
<a-select-option value="20">20</a-select-option>
|
||||
<a-select-option value="50">50</a-select-option>
|
||||
<a-select-option value="100">100</a-select-option>
|
||||
</a-select>
|
||||
<a-select size="small" v-model="logModal.level" style="width:95px;"
|
||||
@change="openLogs()" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="debug">Debug</a-select-option>
|
||||
<a-select-option value="info">Info</a-select-option>
|
||||
<a-select-option value="notice">Notice</a-select-option>
|
||||
<a-select-option value="warning">Warning</a-select-option>
|
||||
<a-select-option value="err">Error</a-select-option>
|
||||
</a-select>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-checkbox v-model="logModal.syslog" @change="openLogs()">SysLog</a-checkbox>
|
||||
</a-form-item>
|
||||
<a-form-item style="float: right;">
|
||||
<a-button type="primary" icon="download"
|
||||
:href="'data:application/text;charset=utf-8,' + encodeURIComponent(logModal.logs?.join('\n'))" download="x-ui.log">
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<div class="ant-input" style="height: auto; max-height: 500px; overflow: auto; margin-top: 0.5rem;" v-html="logModal.formattedLogs"></div>
|
||||
</a-modal>
|
||||
|
||||
<a-modal id="backup-modal" v-model="backupModal.visible" :title="backupModal.title"
|
||||
:closable="true" footer=""
|
||||
:class="themeSwitcher.currentTheme">
|
||||
<a-alert type="warning" style="margin-bottom: 10px; width: fit-content"
|
||||
:message="backupModal.description"
|
||||
show-icon
|
||||
></a-alert>
|
||||
<a-space direction="horizontal" style="text-align: center" style="margin-bottom: 10px;">
|
||||
<a-button type="primary" @click="exportDatabase()">
|
||||
[[ backupModal.exportText ]]
|
||||
</a-button>
|
||||
<a-button type="primary" @click="importDatabase()">
|
||||
[[ backupModal.importText ]]
|
||||
</a-button>
|
||||
</a-space>
|
||||
:closable="true" footer=""
|
||||
:class="themeSwitcher.currentTheme">
|
||||
<a-alert type="warning" style="margin-bottom: 10px; width: fit-content"
|
||||
:message="backupModal.description"
|
||||
show-icon>
|
||||
</a-alert>
|
||||
<a-space direction="horizontal" style="text-align: center; margin-bottom: 10px;">
|
||||
<a-button type="primary" @click="exportDatabase()">
|
||||
[[ backupModal.exportText ]]
|
||||
</a-button>
|
||||
<a-button type="primary" @click="importDatabase()">
|
||||
[[ backupModal.importText ]]
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-modal>
|
||||
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
{{template "js" .}}
|
||||
<script src="{{ .base_path }}assets/clipboard/clipboard.min.js"></script>
|
||||
{{template "component/themeSwitcher" .}}
|
||||
{{template "textModal"}}
|
||||
<script>
|
||||
|
||||
const State = {
|
||||
Running: "running",
|
||||
Stop: "stop",
|
||||
@@ -668,7 +650,6 @@
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -2,311 +2,374 @@
|
||||
<html lang="en">
|
||||
{{template "head" .}}
|
||||
<style>
|
||||
@media (min-width: 769px) {
|
||||
.ant-layout-content {
|
||||
margin: 24px 16px;
|
||||
}
|
||||
@media (min-width: 769px) {
|
||||
.ant-layout-content {
|
||||
margin: 24px 16px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.ant-tabs-nav .ant-tabs-tab {
|
||||
margin: 0;
|
||||
padding: 12px .5rem;
|
||||
}
|
||||
}
|
||||
.ant-tabs-bar {
|
||||
margin: 0;
|
||||
}
|
||||
.ant-list-item {
|
||||
display: block;
|
||||
}
|
||||
.alert-msg {
|
||||
color: rgb(194, 117, 18);
|
||||
font-weight: normal;
|
||||
font-size: 16px;
|
||||
padding: .5rem 1rem;
|
||||
text-align: center;
|
||||
background: rgb(255 145 0 / 15%);
|
||||
margin: 1.5rem 2.5rem 0rem 2.5rem;
|
||||
border-radius: .5rem;
|
||||
transition: all 0.5s;
|
||||
animation: signal 3s cubic-bezier(0.18, 0.89, 0.32, 1.28) infinite;
|
||||
}
|
||||
.alert-msg:hover {
|
||||
cursor: default;
|
||||
transition-duration: .3s;
|
||||
animation: signal 0.9s ease infinite;
|
||||
}
|
||||
@keyframes signal {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(194, 118, 18, 0.5);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.ant-tabs-nav .ant-tabs-tab {
|
||||
margin: 0;
|
||||
padding: 12px .5rem;
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 0 6px rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.ant-tabs-bar {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ant-list-item {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.alert-msg {
|
||||
color: rgb(194, 117, 18);
|
||||
font-weight: normal;
|
||||
font-size: 16px;
|
||||
padding: .5rem 1rem;
|
||||
text-align: center;
|
||||
background: rgb(255 145 0 / 15%);
|
||||
margin: 1.5rem 2.5rem 0rem 2.5rem;
|
||||
border-radius: .5rem;
|
||||
transition: all 0.5s;
|
||||
animation: signal 3s cubic-bezier(0.18, 0.89, 0.32, 1.28) infinite;
|
||||
}
|
||||
.alert-msg:hover {
|
||||
cursor: default;
|
||||
transition-duration: .3s;
|
||||
animation: signal 0.9s ease infinite;
|
||||
}
|
||||
@keyframes signal{
|
||||
0%{
|
||||
box-shadow: 0 0 0 0 rgba(194, 118, 18, 0.5);
|
||||
}
|
||||
50%{
|
||||
box-shadow: 0 0 0 6px rgba(0 ,0,0,0);
|
||||
}
|
||||
100%{
|
||||
box-shadow: 0 0 0 6px rgba(0 ,0,0,0);
|
||||
}
|
||||
}
|
||||
.alert-msg > i {
|
||||
color: inherit;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.collapse-title {
|
||||
color: inherit;
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
padding: 10px 20px;
|
||||
border-bottom: 2px solid;
|
||||
}
|
||||
|
||||
.collapse-title > i {
|
||||
color: inherit;
|
||||
font-size: 24px;
|
||||
100% {
|
||||
box-shadow: 0 0 0 6px rgba(0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
.alert-msg>i {
|
||||
color: inherit;
|
||||
font-size: 24px;
|
||||
}
|
||||
.collapse-title {
|
||||
color: inherit;
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
padding: 10px 20px;
|
||||
border-bottom: 2px solid;
|
||||
}
|
||||
.collapse-title>i {
|
||||
color: inherit;
|
||||
font-size: 24px;
|
||||
}
|
||||
.ant-collapse-content-box .ant-list-item {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
|
||||
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
|
||||
{{ template "commonSider" . }}
|
||||
<a-layout id="content-layout">
|
||||
<a-layout-content>
|
||||
<a-spin :spinning="spinning" :delay="500" tip='{{ i18n "loading"}}'>
|
||||
<transition name="list" appear>
|
||||
<a-alert type="error" v-if="confAlerts.length>0" style="margin: 10px 5px;"
|
||||
message='{{ i18n "secAlertTitle" }}'
|
||||
color="red"
|
||||
show-icon
|
||||
closable
|
||||
>
|
||||
<template slot="description">
|
||||
<b>{{ i18n "secAlertConf" }}</b>
|
||||
<ul><li v-for="a in confAlerts">[[ a ]]</li></ul>
|
||||
</template>
|
||||
</a-alert>
|
||||
</transition>
|
||||
<a-space direction="vertical">
|
||||
<a-card hoverable style="margin-bottom: .5rem; overflow-x: hidden;">
|
||||
<a-row style="display: flex; flex-wrap: wrap; align-items: center;">
|
||||
<a-col :xs="24" :sm="10" style="padding: 4px;">
|
||||
<a-space direction="horizontal">
|
||||
<a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n "pages.settings.save" }}</a-button>
|
||||
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.settings.restartPanel" }}</a-button>
|
||||
</a-space>
|
||||
<a-layout-content>
|
||||
<a-spin :spinning="spinning" :delay="500" tip='{{ i18n "loading"}}'>
|
||||
<transition name="list" appear>
|
||||
<a-alert type="error" v-if="confAlerts.length>0" style="margin: 10px 5px;"
|
||||
message='{{ i18n "secAlertTitle" }}'
|
||||
color="red"
|
||||
show-icon closable>
|
||||
<template slot="description">
|
||||
<b>{{ i18n "secAlertConf" }}</b>
|
||||
<ul><li v-for="a in confAlerts">[[ a ]]</li></ul>
|
||||
</template>
|
||||
</a-alert>
|
||||
</transition>
|
||||
<a-space direction="vertical">
|
||||
<a-card hoverable style="margin-bottom: .5rem; overflow-x: hidden;">
|
||||
<a-row style="display: flex; flex-wrap: wrap; align-items: center;">
|
||||
<a-col :xs="24" :sm="10" style="padding: 4px;">
|
||||
<a-space direction="horizontal">
|
||||
<a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n "pages.settings.save" }}</a-button>
|
||||
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.settings.restartPanel" }}</a-button>
|
||||
</a-space>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="14">
|
||||
<template>
|
||||
<div>
|
||||
<a-back-top :target="() => document.getElementById('content-layout')" visibility-height="200"></a-back-top>
|
||||
<a-alert type="warning" style="float: right; width: fit-content"
|
||||
message='{{ i18n "pages.settings.infoDesc" }}'
|
||||
show-icon>
|
||||
</a-alert>
|
||||
</div>
|
||||
</template>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
<a-tabs default-active-key="1">
|
||||
<a-tab-pane key="1" tab='{{ i18n "pages.settings.panelSettings"}}'>
|
||||
<a-list item-layout="horizontal">
|
||||
<a-list-item>
|
||||
<a-row style="padding: 20px">
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta title='{{ i18n "pages.settings.remarkModel"}}'>
|
||||
<template slot="description">{{ i18n "pages.settings.sampleRemark"}}: <i>#[[ remarkSample ]]</i></template>
|
||||
</a-list-item-meta>
|
||||
</a-col>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-input-group style="width: 100%;">
|
||||
<a-select style="padding-right: .5rem; min-width: 80%; width: auto;"
|
||||
mode="multiple"
|
||||
v-model="remarkModel"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="(value, key) in remarkModels" :value="key">[[ value ]]</a-select-option>
|
||||
</a-select>
|
||||
<a-select style="width: 20%;" v-model="remarkSeparator" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in remarkSeparators" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-input-group>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.panelListeningIP"}}' desc='{{ i18n "pages.settings.panelListeningIPDesc"}}' v-model="allSetting.webListen"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.panelListeningDomain"}}' desc='{{ i18n "pages.settings.panelListeningDomainDesc"}}' v-model="allSetting.webDomain"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.settings.panelPort"}}' desc='{{ i18n "pages.settings.panelPortDesc"}}' v-model="allSetting.webPort" :min="1" :max="65531"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.publicKeyPath"}}' desc='{{ i18n "pages.settings.publicKeyPathDesc"}}' v-model="allSetting.webCertFile"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.privateKeyPath"}}' desc='{{ i18n "pages.settings.privateKeyPathDesc"}}' v-model="allSetting.webKeyFile"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.panelUrlPath"}}' desc='{{ i18n "pages.settings.panelUrlPathDesc"}}' v-model="allSetting.webBasePath"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.settings.sessionMaxAge" }}' desc='{{ i18n "pages.settings.sessionMaxAgeDesc" }}' v-model="allSetting.sessionMaxAge" :min="0"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.settings.pageSize" }}' desc='{{ i18n "pages.settings.pageSizeDesc" }}' v-model="allSetting.pageSize" :min="0" :step="5"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.settings.expireTimeDiff" }}' desc='{{ i18n "pages.settings.expireTimeDiffDesc" }}' v-model="allSetting.expireDiff" :min="0"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.settings.trafficDiff" }}' desc='{{ i18n "pages.settings.trafficDiffDesc" }}' v-model="allSetting.trafficDiff" :min="0"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.timeZone"}}' desc='{{ i18n "pages.settings.timeZoneDesc"}}' v-model="allSetting.timeLocation"></setting-list-item>
|
||||
<a-list-item>
|
||||
<a-row style="padding: 20px">
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta title='{{ i18n "pages.settings.datepicker"}}'>
|
||||
<template slot="description">{{ i18n "pages.settings.datepickerDescription"}}</template>
|
||||
</a-list-item-meta>
|
||||
</a-col>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<template>
|
||||
<a-select style="width: 100%" :dropdown-class-name="themeSwitcher.currentTheme" v-model="datepicker">
|
||||
<a-select-option v-for="item in datepickerList" :value="item.value">
|
||||
<span v-text="item.name"></span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
<a-row style="padding: 20px">
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta title="Language"></a-list-item-meta>
|
||||
</a-col>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<template>
|
||||
<a-select ref="selectLang"
|
||||
v-model="lang"
|
||||
@change="setLang(lang)"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||
style="width: 100%">
|
||||
<a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs">
|
||||
<span role="img" :aria-label="l.name" v-text="l.icon"></span> <span v-text="l.name"></span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2" tab='{{ i18n "pages.settings.securitySettings"}}' style="padding: 20px;">
|
||||
<a-divider>{{ i18n "pages.settings.security.admin"}}</a-divider>
|
||||
<a-form layout="horizontal" :colon="false" style="float: left; margin-bottom: 2rem;" :label-col="{ md: {span:10} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "pages.settings.oldUsername"}}'>
|
||||
<a-input autocomplete="username" v-model="user.oldUsername"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.settings.currentPassword"}}'>
|
||||
<password-input autocomplete="current-password" v-model="user.oldPassword"></password-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.settings.newUsername"}}'>
|
||||
<a-input v-model="user.newUsername"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.settings.newPassword"}}'>
|
||||
<password-input autocomplete="new-password" v-model="user.newPassword"></password-input>
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
<a-button type="primary" @click="updateUser">{{ i18n "confirm" }}</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-divider>{{ i18n "pages.settings.security.secret"}}</a-divider>
|
||||
<a-form style="padding: 0 20px;">
|
||||
<a-list-item>
|
||||
<a-row>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta title='{{ i18n "pages.settings.security.loginSecurity" }}'
|
||||
description='{{ i18n "pages.settings.security.loginSecurityDesc" }}'>
|
||||
</a-list-item-meta>
|
||||
</a-col>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<template>
|
||||
<a-switch @change="toggleToken(allSetting.secretEnable)" v-model="allSetting.secretEnable"></a-switch>
|
||||
<a-icon style="margin-left: 1rem;" v-if="allSetting.secretEnable" :spin="this.changeSecret" type="sync" @click="getNewSecret"></a-icon>
|
||||
</template>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
<a-row>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta title='{{ i18n "pages.settings.security.secretToken" }}'
|
||||
description='{{ i18n "pages.settings.security.secretTokenDesc" }}'>
|
||||
</a-list-item-meta>
|
||||
</a-col>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<template>
|
||||
<a-textarea type="text" :disabled="!allSetting.secretEnable" v-model="user.loginSecret"></a-textarea>
|
||||
</template>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-list-item>
|
||||
<a-button type="primary" :loading="this.changeSecret" @click="updateSecret">{{ i18n "confirm" }}</a-button>
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="3" tab='{{ i18n "pages.settings.TGBotSettings"}}'>
|
||||
<a-list item-layout="horizontal">
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.telegramBotEnable" }}' desc='{{ i18n "pages.settings.telegramBotEnableDesc" }}' v-model="allSetting.tgBotEnable"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.telegramToken"}}' desc='{{ i18n "pages.settings.telegramTokenDesc"}}' v-model="allSetting.tgBotToken"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.telegramChatId"}}' desc='{{ i18n "pages.settings.telegramChatIdDesc"}}' v-model="allSetting.tgBotChatId"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.telegramNotifyTime"}}' desc='{{ i18n "pages.settings.telegramNotifyTimeDesc"}}' v-model="allSetting.tgRunTime"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.tgNotifyBackup" }}' desc='{{ i18n "pages.settings.tgNotifyBackupDesc" }}' v-model="allSetting.tgBotBackup"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.tgNotifyLogin" }}' desc='{{ i18n "pages.settings.tgNotifyLoginDesc" }}' v-model="allSetting.tgBotLoginNotify"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.settings.tgNotifyCpu" }}' desc='{{ i18n "pages.settings.tgNotifyCpuDesc" }}' v-model="allSetting.tgCpu" :min="0" :max="100"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.telegramProxy"}}' desc='{{ i18n "pages.settings.telegramProxyDesc"}}' v-model="allSetting.tgBotProxy" placeholder="socks5://user:pass@host:port"></setting-list-item>
|
||||
<a-list-item>
|
||||
<a-row style="padding: 20px">
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta title="Telegram Bot Language" />
|
||||
</a-col>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<template>
|
||||
<a-select ref="selectBotLang" v-model="allSetting.tgLang" :dropdown-class-name="themeSwitcher.currentTheme" style="width: 100%">
|
||||
<a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs">
|
||||
<span role="img" :aria-label="l.name" v-text="l.icon"></span> <span v-text="l.name"></span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="4" tab='{{ i18n "pages.settings.subSettings" }}'>
|
||||
<a-list item-layout="horizontal">
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.subEnable"}}' desc='{{ i18n "pages.settings.subEnableDesc"}}' v-model="allSetting.subEnable"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.subEncrypt"}}' desc='{{ i18n "pages.settings.subEncryptDesc"}}' v-model="allSetting.subEncrypt"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.subShowInfo"}}' desc='{{ i18n "pages.settings.subShowInfoDesc"}}' v-model="allSetting.subShowInfo"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subListen"}}' desc='{{ i18n "pages.settings.subListenDesc"}}' v-model="allSetting.subListen"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subDomain"}}' desc='{{ i18n "pages.settings.subDomainDesc"}}' v-model="allSetting.subDomain"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.settings.subPort"}}' desc='{{ i18n "pages.settings.subPortDesc"}}' v-model.number="allSetting.subPort" :min="1" :max="65531"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subPath"}}' desc='{{ i18n "pages.settings.subPathDesc"}}' v-model="allSetting.subPath"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subCertPath"}}' desc='{{ i18n "pages.settings.subCertPathDesc"}}' v-model="allSetting.subCertFile"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subKeyPath"}}' desc='{{ i18n "pages.settings.subKeyPathDesc"}}' v-model="allSetting.subKeyFile"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subURI"}}' desc='{{ i18n "pages.settings.subURIDesc"}}' v-model="allSetting.subURI" placeholder="(http|https)://domain[:port]/path/"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.settings.subUpdates"}}' desc='{{ i18n "pages.settings.subUpdatesDesc"}}' v-model="allSetting.subUpdates" :min="1"></setting-list-item>
|
||||
</a-list>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="5" tab='{{ i18n "pages.settings.subSettings" }} Json' v-if="allSetting.subEnable">
|
||||
<a-list item-layout="horizontal">
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subPath"}}' desc='{{ i18n "pages.settings.subPathDesc"}}' v-model="allSetting.subJsonPath"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subURI"}}' desc='{{ i18n "pages.settings.subURIDesc"}}' v-model="allSetting.subJsonURI" placeholder="(http|https)://domain[:port]/path/"></setting-list-item>
|
||||
<a-list-item style="padding: 20px">
|
||||
<a-row>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta title='{{ i18n "pages.settings.fragment"}}'>
|
||||
<template slot="description">{{ i18n "pages.settings.fragmentDesc"}}</template>
|
||||
</a-list-item-meta>
|
||||
</a-col>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-switch v-model="fragment"></a-switch>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-collapse v-if="fragment" style="margin-top: 14px;">
|
||||
<a-collapse-panel header='{{ i18n "pages.settings.fragmentSett"}}' v-if="fragment">
|
||||
<a-list-item style="padding: 10px 20px">
|
||||
<a-row>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta title='Packets'></a-list-item-meta>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="14">
|
||||
<template>
|
||||
<div>
|
||||
<a-back-top :target="() => document.getElementById('content-layout')" visibility-height="200">
|
||||
</a-back-top>
|
||||
<a-alert type="warning" style="float: right; width: fit-content"
|
||||
message='{{ i18n "pages.settings.infoDesc" }}'
|
||||
show-icon
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-select v-model="fragmentPackets" style="width: 100%" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option :value="p" :label="p" v-for="p in ['1-1', '1-3', 'tlshello']"> [[ p ]] </a-select-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
<a-tabs default-active-key="1">
|
||||
<a-tab-pane key="1" tab='{{ i18n "pages.settings.panelSettings"}}'>
|
||||
<a-list item-layout="horizontal">
|
||||
<a-list-item>
|
||||
<a-row style="padding: 20px">
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta title='{{ i18n "pages.settings.remarkModel"}}'>
|
||||
<template slot="description">{{ i18n "pages.settings.sampleRemark"}}: <i>#[[ remarkSample ]]</i></template>
|
||||
</a-list-item-meta>
|
||||
</a-col>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-input-group style="width: 100%;">
|
||||
<a-select style="padding-right: .5rem; min-width: 80%; width: auto;"
|
||||
mode="multiple"
|
||||
v-model="remarkModel"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="(value, key) in remarkModels" :value="key">[[ value ]]</a-select-option>
|
||||
</a-select>
|
||||
<a-select style="width: 20%;" v-model="remarkSeparator" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in remarkSeparators" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-input-group>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.panelListeningIP"}}' desc='{{ i18n "pages.settings.panelListeningIPDesc"}}' v-model="allSetting.webListen"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.panelListeningDomain"}}' desc='{{ i18n "pages.settings.panelListeningDomainDesc"}}' v-model="allSetting.webDomain"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.settings.panelPort"}}' desc='{{ i18n "pages.settings.panelPortDesc"}}' v-model="allSetting.webPort" :min="0"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.publicKeyPath"}}' desc='{{ i18n "pages.settings.publicKeyPathDesc"}}' v-model="allSetting.webCertFile"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.privateKeyPath"}}' desc='{{ i18n "pages.settings.privateKeyPathDesc"}}' v-model="allSetting.webKeyFile"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.panelUrlPath"}}' desc='{{ i18n "pages.settings.panelUrlPathDesc"}}' v-model="allSetting.webBasePath"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.settings.sessionMaxAge" }}' desc='{{ i18n "pages.settings.sessionMaxAgeDesc" }}' v-model="allSetting.sessionMaxAge" :min="0"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.settings.pageSize" }}' desc='{{ i18n "pages.settings.pageSizeDesc" }}' v-model="allSetting.pageSize" :min="0" :step="5"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.settings.expireTimeDiff" }}' desc='{{ i18n "pages.settings.expireTimeDiffDesc" }}' v-model="allSetting.expireDiff" :min="0"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.settings.trafficDiff" }}' desc='{{ i18n "pages.settings.trafficDiffDesc" }}' v-model="allSetting.trafficDiff" :min="0"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.timeZone"}}' desc='{{ i18n "pages.settings.timeZoneDesc"}}' v-model="allSetting.timeLocation"></setting-list-item>
|
||||
<a-list-item>
|
||||
<a-row style="padding: 20px">
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta title='{{ i18n "pages.settings.datepicker"}}'>
|
||||
<template slot="description">{{ i18n "pages.settings.datepickerDescription"}}</template>
|
||||
</a-list-item-meta>
|
||||
</a-col>
|
||||
|
||||
<a-col :lg="24" :xl="12">
|
||||
<template>
|
||||
<a-select style="width: 100%"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||
v-model="datepicker">
|
||||
<a-select-option v-for="item in datepickerList" :value="item.value">
|
||||
<span v-text="item.name"></span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
<a-row style="padding: 20px">
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta title="Language" />
|
||||
</a-col>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<template>
|
||||
<a-select
|
||||
ref="selectLang"
|
||||
v-model="lang"
|
||||
@change="setLang(lang)"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||
style="width: 100%"
|
||||
>
|
||||
<a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs">
|
||||
<span role="img" :aria-label="l.name" v-text="l.icon"></span>
|
||||
<span v-text="l.name"></span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2" tab='{{ i18n "pages.settings.securitySettings"}}' style="padding: 20px;">
|
||||
<a-divider>{{ i18n "pages.settings.security.admin"}}</a-divider>
|
||||
<a-form layout="horizontal" :colon="false" style="float: left; margin: 10px 0;" :label-col="{ md: {span:10} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "pages.settings.oldUsername"}}'>
|
||||
<a-input v-model="user.oldUsername"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.settings.currentPassword"}}'>
|
||||
<password-input v-model="user.oldPassword"></password-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.settings.newUsername"}}'>
|
||||
<a-input v-model="user.newUsername"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.settings.newPassword"}}'>
|
||||
<password-input v-model="user.newPassword"></password-input>
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
<a-button type="primary" @click="updateUser">{{ i18n "confirm" }}</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-divider>{{ i18n "pages.settings.security.secret"}}</a-divider>
|
||||
<a-form style="padding: 20px;">
|
||||
<a-list-item>
|
||||
<a-row>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta title='{{ i18n "pages.settings.security.loginSecurity" }}' description='{{ i18n "pages.settings.security.loginSecurityDesc" }}' />
|
||||
</a-col>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<template>
|
||||
<a-switch @change="toggleToken(allSetting.secretEnable)" v-model="allSetting.secretEnable"></a-switch>
|
||||
<a-icon style="margin-left: 1rem;" v-if="allSetting.secretEnable" :spin="this.changeSecret" type="sync" @click="getNewSecret"></a-icon>
|
||||
</template>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
<a-row>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta title='{{ i18n "pages.settings.security.secretToken" }}' description='{{ i18n "pages.settings.security.secretTokenDesc" }}' />
|
||||
</a-col>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<template>
|
||||
<a-textarea type="text" :disabled="!allSetting.secretEnable" v-model="user.loginSecret"></a-textarea>
|
||||
</template>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-list-item>
|
||||
<a-button type="primary" :loading="this.changeSecret" @click="updateSecret">{{ i18n "confirm" }}</a-button>
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="3" tab='{{ i18n "pages.settings.TGBotSettings"}}'>
|
||||
<a-list item-layout="horizontal">
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.telegramBotEnable" }}' desc='{{ i18n "pages.settings.telegramBotEnableDesc" }}' v-model="allSetting.tgBotEnable"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.telegramToken"}}' desc='{{ i18n "pages.settings.telegramTokenDesc"}}' v-model="allSetting.tgBotToken"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.telegramChatId"}}' desc='{{ i18n "pages.settings.telegramChatIdDesc"}}' v-model="allSetting.tgBotChatId"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.telegramNotifyTime"}}' desc='{{ i18n "pages.settings.telegramNotifyTimeDesc"}}' v-model="allSetting.tgRunTime"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.tgNotifyBackup" }}' desc='{{ i18n "pages.settings.tgNotifyBackupDesc" }}' v-model="allSetting.tgBotBackup"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.tgNotifyLogin" }}' desc='{{ i18n "pages.settings.tgNotifyLoginDesc" }}' v-model="allSetting.tgBotLoginNotify"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.settings.tgNotifyCpu" }}' desc='{{ i18n "pages.settings.tgNotifyCpuDesc" }}' v-model="allSetting.tgCpu" :min="0" :max="100"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.telegramProxy"}}' desc='{{ i18n "pages.settings.telegramProxyDesc"}}' v-model="allSetting.tgBotProxy" placeholder="socks5://user:pass@host:port"></setting-list-item>
|
||||
<a-list-item>
|
||||
<a-row style="padding: 20px">
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta title="Telegram Bot Language" />
|
||||
</a-col>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<template>
|
||||
<a-select
|
||||
ref="selectBotLang"
|
||||
v-model="allSetting.tgLang"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||
style="width: 100%">
|
||||
<a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs">
|
||||
<span role="img" :aria-label="l.name" v-text="l.icon"></span>
|
||||
<span v-text="l.name"></span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="4" tab='{{ i18n "pages.settings.subSettings" }}'>
|
||||
<a-list item-layout="horizontal">
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.subEnable"}}' desc='{{ i18n "pages.settings.subEnableDesc"}}' v-model="allSetting.subEnable"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.subEncrypt"}}' desc='{{ i18n "pages.settings.subEncryptDesc"}}' v-model="allSetting.subEncrypt"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.subShowInfo"}}' desc='{{ i18n "pages.settings.subShowInfoDesc"}}' v-model="allSetting.subShowInfo"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subListen"}}' desc='{{ i18n "pages.settings.subListenDesc"}}' v-model="allSetting.subListen"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subDomain"}}' desc='{{ i18n "pages.settings.subDomainDesc"}}' v-model="allSetting.subDomain"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.settings.subPort"}}' desc='{{ i18n "pages.settings.subPortDesc"}}' v-model.number="allSetting.subPort"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subPath"}}' desc='{{ i18n "pages.settings.subPathDesc"}}' v-model="allSetting.subPath"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subCertPath"}}' desc='{{ i18n "pages.settings.subCertPathDesc"}}' v-model="allSetting.subCertFile"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subKeyPath"}}' desc='{{ i18n "pages.settings.subKeyPathDesc"}}' v-model="allSetting.subKeyFile"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subURI"}}' desc='{{ i18n "pages.settings.subURIDesc"}}' v-model="allSetting.subURI" placeholder="(http|https)://domain[:port]/path/"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.settings.subUpdates"}}' desc='{{ i18n "pages.settings.subUpdatesDesc"}}' v-model="allSetting.subUpdates"></setting-list-item>
|
||||
</a-list>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="5" tab='{{ i18n "pages.settings.subSettings" }} Json' v-if="allSetting.subEnable">
|
||||
<a-list item-layout="horizontal">
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subPath"}}' desc='{{ i18n "pages.settings.subPathDesc"}}' v-model="allSetting.subJsonPath"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subURI"}}' desc='{{ i18n "pages.settings.subURIDesc"}}' v-model="allSetting.subJsonURI" placeholder="(http|https)://domain[:port]/path/"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.fragment"}}' desc='{{ i18n "pages.settings.fragmentDesc"}}' v-model="fragment"></setting-list-item>
|
||||
<template v-if="fragment">
|
||||
<setting-list-item type="text" title='length' v-model="fragmentLength" placeholder="100-200"></setting-list-item>
|
||||
<setting-list-item type="text" title='Interval' v-model="fragmentInterval" placeholder="10-20"></setting-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-space>
|
||||
</a-spin>
|
||||
</a-layout-content>
|
||||
</a-row>
|
||||
</a-list-item>
|
||||
<setting-list-item style="padding: 10px 20px" type="text" title='Length' v-model="fragmentLength" placeholder="100-200"></setting-list-item>
|
||||
<setting-list-item style="padding: 10px 20px" type="text" title='Interval' v-model="fragmentInterval" placeholder="10-20"></setting-list-item>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</a-list-item>
|
||||
<a-list-item style="padding: 20px">
|
||||
<a-row>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta title='{{ i18n "pages.settings.mux"}}'>
|
||||
<template slot="description">{{ i18n "pages.settings.muxDesc"}}</template>
|
||||
</a-list-item-meta>
|
||||
</a-col>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-switch v-model="enableMux"></a-switch>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-collapse v-if="enableMux" style="margin-top: 14px;">
|
||||
<a-collapse-panel header='{{ i18n "pages.settings.muxSett"}}'>
|
||||
<setting-list-item style="padding: 10px 20px" type="number" title='Concurrency' v-model="muxConcurrency" :min="-1" :max="1024"></setting-list-item>
|
||||
<setting-list-item style="padding: 10px 20px" type="number" title='xudp Concurrency' v-model="muxXudpConcurrency" :min="-1" :max="1024"></setting-list-item>
|
||||
<a-list-item style="padding: 10px 20px">
|
||||
<a-row>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta title='xudp UDP 443'></a-list-item-meta>
|
||||
</a-col>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-select v-model="muxXudpProxyUDP443" style="width: 100%" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option :value="p" :label="p" v-for="p in ['reject', 'allow', 'skip']"> [[ p ]] </a-select-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-list-item>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</a-list-item>
|
||||
<a-list-item style="padding: 20px">
|
||||
<a-row>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta title='{{ i18n "pages.settings.direct"}}'>
|
||||
<template slot="description">{{ i18n "pages.settings.directDesc"}}</template>
|
||||
</a-list-item-meta>
|
||||
</a-col>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-switch v-model="enableDirect"></a-switch>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-collapse v-if="enableDirect" style="margin-top: 14px;">
|
||||
<a-collapse-panel header='{{ i18n "pages.settings.directSett"}}'>
|
||||
<a-list-item style="padding: 10px 20px">
|
||||
<a-checkbox-group v-model="directCountries" name="Countries" :options="countryOptions"></a-checkbox-group>
|
||||
</a-list-item>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-space>
|
||||
</a-spin>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
{{template "js" .}}
|
||||
<script src="{{ .base_path }}assets/js/model/setting.js?{{ .cur_ver }}"></script>
|
||||
{{template "component/themeSwitcher" .}}
|
||||
@@ -348,6 +411,40 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
defaultMux: {
|
||||
enabled: true,
|
||||
concurrency: 8,
|
||||
xudpConcurrency: 16,
|
||||
xudpProxyUDP443: "reject"
|
||||
},
|
||||
defaultRules: [
|
||||
{
|
||||
type: "field",
|
||||
outboundTag: "direct",
|
||||
domain: [
|
||||
"geosite:category-ir",
|
||||
"geosite:cn"
|
||||
],
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
type: "field",
|
||||
outboundTag: "direct",
|
||||
ip: [
|
||||
"geoip:private",
|
||||
"geoip:ir",
|
||||
"geoip:cn"
|
||||
],
|
||||
enabled: true
|
||||
},
|
||||
],
|
||||
countryOptions: [
|
||||
{ label: 'Private IP/Domain', value: 'private' },
|
||||
{ label: '🇮🇷 Iran', value: 'ir' },
|
||||
{ label: '🇨🇳 China', value: 'cn' },
|
||||
{ label: '🇷🇺 Russia', value: 'ru' },
|
||||
{ label: '🇻🇳 Vietnam', value: 'vn' },
|
||||
],
|
||||
get remarkModel() {
|
||||
rm = this.allSetting.remarkModel;
|
||||
return rm.length>1 ? rm.substring(1).split('') : [];
|
||||
@@ -406,7 +503,6 @@
|
||||
this.loading(false);
|
||||
if (msg.success) {
|
||||
this.user = {};
|
||||
window.location.replace(basePath + "logout");
|
||||
}
|
||||
},
|
||||
async restartPanel() {
|
||||
@@ -443,9 +539,8 @@
|
||||
async updateSecret() {
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.post("/panel/setting/updateUserSecret", this.user);
|
||||
if (msg.success) {
|
||||
if (msg && msg.obj) {
|
||||
this.user = msg.obj;
|
||||
window.location.replace(basePath + "logout");
|
||||
}
|
||||
this.loading(false);
|
||||
await this.updateAllSetting();
|
||||
@@ -483,6 +578,16 @@
|
||||
this.allSetting.subJsonFragment = v ? JSON.stringify(this.defaultFragment) : "";
|
||||
}
|
||||
},
|
||||
fragmentPackets: {
|
||||
get: function() { return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).settings.fragment.packets : ""; },
|
||||
set: function(v) {
|
||||
if (v != ""){
|
||||
newFragment = JSON.parse(this.allSetting.subJsonFragment);
|
||||
newFragment.settings.fragment.packets = v;
|
||||
this.allSetting.subJsonFragment = JSON.stringify(newFragment);
|
||||
}
|
||||
}
|
||||
},
|
||||
fragmentLength: {
|
||||
get: function() { return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).settings.fragment.length : ""; },
|
||||
set: function(v) {
|
||||
@@ -503,6 +608,61 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
enableMux: {
|
||||
get: function() { return this.allSetting?.subJsonMux != ""; },
|
||||
set: function (v) {
|
||||
this.allSetting.subJsonMux = v ? JSON.stringify(this.defaultMux) : "";
|
||||
}
|
||||
},
|
||||
muxConcurrency: {
|
||||
get: function() { return this.enableMux ? JSON.parse(this.allSetting.subJsonMux).concurrency : -1; },
|
||||
set: function(v) {
|
||||
newMux = JSON.parse(this.allSetting.subJsonMux);
|
||||
newMux.concurrency = v;
|
||||
this.allSetting.subJsonMux = JSON.stringify(newMux);
|
||||
}
|
||||
},
|
||||
muxXudpConcurrency: {
|
||||
get: function() { return this.enableMux ? JSON.parse(this.allSetting.subJsonMux).xudpConcurrency : -1; },
|
||||
set: function(v) {
|
||||
newMux = JSON.parse(this.allSetting.subJsonMux);
|
||||
newMux.xudpConcurrency = v;
|
||||
this.allSetting.subJsonMux = JSON.stringify(newMux);
|
||||
}
|
||||
},
|
||||
muxXudpProxyUDP443: {
|
||||
get: function() { return this.enableMux ? JSON.parse(this.allSetting.subJsonMux).xudpProxyUDP443 : "reject"; },
|
||||
set: function(v) {
|
||||
newMux = JSON.parse(this.allSetting.subJsonMux);
|
||||
newMux.xudpProxyUDP443 = v;
|
||||
this.allSetting.subJsonMux = JSON.stringify(newMux);
|
||||
}
|
||||
},
|
||||
enableDirect: {
|
||||
get: function() { return this.allSetting?.subJsonRules != ""; },
|
||||
set: function (v) {
|
||||
this.allSetting.subJsonRules = v ? JSON.stringify(this.defaultRules) : "";
|
||||
}
|
||||
},
|
||||
directCountries: {
|
||||
get: function() {
|
||||
if (!this.enableDirect) return [];
|
||||
rules = JSON.parse(this.allSetting.subJsonRules);
|
||||
return Array.isArray(rules) ? rules[1].ip.map(d => d.replace("geoip:","")) : [];
|
||||
},
|
||||
set: function (v) {
|
||||
rules = JSON.parse(this.allSetting.subJsonRules);
|
||||
if (!Array.isArray(rules)) return;
|
||||
rules[0].domain = [];
|
||||
rules[1].ip = [];
|
||||
v.forEach(d => {
|
||||
category = ["cn","private"].includes(d) ? "" : "category-";
|
||||
rules[0].domain.push("geosite:"+category+d);
|
||||
rules[1].ip.push("geoip:"+d);
|
||||
});
|
||||
this.allSetting.subJsonRules = JSON.stringify(rules);
|
||||
}
|
||||
},
|
||||
confAlerts: {
|
||||
get: function() {
|
||||
if (!this.allSetting) return [];
|
||||
@@ -531,4 +691,4 @@
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<td>[[ warpModal.warpData.access_token ]]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Devide ID</td>
|
||||
<td>Device ID</td>
|
||||
<td>[[ warpModal.warpData.device_id ]]</td>
|
||||
</tr>
|
||||
<tr class="client-table-odd-row">
|
||||
@@ -24,19 +24,19 @@
|
||||
<td>[[ warpModal.warpData.private_key ]]</td>
|
||||
</tr>
|
||||
</table>
|
||||
<a-divider style="margin: 0;">{{ i18n "pages.settings.toasts.modifySettings" }}</a-divider>
|
||||
<a-divider style="margin: 0;">{{ i18n "pages.xray.outbound.settings" }}</a-divider>
|
||||
<a-collapse style="margin: 10px 0;">
|
||||
<a-collapse-panel header='WARP/WARP+ License Key'>
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="License Key">
|
||||
<a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="Key">
|
||||
<a-input v-model="warpPlus"></a-input>
|
||||
<a-button @click="updateLicense(warpPlus)" :disabled="warpPlus.length<26" :loading="warpModal.confirmLoading">{{ i18n "pages.inbounds.update" }}</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
<a-divider style="margin: 0;">{{ i18n "pages.settings.toasts.getSettings" }}</a-divider>
|
||||
<a-button icon="sync" @click="getConfig" style="margin-bottom: 10px;" :loading="warpModal.confirmLoading">{{ i18n "info" }}</a-button>
|
||||
<a-divider style="margin: 0;">{{ i18n "pages.xray.outbound.accountInfo" }}</a-divider>
|
||||
<a-button icon="sync" @click="getConfig" style="margin-top: 5px; margin-bottom: 10px;" :loading="warpModal.confirmLoading" type="primary">{{ i18n "info" }}</a-button>
|
||||
<template v-if="!ObjectUtil.isEmpty(warpModal.warpConfig)">
|
||||
<table style="width: 100%">
|
||||
<tr class="client-table-odd-row">
|
||||
@@ -48,7 +48,7 @@
|
||||
<td>[[ warpModal.warpConfig.model ]]</td>
|
||||
</tr>
|
||||
<tr class="client-table-odd-row">
|
||||
<td>Device Active</td>
|
||||
<td>Device Enabled</td>
|
||||
<td>[[ warpModal.warpConfig.enabled ]]</td>
|
||||
</tr>
|
||||
<template v-if="!ObjectUtil.isEmpty(warpModal.warpConfig.account)">
|
||||
@@ -61,7 +61,7 @@
|
||||
<td>[[ warpModal.warpConfig.account.role ]]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Premium Data</td>
|
||||
<td>WARP+ Data</td>
|
||||
<td>[[ sizeFormat(warpModal.warpConfig.account.premium_data) ]]</td>
|
||||
</tr>
|
||||
<tr class="client-table-odd-row">
|
||||
@@ -74,16 +74,15 @@
|
||||
</tr>
|
||||
</template>
|
||||
</table>
|
||||
<a-divider style="margin: 10px 0;">WARP {{ i18n "pages.xray.rules.outbound" }}</a-divider>
|
||||
<a-divider style="margin: 10px 0;">{{ i18n "pages.xray.outbound.outboundStatus" }}</a-divider>
|
||||
<a-form :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "status" }}'>
|
||||
<template v-if="warpOutboundIndex>=0">
|
||||
<a-tag color="green">{{ i18n "enabled" }}</a-tag>
|
||||
<a-button @click="resetOutbound" :loading="warpModal.confirmLoading">{{ i18n "reset" }}</a-button>
|
||||
<a-tag color="green" style="line-height: 31px;">{{ i18n "enabled" }}</a-tag>
|
||||
<a-button @click="resetOutbound" :loading="warpModal.confirmLoading" type="danger">{{ i18n "reset" }}</a-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-tag color="orange">{{ i18n "disabled" }}</a-tag>
|
||||
<a-button @click="addOutbound" :loading="warpModal.confirmLoading">{{ i18n "pages.xray.outbound.addOutbound" }}</a-button>
|
||||
<a-tag color="orange" style="line-height: 31px;">{{ i18n "disabled" }}</a-tag>
|
||||
<a-button @click="addOutbound" :loading="warpModal.confirmLoading" type="primary">{{ i18n "pages.xray.outbound.addOutbound" }}</a-button>
|
||||
</template>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,8 @@
|
||||
<a-select v-model="balancerModal.balancer.strategy" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="random">Random</a-select-option>
|
||||
<a-select-option value="roundRobin">Round Robin</a-select-option>
|
||||
<a-select-option value="leastload">Least Load</a-select-option>
|
||||
<a-select-option value="leastping">Least Ping</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.balancer.balancerSelectors" }}' has-feedback
|
||||
|
||||
@@ -38,7 +38,6 @@
|
||||
:options="reverseModal.inboundTags"></a-checkbox-group>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</table>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
<script>
|
||||
@@ -114,6 +113,7 @@
|
||||
this.isEdit = isEdit;
|
||||
this.inboundTags = app.templateSettings.inbounds.filter((i) => !ObjectUtil.isEmpty(i.tag)).map(obj => obj.tag);
|
||||
this.inboundTags.push(...app.inboundTags);
|
||||
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);
|
||||
},
|
||||
close() {
|
||||
|
||||
@@ -195,6 +195,7 @@
|
||||
this.isEdit = isEdit;
|
||||
this.inboundTags = app.templateSettings.inbounds.filter((i) => !ObjectUtil.isEmpty(i.tag)).map(obj => obj.tag);
|
||||
this.inboundTags.push(...app.inboundTags);
|
||||
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)];
|
||||
if(app.templateSettings.reverse){
|
||||
if(app.templateSettings.reverse.bridges) {
|
||||
|
||||
@@ -14,23 +14,16 @@ import (
|
||||
|
||||
"x-ui/database"
|
||||
"x-ui/database/model"
|
||||
"x-ui/config"
|
||||
"x-ui/logger"
|
||||
"x-ui/xray"
|
||||
)
|
||||
|
||||
type CheckClientIpJob struct {
|
||||
lastClear int64
|
||||
disAllowedIps []string
|
||||
}
|
||||
|
||||
var job *CheckClientIpJob
|
||||
var ipFiles = []string{
|
||||
xray.GetIPLimitLogPath(),
|
||||
xray.GetIPLimitBannedLogPath(),
|
||||
xray.GetIPLimitBannedPrevLogPath(),
|
||||
xray.GetAccessPersistentLogPath(),
|
||||
xray.GetAccessPersistentPrevLogPath(),
|
||||
}
|
||||
|
||||
func NewCheckClientIpJob() *CheckClientIpJob {
|
||||
job = new(CheckClientIpJob)
|
||||
@@ -38,52 +31,53 @@ func NewCheckClientIpJob() *CheckClientIpJob {
|
||||
}
|
||||
|
||||
func (j *CheckClientIpJob) Run() {
|
||||
|
||||
// create files and dirs required for iplimit if not exists
|
||||
for i := 0; i < len(ipFiles); i++ {
|
||||
err := os.MkdirAll(config.GetLogFolder(), 0770)
|
||||
j.checkError(err)
|
||||
file, err := os.OpenFile(ipFiles[i], os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
|
||||
j.checkError(err)
|
||||
defer file.Close()
|
||||
if j.lastClear == 0 {
|
||||
j.lastClear = time.Now().Unix()
|
||||
}
|
||||
|
||||
// check for limit ip
|
||||
shouldClearAccessLog := false
|
||||
f2bInstalled := j.checkFail2BanInstalled()
|
||||
isAccessLogAvailable := j.checkAccessLogAvailable(f2bInstalled)
|
||||
|
||||
if j.hasLimitIp() {
|
||||
j.checkFail2BanInstalled()
|
||||
j.processLogFile()
|
||||
if f2bInstalled && isAccessLogAvailable {
|
||||
shouldClearAccessLog = j.processLogFile()
|
||||
} else {
|
||||
if !f2bInstalled {
|
||||
logger.Warning("[iplimit] fail2ban is not installed. IP limiting may not work properly.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !j.hasLimitIp() && xray.GetAccessLogPath() == "./access.log" {
|
||||
go j.clearLogTime()
|
||||
}
|
||||
}
|
||||
|
||||
func (j *CheckClientIpJob) clearLogTime() {
|
||||
for {
|
||||
time.Sleep(time.Hour)
|
||||
if shouldClearAccessLog || isAccessLogAvailable && time.Now().Unix()-j.lastClear > 3600 {
|
||||
j.clearAccessLog()
|
||||
}
|
||||
}
|
||||
|
||||
func (j *CheckClientIpJob) clearAccessLog() {
|
||||
accessLogPath := xray.GetAccessLogPath()
|
||||
logAccessP, err := os.OpenFile(xray.GetAccessPersistentLogPath(), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
|
||||
logAccessP, err := os.OpenFile(xray.GetAccessPersistentLogPath(), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
|
||||
j.checkError(err)
|
||||
|
||||
// get access log path to open it
|
||||
accessLogPath, err := xray.GetAccessLogPath()
|
||||
j.checkError(err)
|
||||
defer logAccessP.Close()
|
||||
|
||||
// reopen the access log file for reading
|
||||
file, err := os.Open(accessLogPath)
|
||||
j.checkError(err)
|
||||
defer file.Close()
|
||||
|
||||
// copy access log content to persistent file
|
||||
_, err = io.Copy(logAccessP, file)
|
||||
j.checkError(err)
|
||||
|
||||
// close the file after copying content
|
||||
logAccessP.Close()
|
||||
file.Close()
|
||||
|
||||
// clean access log
|
||||
err = os.Truncate(accessLogPath, 0)
|
||||
j.checkError(err)
|
||||
j.lastClear = time.Now().Unix()
|
||||
}
|
||||
|
||||
func (j *CheckClientIpJob) hasLimitIp() bool {
|
||||
@@ -115,32 +109,12 @@ func (j *CheckClientIpJob) hasLimitIp() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (j *CheckClientIpJob) checkFail2BanInstalled() {
|
||||
cmd := "fail2ban-client"
|
||||
args := []string{"-h"}
|
||||
|
||||
err := exec.Command(cmd, args...).Run()
|
||||
if err != nil {
|
||||
logger.Warning("fail2ban is not installed. IP limiting may not work properly.")
|
||||
}
|
||||
}
|
||||
|
||||
func (j *CheckClientIpJob) processLogFile() {
|
||||
accessLogPath := xray.GetAccessLogPath()
|
||||
|
||||
if accessLogPath == "none" {
|
||||
logger.Warning("Access log is set to 'none' check your Xray Configs")
|
||||
return
|
||||
}
|
||||
|
||||
if accessLogPath == "" {
|
||||
logger.Warning("Access log doesn't exist in your Xray Configs")
|
||||
return
|
||||
}
|
||||
func (j *CheckClientIpJob) processLogFile() bool {
|
||||
accessLogPath, err := xray.GetAccessLogPath()
|
||||
j.checkError(err)
|
||||
|
||||
file, err := os.Open(accessLogPath)
|
||||
j.checkError(err)
|
||||
defer file.Close()
|
||||
|
||||
InboundClientIps := make(map[string][]string)
|
||||
|
||||
@@ -176,6 +150,7 @@ func (j *CheckClientIpJob) processLogFile() {
|
||||
}
|
||||
|
||||
j.checkError(scanner.Err())
|
||||
file.Close()
|
||||
|
||||
shouldCleanLog := false
|
||||
|
||||
@@ -189,12 +164,38 @@ func (j *CheckClientIpJob) processLogFile() {
|
||||
}
|
||||
}
|
||||
|
||||
// added delay before cleaning logs to reduce chance of logging IP that already has been banned
|
||||
time.Sleep(time.Second * 2)
|
||||
return shouldCleanLog
|
||||
}
|
||||
|
||||
if shouldCleanLog {
|
||||
j.clearAccessLog()
|
||||
func (j *CheckClientIpJob) checkFail2BanInstalled() bool {
|
||||
cmd := "fail2ban-client"
|
||||
args := []string{"-h"}
|
||||
err := exec.Command(cmd, args...).Run()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (j *CheckClientIpJob) checkAccessLogAvailable(handleWarning bool) bool {
|
||||
isAvailable := true
|
||||
warningMsg := ""
|
||||
accessLogPath, err := xray.GetAccessLogPath()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// access log is not available if it is set to 'none' or an empty string
|
||||
switch accessLogPath {
|
||||
case "none":
|
||||
warningMsg = "Access log is set to 'none', check your Xray Configs"
|
||||
isAvailable = false
|
||||
case "":
|
||||
warningMsg = "Access log doesn't exist in your Xray Configs"
|
||||
isAvailable = false
|
||||
}
|
||||
|
||||
if handleWarning && warningMsg != "" {
|
||||
logger.Warning(warningMsg)
|
||||
}
|
||||
return isAvailable
|
||||
}
|
||||
|
||||
func (j *CheckClientIpJob) checkError(e error) {
|
||||
@@ -272,7 +273,7 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun
|
||||
j.disAllowedIps = []string{}
|
||||
|
||||
// create iplimit log file channel
|
||||
logIpFile, err := os.OpenFile(xray.GetIPLimitLogPath(), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
|
||||
logIpFile, err := os.OpenFile(xray.GetIPLimitLogPath(), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
logger.Errorf("failed to create or open ip limit log file: %s", err)
|
||||
}
|
||||
@@ -305,9 +306,8 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun
|
||||
|
||||
db := database.GetDB()
|
||||
err = db.Save(inboundClientIps).Error
|
||||
if err != nil {
|
||||
return shouldCleanLog
|
||||
}
|
||||
j.checkError(err)
|
||||
|
||||
return shouldCleanLog
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package job
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"x-ui/web/service"
|
||||
|
||||
"github.com/shirou/gopsutil/v3/cpu"
|
||||
|
||||
@@ -20,7 +20,7 @@ func (j *CheckXrayRunningJob) Run() {
|
||||
j.checkTime = 0
|
||||
} else {
|
||||
j.checkTime++
|
||||
//only restart if it's down 2 times in a row
|
||||
// only restart if it's down 2 times in a row
|
||||
if j.checkTime > 1 {
|
||||
err := j.xrayService.RestartXray(false)
|
||||
j.checkTime = 0
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package job
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"x-ui/logger"
|
||||
"x-ui/xray"
|
||||
)
|
||||
@@ -16,33 +18,28 @@ func NewClearLogsJob() *ClearLogsJob {
|
||||
func (j *ClearLogsJob) Run() {
|
||||
logFiles := []string{xray.GetIPLimitLogPath(), xray.GetIPLimitBannedLogPath(), xray.GetAccessPersistentLogPath()}
|
||||
logFilesPrev := []string{xray.GetIPLimitBannedPrevLogPath(), xray.GetAccessPersistentPrevLogPath()}
|
||||
|
||||
// clear old previous logs
|
||||
for i := 0; i < len(logFilesPrev); i++ {
|
||||
if err := os.Truncate(logFilesPrev[i], 0); err != nil {
|
||||
logger.Warning("clear logs job err:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// clear log files and copy to previous logs
|
||||
for i := 0; i < len(logFiles); i++ {
|
||||
if i > 0 {
|
||||
// copy to previous logs
|
||||
logFilePrev, err := os.OpenFile(logFilesPrev[i-1], os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
|
||||
logFilePrev, err := os.OpenFile(logFilesPrev[i-1], os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
logger.Warning("clear logs job err:", err)
|
||||
}
|
||||
|
||||
logFile, err := os.ReadFile(logFiles[i])
|
||||
if err != nil {
|
||||
logFile, err := os.OpenFile(logFiles[i], os.O_CREATE|os.O_RDONLY, 0644)
|
||||
if err == nil {
|
||||
_, err = io.Copy(logFilePrev, logFile)
|
||||
if err != nil {
|
||||
logger.Warning("clear logs job err:", err)
|
||||
}
|
||||
} else {
|
||||
logger.Warning("clear logs job err:", err)
|
||||
}
|
||||
|
||||
_, err = logFilePrev.Write(logFile)
|
||||
if err != nil {
|
||||
logger.Warning("clear logs job err:", err)
|
||||
}
|
||||
defer logFilePrev.Close()
|
||||
logFile.Close()
|
||||
logFilePrev.Close()
|
||||
}
|
||||
|
||||
err := os.Truncate(logFiles[i], 0)
|
||||
|
||||
@@ -36,5 +36,4 @@ func (j *XrayTrafficJob) Run() {
|
||||
if needRestart0 || needRestart1 {
|
||||
j.xrayService.SetToNeedRestart()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"strings"
|
||||
|
||||
"x-ui/logger"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -12,9 +13,11 @@ import (
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
var i18nBundle *i18n.Bundle
|
||||
var LocalizerWeb *i18n.Localizer
|
||||
var LocalizerBot *i18n.Localizer
|
||||
var (
|
||||
i18nBundle *i18n.Bundle
|
||||
LocalizerWeb *i18n.Localizer
|
||||
LocalizerBot *i18n.Localizer
|
||||
)
|
||||
|
||||
type I18nType string
|
||||
|
||||
@@ -79,7 +82,6 @@ func I18n(i18nType I18nType, key string, params ...string) string {
|
||||
MessageID: key,
|
||||
TemplateData: templateData,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to localize message: %v", err)
|
||||
return ""
|
||||
@@ -135,7 +137,6 @@ func parseTranslationFiles(i18nFS embed.FS, i18nBundle *i18n.Bundle) error {
|
||||
_, err = i18nBundle.ParseMessageFileBytes(data, path)
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -28,7 +28,9 @@
|
||||
{
|
||||
"tag": "direct",
|
||||
"protocol": "freedom",
|
||||
"settings": {}
|
||||
"settings": {
|
||||
"domainStrategy": "UseIP"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "blocked",
|
||||
@@ -51,7 +53,7 @@
|
||||
}
|
||||
},
|
||||
"routing": {
|
||||
"domainStrategy": "IPIfNonMatch",
|
||||
"domainStrategy": "AsIs",
|
||||
"rules": [
|
||||
{
|
||||
"type": "field",
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"x-ui/database"
|
||||
"x-ui/database/model"
|
||||
"x-ui/logger"
|
||||
@@ -90,7 +91,6 @@ func (s *InboundService) getAllEmails() ([]string, error) {
|
||||
FROM inbounds,
|
||||
JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client
|
||||
`).Scan(&emails).Error
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -172,6 +172,23 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
|
||||
return inbound, false, err
|
||||
}
|
||||
|
||||
// Secure client ID
|
||||
for _, client := range clients {
|
||||
if inbound.Protocol == "trojan" {
|
||||
if client.Password == "" {
|
||||
return inbound, false, common.NewError("empty client ID")
|
||||
}
|
||||
} else if inbound.Protocol == "shadowsocks" {
|
||||
if client.Email == "" {
|
||||
return inbound, false, common.NewError("empty client ID")
|
||||
}
|
||||
} else {
|
||||
if client.ID == "" {
|
||||
return inbound, false, common.NewError("empty client ID")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db := database.GetDB()
|
||||
tx := db.Begin()
|
||||
defer func() {
|
||||
@@ -413,6 +430,23 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Secure client ID
|
||||
for _, client := range clients {
|
||||
if oldInbound.Protocol == "trojan" {
|
||||
if client.Password == "" {
|
||||
return false, common.NewError("empty client ID")
|
||||
}
|
||||
} else if oldInbound.Protocol == "shadowsocks" {
|
||||
if client.Email == "" {
|
||||
return false, common.NewError("empty client ID")
|
||||
}
|
||||
} else {
|
||||
if client.ID == "" {
|
||||
return false, common.NewError("empty client ID")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var oldSettings map[string]interface{}
|
||||
err = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings)
|
||||
if err != nil {
|
||||
@@ -496,9 +530,9 @@ func (s *InboundService) DelInboundClient(inboundId int, clientId string) (bool,
|
||||
client_key = "email"
|
||||
}
|
||||
|
||||
inerfaceClients := settings["clients"].([]interface{})
|
||||
interfaceClients := settings["clients"].([]interface{})
|
||||
var newClients []interface{}
|
||||
for _, client := range inerfaceClients {
|
||||
for _, client := range interfaceClients {
|
||||
c := client.(map[string]interface{})
|
||||
c_id := c[client_key].(string)
|
||||
if c_id == clientId {
|
||||
@@ -573,15 +607,19 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
|
||||
}
|
||||
|
||||
oldEmail := ""
|
||||
newClientId := ""
|
||||
clientIndex := 0
|
||||
for index, oldClient := range oldClients {
|
||||
oldClientId := ""
|
||||
if oldInbound.Protocol == "trojan" {
|
||||
oldClientId = oldClient.Password
|
||||
newClientId = clients[0].Password
|
||||
} else if oldInbound.Protocol == "shadowsocks" {
|
||||
oldClientId = oldClient.Email
|
||||
newClientId = clients[0].Email
|
||||
} else {
|
||||
oldClientId = oldClient.ID
|
||||
newClientId = clients[0].ID
|
||||
}
|
||||
if clientId == oldClientId {
|
||||
oldEmail = oldClient.Email
|
||||
@@ -590,6 +628,11 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
|
||||
}
|
||||
}
|
||||
|
||||
// Validate new client ID
|
||||
if newClientId == "" {
|
||||
return false, common.NewError("empty client ID")
|
||||
}
|
||||
|
||||
if len(clients[0].Email) > 0 && clients[0].Email != oldEmail {
|
||||
existEmail, err := s.checkEmailsExistForClients(clients)
|
||||
if err != nil {
|
||||
@@ -682,7 +725,7 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
|
||||
return needRestart, tx.Save(oldInbound).Error
|
||||
}
|
||||
|
||||
func (s *InboundService) AddTraffic(traffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) {
|
||||
func (s *InboundService) AddTraffic(inboundTraffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) {
|
||||
var err error
|
||||
db := database.GetDB()
|
||||
tx := db.Begin()
|
||||
@@ -694,7 +737,7 @@ func (s *InboundService) AddTraffic(traffics []*xray.Traffic, clientTraffics []*
|
||||
tx.Commit()
|
||||
}
|
||||
}()
|
||||
err = s.addInboundTraffic(tx, traffics)
|
||||
err = s.addInboundTraffic(tx, inboundTraffics)
|
||||
if err != nil {
|
||||
return err, false
|
||||
}
|
||||
@@ -969,7 +1012,7 @@ func (s *InboundService) disableInvalidInbounds(tx *gorm.DB) (bool, int64, error
|
||||
s.xrayApi.Init(p.GetAPIPort())
|
||||
for _, tag := range tags {
|
||||
err1 := s.xrayApi.DelInbound(tag)
|
||||
if err == nil {
|
||||
if err1 == nil {
|
||||
logger.Debug("Inbound disabled by api:", tag)
|
||||
} else {
|
||||
logger.Debug("Error in disabling inbound by api:", err1)
|
||||
@@ -1060,10 +1103,7 @@ func (s *InboundService) AddClientStat(tx *gorm.DB, inboundId int, client *model
|
||||
clientTraffic.Reset = client.Reset
|
||||
result := tx.Create(&clientTraffic)
|
||||
err := result.Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *InboundService) UpdateClientStat(tx *gorm.DB, email string, client *model.Client) error {
|
||||
@@ -1074,12 +1114,10 @@ func (s *InboundService) UpdateClientStat(tx *gorm.DB, email string, client *mod
|
||||
"email": client.Email,
|
||||
"total": client.TotalGB,
|
||||
"expiry_time": client.ExpiryTime,
|
||||
"reset": client.Reset})
|
||||
"reset": client.Reset,
|
||||
})
|
||||
err := result.Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *InboundService) UpdateClientIPs(tx *gorm.DB, oldEmail string, newEmail string) error {
|
||||
@@ -1148,20 +1186,20 @@ func (s *InboundService) GetClientByEmail(clientEmail string) (*xray.ClientTraff
|
||||
return nil, nil, common.NewError("Client Not Found In Inbound For Email:", clientEmail)
|
||||
}
|
||||
|
||||
func (s *InboundService) SetClientTelegramUserID(trafficId int, tgId string) error {
|
||||
func (s *InboundService) SetClientTelegramUserID(trafficId int, tgId string) (bool, error) {
|
||||
traffic, inbound, err := s.GetClientInboundByTrafficID(trafficId)
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
if inbound == nil {
|
||||
return common.NewError("Inbound Not Found For Traffic ID:", trafficId)
|
||||
return false, common.NewError("Inbound Not Found For Traffic ID:", trafficId)
|
||||
}
|
||||
|
||||
clientEmail := traffic.Email
|
||||
|
||||
oldClients, err := s.GetClients(inbound)
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
|
||||
clientId := ""
|
||||
@@ -1180,13 +1218,13 @@ func (s *InboundService) SetClientTelegramUserID(trafficId int, tgId string) err
|
||||
}
|
||||
|
||||
if len(clientId) == 0 {
|
||||
return common.NewError("Client Not Found For Email:", clientEmail)
|
||||
return false, common.NewError("Client Not Found For Email:", clientEmail)
|
||||
}
|
||||
|
||||
var settings map[string]interface{}
|
||||
err = json.Unmarshal([]byte(inbound.Settings), &settings)
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
clients := settings["clients"].([]interface{})
|
||||
var newClients []interface{}
|
||||
@@ -1200,14 +1238,11 @@ func (s *InboundService) SetClientTelegramUserID(trafficId int, tgId string) err
|
||||
settings["clients"] = newClients
|
||||
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
inbound.Settings = string(modifiedSettings)
|
||||
_, err = s.UpdateInboundClient(inbound, clientId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
needRestart, err := s.UpdateInboundClient(inbound, clientId)
|
||||
return needRestart, err
|
||||
}
|
||||
|
||||
func (s *InboundService) checkIsEnabledByEmail(clientEmail string) (bool, error) {
|
||||
@@ -1236,18 +1271,18 @@ func (s *InboundService) checkIsEnabledByEmail(clientEmail string) (bool, error)
|
||||
return isEnable, err
|
||||
}
|
||||
|
||||
func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, error) {
|
||||
func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, bool, error) {
|
||||
_, inbound, err := s.GetClientInboundByEmail(clientEmail)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, false, err
|
||||
}
|
||||
if inbound == nil {
|
||||
return false, common.NewError("Inbound Not Found For Email:", clientEmail)
|
||||
return false, false, common.NewError("Inbound Not Found For Email:", clientEmail)
|
||||
}
|
||||
|
||||
oldClients, err := s.GetClients(inbound)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, false, err
|
||||
}
|
||||
|
||||
clientId := ""
|
||||
@@ -1268,13 +1303,13 @@ func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, er
|
||||
}
|
||||
|
||||
if len(clientId) == 0 {
|
||||
return false, common.NewError("Client Not Found For Email:", clientEmail)
|
||||
return false, false, common.NewError("Client Not Found For Email:", clientEmail)
|
||||
}
|
||||
|
||||
var settings map[string]interface{}
|
||||
err = json.Unmarshal([]byte(inbound.Settings), &settings)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, false, err
|
||||
}
|
||||
clients := settings["clients"].([]interface{})
|
||||
var newClients []interface{}
|
||||
@@ -1288,30 +1323,30 @@ func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, er
|
||||
settings["clients"] = newClients
|
||||
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, false, err
|
||||
}
|
||||
inbound.Settings = string(modifiedSettings)
|
||||
|
||||
_, err = s.UpdateInboundClient(inbound, clientId)
|
||||
needRestart, err := s.UpdateInboundClient(inbound, clientId)
|
||||
if err != nil {
|
||||
return false, needRestart, err
|
||||
}
|
||||
|
||||
return !clientOldEnabled, needRestart, nil
|
||||
}
|
||||
|
||||
func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int) (bool, error) {
|
||||
_, inbound, err := s.GetClientInboundByEmail(clientEmail)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return !clientOldEnabled, nil
|
||||
}
|
||||
|
||||
func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int) error {
|
||||
_, inbound, err := s.GetClientInboundByEmail(clientEmail)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if inbound == nil {
|
||||
return common.NewError("Inbound Not Found For Email:", clientEmail)
|
||||
return false, common.NewError("Inbound Not Found For Email:", clientEmail)
|
||||
}
|
||||
|
||||
oldClients, err := s.GetClients(inbound)
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
|
||||
clientId := ""
|
||||
@@ -1330,13 +1365,13 @@ func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int
|
||||
}
|
||||
|
||||
if len(clientId) == 0 {
|
||||
return common.NewError("Client Not Found For Email:", clientEmail)
|
||||
return false, common.NewError("Client Not Found For Email:", clientEmail)
|
||||
}
|
||||
|
||||
var settings map[string]interface{}
|
||||
err = json.Unmarshal([]byte(inbound.Settings), &settings)
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
clients := settings["clients"].([]interface{})
|
||||
var newClients []interface{}
|
||||
@@ -1350,28 +1385,25 @@ func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int
|
||||
settings["clients"] = newClients
|
||||
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
inbound.Settings = string(modifiedSettings)
|
||||
_, err = s.UpdateInboundClient(inbound, clientId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
needRestart, err := s.UpdateInboundClient(inbound, clientId)
|
||||
return needRestart, err
|
||||
}
|
||||
|
||||
func (s *InboundService) ResetClientExpiryTimeByEmail(clientEmail string, expiry_time int64) error {
|
||||
func (s *InboundService) ResetClientExpiryTimeByEmail(clientEmail string, expiry_time int64) (bool, error) {
|
||||
_, inbound, err := s.GetClientInboundByEmail(clientEmail)
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
if inbound == nil {
|
||||
return common.NewError("Inbound Not Found For Email:", clientEmail)
|
||||
return false, common.NewError("Inbound Not Found For Email:", clientEmail)
|
||||
}
|
||||
|
||||
oldClients, err := s.GetClients(inbound)
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
|
||||
clientId := ""
|
||||
@@ -1390,13 +1422,13 @@ func (s *InboundService) ResetClientExpiryTimeByEmail(clientEmail string, expiry
|
||||
}
|
||||
|
||||
if len(clientId) == 0 {
|
||||
return common.NewError("Client Not Found For Email:", clientEmail)
|
||||
return false, common.NewError("Client Not Found For Email:", clientEmail)
|
||||
}
|
||||
|
||||
var settings map[string]interface{}
|
||||
err = json.Unmarshal([]byte(inbound.Settings), &settings)
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
clients := settings["clients"].([]interface{})
|
||||
var newClients []interface{}
|
||||
@@ -1410,31 +1442,28 @@ func (s *InboundService) ResetClientExpiryTimeByEmail(clientEmail string, expiry
|
||||
settings["clients"] = newClients
|
||||
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
inbound.Settings = string(modifiedSettings)
|
||||
_, err = s.UpdateInboundClient(inbound, clientId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
needRestart, err := s.UpdateInboundClient(inbound, clientId)
|
||||
return needRestart, err
|
||||
}
|
||||
|
||||
func (s *InboundService) ResetClientTrafficLimitByEmail(clientEmail string, totalGB int) error {
|
||||
func (s *InboundService) ResetClientTrafficLimitByEmail(clientEmail string, totalGB int) (bool, error) {
|
||||
if totalGB < 0 {
|
||||
return common.NewError("totalGB must be >= 0")
|
||||
return false, common.NewError("totalGB must be >= 0")
|
||||
}
|
||||
_, inbound, err := s.GetClientInboundByEmail(clientEmail)
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
if inbound == nil {
|
||||
return common.NewError("Inbound Not Found For Email:", clientEmail)
|
||||
return false, common.NewError("Inbound Not Found For Email:", clientEmail)
|
||||
}
|
||||
|
||||
oldClients, err := s.GetClients(inbound)
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
|
||||
clientId := ""
|
||||
@@ -1453,13 +1482,13 @@ func (s *InboundService) ResetClientTrafficLimitByEmail(clientEmail string, tota
|
||||
}
|
||||
|
||||
if len(clientId) == 0 {
|
||||
return common.NewError("Client Not Found For Email:", clientEmail)
|
||||
return false, common.NewError("Client Not Found For Email:", clientEmail)
|
||||
}
|
||||
|
||||
var settings map[string]interface{}
|
||||
err = json.Unmarshal([]byte(inbound.Settings), &settings)
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
clients := settings["clients"].([]interface{})
|
||||
var newClients []interface{}
|
||||
@@ -1473,14 +1502,11 @@ func (s *InboundService) ResetClientTrafficLimitByEmail(clientEmail string, tota
|
||||
settings["clients"] = newClients
|
||||
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
inbound.Settings = string(modifiedSettings)
|
||||
_, err = s.UpdateInboundClient(inbound, clientId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
needRestart, err := s.UpdateInboundClient(inbound, clientId)
|
||||
return needRestart, err
|
||||
}
|
||||
|
||||
func (s *InboundService) ResetClientTrafficByEmail(clientEmail string) error {
|
||||
@@ -1573,11 +1599,7 @@ func (s *InboundService) ResetAllClientTraffics(id int) error {
|
||||
Updates(map[string]interface{}{"enable": true, "up": 0, "down": 0})
|
||||
|
||||
err := result.Error
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *InboundService) ResetAllTraffics() error {
|
||||
@@ -1588,11 +1610,7 @@ func (s *InboundService) ResetAllTraffics() error {
|
||||
Updates(map[string]interface{}{"up": 0, "down": 0})
|
||||
|
||||
err := result.Error
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *InboundService) DelDepletedClients(id int) (err error) {
|
||||
@@ -1666,11 +1684,7 @@ func (s *InboundService) DelDepletedClients(id int) (err error) {
|
||||
}
|
||||
|
||||
err = tx.Where(whereText+" and enable = ?", id, false).Delete(xray.ClientTraffic{}).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *InboundService) GetClientTrafficTgBot(tgId string) ([]*xray.ClientTraffic, error) {
|
||||
|
||||
@@ -9,8 +9,7 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type OutboundService struct {
|
||||
}
|
||||
type OutboundService struct{}
|
||||
|
||||
func (s *OutboundService) AddTraffic(traffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) {
|
||||
var err error
|
||||
|
||||
@@ -4,11 +4,11 @@ import (
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"x-ui/logger"
|
||||
)
|
||||
|
||||
type PanelService struct {
|
||||
}
|
||||
type PanelService struct{}
|
||||
|
||||
func (s *PanelService) RestartPanel(delay time.Duration) error {
|
||||
p, err := os.FindProcess(syscall.Getpid())
|
||||
|
||||
@@ -382,7 +382,6 @@ func (s *ServerService) UpdateXray(version string) error {
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (s *ServerService) GetLogs(count string, level string, syslog string) []string {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"x-ui/database"
|
||||
"x-ui/database/model"
|
||||
"x-ui/logger"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
"x-ui/util/random"
|
||||
"x-ui/util/reflect_util"
|
||||
"x-ui/web/entity"
|
||||
"x-ui/xray"
|
||||
)
|
||||
|
||||
//go:embed config.json
|
||||
@@ -60,12 +62,13 @@ var defaultValueMap = map[string]string{
|
||||
"subJsonPath": "/json/",
|
||||
"subJsonURI": "",
|
||||
"subJsonFragment": "",
|
||||
"subJsonMux": "",
|
||||
"subJsonRules": "",
|
||||
"datepicker": "gregorian",
|
||||
"warp": "",
|
||||
}
|
||||
|
||||
type SettingService struct {
|
||||
}
|
||||
type SettingService struct{}
|
||||
|
||||
func (s *SettingService) GetDefaultJsonConfig() (interface{}, error) {
|
||||
var jsonData interface{}
|
||||
@@ -437,6 +440,14 @@ func (s *SettingService) GetSubJsonFragment() (string, error) {
|
||||
return s.getString("subJsonFragment")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubJsonMux() (string, error) {
|
||||
return s.getString("subJsonMux")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubJsonRules() (string, error) {
|
||||
return s.getString("subJsonRules")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetDatepicker() (string, error) {
|
||||
return s.getString("datepicker")
|
||||
}
|
||||
@@ -444,10 +455,19 @@ func (s *SettingService) GetDatepicker() (string, error) {
|
||||
func (s *SettingService) GetWarp() (string, error) {
|
||||
return s.getString("warp")
|
||||
}
|
||||
|
||||
func (s *SettingService) SetWarp(data string) error {
|
||||
return s.setString("warp", data)
|
||||
}
|
||||
|
||||
func (s *SettingService) GetIpLimitEnable() (bool, error) {
|
||||
accessLogPath, err := xray.GetAccessLogPath()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return (accessLogPath != "none" && accessLogPath != ""), nil
|
||||
}
|
||||
|
||||
func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error {
|
||||
if err := allSetting.CheckValid(); err != nil {
|
||||
return err
|
||||
@@ -481,17 +501,18 @@ func (s *SettingService) GetDefaultXrayConfig() (interface{}, error) {
|
||||
func (s *SettingService) GetDefaultSettings(host string) (interface{}, error) {
|
||||
type settingFunc func() (interface{}, error)
|
||||
settings := map[string]settingFunc{
|
||||
"expireDiff": func() (interface{}, error) { return s.GetExpireDiff() },
|
||||
"trafficDiff": func() (interface{}, error) { return s.GetTrafficDiff() },
|
||||
"pageSize": func() (interface{}, error) { return s.GetPageSize() },
|
||||
"defaultCert": func() (interface{}, error) { return s.GetCertFile() },
|
||||
"defaultKey": func() (interface{}, error) { return s.GetKeyFile() },
|
||||
"tgBotEnable": func() (interface{}, error) { return s.GetTgbotenabled() },
|
||||
"subEnable": func() (interface{}, error) { return s.GetSubEnable() },
|
||||
"subURI": func() (interface{}, error) { return s.GetSubURI() },
|
||||
"subJsonURI": func() (interface{}, error) { return s.GetSubJsonURI() },
|
||||
"remarkModel": func() (interface{}, error) { return s.GetRemarkModel() },
|
||||
"datepicker": func() (interface{}, error) { return s.GetDatepicker() },
|
||||
"expireDiff": func() (interface{}, error) { return s.GetExpireDiff() },
|
||||
"trafficDiff": func() (interface{}, error) { return s.GetTrafficDiff() },
|
||||
"pageSize": func() (interface{}, error) { return s.GetPageSize() },
|
||||
"defaultCert": func() (interface{}, error) { return s.GetCertFile() },
|
||||
"defaultKey": func() (interface{}, error) { return s.GetKeyFile() },
|
||||
"tgBotEnable": func() (interface{}, error) { return s.GetTgbotenabled() },
|
||||
"subEnable": func() (interface{}, error) { return s.GetSubEnable() },
|
||||
"subURI": func() (interface{}, error) { return s.GetSubURI() },
|
||||
"subJsonURI": func() (interface{}, error) { return s.GetSubJsonURI() },
|
||||
"remarkModel": func() (interface{}, error) { return s.GetRemarkModel() },
|
||||
"datepicker": func() (interface{}, error) { return s.GetDatepicker() },
|
||||
"ipLimitEnable": func() (interface{}, error) { return s.GetIpLimitEnable() },
|
||||
}
|
||||
|
||||
result := make(map[string]interface{})
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"x-ui/config"
|
||||
"x-ui/database"
|
||||
"x-ui/database/model"
|
||||
@@ -26,12 +27,14 @@ import (
|
||||
"github.com/valyala/fasthttp/fasthttpproxy"
|
||||
)
|
||||
|
||||
var bot *telego.Bot
|
||||
var botHandler *th.BotHandler
|
||||
var adminIds []int64
|
||||
var isRunning bool
|
||||
var hostname string
|
||||
var hashStorage *global.HashStorage
|
||||
var (
|
||||
bot *telego.Bot
|
||||
botHandler *th.BotHandler
|
||||
adminIds []int64
|
||||
isRunning bool
|
||||
hostname string
|
||||
hashStorage *global.HashStorage
|
||||
)
|
||||
|
||||
type LoginStatus byte
|
||||
|
||||
@@ -208,7 +211,10 @@ func (t *Tgbot) OnReceive() {
|
||||
for _, userID := range message.UsersShared.UserIDs {
|
||||
userIDsStr += strconv.FormatInt(userID, 10) + " "
|
||||
}
|
||||
err := t.inboundService.SetClientTelegramUserID(message.UsersShared.RequestID, userIDsStr)
|
||||
needRestart, err := t.inboundService.SetClientTelegramUserID(message.UsersShared.RequestID, userIDsStr)
|
||||
if needRestart {
|
||||
t.xrayService.SetToNeedRestart()
|
||||
}
|
||||
output := ""
|
||||
if err != nil {
|
||||
output += t.I18nBot("tgbot.messages.selectUserFailed")
|
||||
@@ -280,7 +286,6 @@ func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin boo
|
||||
}
|
||||
|
||||
func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool) {
|
||||
|
||||
chatId := callbackQuery.Message.GetChat().ID
|
||||
|
||||
if isAdmin {
|
||||
@@ -329,7 +334,6 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
case "reset_traffic_c":
|
||||
err := t.inboundService.ResetClientTrafficByEmail(email)
|
||||
if err == nil {
|
||||
t.xrayService.SetToNeedRestart()
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.resetTrafficSuccess", "Email=="+email))
|
||||
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
} else {
|
||||
@@ -370,9 +374,11 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
if len(dataArray) == 3 {
|
||||
limitTraffic, err := strconv.Atoi(dataArray[2])
|
||||
if err == nil {
|
||||
err := t.inboundService.ResetClientTrafficLimitByEmail(email, limitTraffic)
|
||||
if err == nil {
|
||||
needRestart, err := t.inboundService.ResetClientTrafficLimitByEmail(email, limitTraffic)
|
||||
if needRestart {
|
||||
t.xrayService.SetToNeedRestart()
|
||||
}
|
||||
if err == nil {
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.setTrafficLimitSuccess", "Email=="+email))
|
||||
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
return
|
||||
@@ -499,9 +505,11 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
}
|
||||
|
||||
}
|
||||
err := t.inboundService.ResetClientExpiryTimeByEmail(email, date)
|
||||
if err == nil {
|
||||
needRestart, err := t.inboundService.ResetClientExpiryTimeByEmail(email, date)
|
||||
if needRestart {
|
||||
t.xrayService.SetToNeedRestart()
|
||||
}
|
||||
if err == nil {
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.expireResetSuccess", "Email=="+email))
|
||||
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
return
|
||||
@@ -604,9 +612,11 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
if len(dataArray) == 3 {
|
||||
count, err := strconv.Atoi(dataArray[2])
|
||||
if err == nil {
|
||||
err := t.inboundService.ResetClientIpLimitByEmail(email, count)
|
||||
if err == nil {
|
||||
needRestart, err := t.inboundService.ResetClientIpLimitByEmail(email, count)
|
||||
if needRestart {
|
||||
t.xrayService.SetToNeedRestart()
|
||||
}
|
||||
if err == nil {
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.resetIpSuccess", "Email=="+email, "Count=="+strconv.Itoa(count)))
|
||||
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
return
|
||||
@@ -716,7 +726,10 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
|
||||
return
|
||||
}
|
||||
err = t.inboundService.SetClientTelegramUserID(traffic.Id, "")
|
||||
needRestart, err := t.inboundService.SetClientTelegramUserID(traffic.Id, "")
|
||||
if needRestart {
|
||||
t.xrayService.SetToNeedRestart()
|
||||
}
|
||||
if err == nil {
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.removedTGUserSuccess", "Email=="+email))
|
||||
t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
@@ -734,9 +747,11 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)
|
||||
case "toggle_enable_c":
|
||||
enabled, err := t.inboundService.ToggleClientEnableByEmail(email)
|
||||
if err == nil {
|
||||
enabled, needRestart, err := t.inboundService.ToggleClientEnableByEmail(email)
|
||||
if needRestart {
|
||||
t.xrayService.SetToNeedRestart()
|
||||
}
|
||||
if err == nil {
|
||||
if enabled {
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.enableSuccess", "Email=="+email))
|
||||
} else {
|
||||
@@ -866,7 +881,7 @@ func (t *Tgbot) SendMsgToTgbot(chatId int64, msg string, replyMarkup ...telego.R
|
||||
Text: message,
|
||||
ParseMode: "HTML",
|
||||
}
|
||||
//only add replyMarkup to last message
|
||||
// only add replyMarkup to last message
|
||||
if len(replyMarkup) > 0 && n == (len(allMessages)-1) {
|
||||
params.ReplyMarkup = replyMarkup[0]
|
||||
}
|
||||
@@ -1030,9 +1045,15 @@ func (t *Tgbot) getInboundUsages() string {
|
||||
return info
|
||||
}
|
||||
|
||||
func (t *Tgbot) clientInfoMsg(traffic *xray.ClientTraffic, printEnabled bool, printOnline bool, printActive bool,
|
||||
printDate bool, printTraffic bool, printRefreshed bool) string {
|
||||
|
||||
func (t *Tgbot) clientInfoMsg(
|
||||
traffic *xray.ClientTraffic,
|
||||
printEnabled bool,
|
||||
printOnline bool,
|
||||
printActive bool,
|
||||
printDate bool,
|
||||
printTraffic bool,
|
||||
printRefreshed bool,
|
||||
) string {
|
||||
now := time.Now().Unix()
|
||||
expiryTime := ""
|
||||
flag := false
|
||||
@@ -1448,7 +1469,7 @@ func (t *Tgbot) notifyExhausted() {
|
||||
var disabledClients []xray.ClientTraffic
|
||||
var exhaustedClients []xray.ClientTraffic
|
||||
traffics, err := t.inboundService.GetClientTrafficTgBot(client.TgID)
|
||||
if err == nil {
|
||||
if err == nil && len(traffics) > 0 {
|
||||
output := t.I18nBot("tgbot.messages.exhaustedCount", "Type=="+t.I18nBot("tgbot.clients"))
|
||||
for _, traffic := range traffics {
|
||||
if traffic.Enable {
|
||||
@@ -1544,7 +1565,6 @@ func (t *Tgbot) sendBackup(chatId int64) {
|
||||
}
|
||||
} else {
|
||||
logger.Error("Error in opening db file for backup: ", err)
|
||||
|
||||
}
|
||||
|
||||
file, err = os.Open(xray.GetConfigPath())
|
||||
@@ -1560,8 +1580,6 @@ func (t *Tgbot) sendBackup(chatId int64) {
|
||||
} else {
|
||||
logger.Error("Error in opening config.json file for backup: ", err)
|
||||
}
|
||||
|
||||
t.sendBanLogs(chatId, false)
|
||||
}
|
||||
|
||||
func (t *Tgbot) sendBanLogs(chatId int64, dt bool) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"x-ui/database"
|
||||
"x-ui/database/model"
|
||||
"x-ui/logger"
|
||||
@@ -9,8 +10,7 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UserService struct {
|
||||
}
|
||||
type UserService struct{}
|
||||
|
||||
func (s *UserService) GetFirstUser() (*model.User, error) {
|
||||
db := database.GetDB()
|
||||
@@ -79,6 +79,21 @@ func (s *UserService) GetUserSecret(id int) *model.User {
|
||||
return user
|
||||
}
|
||||
|
||||
func (s *UserService) CheckSecretExistence() (bool, error) {
|
||||
db := database.GetDB()
|
||||
|
||||
var count int64
|
||||
err := db.Model(model.User{}).
|
||||
Where("login_secret IS NOT NULL").
|
||||
Count(&count).
|
||||
Error
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (s *UserService) UpdateFirstUser(username string, password string) error {
|
||||
if username == "" {
|
||||
return errors.New("username can not be empty")
|
||||
|
||||
@@ -4,16 +4,19 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"x-ui/logger"
|
||||
"x-ui/xray"
|
||||
|
||||
"go.uber.org/atomic"
|
||||
)
|
||||
|
||||
var p *xray.Process
|
||||
var lock sync.Mutex
|
||||
var isNeedXrayRestart atomic.Bool
|
||||
var result string
|
||||
var (
|
||||
p *xray.Process
|
||||
lock sync.Mutex
|
||||
isNeedXrayRestart atomic.Bool
|
||||
result string
|
||||
)
|
||||
|
||||
type XrayService struct {
|
||||
inboundService InboundService
|
||||
@@ -87,7 +90,6 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
||||
// check users active or not
|
||||
clientStats := inbound.ClientStats
|
||||
for _, clientTraffic := range clientStats {
|
||||
|
||||
indexDecrease := 0
|
||||
for index, client := range clients {
|
||||
c := client.(map[string]interface{})
|
||||
@@ -96,20 +98,15 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
||||
clients = RemoveIndex(clients, index-indexDecrease)
|
||||
indexDecrease++
|
||||
logger.Info("Remove Inbound User ", c["email"], " due the expire or traffic limit")
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// clear client config for additional parameters
|
||||
var final_clients []interface{}
|
||||
for _, client := range clients {
|
||||
|
||||
c := client.(map[string]interface{})
|
||||
|
||||
if c["enable"] != nil {
|
||||
if enable, ok := c["enable"].(bool); ok && !enable {
|
||||
continue
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"x-ui/util/common"
|
||||
"x-ui/xray"
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@ package session
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
|
||||
"x-ui/database/model"
|
||||
|
||||
sessions "github.com/Calidity/gin-sessions"
|
||||
|
||||
@@ -201,7 +201,7 @@
|
||||
"last" = "Last"
|
||||
"prefix" = "Prefix"
|
||||
"postfix" = "Postfix"
|
||||
"delayedStart" = "Start on Initial Use"
|
||||
"delayedStart" = "Start After First Use"
|
||||
"expireDays" = "Duration"
|
||||
"days" = "Day(s)"
|
||||
"renew" = "Auto Renew"
|
||||
@@ -310,7 +310,14 @@
|
||||
"subURI" = "Reverse Proxy URI"
|
||||
"subURIDesc" = "The URI path of the subscription URL for use behind proxies."
|
||||
"fragment" = "Fragmentation"
|
||||
"fragmentDesc" = "Enable fragmentation for TLS hello packet"
|
||||
"fragmentDesc" = "Enable fragmentation for TLS hello packet."
|
||||
"fragmentSett" = "Fragmentation Settings"
|
||||
"mux" = "Mux"
|
||||
"muxDesc" = "Transmit multiple independent data streams within an established data stream."
|
||||
"muxSett" = "Mux Settings"
|
||||
"direct" = "Direct Connection"
|
||||
"directDesc" = "Directly establishes connections with domains or IP ranges of a specific country."
|
||||
"directSett" = "Direct Connection Options"
|
||||
|
||||
[pages.xray]
|
||||
"title" = "Xray Configs"
|
||||
@@ -327,7 +334,7 @@
|
||||
"blockCountryConfigs" = "Block Country"
|
||||
"blockCountryConfigsDesc" = "These options will block traffic based on the specific requested country."
|
||||
"directCountryConfigs" = "Direct Country"
|
||||
"directCountryConfigsDesc" = "These options will directly forward traffic based on the specific requested country."
|
||||
"directCountryConfigsDesc" = "A direct connection ensures that specific traffic is not routed through another server."
|
||||
"ipv4Configs" = "IPv4 Routing"
|
||||
"ipv4ConfigsDesc" = "These options will route traffic based on a specific destination via IPv4."
|
||||
"warpConfigs" = "WARP Routing"
|
||||
@@ -446,6 +453,10 @@
|
||||
"bridge" = "Bridge"
|
||||
"portal" = "Portal"
|
||||
"intercon" = "Interconnection"
|
||||
"settings" = "Settings"
|
||||
"accountInfo" = "Account Information"
|
||||
"outboundStatus" = "Outbound Status"
|
||||
"sendThrough" = "Send Through"
|
||||
|
||||
[pages.xray.balancer]
|
||||
"addBalancer" = "Add Balancer"
|
||||
@@ -467,6 +478,8 @@
|
||||
[pages.xray.dns]
|
||||
"enable" = "Enable DNS"
|
||||
"enableDesc" = "Enable built-in DNS server"
|
||||
"tag" = "DNS Inbound Tag"
|
||||
"tagDesc" = "This tag will be available as an Inbound tag in routing rules."
|
||||
"strategy" = "Query Strategy"
|
||||
"strategyDesc" = "Overall strategy to resolve domain names"
|
||||
"add" = "Add Server"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -309,8 +309,15 @@
|
||||
"subShowInfoDesc" = "ترافیک و زمان باقیمانده را در برنامههای کاربری نمایش میدهد"
|
||||
"subURI" = "پروکسی معکوس URI مسیر"
|
||||
"subURIDesc" = "سابسکریپشن را برای استفاده در پشت پراکسیها تغییر میدهد URI مسیر"
|
||||
"fragment" = "تکهتکه شدن"
|
||||
"fragmentDesc" = "فعال کردن تکه تکه شدن برای بسته نخست تیالاس"
|
||||
"fragment" = "فرگمنت"
|
||||
"fragmentDesc" = "فعال کردن فرگمنت برای بستهی نخست تیالاس"
|
||||
"fragmentSett" = "تنظیمات فرگمنت"
|
||||
"mux" = "ماکس"
|
||||
"muxDesc" = "چندین جریان داده مستقل را در یک جریان داده ثابت منتقل می کند"
|
||||
"muxSett" = "تنظیمات ماکس"
|
||||
"direct" = "اتصال مستقیم"
|
||||
"directDesc" = "به طور مستقیم با دامنه ها یا محدوده آیپی یک کشور خاص ارتباط برقرار می کند"
|
||||
"directSett" = "گزینه های اتصال مستقیم"
|
||||
|
||||
[pages.xray]
|
||||
"title" = "پیکربندی ایکسری"
|
||||
@@ -327,7 +334,7 @@
|
||||
"blockCountryConfigs" = "مسدودسازی کشور"
|
||||
"blockCountryConfigsDesc" = "این گزینهها ترافیک را بر اساس کشور درخواستی خاص مسدود میکند"
|
||||
"directCountryConfigs" = "اتصال مستقیم کشور"
|
||||
"directCountryConfigsDesc" = "این گزینهها ترافیک را بر اساس کشور درخواستی خاص بصورت مستقیم ارسال میکند"
|
||||
"directCountryConfigsDesc" = "اتصال مستقیم اطمینان حاصل میکند که ترافیک خاص از طریق یک سرور دیگر هدایت نمیشود."
|
||||
"ipv4Configs" = "IPv4 مسیریابی"
|
||||
"ipv4ConfigsDesc" = "این گزینهها ترافیک را از طریق آیپینسخه4 به مقصد هدایت میکند"
|
||||
"warpConfigs" = "WARP مسیریابی"
|
||||
@@ -446,6 +453,10 @@
|
||||
"bridge" = "پل"
|
||||
"portal" = "پورتال"
|
||||
"intercon" = "اتصال میانی"
|
||||
"settings" = "تنظیمات"
|
||||
"accountInfo" = "اطلاعات حساب"
|
||||
"outboundStatus" = "وضعیت خروجی"
|
||||
"sendThrough" = "ارسال با"
|
||||
|
||||
[pages.xray.balancer]
|
||||
"addBalancer" = "افزودن بالانسر"
|
||||
@@ -467,6 +478,8 @@
|
||||
[pages.xray.dns]
|
||||
"enable" = "فعال کردن حل دامنه"
|
||||
"enableDesc" = "سرور حل دامنه داخلی را فعال کنید"
|
||||
"tag" = "برچسب"
|
||||
"tagDesc" = "این برچسب در قوانین مسیریابی به عنوان یک برچسب ورودی قابل استفاده خواهد بود"
|
||||
"strategy" = "استراتژی پرسوجو"
|
||||
"strategyDesc" = "استراتژی کلی برای حل نام دامنه"
|
||||
"add" = "افزودن سرور"
|
||||
|
||||
@@ -311,6 +311,13 @@
|
||||
"subURIDesc" = "URI path URL langganan untuk penggunaan di belakang proxy."
|
||||
"fragment" = "Fragmentasi"
|
||||
"fragmentDesc" = "Aktifkan fragmentasi untuk paket hello TLS"
|
||||
"fragmentSett" = "Pengaturan Fragmentasi"
|
||||
"mux" = "Mux"
|
||||
"muxDesc" = "Mengirimkan beberapa aliran data independen dalam aliran data yang sudah ada."
|
||||
"muxSett" = "Pengaturan Mux"
|
||||
"direct" = "Koneksi langsung"
|
||||
"directDesc" = "Secara langsung membuat koneksi dengan domain atau rentang IP negara tertentu."
|
||||
"directSett" = "Opsi Koneksi Langsung"
|
||||
|
||||
[pages.xray]
|
||||
"title" = "Konfigurasi Xray"
|
||||
@@ -327,7 +334,7 @@
|
||||
"blockCountryConfigs" = "Blokir Negara"
|
||||
"blockCountryConfigsDesc" = "Opsi ini akan memblokir lalu lintas berdasarkan negara yang diminta."
|
||||
"directCountryConfigs" = "Langsung ke Negara"
|
||||
"directCountryConfigsDesc" = "Opsi ini akan langsung meneruskan lalu lintas berdasarkan negara yang diminta."
|
||||
"directCountryConfigsDesc" = "Koneksi langsung memastikan bahwa lalu lintas tertentu tidak diarahkan melalui server lain."
|
||||
"ipv4Configs" = "Pengalihan IPv4"
|
||||
"ipv4ConfigsDesc" = "Opsi ini akan mengalihkan lalu lintas berdasarkan tujuan tertentu melalui IPv4."
|
||||
"warpConfigs" = "Pengalihan WARP"
|
||||
@@ -446,6 +453,10 @@
|
||||
"bridge" = "Jembatan"
|
||||
"portal" = "Portal"
|
||||
"intercon" = "Interkoneksi"
|
||||
"settings" = "Pengaturan"
|
||||
"accountInfo" = "Informasi Akun"
|
||||
"outboundStatus" = "Status Keluar"
|
||||
"sendThrough" = "Kirim Melalui"
|
||||
|
||||
[pages.xray.balancer]
|
||||
"addBalancer" = "Tambahkan Penyeimbang"
|
||||
@@ -467,6 +478,8 @@
|
||||
[pages.xray.dns]
|
||||
"enable" = "Aktifkan DNS"
|
||||
"enableDesc" = "Aktifkan server DNS bawaan"
|
||||
"tag" = "Tanda DNS Masuk"
|
||||
"tagDesc" = "Tanda ini akan tersedia sebagai tanda masuk dalam aturan penataan."
|
||||
"strategy" = "Strategi Kueri"
|
||||
"strategyDesc" = "Strategi keseluruhan untuk menyelesaikan nama domain"
|
||||
"add" = "Tambahkan Server"
|
||||
|
||||
@@ -311,6 +311,13 @@
|
||||
"subURIDesc" = "Изменить базовый URI URL-адреса подписки для использования за прокси-серверами"
|
||||
"fragment" = "Фрагментация"
|
||||
"fragmentDesc" = "Включить фрагментацию для пакета приветствия TLS"
|
||||
"fragmentSett" = "Настройки фрагментации"
|
||||
"mux" = "Mux"
|
||||
"muxDesc" = "Передача нескольких независимых потоков данных в рамках установленного потока данных."
|
||||
"muxSett" = "Mux Настройки"
|
||||
"direct" = "Прямая связь"
|
||||
"directDesc" = "Напрямую устанавливает соединения с доменами или диапазонами IP конкретной страны."
|
||||
"directSett" = "Варианты прямого подключения"
|
||||
|
||||
[pages.xray]
|
||||
"title" = "Настройки Xray"
|
||||
@@ -327,7 +334,7 @@
|
||||
"blockCountryConfigs" = "Конфигурации блокировки страны"
|
||||
"blockCountryConfigsDesc" = "Эти параметры не позволят пользователям подключаться к доменам определенной страны"
|
||||
"directCountryConfigs" = "Настройки прямого подключения для страны"
|
||||
"directCountryConfigsDesc" = "Эти параметры позволят пользователям подключаться напрямую к доменам определенной страны"
|
||||
"directCountryConfigsDesc" = "Прямое подключение обеспечивает, что конкретный трафик не направляется через другой сервер."
|
||||
"ipv4Configs" = "Настройки IPv4"
|
||||
"ipv4ConfigsDesc" = "Эти параметры позволят пользователям маршрутизироваться к целевым доменам только через IPv4"
|
||||
"warpConfigs" = "Настройки WARP"
|
||||
@@ -446,6 +453,10 @@
|
||||
"bridge" = "Мост"
|
||||
"portal" = "Портал"
|
||||
"intercon" = "Соединение"
|
||||
"settings" = "Настройки"
|
||||
"accountInfo" = "Информация Об Учетной Записи"
|
||||
"outboundStatus" = "Исходящий статус"
|
||||
"sendThrough" = "Отправить через"
|
||||
|
||||
[pages.xray.balancer]
|
||||
"addBalancer" = "Добавить балансир"
|
||||
@@ -467,6 +478,8 @@
|
||||
[pages.xray.dns]
|
||||
"enable" = "Включить DNS"
|
||||
"enableDesc" = "Включить встроенный DNS-сервер"
|
||||
"tag" = "Входящий тег DNS"
|
||||
"tagDesc" = "Этот тег будет доступен как входящий тег в правилах маршрутизации."
|
||||
"strategy" = "Стратегия запроса"
|
||||
"strategyDesc" = "Общая стратегия разрешения доменных имен"
|
||||
"add" = "Добавить сервер"
|
||||
|
||||
@@ -311,6 +311,13 @@
|
||||
"subURIDesc" = "URI до URL-адреси підписки для використання за проксі."
|
||||
"fragment" = "Фрагментація"
|
||||
"fragmentDesc" = "Увімкнути фрагментацію для пакету привітання TLS"
|
||||
"fragmentSett" = "Параметри фрагментації"
|
||||
"mux" = "Mux"
|
||||
"muxDesc" = "Передавати кілька незалежних потоків даних у межах встановленого потоку даних."
|
||||
"muxSett" = "Налаштування Mux"
|
||||
"direct" = "Пряме підключення"
|
||||
"directDesc" = "Безпосередньо встановлює з’єднання з доменами або діапазонами IP певної країни."
|
||||
"directSett" = "Параметри прямого підключення"
|
||||
|
||||
[pages.xray]
|
||||
"title" = "Xray конфігурації"
|
||||
@@ -327,7 +334,7 @@
|
||||
"blockCountryConfigs" = "Заблокувати країну"
|
||||
"blockCountryConfigsDesc" = "Ці параметри блокуватимуть трафік на основі конкретної запитуваної країни."
|
||||
"directCountryConfigs" = "Пряма країна"
|
||||
"directCountryConfigsDesc" = "Ці параметри безпосередньо перенаправлятимуть трафік на основі конкретної запитуваної країни."
|
||||
"directCountryConfigsDesc" = "Пряме підключення забезпечує, що конкретний трафік не маршрутизується через інший сервер."
|
||||
"ipv4Configs" = "Маршрутизація IPv4"
|
||||
"ipv4ConfigsDesc" = "Ці параметри спрямовуватимуть трафік на основі певного призначення через IPv4."
|
||||
"warpConfigs" = "WARP маршрутизація"
|
||||
@@ -446,6 +453,10 @@
|
||||
"bridge" = "Міст"
|
||||
"portal" = "Портал"
|
||||
"intercon" = "Взаємозв'язок"
|
||||
"settings" = "Налаштування"
|
||||
"accountInfo" = "Інформація про обліковий запис"
|
||||
"outboundStatus" = "Статус виходу"
|
||||
"sendThrough" = "Надіслати через"
|
||||
|
||||
[pages.xray.balancer]
|
||||
"addBalancer" = "Додати балансир"
|
||||
@@ -467,6 +478,8 @@
|
||||
[pages.xray.dns]
|
||||
"enable" = "Увімкнути DNS"
|
||||
"enableDesc" = "Увімкнути вбудований DNS-сервер"
|
||||
"tag" = "Мітка вхідного DNS"
|
||||
"tagDesc" = "Ця мітка буде доступна як вхідна мітка в правилах маршрутизації."
|
||||
"strategy" = "Стратегія запиту"
|
||||
"strategyDesc" = "Загальна стратегія вирішення доменних імен"
|
||||
"add" = "Додати сервер"
|
||||
|
||||
@@ -311,6 +311,13 @@
|
||||
"subURIDesc" = "Thay đổi URI cơ sở của URL gói đăng ký để sử dụng cho proxy trung gian"
|
||||
"fragment" = "Sự phân mảnh"
|
||||
"fragmentDesc" = "Kích hoạt phân mảnh cho gói TLS hello"
|
||||
"fragmentSett" = "Cài đặt phân mảnh"
|
||||
"mux" = "Mux"
|
||||
"muxDesc" = "Truyền nhiều luồng dữ liệu độc lập trong luồng dữ liệu đã thiết lập."
|
||||
"muxSett" = "Mux Cài đặt"
|
||||
"direct" = "Kết nối trực tiếp"
|
||||
"directDesc" = "Trực tiếp thiết lập kết nối với tên miền hoặc dải IP của một quốc gia cụ thể."
|
||||
"directSett" = "Tùy chọn kết nối trực tiếp"
|
||||
|
||||
[pages.xray]
|
||||
"title" = "Cài đặt Xray"
|
||||
@@ -327,7 +334,7 @@
|
||||
"blockCountryConfigs" = "Cấu hình Chặn Quốc gia"
|
||||
"blockCountryConfigsDesc" = "Những tùy chọn này sẽ ngăn người dùng kết nối đến các tên miền quốc gia cụ thể."
|
||||
"directCountryConfigs" = "Cấu hình Kết nối Trực tiếp Quốc gia"
|
||||
"directCountryConfigsDesc" = "Những tùy chọn này sẽ kết nối người dùng trực tiếp đến các tên miền quốc gia cụ thể."
|
||||
"directCountryConfigsDesc" = "Một kết nối trực tiếp đảm bảo rằng lưu lượng cụ thể không được định tuyến qua một máy chủ khác."
|
||||
"ipv4Configs" = "Cấu hình IPv4"
|
||||
"ipv4ConfigsDesc" = "Những tùy chọn này sẽ chỉ định kết nối đến các tên miền mục tiêu qua IPv4."
|
||||
"warpConfigs" = "Cấu hình WARP"
|
||||
@@ -446,6 +453,10 @@
|
||||
"bridge" = "Cầu"
|
||||
"portal" = "Cổng thông tin"
|
||||
"intercon" = "Kết nối"
|
||||
"settings" = "cài đặt"
|
||||
"accountInfo" = "Thông tin tài khoản"
|
||||
"outboundStatus" = "Trạng thái đầu ra"
|
||||
"sendThrough" = "Gửi qua"
|
||||
|
||||
[pages.xray.balancer]
|
||||
"addBalancer" = "Thêm cân bằng"
|
||||
@@ -467,6 +478,8 @@
|
||||
[pages.xray.dns]
|
||||
"enable" = "Kích hoạt DNS"
|
||||
"enableDesc" = "Kích hoạt máy chủ DNS tích hợp"
|
||||
"tag" = "Thẻ gửi đến DNS"
|
||||
"tagDesc" = "Thẻ này sẽ có sẵn dưới dạng thẻ Gửi đến trong quy tắc định tuyến."
|
||||
"strategy" = "Chiến lược truy vấn"
|
||||
"strategyDesc" = "Chiến lược tổng thể để phân giải tên miền"
|
||||
"add" = "Thêm máy chủ"
|
||||
|
||||
@@ -30,8 +30,8 @@
|
||||
"sure" = "确定"
|
||||
"encryption" = "加密"
|
||||
"transmission" = "传输"
|
||||
"host" = "Host"
|
||||
"path" = "Path"
|
||||
"host" = "主机"
|
||||
"path" = "路径"
|
||||
"camouflage" = "伪装"
|
||||
"status" = "状态"
|
||||
"enabled" = "开启"
|
||||
@@ -48,15 +48,15 @@
|
||||
"getVersion" = "获取版本"
|
||||
"install" = "安装"
|
||||
"clients" = "客户端"
|
||||
"usage" = "用法"
|
||||
"usage" = "使用情况"
|
||||
"secretToken" = "安全密钥"
|
||||
"remained" = "剩余"
|
||||
"security" = "安全"
|
||||
"secAlertTitle" = "安全警报"
|
||||
"secAlertSsl" = "此连接不安全;在激活 TLS 进行数据保护之前,请勿输入敏感信息"
|
||||
"secAlertConf" = "某些设置容易受到攻击。建议加强安全协议以防止潜在的违规行为。"
|
||||
"secAlertSSL" = "面板缺乏安全连接。请安装 TLS 证书以保护数据。"
|
||||
"secAlertPanelPort" = "面板默认端口存在漏洞。请配置随机或特定端口。"
|
||||
"secAlertSsl" = "此连接不安全。在激活 TLS 进行数据保护之前,请勿输入敏感信息。"
|
||||
"secAlertConf" = "某些设置易受攻击。建议加强安全协议以防止潜在漏洞。"
|
||||
"secAlertSSL" = "面板缺少安全连接。请安装 TLS 证书以保护数据安全。"
|
||||
"secAlertPanelPort" = "面板默认端口存在安全风险。请配置随机端口或特定端口。"
|
||||
"secAlertPanelURI" = "面板默认 URI 路径不安全。请配置复杂的 URI 路径。"
|
||||
"secAlertSubURI" = "订阅默认 URI 路径不安全。请配置复杂的 URI 路径。"
|
||||
"secAlertSubJsonURI" = "订阅 JSON 默认 URI 路径不安全。请配置复杂的 URI 路径。"
|
||||
@@ -84,33 +84,33 @@
|
||||
[pages.index]
|
||||
"title" = "系统状态"
|
||||
"memory" = "内存"
|
||||
"hard" = "硬盘"
|
||||
"hard" = "磁盘"
|
||||
"xrayStatus" = "Xray"
|
||||
"stopXray" = "停止"
|
||||
"restartXray" = "重启"
|
||||
"xraySwitch" = "版本"
|
||||
"xraySwitchClick" = "点击你想切换的版本"
|
||||
"xraySwitchClickDesk" = "请谨慎选择,旧版本可能配置不兼容"
|
||||
"xraySwitchClick" = "选择你要切换到的版本"
|
||||
"xraySwitchClickDesk" = "请谨慎选择,因为较旧版本可能与当前配置不兼容"
|
||||
"operationHours" = "系统正常运行时间"
|
||||
"systemLoad" = "系统负载"
|
||||
"systemLoadDesc" = "过去 1、5 和 15 分钟的系统平均负载"
|
||||
"connectionTcpCountDesc" = "所有网卡的总 TCP 连接数。"
|
||||
"connectionUdpCountDesc" = "所有网卡的总 UDP 连接数。"
|
||||
"connectionTcpCountDesc" = "系统中所有 TCP 连接数"
|
||||
"connectionUdpCountDesc" = "系统中所有 UDP 连接数"
|
||||
"connectionCount" = "连接数"
|
||||
"upSpeed" = "所有网卡的总上传速度"
|
||||
"downSpeed" = "所有网卡的总下载速度"
|
||||
"totalSent" = "系统启动以来所有网卡的总上传流量"
|
||||
"totalReceive" = "系统启动以来所有网卡的总下载流量"
|
||||
"xraySwitchVersionDialog" = "切换 xray 版本"
|
||||
"xraySwitchVersionDialogDesc" = "是否切换 xray 版本至"
|
||||
"dontRefresh" = "安装中,请不要刷新此页面"
|
||||
"upSpeed" = "总上传速度"
|
||||
"downSpeed" = "总下载速度"
|
||||
"totalSent" = "系统启动以来发送的总数据量"
|
||||
"totalReceive" = "系统启动以来接收的总数据量"
|
||||
"xraySwitchVersionDialog" = "切换 Xray 版本"
|
||||
"xraySwitchVersionDialogDesc" = "是否切换 Xray 版本至"
|
||||
"dontRefresh" = "安装中,请勿刷新此页面"
|
||||
"logs" = "日志"
|
||||
"config" = "配置"
|
||||
"backup" = "备份还原"
|
||||
"backup" = "备份和恢复"
|
||||
"backupTitle" = "备份和恢复数据库"
|
||||
"backupDescription" = "请记住在导入新数据库之前进行备份。"
|
||||
"exportDatabase" = "下载数据库"
|
||||
"importDatabase" = "上传数据库"
|
||||
"backupDescription" = "恢复数据库之前建议进行备份"
|
||||
"exportDatabase" = "备份"
|
||||
"importDatabase" = "恢复"
|
||||
|
||||
[pages.inbounds]
|
||||
"title" = "入站列表"
|
||||
@@ -133,20 +133,20 @@
|
||||
"update" = "修改"
|
||||
"modifyInbound" = "修改入站"
|
||||
"deleteInbound" = "删除入站"
|
||||
"deleteInboundContent" = "确定要删除入站吗?"
|
||||
"deleteInboundContent" = "确定要删除入站吗?"
|
||||
"deleteClient" = "删除客户端"
|
||||
"deleteClientContent" = "您确定要删除客户端吗?"
|
||||
"resetTrafficContent" = "确定要重置流量吗?"
|
||||
"deleteClientContent" = "确定要删除客户端吗?"
|
||||
"resetTrafficContent" = "确定要重置流量吗?"
|
||||
"copyLink" = "复制链接"
|
||||
"address" = "地址"
|
||||
"network" = "网络"
|
||||
"destinationPort" = "目标端口"
|
||||
"targetAddress" = "目标地址"
|
||||
"monitorDesc" = "默认留空即可"
|
||||
"monitorDesc" = "留空表示监听所有 IP"
|
||||
"meansNoLimit" = " = 无限制(单位:GB)"
|
||||
"totalFlow" = "总流量"
|
||||
"leaveBlankToNeverExpire" = "留空则永不到期"
|
||||
"noRecommendKeepDefault" = "没有特殊需求保持默认即可"
|
||||
"leaveBlankToNeverExpire" = "留空表示永不过期"
|
||||
"noRecommendKeepDefault" = "建议保留默认值"
|
||||
"certificatePath" = "文件路径"
|
||||
"certificateContent" = "文件内容"
|
||||
"publicKey" = "公钥"
|
||||
@@ -156,71 +156,71 @@
|
||||
"export" = "导出链接"
|
||||
"clone" = "克隆"
|
||||
"cloneInbound" = "克隆"
|
||||
"cloneInboundContent" = "此入站的所有项目除 Port、Listening IP、Clients 将应用于克隆"
|
||||
"cloneInboundOk" = "从创建克隆"
|
||||
"cloneInboundContent" = "此入站规则除端口(Port)、监听 IP(Listening IP)和客户端(Clients)以外的所有配置都将应用于克隆"
|
||||
"cloneInboundOk" = "创建克隆"
|
||||
"resetAllTraffic" = "重置所有入站流量"
|
||||
"resetAllTrafficTitle" = "重置所有入站流量"
|
||||
"resetAllTrafficContent" = "您确定要重置所有入站流量吗?"
|
||||
"resetAllTrafficContent" = "确定要重置所有入站流量吗?"
|
||||
"resetInboundClientTraffics" = "重置客户端流量"
|
||||
"resetInboundClientTrafficTitle" = "重置所有客户端流量"
|
||||
"resetInboundClientTrafficContent" = "您确定要重置此入站客户端的所有流量吗?"
|
||||
"resetInboundClientTrafficContent" = "确定要重置此入站客户端的所有流量吗?"
|
||||
"resetAllClientTraffics" = "重置所有客户端流量"
|
||||
"resetAllClientTrafficTitle" = "重置所有客户端流量"
|
||||
"resetAllClientTrafficContent" = "你确定要重置所有客户端的所有流量吗?"
|
||||
"delDepletedClients" = "删除耗尽的客户端"
|
||||
"delDepletedClientsTitle" = "删除耗尽的客户端"
|
||||
"delDepletedClientsContent" = "你确定要删除所有耗尽的客户端吗?"
|
||||
"resetAllClientTrafficContent" = "确定要重置所有客户端的所有流量吗?"
|
||||
"delDepletedClients" = "删除流量耗尽的客户端"
|
||||
"delDepletedClientsTitle" = "删除流量耗尽的客户端"
|
||||
"delDepletedClientsContent" = "确定要删除所有流量耗尽的客户端吗?"
|
||||
"email" = "电子邮件"
|
||||
"emailDesc" = "电子邮件必须完全唯一"
|
||||
"IPLimit" = "IP限制"
|
||||
"IPLimitDesc" = "如果超过输入的计数则禁用入站(0 表示禁用限制 ip)"
|
||||
"IPLimitlog" = "IP日志"
|
||||
"IPLimitlogDesc" = "IP 历史日志 (通过IP限制禁用inbound之前,需要清空日志)"
|
||||
"IPLimit" = "IP 限制"
|
||||
"IPLimitDesc" = "如果数量超过设置值,则禁用入站流量。(0 = 禁用)"
|
||||
"IPLimitlog" = "IP 日志"
|
||||
"IPLimitlogDesc" = "IP 历史日志(要启用被禁用的入站流量,请清除日志)"
|
||||
"IPLimitlogclear" = "清除日志"
|
||||
"setDefaultCert" = "从面板设置证书"
|
||||
"xtlsDesc" = "Xray核心需要1.7.5"
|
||||
"realityDesc" = "Xray核心需要1.8.0及以上版本"
|
||||
"telegramDesc" = "仅使用聊天 ID(可以在 @userinfobot 处获取,或在机器人中使用'/id'命令)"
|
||||
"subscriptionDesc" = "您可以在详细信息上找到您的子链接,也可以对多个配置使用相同的名称"
|
||||
"xtlsDesc" = "Xray 核心需要 1.7.5"
|
||||
"realityDesc" = "Xray 核心需要 1.8.0 及以上版本"
|
||||
"telegramDesc" = "请输入电报 (Telegram) 或聊天 ID,无需添加 '@' 符号。(可通过 @userinfobot 获取,或在机器人中使用 '/id' 命令)"
|
||||
"subscriptionDesc" = "要找到你的订阅 URL,请导航到“详细信息”。此外,你可以为多个客户端使用相同的名称。"
|
||||
"info" = "信息"
|
||||
"same" = "相同"
|
||||
"inboundData" = "入站数据"
|
||||
"exportInbound" = "出口 入境"
|
||||
"exportInbound" = "导出入站规则"
|
||||
"import"="导入"
|
||||
"importInbound" = "导入入站"
|
||||
"importInbound" = "导入入站规则"
|
||||
|
||||
[pages.client]
|
||||
"add" = "添加客户端"
|
||||
"edit" = "编辑客户端"
|
||||
"submitAdd" = "添加客户端"
|
||||
"submitEdit" = "保存修改"
|
||||
"clientCount" = "客户数量"
|
||||
"clientCount" = "客户端数量"
|
||||
"bulk" = "批量创建"
|
||||
"method" = "方法"
|
||||
"first" = "第一"
|
||||
"last" = "最后"
|
||||
"first" = "置顶"
|
||||
"last" = "置底"
|
||||
"prefix" = "前缀"
|
||||
"postfix" = "后缀"
|
||||
"delayedStart" = "首次使用后开始"
|
||||
"expireDays" = "期间"
|
||||
"days" = "天"
|
||||
"renew" = "自动续订"
|
||||
"renewDesc" = "到期后自动续订。(0 = 禁用)(单元: 天)"
|
||||
"renewDesc" = "到期后自动续订。(0 = 禁用)(单位: 天)"
|
||||
|
||||
[pages.inbounds.toasts]
|
||||
"obtain" = "获取"
|
||||
|
||||
[pages.inbounds.stream.general]
|
||||
"request" = "要求"
|
||||
"response" = "回复"
|
||||
"name" = "姓名"
|
||||
"value" = "价值"
|
||||
"request" = "请求"
|
||||
"response" = "响应"
|
||||
"name" = "名称"
|
||||
"value" = "值"
|
||||
|
||||
[pages.inbounds.stream.tcp]
|
||||
"version" = "版本"
|
||||
"method" = "方法"
|
||||
"path" = "小路"
|
||||
"status" = "地位"
|
||||
"path" = "路径"
|
||||
"status" = "状态"
|
||||
"statusDescription" = "状态说明"
|
||||
"requestHeader" = "请求头"
|
||||
"responseHeader" = "响应头"
|
||||
@@ -229,16 +229,16 @@
|
||||
"encryption" = "加密"
|
||||
|
||||
[pages.settings]
|
||||
"title" = "设置"
|
||||
"save" = "保存配置"
|
||||
"title" = "面板设置"
|
||||
"save" = "保存"
|
||||
"infoDesc" = "此处的所有更改都需要保存并重启面板才能生效"
|
||||
"restartPanel" = "重启面板"
|
||||
"restartPanelDesc" = "确定要重启面板吗?点击确定将于 3 秒后重启,若重启后无法访问面板,请前往服务器查看面板日志信息"
|
||||
"actions" = "动作"
|
||||
"restartPanelDesc" = "确定要重启面板吗?若重启后无法访问面板,请前往服务器查看面板日志信息"
|
||||
"actions" = "操作"
|
||||
"resetDefaultConfig" = "重置为默认配置"
|
||||
"panelSettings" = "面板配置"
|
||||
"panelSettings" = "常规"
|
||||
"securitySettings" = "安全设定"
|
||||
"TGBotSettings" = "TG提醒相关设置"
|
||||
"TGBotSettings" = "Telegram 机器人配置"
|
||||
"panelListeningIP" = "面板监听 IP"
|
||||
"panelListeningIPDesc" = "默认留空监听所有 IP"
|
||||
"panelListeningDomain" = "面板监听域名"
|
||||
@@ -262,171 +262,178 @@
|
||||
"currentPassword" = "原密码"
|
||||
"newUsername" = "新用户名"
|
||||
"newPassword" = "新密码"
|
||||
"telegramBotEnable" = "启用电报机器人"
|
||||
"telegramBotEnableDesc" = "重启面板生效"
|
||||
"telegramToken" = "电报机器人TOKEN"
|
||||
"telegramTokenDesc" = "重启面板生效"
|
||||
"telegramProxy" = "Socks5 代理"
|
||||
"telegramProxyDesc" = "如果您需要 Socks5 代理来连接 Telegram。 根据指南调整其设置。"
|
||||
"telegramChatId" = "以逗号分隔的多个 chatID 重启面板生效"
|
||||
"telegramChatIdDesc" = "多个聊天 ID 用逗号分隔。使用 @userinfobot 或在机器人中使用'/id'命令获取您的聊天 ID。"
|
||||
"telegramNotifyTime" = "电报机器人通知时间"
|
||||
"telegramNotifyTimeDesc" = "采用Crontab定时格式,重启面板生效"
|
||||
"telegramBotEnable" = "启用 Telegram 机器人"
|
||||
"telegramBotEnableDesc" = "启用 Telegram 机器人功能"
|
||||
"telegramToken" = "Telegram 机器人令牌(token)"
|
||||
"telegramTokenDesc" = "从 '@BotFather' 获取的 Telegram 机器人令牌"
|
||||
"telegramProxy" = "SOCKS5 Proxy"
|
||||
"telegramProxyDesc" = "启用 SOCKS5 代理连接到 Telegram(根据指南调整设置)"
|
||||
"telegramChatId" = "管理员聊天 ID"
|
||||
"telegramChatIdDesc" = "Telegram 管理员聊天 ID (多个以逗号分隔)(可通过 @userinfobot 获取,或在机器人中使用 '/id' 命令获取)"
|
||||
"telegramNotifyTime" = "通知时间"
|
||||
"telegramNotifyTimeDesc" = "设置周期性的 Telegram 机器人通知时间(使用 crontab 时间格式)"
|
||||
"tgNotifyBackup" = "数据库备份"
|
||||
"tgNotifyBackupDesc" = "正在发送数据库备份文件和报告通知"
|
||||
"tgNotifyBackupDesc" = "发送带有报告的数据库备份文件"
|
||||
"tgNotifyLogin" = "登录通知"
|
||||
"tgNotifyLoginDesc" = "当有人试图登录您的面板时显示用户名、IP 地址和时间"
|
||||
"sessionMaxAge" = "会话最大年龄"
|
||||
"sessionMaxAgeDesc" = "您可以保持登录状态的时间(单位:分钟)"
|
||||
"expireTimeDiff" = "耗尽时间阈值"
|
||||
"expireTimeDiffDesc" = "到期前检测耗尽(单位:天)"
|
||||
"trafficDiff" = "耗尽流量阈值"
|
||||
"trafficDiffDesc" = "完成流量前检测耗尽(单位:GB)"
|
||||
"tgNotifyCpu" = "CPU 百分比警报阈值"
|
||||
"tgNotifyCpuDesc" = "如果 CPU 使用率超过此百分比(单位:%),此 talegram bot 将向您发送通知"
|
||||
"tgNotifyLoginDesc" = "当有人试图登录你的面板时显示用户名、IP 地址和时间"
|
||||
"sessionMaxAge" = "会话时长"
|
||||
"sessionMaxAgeDesc" = "保持登录状态的时长(单位:分钟)"
|
||||
"expireTimeDiff" = "到期通知阈值"
|
||||
"expireTimeDiffDesc" = "达到此阈值时,将收到有关到期时间的通知(单位:天)"
|
||||
"trafficDiff" = "流量耗尽阈值"
|
||||
"trafficDiffDesc" = "达到此阈值时,将收到有关流量耗尽的通知(单位:GB)"
|
||||
"tgNotifyCpu" = "CPU 负载通知阈值"
|
||||
"tgNotifyCpuDesc" = "CPU 负载超过此阈值时,将收到通知(单位:%)"
|
||||
"timeZone" = "时区"
|
||||
"timeZoneDesc" = "定时任务按照该时区的时间运行"
|
||||
"subSettings" = "订阅"
|
||||
"subEnable" = "启用服务"
|
||||
"subEnableDesc" = "具有单独配置的订阅功能"
|
||||
"subListen" = "监听IP"
|
||||
"subListenDesc" = "留空默认监听所有IP"
|
||||
"subPort" = "订阅端口"
|
||||
"subPortDesc" = "服务订阅服务的端口号必须在服务器中未使用"
|
||||
"subCertPath" = "订阅证书公钥文件路径"
|
||||
"subCertPathDesc" = "填写以'/'开头的绝对路径"
|
||||
"subKeyPath" = "订阅证书私钥文件路径"
|
||||
"subKeyPathDesc" = "填写以'/'开头的绝对路径"
|
||||
"subPath" = "订阅 URL 根路径"
|
||||
"subPathDesc" = "必须以'/'开始并以'/'结束"
|
||||
"timeZoneDesc" = "定时任务将按照该时区的时间运行"
|
||||
"subSettings" = "订阅设置"
|
||||
"subEnable" = "启用订阅服务"
|
||||
"subEnableDesc" = "启用订阅服务功能"
|
||||
"subListen" = "监听 IP"
|
||||
"subListenDesc" = "订阅服务监听的 IP 地址(留空表示监听所有 IP)"
|
||||
"subPort" = "监听端口"
|
||||
"subPortDesc" = "订阅服务监听的端口号(必须是未使用的端口)"
|
||||
"subCertPath" = "公钥路径"
|
||||
"subCertPathDesc" = "订阅服务使用的公钥文件路径(以 '/' 开头)"
|
||||
"subKeyPath" = "私钥路径"
|
||||
"subKeyPathDesc" = "订阅服务使用的私钥文件路径(以 '/' 开头)"
|
||||
"subPath" = "URI 路径"
|
||||
"subPathDesc" = "订阅服务使用的 URI 路径(以 '/' 开头,以 '/' 结尾)"
|
||||
"subDomain" = "监听域名"
|
||||
"subDomainDesc" = "留空默认监控所有域名和IP"
|
||||
"subUpdates" = "订阅更新间隔"
|
||||
"subUpdatesDesc" = "客户端应用程序更新订阅的间隔时间"
|
||||
"subEncrypt" = "加密配置"
|
||||
"subEncryptDesc" = "在订阅中加密返回的配置"
|
||||
"subDomainDesc" = "订阅服务监听的域名(留空表示监听所有域名和 IP)"
|
||||
"subUpdates" = "更新间隔"
|
||||
"subUpdatesDesc" = "客户端应用中订阅 URL 的更新间隔(单位:小时)"
|
||||
"subEncrypt" = "编码"
|
||||
"subEncryptDesc" = "订阅服务返回的内容将采用 Base64 编码"
|
||||
"subShowInfo" = "显示使用信息"
|
||||
"subShowInfoDesc" = "在配置名称后显示剩余流量和日期"
|
||||
"subShowInfoDesc" = "客户端应用中将显示剩余流量和日期信息"
|
||||
"subURI" = "反向代理 URI"
|
||||
"subURIDesc" = "更改订阅 URL 的基本 URI 以在代理后面使用"
|
||||
"fragment" = "碎片"
|
||||
"fragmentDesc" = "启用 TLS hello 数据包分段"
|
||||
"subURIDesc" = "用于代理后面的订阅 URL 的 URI 路径"
|
||||
"fragment" = "分片"
|
||||
"fragmentDesc" = "启用 TLS hello 数据包分片"
|
||||
"fragmentSett" = "设置"
|
||||
"mux" = "多路复用器"
|
||||
"muxDesc" = "在已建立的数据流内传输多个独立的数据流"
|
||||
"muxSett" = "复用器设置"
|
||||
"direct" = "直接连接"
|
||||
"directDesc" = "直接与特定国家的域或IP范围建立连接"
|
||||
"directSett" = "直接连接选项"
|
||||
|
||||
[pages.xray]
|
||||
"title" = "Xray 设置"
|
||||
"save" = "保存设置"
|
||||
"title" = "Xray 配置"
|
||||
"save" = "保存"
|
||||
"restart" = "重新启动 Xray"
|
||||
"basicTemplate" = "基本模板"
|
||||
"advancedTemplate" = "高级模板部件"
|
||||
"generalConfigs" = "通用配置"
|
||||
"generalConfigsDesc" = "这些选项将提供一般调整"
|
||||
"logConfigs"="日志"
|
||||
"logConfigsDesc" = "日志可能会影响您服务器的效率。建议仅在您需要时明智地启用它"
|
||||
"blockConfigs" = "阻塞配置"
|
||||
"basicTemplate" = "基础配置"
|
||||
"advancedTemplate" = "高级配置"
|
||||
"generalConfigs" = "常规配置"
|
||||
"generalConfigsDesc" = "这些选项将决定常规配置"
|
||||
"logConfigs" = "日志"
|
||||
"logConfigsDesc" = "日志可能会影响服务器的性能,建议仅在需要时启用"
|
||||
"blockConfigs" = "防护屏蔽"
|
||||
"blockConfigsDesc" = "这些选项将阻止用户连接到特定协议和网站"
|
||||
"blockCountryConfigs" = "阻止国家配置"
|
||||
"blockCountryConfigsDesc" = "这些选项将阻止用户连接到特定国家/地区的域。"
|
||||
"directCountryConfigs" = "直接国家配置"
|
||||
"directCountryConfigsDesc" = "这些选项会将用户直接连接到特定国家/地区的域。"
|
||||
"ipv4Configs" = "IPv4 配置"
|
||||
"blockCountryConfigs" = "屏蔽国家/地区"
|
||||
"blockCountryConfigsDesc" = "这些选项将阻止用户连接到特定国家/地区"
|
||||
"directCountryConfigs" = "直连国家/地区"
|
||||
"directCountryConfigsDesc" = "直接连接可确保特定流量不会通过其他服务器路由"
|
||||
"ipv4Configs" = "IPv4 路由"
|
||||
"ipv4ConfigsDesc" = "此选项将仅通过 IPv4 路由到目标域"
|
||||
"warpConfigs" = "WARP 配置"
|
||||
"warpConfigsDesc" = "注意:在使用这些选项之前,请按照面板 GitHub 上的步骤在您的服务器上以 socks5 代理模式安装 WARP。 WARP 将通过 Cloudflare 服务器将流量路由到网站。"
|
||||
"Template" = "Xray 配置模板"
|
||||
"TemplateDesc" = "以该模型为基础生成最终的Xray配置文件,重新启动面板生成效率"
|
||||
"FreedomStrategy" = "配置自由协议的策略"
|
||||
"FreedomStrategyDesc" = "在自由协议中设置网络输出策略"
|
||||
"warpConfigs" = "WARP 路由"
|
||||
"warpConfigsDesc" = "注意:在使用这些选项之前,请按照面板 GitHub 上的步骤在你的服务器上以 socks5 代理模式安装 WARP。WARP 将通过 Cloudflare 服务器将流量路由到网站。"
|
||||
"Template" = "高级 Xray 配置模板"
|
||||
"TemplateDesc" = "最终的 Xray 配置文件将基于此模板生成"
|
||||
"FreedomStrategy" = "Freedom 协议策略"
|
||||
"FreedomStrategyDesc" = "设置 Freedom 协议中网络的输出策略"
|
||||
"RoutingStrategy" = "配置路由域策略"
|
||||
"RoutingStrategyDesc" = "设置DNS解析的整体路由策略"
|
||||
"Torrent" = "禁止使用 bittorrent"
|
||||
"TorrentDesc" = "更改配置模板避免用户使用bittorrent"
|
||||
"PrivateIp" = "禁止私人 IP 范围连接"
|
||||
"PrivateIpDesc" = "更改配置模板以避免连接私有 IP 范围"
|
||||
"RoutingStrategyDesc" = "设置 DNS 解析的整体路由策略"
|
||||
"Torrent" = "屏蔽 BitTorrent 协议"
|
||||
"TorrentDesc" = "禁止使用 BitTorrent"
|
||||
"PrivateIp" = "屏蔽私有 IP"
|
||||
"PrivateIpDesc" = "阻止连接到私有 IP"
|
||||
"Ads" = "屏蔽广告"
|
||||
"AdsDesc" = "修改配置模板屏蔽广告"
|
||||
"Family" = "阻止恶意软件和成人内容"
|
||||
"FamilyDesc" = "Cloudflare DNS 解析器可阻止恶意软件和成人内容以保护家庭."
|
||||
"Security" = "阻止恶意软件、网络钓鱼和加密货币挖矿网站"
|
||||
"SecurityDesc" = "更改安全防护配置模板."
|
||||
"Speedtest" = "阻止测速网站"
|
||||
"SpeedtestDesc" = "更改配置模板以避免连接到速度测试网站。 重新启动面板以应用更改。"
|
||||
"IRIp" = "禁止伊朗 IP 范围连接"
|
||||
"IRIpDesc" = "修改配置模板避免连接伊朗IP段"
|
||||
"IRDomain" = "禁止伊朗域连接"
|
||||
"IRDomainDesc" = "更改配置模板避免连接伊朗域名"
|
||||
"ChinaIp" = "禁止中国 IP 范围连接"
|
||||
"ChinaIpDesc" = "修改配置模板避免连接中国IP段"
|
||||
"ChinaDomain" = "禁止中国域名连接"
|
||||
"ChinaDomainDesc" = "更改配置模板避免连接中国域"
|
||||
"RussiaIp" = "禁止俄罗斯 IP 范围连接"
|
||||
"RussiaIpDesc" = "修改配置模板避免连接俄罗斯IP范围"
|
||||
"RussiaDomain" = "禁止俄罗斯域连接"
|
||||
"RussiaDomainDesc" = "更改配置模板避免连接俄罗斯域"
|
||||
"VNIp" = "禁用与越南 IP 的连接"
|
||||
"VNIpDesc" = "更改配置模板以避免连接到越南 IP 范围"
|
||||
"VNDomain" = "禁用与越南域的连接"
|
||||
"VNDomainDesc" = "更改配置模板以避免连接到越南域"
|
||||
"DirectIRIp" = "直接连接到伊朗 IP 范围"
|
||||
"DirectIRIpDesc" = "更改直接连接到伊朗 IP 范围的配置模板"
|
||||
"DirectIRDomain" = "直接连接到伊朗域"
|
||||
"DirectIRDomainDesc" = "更改直接连接到伊朗域的配置模板"
|
||||
"DirectChinaIp" = "直连中国IP范围"
|
||||
"DirectChinaIpDesc" = "更改直连中国 IP 范围的配置模板"
|
||||
"AdsDesc" = "屏蔽广告网站"
|
||||
"Family" = "家庭保护"
|
||||
"FamilyDesc" = "屏蔽成人内容和恶意网站"
|
||||
"Security" = "安全防护"
|
||||
"SecurityDesc" = "屏蔽恶意软件、网络钓鱼和挖矿网站"
|
||||
"Speedtest" = "屏蔽测速网站"
|
||||
"SpeedtestDesc" = "阻止连接到测速网站"
|
||||
"IRIp" = "屏蔽连接到伊朗 IP"
|
||||
"IRIpDesc" = "阻止建立到伊朗 IP 范围的连接"
|
||||
"IRDomain" = "屏蔽连接到伊朗域名"
|
||||
"IRDomainDesc" = "阻止建立到伊朗域名的连接"
|
||||
"ChinaIp" = "屏蔽连接到中国 IP"
|
||||
"ChinaIpDesc" = "阻止建立到中国 IP 范围的连接"
|
||||
"ChinaDomain" = "屏蔽连接到中国域名"
|
||||
"ChinaDomainDesc" = "阻止建立到中国域名的连接"
|
||||
"RussiaIp" = "屏蔽连接到俄罗斯 IP"
|
||||
"RussiaIpDesc" = "阻止建立到俄罗斯 IP 范围的连接"
|
||||
"RussiaDomain" = "屏蔽连接到俄罗斯域名"
|
||||
"RussiaDomainDesc" = "阻止建立到俄罗斯域名的连接"
|
||||
"VNIp" = "屏蔽连接到越南 IP"
|
||||
"VNIpDesc" = "阻止建立到越南 IP 范围的连接"
|
||||
"VNDomain" = "屏蔽连接到越南域名"
|
||||
"VNDomainDesc" = "阻止建立到越南域名的连接"
|
||||
"DirectIRIp" = "直连伊朗 IP"
|
||||
"DirectIRIpDesc" = "直接建立到伊朗 IP 范围的连接"
|
||||
"DirectIRDomain" = "直连伊朗域名"
|
||||
"DirectIRDomainDesc" = "直接建立到伊朗域名的连接"
|
||||
"DirectChinaIp" = "直连中国 IP"
|
||||
"DirectChinaIpDesc" = "直接建立到中国 IP 范围的连接"
|
||||
"DirectChinaDomain" = "直连中国域名"
|
||||
"DirectChinaDomainDesc" = "修改中国域名直连配置模板"
|
||||
"DirectRussiaIp" = "直接连接到俄罗斯 IP 范围"
|
||||
"DirectRussiaIpDesc" = "更改直接连接到俄罗斯 IP 范围的配置模板"
|
||||
"DirectRussiaDomain" = "直接连接到俄罗斯域"
|
||||
"DirectRussiaDomainDesc" = "更改直接连接到俄罗斯域的配置模板"
|
||||
"DirectVNIp" = "直接连接越南IP"
|
||||
"DirectVNIpDesc" = "更改直接连接到越南 IP 范围的配置模板"
|
||||
"DirectVNDomain" = "直接连接至越南域名"
|
||||
"DirectVNDomainDesc" = "更改直连越南域的配置模板。"
|
||||
"GoogleIPv4" = "为谷歌使用 IPv4"
|
||||
"GoogleIPv4Desc" = "添加谷歌连接IPv4的路由"
|
||||
"NetflixIPv4" = "为 Netflix 使用 IPv4"
|
||||
"NetflixIPv4Desc" = "添加Netflix连接IPv4的路由"
|
||||
"DirectChinaDomainDesc" = "直接建立到中国域名的连接"
|
||||
"DirectRussiaIp" = "直连俄罗斯 IP"
|
||||
"DirectRussiaIpDesc" = "直接建立到俄罗斯 IP 范围的连接"
|
||||
"DirectRussiaDomain" = "直连俄罗斯域名"
|
||||
"DirectRussiaDomainDesc" = "直接建立到俄罗斯域名的连接"
|
||||
"DirectVNIp" = "直连越南 IP"
|
||||
"DirectVNIpDesc" = "直接建立到越南 IP 范围的连接"
|
||||
"DirectVNDomain" = "直连越南域名"
|
||||
"DirectVNDomainDesc" = "直接建立到越南域名的连接"
|
||||
"GoogleIPv4" = "Google"
|
||||
"GoogleIPv4Desc" = "通过 IPv4 将流量路由到谷歌"
|
||||
"NetflixIPv4" = "Netflix"
|
||||
"NetflixIPv4Desc" = "通过 IPv4 将流量路由到 Netflix"
|
||||
"GoogleWARP" = "Google"
|
||||
"GoogleWARPDesc" = "通过 WARP 将流量路由到 Google。"
|
||||
"GoogleWARPDesc" = "通过 WARP 将流量路由到 Google"
|
||||
"OpenAIWARP" = "OpenAI (ChatGPT)"
|
||||
"OpenAIWARPDesc" = "通过 WARP 将流量路由到 OpenAI (ChatGPT)。"
|
||||
"OpenAIWARPDesc" = "通过 WARP 将流量路由到 OpenAI (ChatGPT)"
|
||||
"NetflixWARP" = "Netflix"
|
||||
"NetflixWARPDesc" = "通过 WARP 将流量路由到 Netflix。"
|
||||
"NetflixWARPDesc" = "通过 WARP 将流量路由到 Netflix"
|
||||
"MetaWARP"="Meta"
|
||||
"MetaWARPDesc" = "通过 WARP 将流量路由到 Meta(Instagram、Facebook、WhatsApp、Threads...)"
|
||||
"AppleWARP" = "Apple"
|
||||
"AppleWARPDesc" = "通过 WARP 将流量路由到 Apple。"
|
||||
"AppleWARPDesc" = "通过 WARP 将流量路由到 Apple"
|
||||
"RedditWARP" = "Reddit"
|
||||
"RedditWARPDesc" = "通过 WARP 将流量路由到 Reddit。"
|
||||
"RedditWARPDesc" = "通过 WARP 将流量路由到 Reddit"
|
||||
"SpotifyWARP" = "Spotify"
|
||||
"SpotifyWARPDesc" = "通过 WARP 将流量路由到 Spotify。"
|
||||
"IRWARP" = "将伊朗域名路由到 WARP"
|
||||
"IRWARPDesc" = "将伊朗域的路由添加到 WARP。 重启面板生效"
|
||||
"Inbounds" = "入站"
|
||||
"InboundsDesc" = "更改配置模板接受特殊客户端"
|
||||
"Outbounds" = "出站"
|
||||
"Balancers" = "平衡器"
|
||||
"OutboundsDesc" = "更改配置模板定义此服务器的传出方式"
|
||||
"SpotifyWARPDesc" = "通过 WARP 将流量路由到 Spotify"
|
||||
"IRWARP" = "伊朗域名"
|
||||
"IRWARPDesc" = "通过 WARP 将流量路由到伊朗域名"
|
||||
"Inbounds" = "入站规则"
|
||||
"InboundsDesc" = "接受来自特定客户端的流量"
|
||||
"Outbounds" = "出站规则"
|
||||
"Balancers" = "负载均衡"
|
||||
"OutboundsDesc" = "设置出站流量传出方式"
|
||||
"Routings" = "路由规则"
|
||||
"RoutingsDesc" = "每条规则的优先级都很重要"
|
||||
"completeTemplate" = "全部"
|
||||
"logLevel" = "日志级别"
|
||||
"logLevelDesc" = "错误日志的日志级别,表示需要记录的信息。"
|
||||
"logLevelDesc" = "错误日志的日志级别,用于指示需要记录的信息"
|
||||
"accessLog" = "访问日志"
|
||||
"accessLogDesc" = "访问日志的文件路径。 特殊值“none”禁用访问日志"
|
||||
"accessLogDesc" = "访问日志的文件路径。特殊值 'none' 禁用访问日志"
|
||||
"errorLog" = "错误日志"
|
||||
"errorLogDesc" = "错误日志的文件路径。 特殊值“none”禁用错误日志"
|
||||
"errorLogDesc" = "错误日志的文件路径。特殊值 'none' 禁用错误日志"
|
||||
|
||||
[pages.xray.rules]
|
||||
"first" = "第一个"
|
||||
"last" = "最后"
|
||||
"first" = "置顶"
|
||||
"last" = "置底"
|
||||
"up" = "向上"
|
||||
"down" = "向下"
|
||||
"source" = "来源"
|
||||
"dest" = "目的地"
|
||||
"dest" = "目的地址"
|
||||
"inbound" = "入站"
|
||||
"outbound" = "出站"
|
||||
"balancer" = "平衡器"
|
||||
"balancer" = "负载均衡"
|
||||
"info" = "信息"
|
||||
"add" = "添加规则"
|
||||
"edit" = "编辑规则"
|
||||
@@ -438,35 +445,41 @@
|
||||
"editOutbound" = "编辑出站"
|
||||
"editReverse" = "编辑反向"
|
||||
"tag" = "标签"
|
||||
"tagDesc" = "唯一标记"
|
||||
"tagDesc" = "唯一标签"
|
||||
"address" = "地址"
|
||||
"reverse" = "反转"
|
||||
"reverse" = "反向"
|
||||
"domain" = "域名"
|
||||
"type" = "类型"
|
||||
"bridge" = "桥"
|
||||
"portal" = "门户"
|
||||
"bridge" = "Bridge"
|
||||
"portal" = "Portal"
|
||||
"intercon" = "互连"
|
||||
"settings" = "设置"
|
||||
"accountInfo" = "帐户信息"
|
||||
"outboundStatus" = "出站状态"
|
||||
"sendThrough" = "发送通过"
|
||||
|
||||
[pages.xray.balancer]
|
||||
"addBalancer" = "添加平衡器"
|
||||
"editBalancer" = "编辑平衡器"
|
||||
"balancerStrategy" = "战略"
|
||||
"addBalancer" = "添加负载均衡"
|
||||
"editBalancer" = "编辑负载均衡"
|
||||
"balancerStrategy" = "策略"
|
||||
"balancerSelectors" = "选择器"
|
||||
"tag" = "标签"
|
||||
"tagDesc" = "唯一标记"
|
||||
"balancerDesc" = "不能同时使用balancerTag和outboundTag。 如果同时使用,则只有outboundTag起作用。"
|
||||
"tagDesc" = "唯一标签"
|
||||
"balancerDesc" = "无法同时使用 balancerTag 和 outboundTag。如果同时使用,则只有 outboundTag 会生效。"
|
||||
|
||||
[pages.xray.wireguard]
|
||||
"secretKey" = "密钥"
|
||||
"publicKey" = "公钥"
|
||||
"allowedIPs" = "允许的 IP"
|
||||
"endpoint" = "终点"
|
||||
"endpoint" = "端点"
|
||||
"psk" = "共享密钥"
|
||||
"domainStrategy" = "域策略"
|
||||
|
||||
[pages.xray.dns]
|
||||
"enable" = "启用 DNS"
|
||||
"enableDesc" = "启用内置 DNS 服务器"
|
||||
"tag" = "DNS 入站标签"
|
||||
"tagDesc" = "此标签将在路由规则中可用作入站标签"
|
||||
"strategy" = "查询策略"
|
||||
"strategyDesc" = "解析域名的总体策略"
|
||||
"add" = "添加服务器"
|
||||
@@ -481,16 +494,16 @@
|
||||
|
||||
[pages.settings.security]
|
||||
"admin" = "管理员"
|
||||
"secret" = "密钥"
|
||||
"secret" = "安全令牌"
|
||||
"loginSecurity" = "登录安全"
|
||||
"loginSecurityDesc" = "在用户登录页面中切换附加步骤"
|
||||
"secretToken" = "密钥"
|
||||
"secretTokenDesc" = "复制此密钥并将其保存在安全的地方;没有这个你将无法登录。这也无法从 x-ui 命令工具中恢复"
|
||||
"loginSecurityDesc" = "添加额外的身份验证以提高安全性"
|
||||
"secretToken" = "安全令牌"
|
||||
"secretTokenDesc" = "请将此令牌存储在安全的地方。此令牌用于登录,丢失无法恢复。"
|
||||
|
||||
[pages.settings.toasts]
|
||||
"modifySettings" = "修改设置"
|
||||
"getSettings" = "获取设置"
|
||||
"modifyUser" = "修改用户"
|
||||
"modifyUser" = "修改管理员"
|
||||
"originalUserPassIncorrect" = "原用户名或原密码错误"
|
||||
"userPassMustBeNotEmpty" = "新用户名和新密码不能为空"
|
||||
|
||||
@@ -499,7 +512,7 @@
|
||||
"noResult" = "❗ 没有结果!"
|
||||
"noQuery" = "❌ 未找到查询!请重新使用命令!"
|
||||
"wentWrong" = "❌ 出了点问题!"
|
||||
"noIpRecord" = "❗ 没有IP记录!"
|
||||
"noIpRecord" = "❗ 没有 IP 记录!"
|
||||
"noInbounds" = "❗ 没有找到入站连接!"
|
||||
"unlimited" = "♾ 无限制"
|
||||
"add" = "添加"
|
||||
@@ -512,17 +525,17 @@
|
||||
"inbounds" = "入站连接"
|
||||
"clients" = "客户端"
|
||||
"offline" = "🔴 离线"
|
||||
"online" = "🟢 在线的"
|
||||
"online" = "🟢 在线"
|
||||
|
||||
[tgbot.commands]
|
||||
"unknown" = "❗ 未知命令"
|
||||
"pleaseChoose" = "👇 请选择:\r\n"
|
||||
"help" = "🤖 欢迎使用本机器人!它旨在为您提供来自服务器的特定数据,并允许您进行必要的修改。\r\n\r\n"
|
||||
"start" = "👋 你好,<i>{{ .Firstname }}</i>。\r\n"
|
||||
"welcome" = "🤖 欢迎来到<b>{{ .Hostname }}</b>管理机器人。\r\n"
|
||||
"welcome" = "🤖 欢迎来到 <b>{{ .Hostname }}</b> 管理机器人。\r\n"
|
||||
"status" = "✅ 机器人正常运行!"
|
||||
"usage" = "❗ 请输入要搜索的文本!"
|
||||
"getID" = "🆔 您的ID为:<code>{{ .ID }}</code>"
|
||||
"getID" = "🆔 您的 ID 为:<code>{{ .ID }}</code>"
|
||||
"helpAdminCommands" = "搜索客户端邮箱:\r\n<code>/usage [Email]</code>\r\n\r\n搜索入站连接(包含客户端统计信息):\r\n<code>/inbound [Remark]</code>"
|
||||
"helpClientCommands" = "要搜索统计信息,请使用以下命令:\r\n\r\n<code>/usage [Email]</code>"
|
||||
|
||||
@@ -561,8 +574,8 @@
|
||||
"download" = "🔽 下载↓:{{ .Download }}\r\n"
|
||||
"total" = "📊 总计:{{ .UpDown }} / {{ .Total }}\r\n"
|
||||
"TGUser" = "👤 电报用户:{{ .TelegramID }}\r\n"
|
||||
"exhaustedMsg" = "🚨 耗尽的{{ .Type }}:\r\n"
|
||||
"exhaustedCount" = "🚨 耗尽的{{ .Type }}数量:\r\n"
|
||||
"exhaustedMsg" = "🚨 耗尽的 {{ .Type }}:\r\n"
|
||||
"exhaustedCount" = "🚨 耗尽的 {{ .Type }} 数量:\r\n"
|
||||
"onlinesCount" = "🌐 在线客户:{{ .Count }}\r\n"
|
||||
"disabled" = "🛑 禁用:{{ .Disabled }}\r\n"
|
||||
"depleteSoon" = "🔜 即将耗尽:{{ .Deplete }}\r\n\r\n"
|
||||
@@ -585,7 +598,7 @@
|
||||
"getInbounds" = "获取入站信息"
|
||||
"depleteSoon" = "即将耗尽"
|
||||
"clientUsage" = "获取使用情况"
|
||||
"onlines" = "在线客户"
|
||||
"onlines" = "在线客户端"
|
||||
"commands" = "命令"
|
||||
"refresh" = "🔄 刷新"
|
||||
"clearIPs" = "❌ 清除 IP"
|
||||
@@ -601,11 +614,11 @@
|
||||
"custom" = "🔢 风俗"
|
||||
"confirmNumber" = "✅ 确认: {{ .Num }}"
|
||||
"confirmNumberAdd" = "✅ 确认添加:{{ .Num }}"
|
||||
"limitTraffic" = "🚧 交通限制"
|
||||
"limitTraffic" = "🚧 流量限制"
|
||||
"getBanLogs" = "禁止日志"
|
||||
|
||||
[tgbot.answers]
|
||||
"successfulOperation" = "✅ 成功的!"
|
||||
"successfulOperation" = "✅ 成功!"
|
||||
"errorOperation" = "❗ 操作错误。"
|
||||
"getInboundsFailed" = "❌ 获取入站信息失败。"
|
||||
"canceled" = "❌ {{ .Email }}:操作已取消。"
|
||||
@@ -613,7 +626,7 @@
|
||||
"IpRefreshSuccess" = "✅ {{ .Email }}:IP 刷新成功。"
|
||||
"TGIdRefreshSuccess" = "✅ {{ .Email }}:客户端的 Telegram 用户刷新成功。"
|
||||
"resetTrafficSuccess" = "✅ {{ .Email }}:流量已重置成功。"
|
||||
"setTrafficLimitSuccess" = "✅ {{ .Email }} : 流量限制保存成功。"
|
||||
"setTrafficLimitSuccess" = "✅ {{ .Email }}: 流量限制保存成功。"
|
||||
"expireResetSuccess" = "✅ {{ .Email }}:过期天数已重置成功。"
|
||||
"resetIpSuccess" = "✅ {{ .Email }}:成功保存 IP 限制数量为 {{ .Count }}。"
|
||||
"clearIpSuccess" = "✅ {{ .Email }}:IP 已成功清除。"
|
||||
@@ -622,4 +635,4 @@
|
||||
"removedTGUserSuccess" = "✅ {{ .Email }}:Telegram 用户已成功移除。"
|
||||
"enableSuccess" = "✅ {{ .Email }}:已成功启用。"
|
||||
"disableSuccess" = "✅ {{ .Email }}:已成功禁用。"
|
||||
"askToAddUserId" = "未找到您的配置!\r\n请向管理员询问,在您的配置中使用您的 Telegram 用户ID。\r\n\r\n您的用户ID:<code>{{ .TgUserID }}</code>"
|
||||
"askToAddUserId" = "未找到您的配置!\r\n请向管理员询问,在您的配置中使用您的 Telegram 用户 ID。\r\n\r\n您的用户 ID:<code>{{ .TgUserID }}</code>"
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"x-ui/config"
|
||||
"x-ui/logger"
|
||||
"x-ui/util/common"
|
||||
@@ -295,7 +296,7 @@ func (s *Server) startTask() {
|
||||
}
|
||||
|
||||
func (s *Server) Start() (err error) {
|
||||
//This is an anonymous function, no function name
|
||||
// This is an anonymous function, no function name
|
||||
defer func() {
|
||||
if err != nil {
|
||||
s.Stop()
|
||||
|
||||
73
x-ui.sh
73
x-ui.sh
@@ -38,17 +38,23 @@ echo "The OS release is: $release"
|
||||
os_version=""
|
||||
os_version=$(grep -i version_id /etc/os-release | cut -d \" -f2 | cut -d . -f1)
|
||||
|
||||
if [[ "${release}" == "centos" ]]; then
|
||||
if [[ "${release}" == "arch" ]]; then
|
||||
echo "Your OS is Arch Linux"
|
||||
elif [[ "${release}" == "manjaro" ]]; then
|
||||
echo "Your OS is Manjaro"
|
||||
elif [[ "${release}" == "armbian" ]]; then
|
||||
echo "Your OS is Armbian"
|
||||
elif [[ "${release}" == "centos" ]]; then
|
||||
if [[ ${os_version} -lt 8 ]]; then
|
||||
echo -e "${red} Please use CentOS 8 or higher ${plain}\n" && exit 1
|
||||
fi
|
||||
elif [[ "${release}" == "ubuntu" ]]; then
|
||||
if [[ ${os_version} -lt 20 ]]; then
|
||||
echo -e "${red}please use Ubuntu 20 or higher version! ${plain}\n" && exit 1
|
||||
echo -e "${red} Please use Ubuntu 20 or higher version!${plain}\n" && exit 1
|
||||
fi
|
||||
elif [[ "${release}" == "fedora" ]]; then
|
||||
if [[ ${os_version} -lt 36 ]]; then
|
||||
echo -e "${red}please use Fedora 36 or higher version! ${plain}\n" && exit 1
|
||||
echo -e "${red} Please use Fedora 36 or higher version!${plain}\n" && exit 1
|
||||
fi
|
||||
elif [[ "${release}" == "debian" ]]; then
|
||||
if [[ ${os_version} -lt 11 ]]; then
|
||||
@@ -56,18 +62,31 @@ elif [[ "${release}" == "debian" ]]; then
|
||||
fi
|
||||
elif [[ "${release}" == "almalinux" ]]; then
|
||||
if [[ ${os_version} -lt 9 ]]; then
|
||||
echo -e "${red} Please use Almalinux 9 or higher ${plain}\n" && exit 1
|
||||
echo -e "${red} Please use AlmaLinux 9 or higher ${plain}\n" && exit 1
|
||||
fi
|
||||
elif [[ "${release}" == "rocky" ]]; then
|
||||
if [[ ${os_version} -lt 9 ]]; then
|
||||
echo -e "${red} Please use Rockylinux 9 or higher ${plain}\n" && exit 1
|
||||
echo -e "${red} Please use Rocky Linux 9 or higher ${plain}\n" && exit 1
|
||||
fi
|
||||
elif [[ "${release}" == "arch" ]]; then
|
||||
echo "Your OS is ArchLinux"
|
||||
elif [[ "${release}" == "manjaro" ]]; then
|
||||
echo "Your OS is Manjaro"
|
||||
elif [[ "${release}" == "armbian" ]]; then
|
||||
echo "Your OS is Armbian"
|
||||
elif [[ "${release}" == "oracle" ]]; then
|
||||
if [[ ${os_version} -lt 8 ]]; then
|
||||
echo -e "${red} Please use Oracle Linux 8 or higher ${plain}\n" && exit 1
|
||||
fi
|
||||
else
|
||||
echo -e "${red}Your operating system is not supported by this script.${plain}\n"
|
||||
echo "Please ensure you are using one of the following supported operating systems:"
|
||||
echo "- Ubuntu 20.04+"
|
||||
echo "- Debian 11+"
|
||||
echo "- CentOS 8+"
|
||||
echo "- Fedora 36+"
|
||||
echo "- Arch Linux"
|
||||
echo "- Manjaro"
|
||||
echo "- Armbian"
|
||||
echo "- AlmaLinux 9+"
|
||||
echo "- Rocky Linux 9+"
|
||||
echo "- Oracle Linux 8+"
|
||||
exit 1
|
||||
|
||||
fi
|
||||
|
||||
# Declare Variables
|
||||
@@ -394,15 +413,18 @@ enable_bbr() {
|
||||
|
||||
# Check the OS and install necessary packages
|
||||
case "${release}" in
|
||||
ubuntu | debian)
|
||||
ubuntu | debian | armbian)
|
||||
apt-get update && apt-get install -yqq --no-install-recommends ca-certificates
|
||||
;;
|
||||
centos | almalinux | rocky)
|
||||
centos | almalinux | rocky | oracle)
|
||||
yum -y update && yum -y install ca-certificates
|
||||
;;
|
||||
fedora)
|
||||
dnf -y update && dnf -y install ca-certificates
|
||||
;;
|
||||
arch | manjaro)
|
||||
pacman -Sy --noconfirm ca-certificates
|
||||
;;
|
||||
*)
|
||||
echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n"
|
||||
exit 1
|
||||
@@ -721,12 +743,15 @@ ssl_cert_issue() {
|
||||
ubuntu | debian | armbian)
|
||||
apt update && apt install socat -y
|
||||
;;
|
||||
centos | almalinux | rocky)
|
||||
centos | almalinux | rocky | oracle)
|
||||
yum -y update && yum -y install socat
|
||||
;;
|
||||
fedora)
|
||||
dnf -y update && dnf -y install socat
|
||||
;;
|
||||
arch | manjaro)
|
||||
pacman -Sy --noconfirm socat
|
||||
;;
|
||||
*)
|
||||
echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n"
|
||||
exit 1
|
||||
@@ -953,9 +978,15 @@ create_iplimit_jails() {
|
||||
# Uncomment 'allowipv6 = auto' in fail2ban.conf
|
||||
sed -i 's/#allowipv6 = auto/allowipv6 = auto/g' /etc/fail2ban/fail2ban.conf
|
||||
|
||||
#On Debian 12+ fail2ban's default backend should be changed to systemd
|
||||
if [[ "${release}" == "debian" && ${os_version} -ge 12 ]]; then
|
||||
sed -i '0,/action =/s/backend = auto/backend = systemd/' /etc/fail2ban/jail.conf
|
||||
fi
|
||||
|
||||
cat << EOF > /etc/fail2ban/jail.d/3x-ipl.conf
|
||||
[3x-ipl]
|
||||
enabled=true
|
||||
backend=auto
|
||||
filter=3x-ipl
|
||||
action=3x-ipl
|
||||
logpath=${iplimit_log_path}
|
||||
@@ -1076,16 +1107,19 @@ install_iplimit() {
|
||||
|
||||
# Check the OS and install necessary packages
|
||||
case "${release}" in
|
||||
ubuntu | debian)
|
||||
ubuntu | debian | armbian)
|
||||
apt update && apt install fail2ban -y
|
||||
;;
|
||||
centos | almalinux | rocky)
|
||||
centos | almalinux | rocky | oracle)
|
||||
yum update -y && yum install epel-release -y
|
||||
yum -y install fail2ban
|
||||
;;
|
||||
fedora)
|
||||
dnf -y update && dnf -y install fail2ban
|
||||
;;
|
||||
arch | manjaro)
|
||||
pacman -Syu --noconfirm fail2ban
|
||||
;;
|
||||
*)
|
||||
echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n"
|
||||
exit 1
|
||||
@@ -1152,12 +1186,12 @@ remove_iplimit() {
|
||||
rm -rf /etc/fail2ban
|
||||
systemctl stop fail2ban
|
||||
case "${release}" in
|
||||
ubuntu | debian)
|
||||
ubuntu | debian | armbian)
|
||||
apt-get remove -y fail2ban
|
||||
apt-get purge -y fail2ban -y
|
||||
apt-get autoremove -y
|
||||
;;
|
||||
centos | almalinux | rocky)
|
||||
centos | almalinux | rocky | oracle)
|
||||
yum remove fail2ban -y
|
||||
yum autoremove -y
|
||||
;;
|
||||
@@ -1165,6 +1199,9 @@ remove_iplimit() {
|
||||
dnf remove fail2ban -y
|
||||
dnf autoremove -y
|
||||
;;
|
||||
arch | manjaro)
|
||||
pacman -Rns --noconfirm fail2ban
|
||||
;;
|
||||
*)
|
||||
echo -e "${red}Unsupported operating system. Please uninstall Fail2ban manually.${plain}\n"
|
||||
exit 1
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"x-ui/logger"
|
||||
"x-ui/util/common"
|
||||
|
||||
@@ -162,8 +163,8 @@ func (x *XrayAPI) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) {
|
||||
if x.grpcClient == nil {
|
||||
return nil, nil, common.NewError("xray api is not initialized")
|
||||
}
|
||||
var trafficRegex = regexp.MustCompile("(inbound|outbound)>>>([^>]+)>>>traffic>>>(downlink|uplink)")
|
||||
var ClientTrafficRegex = regexp.MustCompile("(user)>>>([^>]+)>>>traffic>>>(downlink|uplink)")
|
||||
trafficRegex := regexp.MustCompile("(inbound|outbound)>>>([^>]+)>>>traffic>>>(downlink|uplink)")
|
||||
ClientTrafficRegex := regexp.MustCompile("(user)>>>([^>]+)>>>traffic>>>(downlink|uplink)")
|
||||
|
||||
client := *x.StatsServiceClient
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
|
||||
@@ -2,22 +2,24 @@ package xray
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"x-ui/util/json_util"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
LogConfig json_util.RawMessage `json:"log"`
|
||||
RouterConfig json_util.RawMessage `json:"routing"`
|
||||
DNSConfig json_util.RawMessage `json:"dns"`
|
||||
InboundConfigs []InboundConfig `json:"inbounds"`
|
||||
OutboundConfigs json_util.RawMessage `json:"outbounds"`
|
||||
Transport json_util.RawMessage `json:"transport"`
|
||||
Policy json_util.RawMessage `json:"policy"`
|
||||
API json_util.RawMessage `json:"api"`
|
||||
Stats json_util.RawMessage `json:"stats"`
|
||||
Reverse json_util.RawMessage `json:"reverse"`
|
||||
FakeDNS json_util.RawMessage `json:"fakedns"`
|
||||
Observatory json_util.RawMessage `json:"observatory"`
|
||||
LogConfig json_util.RawMessage `json:"log"`
|
||||
RouterConfig json_util.RawMessage `json:"routing"`
|
||||
DNSConfig json_util.RawMessage `json:"dns"`
|
||||
InboundConfigs []InboundConfig `json:"inbounds"`
|
||||
OutboundConfigs json_util.RawMessage `json:"outbounds"`
|
||||
Transport json_util.RawMessage `json:"transport"`
|
||||
Policy json_util.RawMessage `json:"policy"`
|
||||
API json_util.RawMessage `json:"api"`
|
||||
Stats json_util.RawMessage `json:"stats"`
|
||||
Reverse json_util.RawMessage `json:"reverse"`
|
||||
FakeDNS json_util.RawMessage `json:"fakedns"`
|
||||
Observatory json_util.RawMessage `json:"observatory"`
|
||||
BurstObservatory json_util.RawMessage `json:"burstObservatory"`
|
||||
}
|
||||
|
||||
func (c *Config) Equals(other *Config) bool {
|
||||
|
||||
@@ -2,6 +2,7 @@ package xray
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"x-ui/util/json_util"
|
||||
)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package xray
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"x-ui/logger"
|
||||
)
|
||||
|
||||
|
||||
@@ -57,28 +57,28 @@ func GetAccessPersistentPrevLogPath() string {
|
||||
return config.GetLogFolder() + "/3xipl-ap.prev.log"
|
||||
}
|
||||
|
||||
func GetAccessLogPath() string {
|
||||
func GetAccessLogPath() (string, error) {
|
||||
config, err := os.ReadFile(GetConfigPath())
|
||||
if err != nil {
|
||||
logger.Warningf("Something went wrong: %s", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
jsonConfig := map[string]interface{}{}
|
||||
err = json.Unmarshal([]byte(config), &jsonConfig)
|
||||
if err != nil {
|
||||
logger.Warningf("Something went wrong: %s", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
if jsonConfig["log"] != nil {
|
||||
jsonLog := jsonConfig["log"].(map[string]interface{})
|
||||
if jsonLog["access"] != nil {
|
||||
|
||||
accessLogPath := jsonLog["access"].(string)
|
||||
|
||||
return accessLogPath
|
||||
return accessLogPath, nil
|
||||
}
|
||||
}
|
||||
return ""
|
||||
return "", err
|
||||
}
|
||||
|
||||
func stopProcess(p *Process) {
|
||||
@@ -194,14 +194,21 @@ func (p *process) Start() (err error) {
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
logger.Error("Failure in running xray-core process: ", err)
|
||||
p.exitErr = err
|
||||
}
|
||||
}()
|
||||
|
||||
data, err := json.MarshalIndent(p.config, "", " ")
|
||||
if err != nil {
|
||||
return common.NewErrorf("Failed to generate xray configuration file: %v", err)
|
||||
return common.NewErrorf("Failed to generate XRAY configuration files: %v", err)
|
||||
}
|
||||
|
||||
err = os.MkdirAll(config.GetLogFolder(), 0o770)
|
||||
if err != nil {
|
||||
logger.Warningf("Something went wrong: %s", err)
|
||||
}
|
||||
|
||||
configPath := GetConfigPath()
|
||||
err = os.WriteFile(configPath, data, fs.ModePerm)
|
||||
if err != nil {
|
||||
@@ -217,6 +224,7 @@ func (p *process) Start() (err error) {
|
||||
go func() {
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
logger.Error("Failure in running xray-core: ", err)
|
||||
p.exitErr = err
|
||||
}
|
||||
}()
|
||||
|
||||
Reference in New Issue
Block a user