Compare commits

..

1 Commits

Author SHA1 Message Date
MHSanaei
5cd37abbaa v2.0.2 2023-12-19 14:26:00 +03:30
111 changed files with 3181 additions and 10275 deletions

View File

@@ -1,4 +1,4 @@
name: Release 3X-UI for Docker
name: Release X-ui dockerhub
on:
push:
tags:
@@ -11,15 +11,15 @@ jobs:
steps:
- name: Check out the code
uses: actions/checkout@v4
uses: actions/checkout@v4.1.1
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v3.0.0
- name: Login to GHCR
uses: docker/login-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3.0.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -27,15 +27,15 @@ jobs:
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v5.4.0
with:
images: ghcr.io/${{ github.repository }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
uses: docker/build-push-action@v5.1.0
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
platforms: linux/amd64, linux/arm64/v8, linux/arm/v7, linux/arm/v6, linux/386
platforms: linux/amd64, linux/arm64/v8, linux/arm/v7
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -1,4 +1,4 @@
name: Release 3X-UI
name: Release 3X-ui
on:
push:
@@ -10,36 +10,24 @@ jobs:
build:
strategy:
matrix:
platform:
- amd64
- arm64
- armv7
- armv6
- 386
- armv5
platform: [amd64, arm64, arm]
runs-on: ubuntu-20.04
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v4.1.1
- name: Setup Go
uses: actions/setup-go@v5
uses: actions/setup-go@v5.0.0
with:
go-version: '1.22'
- name: Install dependencies
go-version: '1.21'
- name: Install dependencies for arm64 and arm
if: matrix.platform == 'arm64' || matrix.platform == 'arm'
run: |
sudo apt-get update
if [ "${{ matrix.platform }}" == "arm64" ]; then
sudo apt install gcc-aarch64-linux-gnu
elif [ "${{ matrix.platform }}" == "armv7" ]; then
sudo apt install gcc-aarch64-linux-gnu
if [ "${{ matrix.platform }}" == "arm" ]; then
sudo apt install gcc-arm-linux-gnueabihf
elif [ "${{ matrix.platform }}" == "armv6" ]; then
sudo apt install gcc-arm-linux-gnueabihf
elif [ "${{ matrix.platform }}" == "386" ]; then
sudo apt install gcc-i686-linux-gnu
elif [ "${{ matrix.platform }}" == "armv5" ]; then
sudo apt install gcc-arm-linux-gnueabi
fi
- name: Build x-ui
@@ -48,23 +36,9 @@ jobs:
export GOOS=linux
export GOARCH=${{ matrix.platform }}
if [ "${{ matrix.platform }}" == "arm64" ]; then
export GOARCH=arm64
export CC=aarch64-linux-gnu-gcc
elif [ "${{ matrix.platform }}" == "armv7" ]; then
export GOARCH=arm
export GOARM=7
elif [ "${{ matrix.platform }}" == "arm" ]; then
export CC=arm-linux-gnueabihf-gcc
elif [ "${{ matrix.platform }}" == "armv6" ]; then
export GOARCH=arm
export GOARM=6
export CC=arm-linux-gnueabihf-gcc
elif [ "${{ matrix.platform }}" == "386" ]; then
export GOARCH=386
export CC=i686-linux-gnu-gcc
elif [ "${{ matrix.platform }}" == "armv5" ]; then
export GOARCH=arm
export GOARM=5
export CC=arm-linux-gnueabi-gcc
fi
go build -o xui-release -v main.go
@@ -77,33 +51,20 @@ jobs:
cd x-ui/bin
# Download dependencies
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v1.8.8/"
if [ "${{ matrix.platform }}" == "amd64" ]; then
wget ${Xray_URL}Xray-linux-64.zip
wget https://github.com/XTLS/Xray-core/releases/download/v1.8.6/Xray-linux-64.zip
unzip Xray-linux-64.zip
rm -f Xray-linux-64.zip
elif [ "${{ matrix.platform }}" == "arm64" ]; then
wget ${Xray_URL}Xray-linux-arm64-v8a.zip
wget https://github.com/XTLS/Xray-core/releases/download/v1.8.6/Xray-linux-arm64-v8a.zip
unzip Xray-linux-arm64-v8a.zip
rm -f Xray-linux-arm64-v8a.zip
elif [ "${{ matrix.platform }}" == "armv7" ]; then
wget ${Xray_URL}Xray-linux-arm32-v7a.zip
else
wget https://github.com/XTLS/Xray-core/releases/latest/download/Xray-linux-arm32-v7a.zip
unzip Xray-linux-arm32-v7a.zip
rm -f Xray-linux-arm32-v7a.zip
elif [ "${{ matrix.platform }}" == "armv6" ]; then
wget ${Xray_URL}Xray-linux-arm32-v6.zip
unzip Xray-linux-arm32-v6.zip
rm -f Xray-linux-arm32-v6.zip
elif [ "${{ matrix.platform }}" == "386" ]; then
wget ${Xray_URL}Xray-linux-32.zip
unzip Xray-linux-32.zip
rm -f Xray-linux-32.zip
elif [ "${{ matrix.platform }}" == "armv5" ]; then
wget ${Xray_URL}Xray-linux-arm32-v5.zip
unzip Xray-linux-arm32-v5.zip
rm -f Xray-linux-arm32-v5.zip
fi
rm -f geoip.dat geosite.dat
rm -f geoip.dat geosite.dat geoip_IR.dat geosite_IR.dat geoip_VN.dat geosite_VN.dat
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
wget -O geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
@@ -116,11 +77,12 @@ jobs:
- name: Package
run: tar -zcvf x-ui-linux-${{ matrix.platform }}.tar.gz x-ui
- name: Upload files to GH release
uses: svenstaro/upload-release-action@v2
- name: Upload
uses: svenstaro/upload-release-action@2.7.0
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref }}
file: x-ui-linux-${{ matrix.platform }}.tar.gz
asset_name: x-ui-linux-${{ matrix.platform }}.tar.gz
prerelease: true
overwrite: true

View File

@@ -1,13 +1,10 @@
#!/bin/sh
case $1 in
amd64)
ARCH="64"
FNAME="amd64"
;;
i386)
ARCH="32"
FNAME="i386"
;;
armv8 | arm64 | aarch64)
ARCH="arm64-v8a"
FNAME="arm64"
@@ -16,25 +13,23 @@ case $1 in
ARCH="arm32-v7a"
FNAME="arm32"
;;
armv6)
ARCH="arm32-v6"
FNAME="armv6"
;;
*)
ARCH="64"
FNAME="amd64"
;;
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.6/Xray-linux-${ARCH}.zip"
unzip "Xray-linux-${ARCH}.zip"
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat geoip_IR.dat geosite_IR.dat geoip_VN.dat geosite_VN.dat
mv xray "xray-linux-${FNAME}"
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
wget -O geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
wget -O geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
wget -O geoip_VN.dat https://github.com/vuong2023/vn-v2ray-rules/releases/latest/download/geoip.dat
wget -O geosite_VN.dat https://github.com/vuong2023/vn-v2ray-rules/releases/latest/download/geosite.dat
cd ../../

View File

@@ -1,9 +1,11 @@
# ========================================================
# Stage: Builder
# ========================================================
FROM golang:1.22-alpine AS builder
FROM --platform=$BUILDPLATFORM golang:1.21-alpine AS builder
WORKDIR /app
ARG TARGETARCH
ENV CGO_ENABLED=1
ENV CGO_CFLAGS="-D_LARGEFILE64_SOURCE"
RUN apk --no-cache --update add \
build-base \
@@ -13,8 +15,6 @@ RUN apk --no-cache --update add \
COPY . .
ENV CGO_ENABLED=1
ENV CGO_CFLAGS="-D_LARGEFILE64_SOURCE"
RUN go build -o build/x-ui main.go
RUN ./DockerInit.sh "$TARGETARCH"
@@ -28,13 +28,11 @@ WORKDIR /app
RUN apk add --no-cache --update \
ca-certificates \
tzdata \
fail2ban \
bash
COPY --from=builder /app/build/ /app/
COPY --from=builder /app/DockerEntrypoint.sh /app/
COPY --from=builder /app/x-ui.sh /usr/bin/x-ui
fail2ban
COPY --from=builder /app/build/ /app/
COPY --from=builder /app/DockerEntrypoint.sh /app/
COPY --from=builder /app/x-ui.sh /usr/bin/x-ui
# Configure fail2ban
RUN rm -f /etc/fail2ban/jail.d/alpine-ssh.conf \
@@ -49,5 +47,4 @@ RUN chmod +x \
/usr/bin/x-ui
VOLUME [ "/etc/x-ui" ]
CMD [ "./x-ui" ]
ENTRYPOINT [ "/app/DockerEntrypoint.sh" ]

346
README.md
View File

@@ -1,8 +1,6 @@
# 3X-UI
# 3x-ui
<p align="center"><a href="#"><img src="./media/3X-UI.png" alt="Image"></a></p>
**An Advanced Web Panel • Built on Xray Core**
> **Disclaimer: This project is only for personal learning and communication, please do not use it for illegal purposes, please do not use it in a production environment**
[![](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)](https://github.com/MHSanaei/3x-ui/releases)
[![](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg)](#)
@@ -10,77 +8,64 @@
[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](#)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html)
> **Disclaimer:** This project is only for personal learning and communication, please do not use it for illegal purposes, please do not use it in a production environment
3x-ui panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese,Russian,Vietnamese,Spanish)**
**If you think this project is helpful to you, you may wish to give a** :star2:
**If this project is helpful to you, you may wish to give it a**:star2:
**Buy Me a Coffee :**
<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>
- Tron USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
## Install & Upgrade
# Install & Upgrade
```
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
```
## Install Custom Version
# 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 you can add the version to the end of install command. Example for ver `v2.0.2`:
```
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.0.2
```
## SSL Certificate
# SSL
<details>
<summary>Click for SSL Certificate</summary>
### Cloudflare
The Management script has a built-in SSL certificate application for Cloudflare. To use this script to apply for a certificate, you need the following:
- Cloudflare registered email
- Cloudflare Global API Key
- The domain name has been resolved to the current server through cloudflare
**1:** Run the`x-ui`command on the terminal, then choose `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 is also built into the Management script. You can run the `x-ui` command, then choose `SSL Certificate Management`.*
You also can use `x-ui` menu then select `SSL Certificate Management`
</details>
# Features
## Manual Install & Upgrade
- System Status Monitoring
- Search within all inbounds and clients
- Support Dark/Light theme UI
- Support multi-user multi-protocol, web page visualization operation
- Supported protocols: vmess, vless, trojan, shadowsocks, dokodemo-door, socks, http
- Support for configuring more transport configurations
- Traffic statistics, limit traffic, limit expiration time
- Customizable xray configuration templates
- Support https access panel (self-provided domain name + ssl certificate)
- Support one-click SSL certificate application and automatic renewal
- For more advanced configuration items, please refer to the panel
- Fix api routes (user setting will create with api)
- Support to change configs by different items provided in panel
- Support export/import database from panel
# Manual Install & Upgrade
<details>
<summary>Click for manual install details</summary>
#### Usage
<summary>Click for Manual Install details</summary>
1. To download the latest version of the compressed package directly to your server, run the following command:
```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
[[ "${ARCH}" == "aarch64" || "${ARCH}" == "arm64" ]] && XUI_ARCH="arm64" || XUI_ARCH="amd64"
wget https://github.com/MHSanaei/3x-ui/releases/latest/download/x-ui-linux-${XUI_ARCH}.tar.gz
```
@@ -88,16 +73,7 @@ wget https://github.com/MHSanaei/3x-ui/releases/latest/download/x-ui-linux-${XUI
```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
[[ "${ARCH}" == "aarch64" || "${ARCH}" == "arm64" ]] && XUI_ARCH="arm64" || XUI_ARCH="amd64"
cd /root/
rm -rf x-ui/ /usr/local/x-ui/ /usr/bin/x-ui
tar zxvf x-ui-linux-${XUI_ARCH}.tar.gz
@@ -112,13 +88,11 @@ systemctl restart x-ui
</details>
## Install with Docker
# Install with Docker
<details>
<summary>Click for Docker details</summary>
#### Usage
1. Install Docker:
```sh
@@ -151,132 +125,63 @@ systemctl restart x-ui
ghcr.io/mhsanaei/3x-ui:latest
```
update to latest version
</details>
```sh
cd 3x-ui
docker compose down
docker compose pull 3x-ui
docker compose up -d
```
# Default settings
remove 3x-ui from docker
<details>
<summary>Click for Default settings details</summary>
```sh
docker stop 3x-ui
docker rm 3x-ui
cd --
rm -r 3x-ui
```
- Port: 2053
- username and password will be generated randomly if you skip to modify your own security(x-ui "7")
- database path: /etc/x-ui/x-ui.db
- xray config path: /usr/local/x-ui/bin/config.json
Before you set ssl on settings
- http://ip:2053/panel
- http://domain:2053/panel
After you set ssl on settings
- https://yourdomain:2053/panel
</details>
# Xray Configurations:
<details>
<summary>Click for Xray Configurations details</summary>
**copy and paste to xray Configuration :** (you don't need to do this if you have a fresh install)
- [traffic](./media/configs/traffic.json)
- [traffic + Block all Iran IP address](./media/configs/traffic+block-iran-ip.json)
- [traffic + Block all Iran Domains](./media/configs/traffic+block-iran-domains.json)
- [traffic + Block Ads + Use IPv4 for Google](./media/configs/traffic+block-ads+ipv4-google.json)
- [traffic + Block Ads + Route Google + Netflix + Spotify + OpenAI (ChatGPT) to WARP](./media/configs/traffic+block-ads+warp.json)
</details>
## Recommended OS
- Ubuntu 20.04+
- Debian 11+
- CentOS 8+
- Fedora 36+
- Arch Linux
- Manjaro
- Armbian
- AlmaLinux 9+
- Rockylinux 9+
## Supported Architectures and Devices
# [WARP Configuration](https://gitlab.com/fscarmen/warp) (Optional)
<details>
<summary>Click for Supported Architectures and devices details</summary>
<summary>Click for WARP Configuration details</summary>
Our platform offers compatibility with a diverse range of architectures and devices, ensuring flexibility across various computing environments. The following are key architectures that we support:
If you want to use routing to WARP follow steps as below:
- **amd64**: This prevalent architecture is the standard for personal computers and servers, accommodating most modern operating systems seamlessly.
- **x86 / i386**: Widely adopted in desktop and laptop computers, this architecture enjoys broad support from numerous operating systems and applications, including but not limited to Windows, macOS, and Linux systems.
- **armv8 / arm64 / aarch64**: Tailored for contemporary mobile and embedded devices, such as smartphones and tablets, this architecture is exemplified by devices like Raspberry Pi 4, Raspberry Pi 3, Raspberry Pi Zero 2/Zero 2 W, Orange Pi 3 LTS, and more.
- **armv7 / arm / arm32**: Serving as the architecture for older mobile and embedded devices, it remains widely utilized in devices like Orange Pi Zero LTS, Orange Pi PC Plus, Raspberry Pi 2, among others.
- **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.
</details>
## Languages
- English
- Farsi
- Chinese
- Russian
- Vietnamese
- Spanish
- Indonesian
- Ukrainian
## Features
- System Status Monitoring
- Search within all inbounds and clients
- Dark/Light theme
- Supports multi-user and multi-protocol
- Supports protocols, including VMess, VLESS, Trojan, Shadowsocks, Dokodemo-door, Socks, HTTP, wireguard
- Supports XTLS native Protocols, including RPRX-Direct, Vision, REALITY
- Traffic statistics, traffic limit, expiration time limit
- Customizable Xray configuration templates
- Supports HTTPS access panel (self-provided domain name + SSL certificate)
- Supports One-Click SSL certificate application and automatic renewal
- For more advanced configuration items, please refer to the panel
- Fixes API routes (user setting will be created with API)
- Supports changing configs by different items provided in the panel.
- Supports export/import database from the panel
## Default Settings
<details>
<summary>Click for default settings details</summary>
### Information
- **Port:** 2053
- **Username & Password:** It will be generated randomly if you skip modifying.
- **Database Path:**
- /etc/x-ui/x-ui.db
- **Xray Config Path:**
- /usr/local/x-ui/bin/config.json
- **Web Panel Path w/o Deploying SSL:**
- http://ip:2053/panel
- http://domain:2053/panel
- **Web Panel Path w/ Deploying SSL:**
- https://domain:2053/panel
</details>
## [WARP Configuration](https://gitlab.com/fscarmen/warp)
<details>
<summary>Click for WARP configuration details</summary>
#### Usage
If you want to use routing to WARP before v2.1.0 follow steps as below:
**1.** Install WARP on **SOCKS Proxy Mode**:
```sh
bash <(curl -sSL https://raw.githubusercontent.com/hamid-gh98/x-ui-scripts/main/install_warp_proxy.sh)
```
**2.** If you already installed warp, you can uninstall using below command:
1. If you already installed warp, you can uninstall using below command:
```sh
warp u
```
**3.** Turn on the config you need in panel
2. Install WARP on **socks proxy mode**:
```sh
bash <(curl -sSL https://raw.githubusercontent.com/hamid-gh98/x-ui-scripts/main/install_warp_proxy.sh)
```
3. Turn on the config you need in panel or [Copy and paste this file to Xray Configuration](./media/configs/traffic+block-ads+warp.json)
Config Features:
@@ -286,14 +191,12 @@ If you want to use routing to WARP before v2.1.0 follow steps as below:
</details>
## IP Limit
# IP Limit
<details>
<summary>Click for IP limit details</summary>
<summary>Click for IP Limit details</summary>
#### Usage
**Note:** IP Limit won't work correctly when using IP Tunnel
**Note: IP Limit won't work correctly when using IP Tunnel**
- For versions up to `v1.6.1`:
@@ -307,46 +210,44 @@ If you want to use routing to WARP before v2.1.0 follow steps as below:
2. Select `IP Limit Management`.
3. Choose the appropriate options based on your needs.
- make sure you have ./access.log on your Xray Configuration after v2.1.3 we have an option for it
- make sure you have access.log on your Xray Configuration
```sh
"log": {
"loglevel": "warning",
"access": "./access.log",
"dnsLog": false,
"loglevel": "warning"
"error": "./error.log"
},
```
</details>
## Telegram Bot
# Telegram Bot
<details>
<summary>Click for Telegram bot details</summary>
<summary>Click for Telegram Bot details</summary>
#### Usage
X-UI supports daily traffic notification, panel login reminder and other functions through the Tg robot. To use the Tg robot, you need to apply for the specific application tutorial. You can refer to the [blog](https://coderfan.net/how-to-use-telegram-bot-to-alarm-you-when-someone-login-into-your-vps.html)
Set the robot-related parameters in the panel background, including:
The web panel supports daily traffic, panel login, database backup, system status, client info, and other notification and functions through the Telegram Bot. To use the bot, you need to set the bot-related parameters in the panel, including:
- Tg robot Token
- Tg robot ChatId
- Tg robot cycle runtime, in crontab syntax
- Tg robot Expiration threshold
- Tg robot Traffic threshold
- Tg robot Enable send backup in cycle runtime
- Tg robot Enable CPU usage alarm threshold
- Telegram Token
- Admin Chat ID(s)
- Notification Time (in cron syntax)
- Expiration Date Notification
- Traffic Cap Notification
- Database Backup
- CPU Load Notification
Reference syntax:
- 30 \* \* \* \* \* //Notify at the 30s of each point
- 0 \*/10 \* \* \* \* //Notify at the first second of each 10 minutes
- @hourly // hourly notification
- @daily // Daily notification (00:00 in the morning)
- @weekly // weekly notification
- @every 8h // notify every 8 hours
**Reference syntax:**
- `30 \* \* \* \* \*` - Notify at the 30s of each point
- `0 \*/10 \* \* \* \*` - Notify at the first second of each 10 minutes
- `@hourly` - Hourly notification
- `@daily` - Daily notification (00:00 in the morning)
- `@weekly` - weekly notification
- `@every 8h` - Notify every 8 hours
### Telegram Bot Features
# Telegram Bot Features
- Report periodic
- Login notification
@@ -361,8 +262,9 @@ The web panel supports daily traffic, panel login, database backup, system statu
- Check depleted users
- Receive backup by request and in periodic reports
- Multi language bot
</details>
### Setting up Telegram bot
# Setting up Telegram bot
- Start [Botfather](https://t.me/BotFather) in your Telegram account:
![Botfather](./media/botfather.png)
@@ -382,15 +284,12 @@ Enter the user ID in input field number 4. The Telegram accounts with this id wi
- How to get Telegram user ID? Use this [bot](https://t.me/useridinfobot), Start the bot and it will give you the Telegram user ID.
![User ID](./media/user-id.png)
</details>
## API Routes
# API routes
<details>
<summary>Click for API routes details</summary>
#### Usage
- `/login` with `POST` user data: `{username: '', password: ''}` for login
- `/panel/api/inbounds` base for following actions:
@@ -425,12 +324,10 @@ Enter the user ID in input field number 4. The Telegram accounts with this id wi
- [<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>
## Environment Variables
# Environment Variables
<details>
<summary>Click for environment variables details</summary>
#### Usage
<summary>Click for Environment Variables details</summary>
| Variable | Type | Default |
| -------------- | :--------------------------------------------: | :------------ |
@@ -448,7 +345,27 @@ XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
</details>
## Preview
# A Special Thanks To
- [alireza0](https://github.com/alireza0/)
# Acknowledgment
- [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._
- [Iran Hosted Domains](https://github.com/bootmortis/iran-hosted-domains) (License: **MIT**): _A comprehensive list of Iranian domains and services that are hosted within the country._
- [PersianBlocker](https://github.com/MasterKia/PersianBlocker) (License: **AGPLv3**): _An optimal and extensive list to block ads and trackers on Persian websites._
# Suggestion System
- Ubuntu 20.04+
- Debian 10+
- CentOS 8+
- Fedora 36+
- Arch Linux
- Manjaro
- Armbian (for ARM devices)
# Pictures
![1](./media/1.png)
![2](./media/2.png)
@@ -458,15 +375,6 @@ XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
![6](./media/6.png)
![7](./media/7.png)
## A Special Thanks to
- [alireza0](https://github.com/alireza0/)
## Acknowledgment
- [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._
## Stargazers over Time
## Stargazers over time
[![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg)](https://starchart.cc/MHSanaei/3x-ui)

View File

@@ -1 +1 @@
2.2.1
2.0.2

View File

@@ -21,7 +21,6 @@ var db *gorm.DB
var initializers = []func() error{
initUser,
initInbound,
initOutbound,
initSetting,
initInboundClientIps,
initClientTraffic,
@@ -52,10 +51,6 @@ func initInbound() error {
return db.AutoMigrate(&model.Inbound{})
}
func initOutbound() error {
return db.AutoMigrate(&model.OutboundTraffics{})
}
func initSetting() error {
return db.AutoMigrate(&model.Setting{})
}

View File

@@ -37,22 +37,13 @@ type Inbound struct {
// config part
Listen string `json:"listen" form:"listen"`
Port int `json:"port" form:"port"`
Port int `json:"port" form:"port" gorm:"unique"`
Protocol Protocol `json:"protocol" form:"protocol"`
Settings string `json:"settings" form:"settings"`
StreamSettings string `json:"streamSettings" form:"streamSettings"`
Tag string `json:"tag" form:"tag" gorm:"unique"`
Sniffing string `json:"sniffing" form:"sniffing"`
}
type OutboundTraffics struct {
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
Tag string `json:"tag" form:"tag" gorm:"unique"`
Up int64 `json:"up" form:"up" gorm:"default:0"`
Down int64 `json:"down" form:"down" gorm:"default:0"`
Total int64 `json:"total" form:"total" gorm:"default:0"`
}
type InboundClientIps struct {
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
ClientEmail string `json:"clientEmail" form:"clientEmail" gorm:"unique"`

78
go.mod
View File

@@ -1,71 +1,71 @@
module x-ui
go 1.22.0
go 1.21.4
require (
github.com/Calidity/gin-sessions v1.3.1
github.com/gin-contrib/gzip v0.0.6
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/mymmrac/telego v0.28.0
github.com/nicksnyder/go-i18n/v2 v2.3.0
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pelletier/go-toml/v2 v2.1.1
github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v3 v3.24.1
github.com/valyala/fasthttp v1.52.0
github.com/xtls/xray-core v1.8.8
github.com/shirou/gopsutil/v3 v3.23.11
github.com/xtls/xray-core v1.8.6
go.uber.org/atomic v1.11.0
golang.org/x/text v0.14.0
google.golang.org/grpc v1.62.0
gorm.io/driver/sqlite v1.5.5
gorm.io/gorm v1.25.7
google.golang.org/grpc v1.60.1
gorm.io/driver/sqlite v1.5.4
gorm.io/gorm v1.25.5
)
require (
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/andybalholm/brotli v1.0.6 // indirect
github.com/bytedance/sonic v1.10.2 // 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/cloudflare/circl v1.3.6 // indirect
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect
github.com/fasthttp/router v1.4.22 // indirect
github.com/francoispqt/gojay v1.2.13 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gaukas/godicttls v0.0.4 // 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.15.5 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/pprof v0.0.0-20240225044709-fd706174c886 // 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
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/gorilla/sessions v1.2.1 // indirect
github.com/gorilla/websocket v1.5.1 // indirect
github.com/grbit/go-json v0.11.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
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/klauspost/compress v1.17.2 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/lufia/plan9stats v0.0.0-20231016141302-07b5767bb0ed // indirect
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-sqlite3 v1.14.19 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // 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.13.1 // 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/refraction-networking/utls v1.6.3 // indirect
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
github.com/quic-go/quic-go v0.40.0 // indirect
github.com/refraction-networking/utls v1.5.4 // 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-shadowsocks v0.2.6 // indirect
github.com/sagernet/sing v0.2.17 // indirect
github.com/sagernet/sing-shadowsocks v0.2.5 // indirect
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
@@ -75,25 +75,25 @@ require (
github.com/ugorji/go/codec v1.2.11 // 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/valyala/fasthttp v1.51.0 // 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
go.uber.org/mock v0.4.0 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
go.uber.org/mock v0.3.0 // indirect
go4.org/netipx v0.0.0-20230824141953-6213f710f925 // indirect
golang.org/x/arch v0.6.0 // indirect
golang.org/x/crypto v0.19.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/time v0.5.0 // indirect
golang.org/x/tools v0.18.0 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/net v0.18.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/time v0.4.0 // indirect
golang.org/x/tools v0.15.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
golang.zx2c4.com/wireguard v0.0.0-20231022001213-2e0774f246fb // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 // indirect
google.golang.org/protobuf v1.31.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

171
go.sum
View File

@@ -12,8 +12,8 @@ github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/Calidity/gin-sessions v1.3.1 h1:nF3dCBWa7TZ4j26iYLwGRmzZy9YODhWoOS3fmi+snyE=
github.com/Calidity/gin-sessions v1.3.1/go.mod h1:I0+QE6qkO50TeN/n6If6novvxHk4Isvr23U8EdvPdns=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
@@ -30,8 +30,8 @@ github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLI
github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/cloudflare/circl v1.3.6 h1:/xbKIqSHbZXHwkhbrhrt2YOHIwYJlXH94E3tI/gDlUg=
github.com/cloudflare/circl v1.3.6/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
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=
@@ -49,6 +49,8 @@ github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiD
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
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=
@@ -76,8 +78,8 @@ github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl
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.15.5 h1:LEBecTWb/1j5TNY1YYG2RcOUN3R7NLylN+x8TTueE24=
github.com/go-playground/validator/v10 v10.15.5/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
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=
@@ -88,8 +90,8 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=
github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
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=
@@ -107,25 +109,21 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
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-20231101202521-4ca4178f5c7a h1:fEBsGL/sjAuJrgah5XqmmYsTLzJp/TO9Lhy39gkverk=
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a/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=
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/grbit/go-json v0.11.0 h1:bAbyMdYrYl/OjYsSqLH99N2DyQ291mHy726Mx+sYrnc=
github.com/grbit/go-json v0.11.0/go.mod h1:IYpHsdybQ386+6g3VE6AXQ3uTGa5mquBme5/ZWmtzek=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
@@ -138,11 +136,11 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4=
github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
github.com/klauspost/cpuid/v2 v2.2.6/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=
@@ -158,35 +156,35 @@ github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ic
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/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/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a h1:N9zuLhTvBSRt0gWSiJswwQ2HqDmtX/ZCDJURnKUt1Ik=
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE=
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-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
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=
github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mymmrac/telego v0.29.1 h1:nsNnK0mS18OL+unoDjDI6BVfafJBbT8Wtj7rCzEWoM8=
github.com/mymmrac/telego v0.29.1/go.mod h1:ZLD1+L2TQRr97NPOCoN1V2w8y9kmFov33OfZ3qT8cF4=
github.com/mymmrac/telego v0.28.0 h1:DNXaYISeZw1J9oB81vCNdskLow8gCRRUJxufqLuH3XE=
github.com/mymmrac/telego v0.28.0/go.mod h1:oRperySNzJq8dRTl24+uBF1Uy7tlQGIjid/JQtHDsZg=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
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/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8=
github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
github.com/nicksnyder/go-i18n/v2 v2.3.0 h1:2NPsCsNFCVd7i+Su0xYsBrIhS3bE2XMv5gNTft2O+PQ=
github.com/nicksnyder/go-i18n/v2 v2.3.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4=
github.com/onsi/ginkgo/v2 v2.13.1 h1:LNGfMbR2OVGBfXjvRZIZ2YCTQdGKtPLvuI1rMCCj3OU=
github.com/onsi/ginkgo/v2 v2.13.1/go.mod h1:XStQ8QcGwLyF4HdfcZB8SFOS/MWCgDuXMSBe6zrvLgM=
github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=
github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
@@ -208,10 +206,12 @@ 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/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/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
github.com/quic-go/quic-go v0.40.0 h1:GYd1iznlKm7dpHD7pOVpUvItgMPo/jrMgDWZhMCecqw=
github.com/quic-go/quic-go v0.40.0/go.mod h1:PeN7kuVJ4xZbxSv/4OX6S1USOX8MJvydwpTx31vx60c=
github.com/refraction-networking/utls v1.5.4 h1:9k6EO2b8TaOGsQ7Pl7p9w6PUhx18/ZCeT0WNTZ7Uw4o=
github.com/refraction-networking/utls v1.5.4/go.mod h1:SPuDbBmgLGp8s+HLNc83FuavwZCFoMmExj+ltUHiHUw=
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=
@@ -221,17 +221,17 @@ github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6po
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-shadowsocks v0.2.6 h1:xr7ylAS/q1cQYS8oxKKajhuQcchd5VJJ4K4UZrrpp0s=
github.com/sagernet/sing-shadowsocks v0.2.6/go.mod h1:j2YZBIpWIuElPFL/5sJAj470bcn/3QQ5lxZUNKLDNAM=
github.com/sagernet/sing v0.2.17 h1:vMPKb3MV0Aa5ws4dCJkRI8XEjrsUcDn810czd0FwmzI=
github.com/sagernet/sing v0.2.17/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
github.com/sagernet/sing-shadowsocks v0.2.5 h1:qxIttos4xu6ii7MTVJYA8EFQR7Q3KG6xMqmLJIFtBaY=
github.com/sagernet/sing-shadowsocks v0.2.5/go.mod h1:MGWGkcU2xW2G2mfArT9/QqpVLOGU+dBaahZCtPHdt7A=
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/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.23.11 h1:i3jP9NjCPUz7FiZKxlMnODZkdSIp2gnzfrvsu9CuWEQ=
github.com/shirou/gopsutil/v3 v3.23.11/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM=
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=
@@ -288,10 +288,8 @@ github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF
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=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7gU0=
github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ=
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
github.com/vishvananda/netlink v1.2.1-beta.2.0.20230316163032-ced5aaba43e3 h1:tkMT5pTye+1NlKIXETU78NXw0fyjnaNHmJyyLyzw8+U=
@@ -301,18 +299,18 @@ 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/xtls/xray-core v1.8.6 h1:tr3nk/fZnFfCsmgZv7B3RC72N5qUC88oMGVLlybDey8=
github.com/xtls/xray-core v1.8.6/go.mod h1:hj2EB8rtcLdlTC8zxiWm5xL+C0k2Aie9Pk0mXtDEP6U=
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
github.com/yusufpapurcu/wmi v1.2.3/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=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo=
go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
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=
go4.org/netipx v0.0.0-20230824141953-6213f710f925 h1:eeQDDVKFkx0g4Hyy8pHgmZaK0EqB4SD6rvKbUdN3ziQ=
go4.org/netipx v0.0.0-20230824141953-6213f710f925/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=
@@ -321,16 +319,16 @@ golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnf
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.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
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/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
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.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.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=
@@ -340,8 +338,8 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r
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.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
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=
@@ -351,8 +349,8 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -371,9 +369,8 @@ 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/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
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=
@@ -383,20 +380,20 @@ 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=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
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/time v0.4.0 h1:Z81tqI5ddIoXDPvVQ7/7CC9TnLM7ubaFG2qXYd5BbYY=
golang.org/x/time v0.4.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.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8=
golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk=
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=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
golang.zx2c4.com/wireguard v0.0.0-20231022001213-2e0774f246fb h1:c5tyN8sSp8jSDxdCCDXVOpJwYXXhmTkNMt+g0zTSOic=
golang.zx2c4.com/wireguard v0.0.0-20231022001213-2e0774f246fb/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
@@ -409,19 +406,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-20231106174013-bbf56f31fb17 h1:Jyp0Hsi0bmHXG6k9eATXoYtjd6e2UzZ1SCn/wIupY14=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:oQ5rr10WTTMvP4A36n8JpR1OrO1BEiV4f78CneXZxkA=
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/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU=
google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
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/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
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=
@@ -437,10 +434,10 @@ gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C
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/driver/sqlite v1.5.4 h1:IqXwXi8M/ZlPzH/947tn5uik3aYQslP9BVveoax0nV0=
gorm.io/driver/sqlite v1.5.4/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
gorm.io/gorm v1.25.5/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=

View File

@@ -26,15 +26,11 @@ echo "The OS release is: $release"
arch3xui() {
case "$(uname -m)" in
x86_64 | x64 | amd64) echo 'amd64' ;;
i*86 | x86) echo '386' ;;
armv8* | armv8 | arm64 | aarch64) echo 'arm64' ;;
armv7* | armv7 | arm) echo 'armv7' ;;
armv6* | armv6) echo 'armv6' ;;
armv5* | armv5) echo 'armv5' ;;
armv7* | armv7 | arm | arm32 ) echo 'arm32' ;;
*) echo -e "${green}Unsupported CPU architecture! ${plain}" && rm -f install.sh && exit 1 ;;
esac
}
echo "arch: $(arch3xui)"
os_version=""
@@ -46,27 +42,17 @@ if [[ "${release}" == "centos" ]]; then
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
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
if [[ ${os_version} -lt 10 ]]; then
echo -e "${red} Please use Debian 10 or higher ${plain}\n" && exit 1
fi
elif [[ "${release}" == "arch" ]]; then
echo "Your OS is ArchLinux"
@@ -81,21 +67,19 @@ fi
install_base() {
case "${release}" in
centos | almalinux | rocky)
yum -y update && yum install -y -q wget curl tar tzdata
;;
fedora)
dnf -y update && dnf install -y -q wget curl tar tzdata
;;
arch | manjaro)
pacman -Syu && pacman -Syu --noconfirm wget curl tar tzdata
;;
*)
apt-get update && apt install -y -q wget curl tar tzdata
;;
centos|fedora)
yum -y update && yum install -y -q wget curl tar
;;
arch|manjaro)
pacman -Syu && pacman -Syu --noconfirm wget curl tar
;;
*)
apt-get update && apt-get upgrade -y && apt install -y -q wget curl tar
;;
esac
}
# This function will be called when user installed x-ui out of security
config_after_install() {
echo -e "${yellow}Install/update finished! For security it's recommended to modify panel settings ${plain}"
@@ -123,9 +107,9 @@ config_after_install() {
echo -e "${green}username:${usernameTemp}${plain}"
echo -e "${green}password:${passwordTemp}${plain}"
echo -e "###############################################"
echo -e "${red}if you forgot your login info,you can type x-ui and then type 8 to check after installation${plain}"
echo -e "${red}if you forgot your login info,you can type x-ui and then type 7 to check after installation${plain}"
else
echo -e "${red} this is your upgrade,will keep old settings,if you forgot your login info,you can type x-ui and then type 8 to check${plain}"
echo -e "${red} this is your upgrade,will keep old settings,if you forgot your login info,you can type x-ui and then type 7 to check${plain}"
fi
fi
/usr/local/x-ui/x-ui migrate
@@ -149,7 +133,7 @@ install_x-ui() {
else
last_version=$1
url="https://github.com/MHSanaei/3x-ui/releases/download/${last_version}/x-ui-linux-$(arch3xui).tar.gz"
echo -e "Beginning to install x-ui $1"
echo -e "Begining to install x-ui $1"
wget -N --no-check-certificate -O /usr/local/x-ui-linux-$(arch3xui).tar.gz ${url}
if [[ $? -ne 0 ]]; then
echo -e "${red}Download x-ui $1 failed,please check the version exists ${plain}"
@@ -165,21 +149,18 @@ install_x-ui() {
tar zxvf x-ui-linux-$(arch3xui).tar.gz
rm x-ui-linux-$(arch3xui).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
chmod +x bin/xray-linux-arm
fi
chmod +x x-ui bin/xray-linux-$(arch3xui)
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
chmod +x /usr/bin/x-ui
config_after_install
#echo -e "If it is a new installation, the default web port is ${green}2053${plain}, The username and password are ${green}admin${plain} by default"
#echo -e "Please make sure that this port is not occupied by other procedures,${yellow} And make sure that port 2053 has been released${plain}"
# echo -e "If you want to modify the 2053 to other ports and enter the x-ui command to modify it, you must also ensure that the port you modify is also released"
#echo -e ""
#echo -e "If it is updated panel, access the panel in your previous way"
#echo -e ""
systemctl daemon-reload
systemctl enable x-ui
systemctl start x-ui

View File

@@ -65,16 +65,6 @@ func Infof(format string, args ...interface{}) {
addToBuffer("INFO", fmt.Sprintf(format, args...))
}
func Notice(args ...interface{}) {
logger.Notice(args...)
addToBuffer("NOTICE", fmt.Sprint(args...))
}
func Noticef(format string, args ...interface{}) {
logger.Noticef(format, args...)
addToBuffer("NOTICE", fmt.Sprintf(format, args...))
}
func Warning(args ...interface{}) {
logger.Warning(args...)
addToBuffer("WARNING", fmt.Sprint(args...))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 KiB

View File

@@ -0,0 +1,97 @@
{
"log": {
"loglevel": "warning",
"error": "./error.log"
},
"api": {
"tag": "api",
"services": [
"HandlerService",
"LoggerService",
"StatsService"
]
},
"inbounds": [
{
"tag": "api",
"listen": "127.0.0.1",
"port": 62789,
"protocol": "dokodemo-door",
"settings": {
"address": "127.0.0.1"
}
}
],
"outbounds": [
{
"protocol": "freedom",
"settings": {}
},
{
"tag": "blocked",
"protocol": "blackhole",
"settings": {}
},
{
"tag": "IPv4",
"protocol": "freedom",
"settings": {
"domainStrategy": "UseIPv4"
}
}
],
"policy": {
"levels": {
"0": {
"statsUserDownlink": true,
"statsUserUplink": true
}
},
"system": {
"statsInboundDownlink": true,
"statsInboundUplink": true
}
},
"routing": {
"domainStrategy": "IPIfNonMatch",
"rules": [
{
"type": "field",
"inboundTag": [
"api"
],
"outboundTag": "api"
},
{
"type": "field",
"outboundTag": "blocked",
"ip": [
"geoip:private"
]
},
{
"type": "field",
"outboundTag": "blocked",
"protocol": [
"bittorrent"
]
},
{
"type": "field",
"outboundTag": "blocked",
"domain": [
"geosite:category-ads-all",
"ext:geosite_IR.dat:category-ads-all"
]
},
{
"type": "field",
"outboundTag": "IPv4",
"domain": [
"geosite:google"
]
}
]
},
"stats": {}
}

View File

@@ -0,0 +1,105 @@
{
"log": {
"loglevel": "warning",
"error": "./error.log"
},
"api": {
"tag": "api",
"services": [
"HandlerService",
"LoggerService",
"StatsService"
]
},
"inbounds": [
{
"tag": "api",
"listen": "127.0.0.1",
"port": 62789,
"protocol": "dokodemo-door",
"settings": {
"address": "127.0.0.1"
}
}
],
"outbounds": [
{
"protocol": "freedom",
"settings": {}
},
{
"tag": "blocked",
"protocol": "blackhole",
"settings": {}
},
{
"tag": "WARP",
"protocol": "socks",
"settings": {
"servers": [
{
"address": "127.0.0.1",
"port": 40000
}
]
}
}
],
"policy": {
"levels": {
"0": {
"statsUserDownlink": true,
"statsUserUplink": true
}
},
"system": {
"statsInboundDownlink": true,
"statsInboundUplink": true
}
},
"routing": {
"domainStrategy": "IPIfNonMatch",
"rules": [
{
"type": "field",
"inboundTag": [
"api"
],
"outboundTag": "api"
},
{
"type": "field",
"outboundTag": "blocked",
"ip": [
"geoip:private"
]
},
{
"type": "field",
"outboundTag": "blocked",
"protocol": [
"bittorrent"
]
},
{
"type": "field",
"outboundTag": "blocked",
"domain": [
"geosite:category-ads-all",
"ext:geosite_IR.dat:category-ads-all"
]
},
{
"type": "field",
"outboundTag": "WARP",
"domain": [
"geosite:spotify",
"geosite:netflix",
"geosite:openai",
"geosite:google"
]
}
]
},
"stats": {}
}

View File

@@ -0,0 +1,84 @@
{
"log": {
"loglevel": "warning",
"error": "./error.log"
},
"api": {
"tag": "api",
"services": [
"HandlerService",
"LoggerService",
"StatsService"
]
},
"inbounds": [
{
"tag": "api",
"listen": "127.0.0.1",
"port": 62789,
"protocol": "dokodemo-door",
"settings": {
"address": "127.0.0.1"
}
}
],
"outbounds": [
{
"protocol": "freedom",
"settings": {}
},
{
"tag": "blocked",
"protocol": "blackhole",
"settings": {}
}
],
"policy": {
"levels": {
"0": {
"statsUserDownlink": true,
"statsUserUplink": true
}
},
"system": {
"statsInboundDownlink": true,
"statsInboundUplink": true
}
},
"routing": {
"domainStrategy": "IPIfNonMatch",
"rules": [
{
"type": "field",
"inboundTag": [
"api"
],
"outboundTag": "api"
},
{
"type": "field",
"outboundTag": "blocked",
"ip": [
"geoip:private"
]
},
{
"type": "field",
"outboundTag": "blocked",
"protocol": [
"bittorrent"
]
},
{
"type": "field",
"outboundTag": "blocked",
"domain": [
"regexp:.*\\.ir$",
"regexp:.*\\.xn--mgba3a4f16a$",
"ext:geosite_IR.dat:ir"
]
}
]
},
"stats": {}
}

View File

@@ -0,0 +1,76 @@
{
"log": {
"loglevel": "warning",
"error": "./error.log"
},
"api": {
"tag": "api",
"services": [
"HandlerService",
"LoggerService",
"StatsService"
]
},
"inbounds": [
{
"tag": "api",
"listen": "127.0.0.1",
"port": 62789,
"protocol": "dokodemo-door",
"settings": {
"address": "127.0.0.1"
}
}
],
"outbounds": [
{
"protocol": "freedom",
"settings": {}
},
{
"tag": "blocked",
"protocol": "blackhole",
"settings": {}
}
],
"policy": {
"levels": {
"0": {
"statsUserDownlink": true,
"statsUserUplink": true
}
},
"system": {
"statsInboundDownlink": true,
"statsInboundUplink": true
}
},
"routing": {
"domainStrategy": "IPIfNonMatch",
"rules": [
{
"type": "field",
"inboundTag": [
"api"
],
"outboundTag": "api"
},
{
"type": "field",
"outboundTag": "blocked",
"ip": [
"geoip:private",
"ext:geoip_IR.dat:ir"
]
},
{
"type": "field",
"outboundTag": "blocked",
"protocol": [
"bittorrent"
]
}
]
},
"stats": {}
}

View File

@@ -0,0 +1,65 @@
{
"log": {
"loglevel": "warning",
"error": "./error.log"
},
"api": {
"tag": "api",
"services": ["HandlerService", "LoggerService", "StatsService"]
},
"inbounds": [
{
"tag": "api",
"listen": "127.0.0.1",
"port": 62789,
"protocol": "dokodemo-door",
"settings": {
"address": "127.0.0.1"
}
}
],
"outbounds": [
{
"protocol": "freedom",
"settings": {}
},
{
"tag": "blocked",
"protocol": "blackhole",
"settings": {}
}
],
"policy": {
"levels": {
"0": {
"statsUserDownlink": true,
"statsUserUplink": true
}
},
"system": {
"statsInboundDownlink": true,
"statsInboundUplink": true
}
},
"routing": {
"domainStrategy": "IPIfNonMatch",
"rules": [
{
"type": "field",
"inboundTag": ["api"],
"outboundTag": "api"
},
{
"type": "field",
"outboundTag": "blocked",
"ip": ["geoip:private"]
},
{
"type": "field",
"outboundTag": "blocked",
"protocol": ["bittorrent"]
}
]
},
"stats": {}
}

View File

@@ -1,105 +0,0 @@
{
"dns": {
"tag": "dns_out",
"queryStrategy": "UseIP",
"servers": [
{
"address": "8.8.8.8",
"skipFallback": false
}
]
},
"inbounds": [
{
"port": 10808,
"protocol": "socks",
"settings": {
"auth": "noauth",
"udp": true,
"userLevel": 8
},
"sniffing": {
"destOverride": [
"http",
"tls",
"fakedns"
],
"enabled": true
},
"tag": "socks"
},
{
"port": 10809,
"protocol": "http",
"settings": {
"userLevel": 8
},
"tag": "http"
}
],
"log": {
"loglevel": "warning"
},
"outbounds": [
{
"tag": "direct",
"protocol": "freedom",
"settings": {
"domainStrategy": "UseIP"
}
},
{
"tag": "block",
"protocol": "blackhole",
"settings": {
"response": {
"type": "http"
}
}
}
],
"policy": {
"levels": {
"8": {
"connIdle": 300,
"downlinkOnly": 1,
"handshake": 4,
"uplinkOnly": 1
}
},
"system": {
"statsOutboundUplink": true,
"statsOutboundDownlink": true
}
},
"routing": {
"domainStrategy": "AsIs",
"rules": [
{
"type": "field",
"network": "tcp,udp",
"balancerTag": "all"
}
],
"balancers": [
{
"tag": "all",
"selector": [
"proxy"
],
"strategy": {
"type": "leastPing"
}
}
]
},
"observatory": {
"probeInterval": "5m",
"probeURL": "https://api.github.com/_private/browser/stats",
"subjectSelector": [
"proxy"
],
"EnableConcurrency": true
},
"stats": {}
}

View File

@@ -47,6 +47,11 @@ func (s *Server) initRouter() (*gin.Engine, error) {
engine := gin.Default()
subPath, err := s.settingService.GetSubPath()
if err != nil {
return nil, err
}
subDomain, err := s.settingService.GetSubDomain()
if err != nil {
return nil, err
@@ -56,44 +61,9 @@ func (s *Server) initRouter() (*gin.Engine, error) {
engine.Use(middleware.DomainValidatorMiddleware(subDomain))
}
LinksPath, err := s.settingService.GetSubPath()
if err != nil {
return nil, err
}
g := engine.Group(subPath)
JsonPath, err := s.settingService.GetSubJsonPath()
if err != nil {
return nil, err
}
Encrypt, err := s.settingService.GetSubEncrypt()
if err != nil {
return nil, err
}
ShowInfo, err := s.settingService.GetSubShowInfo()
if err != nil {
return nil, err
}
RemarkModel, err := s.settingService.GetRemarkModel()
if err != nil {
RemarkModel = "-ieo"
}
SubUpdates, err := s.settingService.GetSubUpdates()
if err != nil {
SubUpdates = "10"
}
SubJsonFragment, err := s.settingService.GetSubJsonFragment()
if err != nil {
SubJsonFragment = ""
}
g := engine.Group("/")
s.sub = NewSUBController(g, LinksPath, JsonPath, Encrypt, ShowInfo, RemarkModel, SubUpdates, SubJsonFragment)
s.sub = NewSUBController(g)
return engine, nil
}

View File

@@ -3,56 +3,34 @@ package sub
import (
"encoding/base64"
"strings"
"x-ui/web/service"
"github.com/gin-gonic/gin"
)
type SUBController struct {
subPath string
subJsonPath string
subEncrypt bool
updateInterval string
subService *SubService
subJsonService *SubJsonService
subService SubService
settingService service.SettingService
}
func NewSUBController(
g *gin.RouterGroup,
subPath string,
jsonPath string,
encrypt bool,
showInfo bool,
rModel string,
update string,
jsonFragment string) *SUBController {
a := &SUBController{
subPath: subPath,
subJsonPath: jsonPath,
subEncrypt: encrypt,
updateInterval: update,
subService: NewSubService(showInfo, rModel),
subJsonService: NewSubJsonService(jsonFragment),
}
func NewSUBController(g *gin.RouterGroup) *SUBController {
a := &SUBController{}
a.initRouter(g)
return a
}
func (a *SUBController) initRouter(g *gin.RouterGroup) {
gLink := g.Group(a.subPath)
gJson := g.Group(a.subJsonPath)
g = g.Group("/")
gLink.GET(":subid", a.subs)
gJson.GET(":subid", a.subJsons)
g.GET("/:subid", a.subs)
}
func (a *SUBController) subs(c *gin.Context) {
subEncrypt, _ := a.settingService.GetSubEncrypt()
subShowInfo, _ := a.settingService.GetSubShowInfo()
subId := c.Param("subid")
host := strings.Split(c.Request.Host, ":")[0]
subs, header, err := a.subService.GetSubs(subId, host)
subs, headers, err := a.subService.GetSubs(subId, host, subShowInfo)
if err != nil || len(subs) == 0 {
c.String(400, "Error!")
} else {
@@ -62,31 +40,14 @@ func (a *SUBController) subs(c *gin.Context) {
}
// Add headers
c.Writer.Header().Set("Subscription-Userinfo", header)
c.Writer.Header().Set("Profile-Update-Interval", a.updateInterval)
c.Writer.Header().Set("Profile-Title", subId)
c.Writer.Header().Set("Subscription-Userinfo", headers[0])
c.Writer.Header().Set("Profile-Update-Interval", headers[1])
c.Writer.Header().Set("Profile-Title", headers[2])
if a.subEncrypt {
if subEncrypt {
c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
} else {
c.String(200, result)
}
}
}
func (a *SUBController) subJsons(c *gin.Context) {
subId := c.Param("subid")
host := strings.Split(c.Request.Host, ":")[0]
jsonSub, header, err := a.subJsonService.GetJson(subId, host)
if err != nil || len(jsonSub) == 0 {
c.String(400, "Error!")
} else {
// Add headers
c.Writer.Header().Set("Subscription-Userinfo", header)
c.Writer.Header().Set("Profile-Update-Interval", a.updateInterval)
c.Writer.Header().Set("Profile-Title", subId)
c.String(200, jsonSub)
}
}

View File

@@ -1,355 +0,0 @@
package sub
import (
_ "embed"
"encoding/json"
"fmt"
"strings"
"x-ui/database/model"
"x-ui/logger"
"x-ui/util/json_util"
"x-ui/util/random"
"x-ui/web/service"
"x-ui/xray"
)
//go:embed default.json
var defaultJson string
type SubJsonService struct {
fragmanet string
inboundService service.InboundService
SubService
}
func NewSubJsonService(fragment string) *SubJsonService {
return &SubJsonService{
fragmanet: fragment,
}
}
func (s *SubJsonService) GetJson(subId string, host string) (string, string, error) {
inbounds, err := s.SubService.getInboundsBySubId(subId)
if err != nil || len(inbounds) == 0 {
return "", "", err
}
var header string
var traffic xray.ClientTraffic
var clientTraffics []xray.ClientTraffic
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)
}
}
outbounds := []json_util.RawMessage{}
startIndex := 0
// Prepare Inbounds
for _, inbound := range inbounds {
clients, err := s.inboundService.GetClients(inbound)
if err != nil {
logger.Error("SubJsonService - GetClients: Unable to get clients from inbound")
}
if clients == nil {
continue
}
if len(inbound.Listen) > 0 && inbound.Listen[0] == '@' {
listen, port, streamSettings, err := s.getFallbackMaster(inbound.Listen, inbound.StreamSettings)
if err == nil {
inbound.Listen = listen
inbound.Port = port
inbound.StreamSettings = streamSettings
}
}
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))
}
}
outbound := s.getOutbound(inbound, subClients, host, startIndex)
if outbound != nil {
outbounds = append(outbounds, outbound...)
startIndex += len(outbound)
}
}
if len(outbounds) == 0 {
return "", "", nil
}
// Prepare statistics
for index, clientTraffic := range clientTraffics {
if index == 0 {
traffic.Up = clientTraffic.Up
traffic.Down = clientTraffic.Down
traffic.Total = clientTraffic.Total
if clientTraffic.ExpiryTime > 0 {
traffic.ExpiryTime = clientTraffic.ExpiryTime
}
} else {
traffic.Up += clientTraffic.Up
traffic.Down += clientTraffic.Down
if traffic.Total == 0 || clientTraffic.Total == 0 {
traffic.Total = 0
} else {
traffic.Total += clientTraffic.Total
}
if clientTraffic.ExpiryTime != traffic.ExpiryTime {
traffic.ExpiryTime = 0
}
}
}
if s.fragmanet != "" {
outbounds = append(outbounds, json_util.RawMessage(s.fragmanet))
}
// Combile outbounds
outbounds = append(outbounds, defaultOutbounds...)
configJson["outbounds"] = outbounds
finalJson, _ := json.MarshalIndent(configJson, "", " ")
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
stream := s.streamData(inbound.StreamSettings)
externalProxies, ok := stream["externalProxy"].([]interface{})
if !ok || len(externalProxies) == 0 {
externalProxies = []interface{}{
map[string]interface{}{
"forceTls": "same",
"dest": host,
"port": float64(inbound.Port),
},
}
}
delete(stream, "externalProxy")
config_index := startIndex
for _, ep := range externalProxies {
extPrxy := ep.(map[string]interface{})
inbound.Listen = extPrxy["dest"].(string)
inbound.Port = int(extPrxy["port"].(float64))
newStream := stream
switch extPrxy["forceTls"].(string) {
case "tls":
if newStream["security"] != "tls" {
newStream["security"] = "tls"
newStream["tslSettings"] = map[string]interface{}{}
}
case "none":
if newStream["security"] != "none" {
newStream["security"] = "none"
delete(newStream, "tslSettings")
}
}
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
}
}
return newOutbounds
}
func (s *SubJsonService) streamData(stream string) map[string]interface{} {
var streamSettings map[string]interface{}
json.Unmarshal([]byte(stream), &streamSettings)
security, _ := streamSettings["security"].(string)
if security == "tls" {
streamSettings["tlsSettings"] = s.tlsData(streamSettings["tlsSettings"].(map[string]interface{}))
} else if security == "reality" {
streamSettings["realitySettings"] = s.realityData(streamSettings["realitySettings"].(map[string]interface{}))
}
delete(streamSettings, "sockopt")
if s.fragmanet != "" {
streamSettings["sockopt"] = json_util.RawMessage(`{"dialerProxy": "fragment", "tcpKeepAliveIdle": 100, "tcpNoDelay": true}`)
}
// remove proxy protocol
network, _ := streamSettings["network"].(string)
switch network {
case "tcp":
streamSettings["tcpSettings"] = s.removeAcceptProxy(streamSettings["tcpSettings"])
case "ws":
streamSettings["wsSettings"] = s.removeAcceptProxy(streamSettings["wsSettings"])
}
return streamSettings
}
func (s *SubJsonService) removeAcceptProxy(setting interface{}) map[string]interface{} {
netSettings, ok := setting.(map[string]interface{})
if ok {
delete(netSettings, "acceptProxyProtocol")
}
return netSettings
}
func (s *SubJsonService) tlsData(tData map[string]interface{}) map[string]interface{} {
tlsData := make(map[string]interface{}, 1)
tlsClientSettings := tData["settings"].(map[string]interface{})
tlsData["serverName"] = tData["serverName"]
tlsData["alpn"] = tData["alpn"]
if allowInsecure, ok := tlsClientSettings["allowInsecure"].(bool); ok {
tlsData["allowInsecure"] = allowInsecure
}
if fingerprint, ok := tlsClientSettings["fingerprint"].(string); ok {
tlsData["fingerprint"] = fingerprint
}
return tlsData
}
func (s *SubJsonService) realityData(rData map[string]interface{}) map[string]interface{} {
rltyData := make(map[string]interface{}, 1)
rltyClientSettings := rData["settings"].(map[string]interface{})
rltyData["show"] = false
rltyData["publicKey"] = rltyClientSettings["publicKey"]
rltyData["fingerprint"] = rltyClientSettings["fingerprint"]
// Set random data
rltyData["spiderX"] = "/" + random.Seq(15)
shortIds, ok := rData["shortIds"].([]interface{})
if ok && len(shortIds) > 0 {
rltyData["shortId"] = shortIds[random.Num(len(shortIds))].(string)
} else {
rltyData["shortId"] = ""
}
serverNames, ok := rData["serverNames"].([]interface{})
if ok && len(serverNames) > 0 {
rltyData["serverName"] = serverNames[random.Num(len(serverNames))].(string)
} else {
rltyData["serverName"] = ""
}
return rltyData
}
func (s *SubJsonService) genVnext(inbound *model.Inbound, client model.Client) json_util.RawMessage {
outbound := Outbound{}
usersData := make([]UserVnext, 1)
usersData[0].ID = client.ID
usersData[0].Level = 8
if inbound.Protocol == model.VLESS {
usersData[0].Flow = client.Flow
usersData[0].Encryption = "none"
}
vnextData := make([]VnextSetting, 1)
vnextData[0] = VnextSetting{
Address: inbound.Listen,
Port: inbound.Port,
Users: usersData,
}
outbound.Protocol = string(inbound.Protocol)
outbound.Tag = inbound.Tag
outbound.StreamSettings = json_util.RawMessage(inbound.StreamSettings)
outbound.Settings = OutboundSettings{
Vnext: vnextData,
}
result, _ := json.MarshalIndent(outbound, "", " ")
return result
}
func (s *SubJsonService) genServer(inbound *model.Inbound, client model.Client) json_util.RawMessage {
outbound := Outbound{}
serverData := make([]ServerSetting, 1)
serverData[0] = ServerSetting{
Address: inbound.Listen,
Port: inbound.Port,
Level: 8,
Password: client.Password,
}
if inbound.Protocol == model.Shadowsocks {
var inboundSettings map[string]interface{}
json.Unmarshal([]byte(inbound.Settings), &inboundSettings)
method, _ := inboundSettings["method"].(string)
serverData[0].Method = method
// server password in multi-user 2022 protocols
if strings.HasPrefix(method, "2022") {
if serverPassword, ok := inboundSettings["password"].(string); ok {
serverData[0].Password = fmt.Sprintf("%s:%s", serverPassword, client.Password)
}
}
}
outbound.Protocol = string(inbound.Protocol)
outbound.Tag = inbound.Tag
outbound.StreamSettings = json_util.RawMessage(inbound.StreamSettings)
outbound.Settings = OutboundSettings{
Servers: serverData,
}
result, _ := json.MarshalIndent(outbound, "", " ")
return result
}
type Outbound struct {
Protocol string `json:"protocol"`
Tag string `json:"tag"`
StreamSettings json_util.RawMessage `json:"streamSettings"`
Mux map[string]interface{} `json:"mux,omitempty"`
ProxySettings map[string]interface{} `json:"proxySettings,omitempty"`
Settings OutboundSettings `json:"settings,omitempty"`
}
type OutboundSettings struct {
Vnext []VnextSetting `json:"vnext,omitempty"`
Servers []ServerSetting `json:"servers,omitempty"`
}
type VnextSetting struct {
Address string `json:"address"`
Port int `json:"port"`
Users []UserVnext `json:"users"`
}
type UserVnext struct {
Encryption string `json:"encryption,omitempty"`
Flow string `json:"flow,omitempty"`
ID string `json:"id"`
Level int `json:"level"`
}
type ServerSetting struct {
Password string `json:"password"`
Level int `json:"level"`
Address string `json:"address"`
Port int `json:"port"`
Flow string `json:"flow,omitempty"`
Method string `json:"method,omitempty"`
}

View File

@@ -20,47 +20,47 @@ type SubService struct {
address string
showInfo bool
remarkModel string
datepicker string
inboundService service.InboundService
settingService service.SettingService
}
func NewSubService(showInfo bool, remarkModel string) *SubService {
return &SubService{
showInfo: showInfo,
remarkModel: remarkModel,
}
}
func (s *SubService) GetSubs(subId string, host string) ([]string, string, error) {
func (s *SubService) GetSubs(subId string, host string, showInfo bool) ([]string, []string, error) {
s.address = host
s.showInfo = showInfo
var result []string
var header string
var headers []string
var traffic xray.ClientTraffic
var clientTraffics []xray.ClientTraffic
inbounds, err := s.getInboundsBySubId(subId)
if err != nil {
return nil, "", err
return nil, nil, err
}
s.datepicker, err = s.settingService.GetDatepicker()
s.remarkModel, err = s.settingService.GetRemarkModel()
if err != nil {
s.datepicker = "gregorian"
s.remarkModel = "-ieo"
}
for _, inbound := range inbounds {
clients, err := s.inboundService.GetClients(inbound)
if err != nil {
logger.Error("SubService - GetClients: Unable to get clients from inbound")
logger.Error("SubService - GetSub: Unable to get clients from inbound")
}
if clients == nil {
continue
}
if len(inbound.Listen) > 0 && inbound.Listen[0] == '@' {
listen, port, streamSettings, err := s.getFallbackMaster(inbound.Listen, inbound.StreamSettings)
fallbackMaster, err := s.getFallbackMaster(inbound.Listen)
if err == nil {
inbound.Listen = listen
inbound.Port = port
inbound.StreamSettings = streamSettings
inbound.Listen = fallbackMaster.Listen
inbound.Port = fallbackMaster.Port
var stream map[string]interface{}
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
var masterStream map[string]interface{}
json.Unmarshal([]byte(fallbackMaster.StreamSettings), &masterStream)
stream["security"] = masterStream["security"]
stream["tlsSettings"] = masterStream["tlsSettings"]
stream["externalProxy"] = masterStream["externalProxy"]
modifiedStream, _ := json.MarshalIndent(stream, "", " ")
inbound.StreamSettings = string(modifiedStream)
}
}
for _, client := range clients {
@@ -71,8 +71,6 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, string, error
}
}
}
// Prepare statistics
for index, clientTraffic := range clientTraffics {
if index == 0 {
traffic.Up = clientTraffic.Up
@@ -94,8 +92,11 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, string, error
}
}
}
header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
return result, header, nil
headers = append(headers, fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000))
updateInterval, _ := s.settingService.GetSubUpdates()
headers = append(headers, fmt.Sprintf("%d", updateInterval))
headers = append(headers, subId)
return result, headers, nil
}
func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
@@ -124,7 +125,7 @@ func (s *SubService) getClientTraffics(traffics []xray.ClientTraffic, email stri
return xray.ClientTraffic{}
}
func (s *SubService) getFallbackMaster(dest string, streamSettings string) (string, int, string, error) {
func (s *SubService) getFallbackMaster(dest string) (*model.Inbound, error) {
db := database.GetDB()
var inbound *model.Inbound
err := db.Model(model.Inbound{}).
@@ -132,19 +133,9 @@ func (s *SubService) getFallbackMaster(dest string, streamSettings string) (stri
Where("EXISTS (SELECT * FROM json_each(settings, '$.fallbacks') WHERE json_extract(value, '$.dest') = ?)", dest).
Find(&inbound).Error
if err != nil {
return "", 0, "", err
return nil, err
}
var stream map[string]interface{}
json.Unmarshal([]byte(streamSettings), &stream)
var masterStream map[string]interface{}
json.Unmarshal([]byte(inbound.StreamSettings), &masterStream)
stream["security"] = masterStream["security"]
stream["tlsSettings"] = masterStream["tlsSettings"]
stream["externalProxy"] = masterStream["externalProxy"]
modifiedStream, _ := json.MarshalIndent(stream, "", " ")
return inbound.Listen, inbound.Port, string(modifiedStream), nil
return inbound, nil
}
func (s *SubService) getLink(inbound *model.Inbound, email string) string {
@@ -582,7 +573,6 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
if sniValue, ok := searchKey(tlsSetting, "serverName"); ok {
params["sni"], _ = sniValue.(string)
}
tlsSettings, _ := searchKey(tlsSetting, "settings")
if tlsSetting != nil {
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {

View File

@@ -2,6 +2,7 @@ package random
import (
"math/rand"
"time"
)
var numSeq [10]rune
@@ -12,6 +13,8 @@ var numUpperSeq [36]rune
var allSeq [62]rune
func init() {
rand.Seed(time.Now().UnixNano())
for i := 0; i < 10; i++ {
numSeq[i] = rune('0' + i)
}
@@ -38,7 +41,3 @@ func Seq(n int) string {
}
return string(runes)
}
func Num(n int) int {
return rand.Intn(n)
}

View File

@@ -538,7 +538,7 @@
var on = function(emitter, type, f) {
if (emitter.addEventListener) {
emitter.addEventListener(type, f, { passive: false });
emitter.addEventListener(type, f, false);
} else if (emitter.attachEvent) {
emitter.attachEvent("on" + type, f);
} else {

View File

@@ -45,12 +45,12 @@ THE SOFTWARE.
.cm-s-xq .CodeMirror-activeline-background { background: #e8f2ff; }
.cm-s-xq .CodeMirror-matchingbracket { outline:1px solid grey;color:black !important;background:yellow; }
.dark .cm-s-xq.CodeMirror { background-color: var(--dark-color-surface-200); border-color: var(--dark-color-surface-300); color: rgb(255 255 255 / 65%); }
.dark .cm-s-xq.CodeMirror { background-color: #222D42; border-color: #2c3950; color: rgb(255 255 255 / 65%); }
.dark .cm-s-xq.CodeMirror:hover { background-color: rgb(0 50 42 / 30%); border-color: #008771; transition: all .3s; }
.dark .cm-s-xq div.CodeMirror-selected { background: var(--dark-color-codemirror-line-selection); }
.dark .cm-s-xq .CodeMirror-line::selection, .dark .cm-s-xq .CodeMirror-line > span::selection, .dark .cm-s-xq .CodeMirror-line > span > span::selection { background: var(--dark-color-codemirror-line-selection); }
.dark .cm-s-xq .CodeMirror-line::-moz-selection, .dark .cm-s-xq .CodeMirror-line > span::-moz-selection, .dark .cm-s-xq .CodeMirror-line > span > span::-moz-selection { background: var(--dark-color-codemirror-line-selection); }
.dark .cm-s-xq .CodeMirror-gutters { background: rgb(0 0 0 / 30%); border-right: 1px solid var(--dark-color-surface-300); }
.dark .cm-s-xq div.CodeMirror-selected { background: rgba(0, 0, 0, 0.5); }
.dark .cm-s-xq .CodeMirror-line::selection, .dark .cm-s-xq .CodeMirror-line > span::selection, .dark .cm-s-xq .CodeMirror-line > span > span::selection { background: rgba(39, 0, 122, 0.99); }
.dark .cm-s-xq .CodeMirror-line::-moz-selection, .dark .cm-s-xq .CodeMirror-line > span::-moz-selection, .dark .cm-s-xq .CodeMirror-line > span > span::-moz-selection { background: rgba(39, 0, 122, 0.99); }
.dark .cm-s-xq .CodeMirror-gutters { background: rgb(0 0 0 / 30%); border-right: 1px solid #2c3950; }
.dark .cm-s-xq .CodeMirror-guttermarker { color: #FFBD40; }
.dark .cm-s-xq .CodeMirror-guttermarker-subtle { color: rgb(255 255 255 / 70%); }
.dark .cm-s-xq .CodeMirror-linenumber { color: rgb(255 255 255 / 50%); }
@@ -80,7 +80,7 @@ THE SOFTWARE.
.Line-Hover{transition: all .2s;}
.Line-Hover:hover{ background-color: rgba(0, 102, 85, 0.05) !important; }
.dark .Line-Hover:hover{ background-color: var(--dark-color-codemirror-line-hover) !important; }
.dark .Line-Hover:hover{ background-color: rgb(0 0 0 / 20%) !important; }
.CodeMirror-foldmarker { color: #fc8800; text-shadow: #ffd8aa 1px 1px 2px, #ffd8aa -1px -1px 2px, #ffd8aa 1px -1px 2px, #ffd8aa -1px 1px 2px; font-family: arial; line-height: .3; cursor: pointer; }
.dark .CodeMirror-foldmarker { color: #ffffff; text-shadow: #bbb 1px 1px 2px, #bbb -1px -1px 2px, #bbb 1px -1px 2px, #bbb -1px 1px 2px; font-family: arial; line-height: .3; cursor: pointer; }

View File

@@ -1,87 +1,3 @@
:root {
--color-primary-100: #008771;
--dark-color-background: #0a1222;
--dark-color-surface-100: #151f31;
--dark-color-surface-200: #222d42;
--dark-color-surface-300: #2c3950;
--dark-color-surface-400: rgba(65, 85, 119, 0.5); /* line */
--dark-color-surface-500: #2c3950; /* popover & switch btn */
--dark-color-surface-600: #313f5a; /* dropmenu hover */
--dark-color-surface-700: #111929; /* modals */
--dark-color-table-hover: rgba(44, 57, 80, 0.2);
--dark-color-text-primary: rgba(255, 255, 255, 0.75);
--dark-color-stroke: #2c3950;
--dark-color-btn-danger: #cd3838;
--dark-color-btn-danger-border: transparent;
--dark-color-btn-danger-hover: #e94b4b;
--dark-color-tag-bg: rgba(255, 255, 255, 0.05);
--dark-color-tag-border:rgba(255, 255, 255, 0.15);
--dark-color-tag-color:rgba(255, 255, 255, 0.75);
--dark-color-tag-green-bg: #112421;
--dark-color-tag-green-border: #195141;
--dark-color-tag-green-color: #3ad3ba;
--dark-color-tag-purple-bg: #201425;
--dark-color-tag-purple-border: #5a2969;
--dark-color-tag-purple-color: #d988cd;
--dark-color-tag-red-bg: #291515;
--dark-color-tag-red-border: #5c2626;
--dark-color-tag-red-color: #e04141;
--dark-color-tag-orange-bg: #312313;
--dark-color-tag-orange-border: #593914;
--dark-color-tag-orange-color: #ffa031;
--dark-color-tag-blue-bg: #111a2c;
--dark-color-tag-blue-border: #1348ab;
--dark-color-tag-blue-color: #529fff;
--dark-color-codemirror-line-hover: rgba(0, 135, 113, 0.2);
--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);
}
html[data-theme='ultra-dark'] {
--dark-color-background: #21242a;
--dark-color-surface-100: #0c0e12;
--dark-color-surface-200: #222327;
--dark-color-surface-300: #32353b;
--dark-color-surface-400: rgba(255, 255, 255, 0.1);
--dark-color-surface-500: #3b404b;
--dark-color-surface-600: #505663;
--dark-color-surface-700: #101113;
--dark-color-table-hover: rgba(89, 89, 89, 0.15);
--dark-color-text-primary: rgb(255 255 255 / 85%);
--dark-color-stroke: #202025;
--dark-color-tag-green-bg: #112421;
--dark-color-tag-green-border: #1d5f4d;
--dark-color-tag-green-color: #59cbac;
--dark-color-tag-purple-bg: #241121;
--dark-color-tag-purple-border: #5a2969;
--dark-color-tag-purple-color: #d686ca;
--dark-color-tag-red-bg: #2a1215;
--dark-color-tag-red-border: #58181c;
--dark-color-tag-red-color: #e84749;
--dark-color-tag-orange-bg: #2b1d11;
--dark-color-tag-orange-border: #593815;
--dark-color-tag-orange-color: #e89a3c;
--dark-color-tag-blue-bg: #111a2c;
--dark-color-tag-blue-border: #0f367e;
--dark-color-tag-blue-color: #3c89e8;
--dark-color-codemirror-line-hover: rgba(85, 85, 85, 0.3);
--dark-color-codemirror-line-selection: rgba(85, 85, 85, 0.4);
--dark-color-login-background: #0a2227;
--dark-color-login-wave: #0f2d32;
.ant-dropdown-menu-dark {
background-color: var(--dark-color-surface-500);
}
.dark .ant-dropdown-menu-submenu-title:hover,
.dark .ant-select-dropdown-menu-item-active:not(.ant-select-dropdown-menu-item-disabled),
.dark .ant-select-dropdown-menu-item:hover:not(.ant-select-dropdown-menu-item-disabled) {
background-color: var(--dark-color-surface-300);
}
.dark .waves-header {
background-color: #0a2227;
}
}
html,
body {
height: 100vh;
@@ -100,7 +16,7 @@ body {
font-feature-settings: "tnum";
}
html {
--antd-wave-shadow-color: var(--color-primary-100);
--antd-wave-shadow-color: #008771;
line-height: 1.15;
text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
@@ -110,7 +26,7 @@ html {
}
::selection {
color: var(--color-primary-100);
color: #008771;
background-color: #cfe8e4;
}
@@ -218,7 +134,7 @@ style attribute {
padding: 0.5rem;
}
.ant-modal-body {
padding: 20px;
padding: 10px;
}
.ant-form-item-label {
line-height: 1.5;
@@ -302,7 +218,7 @@ style attribute {
.ant-menu-submenu-active,
.ant-menu-submenu-title:hover,
.ant-menu:not(.ant-menu-inline) .ant-menu-submenu-open {
color: var(--color-primary-100);
color: #008771;
background-color: rgb(232 244 242);
border-radius: 0.5rem;
}
@@ -564,14 +480,14 @@ style attribute {
}
.normal-icon:hover {
color: var(--color-primary-100);
color: #008771;
}
/* DARK THEME */
.dark ::selection {
color: #fff;
background-color: var(--color-primary-100);
background-color: #008771;
}
.dark .normal-icon:hover {
@@ -586,14 +502,13 @@ style attribute {
.dark .ant-table,
.dark .ant-collapse-content,
.dark .ant-tabs {
background-color: var(--dark-color-surface-100);
color: var(--dark-color-text-primary);
background-color: #151f31;
color: #ffffffa6;
}
.dark .ant-card-hoverable:hover,
.dark .ant-space-item > .ant-tabs:hover {
/* box-shadow: 0 1px 10px -1px rgb(154 175 238 / 80%); */
box-shadow: 0 2px 8px transparent;
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 80%);
}
.dark > .ant-layout,
@@ -603,8 +518,8 @@ style attribute {
.dark .ant-table-expanded-row:hover,
.dark .ant-table-expanded-row .ant-table-tbody,
.dark .ant-calendar {
background-color: var(--dark-color-background);
color: var(--dark-color-text-primary);
background-color: #101828;
color: rgb(255 255 255 /65%);
}
.dark .ant-table-expanded-row .ant-table-thead > tr:first-child > th {
@@ -613,7 +528,7 @@ style attribute {
.dark .ant-calendar,
.dark .ant-card-bordered {
border-color: var(--dark-color-background);
border-color: #151f31;
}
.dark .ant-table-bordered,
@@ -625,7 +540,7 @@ style attribute {
.dark .ant-table-bordered .ant-table-thead > tr:not(:last-child) > th,
.dark .ant-table-bordered .ant-table-tbody > tr > td,
.dark .ant-table-bordered .ant-table-thead > tr > th {
border-color: var(--dark-color-surface-400);
border-color: #2c3950;
}
.dark .ant-table-tbody > tr > td,
@@ -638,7 +553,7 @@ style attribute {
.dark .ant-popover-title,
.dark .ant-calendar-header,
.dark .ant-calendar-input-wrap {
border-bottom-color: var(--dark-color-surface-400);
border-bottom-color: #2c3950;
}
.dark .ant-modal-footer,
@@ -646,7 +561,7 @@ style attribute {
.dark .ant-calendar-footer,
.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);
border-top-color: #2c3950;
}
.dark .ant-progress-text,
@@ -682,7 +597,7 @@ style attribute {
.dark .ant-calendar-year-panel-year,
.dark .ant-calendar-month-panel-month,
.dark .ant-calendar-decade-panel-decade {
color: var(--dark-color-text-primary);
color: rgba(255, 255, 255, 0.65);
}
.dark .ant-list-item-meta-description {
@@ -708,12 +623,13 @@ style attribute {
.dark .ant-select-dropdown li,
.dark .ant-select-dropdown-menu-item,
.dark .ant-divider:not(.ant-divider-with-text-center),
.dark .ant-calendar-input,
.dark .client-table-header,
.dark .ant-select-selection--multiple .ant-select-selection__choice,
.dark .ant-calendar-time-picker-inner {
background-color: var(--dark-color-surface-200);
border-color: var(--dark-color-surface-300);
color: var(--dark-color-text-primary);
background-color: #222d42;
border-color: #2c3950;
color: rgba(255, 255, 255, 0.65);
}
.dark .ant-select-selection:hover,
@@ -723,34 +639,34 @@ style attribute {
.dark .ant-input:hover,
.dark .ant-input:focus {
background-color: rgba(0, 135, 113, 0.3);
border-color: var(--color-primary-100);
border-color: #008771;
}
.dark .ant-btn:not(.ant-btn-primary):not(.ant-btn-danger) {
color: var(--dark-color-text-primary);
color: rgba(255, 255, 255, 0.65);
background-color: rgb(10 117 87 / 30%);
border: 1px solid var(--color-primary-100);
border: 1px solid #008771;
}
.dark .ant-radio-button-wrapper,
.dark .ant-radio-button-wrapper:before {
color: var(--dark-color-text-primary);
color: rgb(255 255 255 / 65%);
background-color: rgba(0, 135, 113, 0.3);
border-color: var(--color-primary-100);
border-color: #008771;
}
.dark .ant-btn:focus:not(.ant-btn-primary):not(.ant-btn-danger),
.dark .ant-btn:hover:not(.ant-btn-primary):not(.ant-btn-danger) {
color: #fff;
background-color: rgb(10 117 87 / 50%);
border-color: var(--color-primary-100);
border-color: #008771;
}
.dark .ant-btn-primary[disabled],
.dark .ant-btn-danger[disabled],
.dark .ant-calendar-ok-btn-disabled {
color: rgb(255 255 255 / 35%);
background-color: var(--dark-color-surface-300);
background-color: #2c3950;
border-color: #42516c;
}
@@ -759,7 +675,7 @@ style attribute {
> tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)
> td,
.dark .client-table-odd-row {
background-color: var(--dark-color-table-hover);
background-color: #00877122;
}
.dark .ant-table-row-expand-icon {
@@ -769,37 +685,38 @@ style attribute {
}
.dark .ant-table-row-expand-icon:hover {
color: var(--color-primary-100);
color: #008771;
background-color: #fff0;
border-color: var(--color-primary-100);
border-color: #008771;
}
.dark .ant-switch:not(.ant-switch-checked),
.dark .ant-progress-line .ant-progress-inner {
background-color: var(--dark-color-surface-500);
background-color: #2c3950;
}
.dark .ant-progress-circle-trail {
stroke: var(--dark-color-stroke) !important;
stroke: #2c3950 !important;
}
.ant-dropdown-menu-dark,
.dark .ant-popover-inner {
background-color: var(--dark-color-surface-500);
background-color: #222d42;
}
.dark > .ant-popover-content > .ant-popover-arrow {
border-color: var(--dark-color-surface-500);
border-color: #222d42;
}
.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);
background-color: #313f5a;
}
.ant-menu-dark .ant-menu-item:hover {
background-color: var(--dark-color-surface-300);
background-color: #2c3950;
}
.dark .ant-alert-message {
@@ -807,61 +724,61 @@ style attribute {
}
.dark .ant-tag {
color: var(--dark-color-tag-color);
background-color: var(--dark-color-tag-bg);
border-color: var(--dark-color-tag-border);
color: rgba(255, 255, 255, 0.65);
background-color: #ffffff0a;
border-color: #344461;
}
.dark .ant-tag-blue {
background-color: var(--dark-color-tag-blue-bg);
border-color: var(--dark-color-tag-blue-border);
color: var(--dark-color-tag-blue-color);
background-color: #111a2c;
border-color: #0f367e;
color: #3c89e8;
}
.dark .ant-tag-red,
.dark .ant-alert-error {
background-color: var(--dark-color-tag-red-bg);
border-color: var(--dark-color-tag-red-border);
color: var(--dark-color-tag-red-color);
background-color: #291515;
border-color: #5c2626;
color: #e04141;
}
.dark .ant-tag-orange,
.dark .ant-alert-warning {
background-color: var(--dark-color-tag-orange-bg);
border-color: var(--dark-color-tag-orange-border);
color: var(--dark-color-tag-orange-color);
background-color: #312313;
border-color: #593914;
color: #ffa031;
}
.dark .ant-tag-green {
background-color: var(--dark-color-tag-green-bg);
border-color: var(--dark-color-tag-green-border);
color: var(--dark-color-tag-green-color);
background-color: #112421;
border-color: #144840;
color: #33bca5;
}
.dark .ant-tag-purple {
background-color: var(--dark-color-tag-purple-bg);
border-color: var(--dark-color-tag-purple-border);
color: var(--dark-color-tag-purple-color);
background-color: #2c1e32;
border-color: #49394e;
color: #cfb9cc;
}
.dark .ant-modal-content,
.dark .ant-modal-header {
background-color: var(--dark-color-surface-700);
background-color: #181f2c;
}
.dark .ant-calendar-next-month-btn-day .ant-calendar-date,
.dark .ant-calendar-last-month-cell .ant-calendar-date {
color: var(--dark-color-surface-300);
color: #2c3950;
}
.dark .ant-calendar-selected-day .ant-calendar-date {
background-color: var(--color-primary-100) !important;
background-color: #008771 !important;
color: #fff;
}
.dark .ant-calendar-date:hover,
.dark .ant-calendar-time-picker-select li:hover {
background-color: var(--dark-color-surface-600);
background-color: #313f5a;
color: #fff;
}
@@ -875,15 +792,13 @@ style attribute {
color: #fff;
font-weight: 600;
outline: none;
background-color: var(--color-primary-100);
background-color: #008771;
}
.dark .ant-calendar-time-picker-select {
border-right-color: var(--dark-color-surface-300);
border-right-color: #2c3950;
}
.has-warning .ant-select-selection,
.has-warning .ant-select-selection:hover,
.has-warning .ant-input,
.has-warning .ant-input:hover {
background-color: #ffeee1;
@@ -898,8 +813,6 @@ style attribute {
border-color: #fec093;
}
.dark .has-warning .ant-select-selection,
.dark .has-warning .ant-select-selection:hover,
.dark .has-warning .ant-input,
.dark .has-warning .ant-input:hover {
border-color: #784e1d;
@@ -915,7 +828,7 @@ style attribute {
}
.dark .has-success .anticon {
color: var(--color-primary-100);
color: #61bf39;
animation-name: diffZoomIn1 !important;
}
@@ -961,19 +874,19 @@ style attribute {
}
.ant-calendar-today .ant-calendar-date {
color: var(--color-primary-100);
color: #008771;
font-weight: 700;
border-color: var(--color-primary-100);
border-color: #008771;
}
.dark .ant-calendar-today .ant-calendar-date {
color: #fff;
font-weight: 700;
border-color: var(--color-primary-100);
border-color: #008771;
}
.ant-calendar-selected-day .ant-calendar-date {
background: var(--color-primary-100);
background: #008771;
color: #ffffff;
}
@@ -1007,7 +920,7 @@ li.ant-select-dropdown-menu-item:empty:after {
.ant-select-dropdown.ant-select-dropdown--multiple
.ant-select-dropdown-menu-item-selected:hover
.ant-select-selected-icon {
color: var(--color-primary-100);
color: #008771;
}
.ant-select-selection:hover,
.ant-input-number-focused,
@@ -1016,7 +929,7 @@ li.ant-select-dropdown-menu-item:empty:after {
}
.dark .ant-input-number-handler:active {
background-color: var(--color-primary-100);
background-color: #008771;
}
.dark .ant-input-number-handler:hover .ant-input-number-handler-down-inner,
@@ -1044,7 +957,7 @@ li.ant-select-dropdown-menu-item:empty:after {
}
.dark .ant-calendar-year-panel-header {
border-bottom: 1px solid var(--dark-color-surface-200);
border-bottom: 1px solid #222d42;
}
.dark .ant-calendar-year-panel-last-decade-cell .ant-calendar-year-panel-year,
@@ -1052,11 +965,10 @@ li.ant-select-dropdown-menu-item:empty:after {
color: rgba(255, 255, 255, 0.35);
}
.ant-dropdown-menu-dark,
.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-200);
background-color: #222d42;
}
.dark .ant-calendar-header a:hover {
@@ -1064,13 +976,13 @@ li.ant-select-dropdown-menu-item:empty:after {
}
.dark .ant-calendar-month-panel-header {
background-color: var(--dark-color-background);
border-bottom: 1px solid var(--dark-color-surface-200);
background-color: #101828;
border-bottom: 1px solid #222d42;
}
.dark .ant-calendar-year-panel,
.dark .ant-calendar table {
background-color: var(--dark-color-background);
background-color: #101828;
}
.dark .ant-calendar-year-panel-selected-cell .ant-calendar-year-panel-year,
@@ -1088,7 +1000,7 @@ li.ant-select-dropdown-menu-item:empty:after {
.ant-calendar-decade-panel-selected-cell
.ant-calendar-decade-panel-decade:hover {
color: #fff;
background-color: var(--color-primary-100);
background-color: #008771;
}
.dark .ant-calendar-last-month-cell .ant-calendar-date,
@@ -1102,8 +1014,8 @@ li.ant-select-dropdown-menu-item:empty:after {
.dark .ant-calendar-today .ant-calendar-date:hover {
color: #fff;
border-color: var(--color-primary-100);
background-color: var(--color-primary-100);
border-color: #008771;
background-color: #008771;
}
.dark
@@ -1116,8 +1028,8 @@ li.ant-select-dropdown-menu-item:empty:after {
}
.dark .ant-calendar-decade-panel-header {
border-bottom: 1px solid var(--dark-color-surface-200);
background-color: var(--dark-color-background);
border-bottom: 1px solid #222d42;
background-color: #101828;
}
.dark .ant-checkbox-inner {
@@ -1126,30 +1038,24 @@ li.ant-select-dropdown-menu-item:empty:after {
}
.dark .ant-checkbox-checked .ant-checkbox-inner {
background-color: var(--color-primary-100);
border-color: var(--color-primary-100);
background-color: #008771;
border-color: #008771;
}
.dark .ant-calendar-input {
background-color: var(--dark-color-background);
color: var(--dark-color-text-primary);
background-color: #101828;
}
.dark .ant-calendar-input::placeholder {
color: rgba(255, 255, 255, 0.25);
}
.ant-input-group.ant-input-group-compact-addon:not(:first-child):not(
:last-child
),
.ant-input-group.ant-input-group-compact-wrap:not(:first-child):not(
:last-child
),
.ant-input-group.ant-input-group-compact
> .ant-input:not(:first-child):not(:last-child),
.ant-input-number-handler,
.ant-input-number-handler-wrap {
border-radius: 0;
border-radius: 0;
}
.ant-input-number-handler {
border-radius: 0;
}
.ant-input-number {
@@ -1183,15 +1089,15 @@ li.ant-select-dropdown-menu-item:empty:after {
> td,
.ant-table-thead
> tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)
> td,
.ant-calendar-time-picker-select li:hover {
> td {
background-color: rgb(232 244 242);
}
.dark .ant-dropdown-menu-item:hover,
.dark .ant-dropdown-menu-submenu-title:hover,
.dark .ant-select-dropdown-menu-item-active:not(.ant-select-dropdown-menu-item-disabled),
.dark .ant-select-dropdown-menu-item:hover:not(.ant-select-dropdown-menu-item-disabled) {
background-color: var(--dark-color-surface-600);
background-color: #313f5a;
}
.ant-select-dropdown,
@@ -1204,8 +1110,6 @@ li.ant-select-dropdown-menu-item:empty:after {
}
.qr-bg {
width: 100%;
height: 100%;
background-color: #fff;
display: flex;
justify-content: center;
@@ -1213,39 +1117,3 @@ li.ant-select-dropdown-menu-item:empty:after {
padding: 0.5rem;
border-radius: 1rem;
}
.ant-input-group-addon:not(:first-child):not(:last-child) {
border-radius: 0rem 1rem 1rem 0rem;
}
.ant-tag {
margin-right: 6px;
}
b, strong {
font-weight: 500;
}
.ant-collapse>.ant-collapse-item>.ant-collapse-header {
padding: 10px 16px 10px 40px;
}
.dark .ant-message-notice-content {
background-color: var(--dark-color-surface-200);
border: 1px solid var(--dark-color-surface-300);
color: var(--dark-color-text-primary);
}
.ant-btn-danger {
background-color: var(--dark-color-btn-danger);
border-color: var(--dark-color-btn-danger-border);
}
.ant-btn-danger:focus, .ant-btn-danger:hover {
background-color: var(--dark-color-btn-danger-hover);
border-color: var(--dark-color-btn-danger-hover);
}
.dark .ant-alert-close-icon .anticon-close:hover {
color: rgb(255 255 255);
}

BIN
web/assets/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -29,16 +29,6 @@ const supportLangs = [
value: 'es-ES',
icon: '🇪🇸',
},
{
name: 'Indonesian',
value: 'id-ID',
icon: '🇮🇩',
},
{
name: 'Український',
value: 'uk-UA',
icon: '🇺🇦',
},
];
function getLang() {

View File

@@ -56,10 +56,6 @@ class DBInbound {
return this.protocol === Protocols.HTTP;
}
get isWireguard() {
return this.protocol === Protocols.WIREGUARD;
}
get address() {
let address = location.hostname;
if (!ObjectUtil.isEmpty(this.listen) && this.listen !== "0.0.0.0") {

View File

@@ -8,7 +8,6 @@ const Protocols = {
Shadowsocks: "shadowsocks",
Socks: "socks",
HTTP: "http",
Wireguard: "wireguard"
};
const SSMethods = {
@@ -47,27 +46,18 @@ const ALPN_OPTION = {
HTTP1: "http/1.1",
};
const OutboundDomainStrategies = [
const outboundDomainStrategies = [
"AsIs",
"UseIP",
"UseIPv4",
"UseIPv6"
];
const WireguardDomainStrategy = [
"ForceIP",
"ForceIPv4",
"ForceIPv4v6",
"ForceIPv6",
"ForceIPv6v4"
];
]
Object.freeze(Protocols);
Object.freeze(SSMethods);
Object.freeze(TLS_FLOW_CONTROL);
Object.freeze(ALPN_OPTION);
Object.freeze(OutboundDomainStrategies);
Object.freeze(WireguardDomainStrategy);
Object.freeze(outboundDomainStrategies);
class CommonClass {
@@ -418,7 +408,7 @@ class Outbound extends CommonClass {
}
canEnableTls() {
if (![Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(this.protocol)) return false;
if (![Protocols.VMess, Protocols.VLESS, Protocols.Trojan].includes(this.protocol)) return false;
return ["tcp", "ws", "http", "quic", "grpc"].includes(this.stream.network);
}
@@ -635,7 +625,6 @@ Outbound.Settings = class extends CommonClass {
case Protocols.Shadowsocks: return new Outbound.ShadowsocksSettings();
case Protocols.Socks: return new Outbound.SocksSettings();
case Protocols.HTTP: return new Outbound.HttpSettings();
case Protocols.Wireguard: return new Outbound.WireguardSettings();
default: return null;
}
}
@@ -651,7 +640,6 @@ Outbound.Settings = class extends CommonClass {
case Protocols.Shadowsocks: return Outbound.ShadowsocksSettings.fromJson(json);
case Protocols.Socks: return Outbound.SocksSettings.fromJson(json);
case Protocols.HTTP: return Outbound.HttpSettings.fromJson(json);
case Protocols.Wireguard: return Outbound.WireguardSettings.fromJson(json);
default: return null;
}
}
@@ -850,24 +838,23 @@ Outbound.ShadowsocksSettings = class extends CommonClass {
};
}
};
Outbound.SocksSettings = class extends CommonClass {
constructor(address, port, user, pass) {
constructor(address, port, user, password) {
super();
this.address = address;
this.port = port;
this.user = user;
this.pass = pass;
this.password = password;
}
static fromJson(json={}) {
let servers = json.servers;
servers = json.servers;
if(ObjectUtil.isArrEmpty(servers)) servers=[{users: [{}]}];
return new Outbound.SocksSettings(
servers[0].address,
servers[0].port,
ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].user,
ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].pass,
ObjectUtil.isArrEmpty(servers[0].password) ? '' : servers[0].users[0].password,
);
}
@@ -876,28 +863,28 @@ Outbound.SocksSettings = class extends CommonClass {
servers: [{
address: this.address,
port: this.port,
users: ObjectUtil.isEmpty(this.user) ? [] : [{user: this.user, pass: this.pass}],
users: ObjectUtil.isEmpty(this.user) ? [] : [{user: this.user, password: this.password}],
}],
};
}
};
Outbound.HttpSettings = class extends CommonClass {
constructor(address, port, user, pass) {
constructor(address, port, user, password) {
super();
this.address = address;
this.port = port;
this.user = user;
this.pass = pass;
this.password = password;
}
static fromJson(json={}) {
let servers = json.servers;
servers = json.servers;
if(ObjectUtil.isArrEmpty(servers)) servers=[{users: [{}]}];
return new Outbound.HttpSettings(
servers[0].address,
servers[0].port,
ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].user,
ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].pass,
ObjectUtil.isArrEmpty(servers[0].password) ? '' : servers[0].users[0].password,
);
}
@@ -906,91 +893,8 @@ Outbound.HttpSettings = class extends CommonClass {
servers: [{
address: this.address,
port: this.port,
users: ObjectUtil.isEmpty(this.user) ? [] : [{user: this.user, pass: this.pass}],
users: ObjectUtil.isEmpty(this.user) ? [] : [{user: this.user, password: this.password}],
}],
};
}
};
Outbound.WireguardSettings = class extends CommonClass {
constructor(
mtu=1420, secretKey='',
address=[''], workers=2, domainStrategy='', reserved='',
peers=[new Outbound.WireguardSettings.Peer()], kernelMode=false) {
super();
this.mtu = mtu;
this.secretKey = secretKey;
this.pubKey = secretKey.length>0 ? Wireguard.generateKeypair(secretKey).publicKey : '';
this.address = address instanceof Array ? address.join(',') : address;
this.workers = workers;
this.domainStrategy = domainStrategy;
this.reserved = reserved instanceof Array ? reserved.join(',') : reserved;
this.peers = peers;
this.kernelMode = kernelMode;
}
addPeer() {
this.peers.push(new Outbound.WireguardSettings.Peer());
}
delPeer(index) {
this.peers.splice(index, 1);
}
static fromJson(json={}){
return new Outbound.WireguardSettings(
json.mtu,
json.secretKey,
json.address,
json.workers,
json.domainStrategy,
json.reserved,
json.peers.map(peer => Outbound.WireguardSettings.Peer.fromJson(peer)),
json.kernelMode,
);
}
toJson() {
return {
mtu: this.mtu?? undefined,
secretKey: this.secretKey,
address: this.address ? this.address.split(",") : [],
workers: this.workers?? undefined,
domainStrategy: WireguardDomainStrategy.includes(this.domainStrategy) ? this.domainStrategy : undefined,
reserved: this.reserved ? this.reserved.split(",").map(Number) : undefined,
peers: Outbound.WireguardSettings.Peer.toJsonArray(this.peers),
kernelMode: this.kernelMode,
};
}
};
Outbound.WireguardSettings.Peer = class extends CommonClass {
constructor(publicKey='', psk='', allowedIPs=['0.0.0.0/0','::/0'], endpoint='', keepAlive=0) {
super();
this.publicKey = publicKey;
this.psk = psk;
this.allowedIPs = allowedIPs;
this.endpoint = endpoint;
this.keepAlive = keepAlive;
}
static fromJson(json={}){
return new Outbound.WireguardSettings.Peer(
json.publicKey,
json.preSharedKey,
json.allowedIPs,
json.endpoint,
json.keepAlive
);
}
toJson() {
return {
publicKey: this.publicKey,
preSharedKey: this.psk.length>0 ? this.psk : undefined,
allowedIPs: this.allowedIPs ? this.allowedIPs : undefined,
endpoint: this.endpoint,
keepAlive: this.keepAlive?? undefined,
};
}
};

View File

@@ -12,10 +12,8 @@ class AllSetting {
this.expireDiff = "";
this.trafficDiff = "";
this.remarkModel = "-ieo";
this.datepicker = "gregorian";
this.tgBotEnable = false;
this.tgBotToken = "";
this.tgBotProxy = "";
this.tgBotChatId = "";
this.tgRunTime = "@daily";
this.tgBotBackup = false;
@@ -28,7 +26,6 @@ class AllSetting {
this.subListen = "";
this.subPort = "2096";
this.subPath = "/sub/";
this.subJsonPath = "/json/";
this.subDomain = "";
this.subCertFile = "";
this.subKeyFile = "";
@@ -36,8 +33,6 @@ class AllSetting {
this.subEncrypt = true;
this.subShowInfo = false;
this.subURI = '';
this.subJsonURI = '';
this.subJsonFragment = '';
this.timeLocation = "Asia/Tehran";

View File

@@ -6,7 +6,6 @@ const Protocols = {
DOKODEMO: 'dokodemo-door',
SOCKS: 'socks',
HTTP: 'http',
WIREGUARD: 'wireguard',
};
const SSMethods = {
@@ -236,7 +235,6 @@ TcpStreamSettings.TcpRequest = class extends XrayCommonClass {
toJson() {
return {
version: this.version,
method: this.method,
path: ObjectUtil.clone(this.path),
headers: XrayCommonClass.toV2Headers(this.headers),
@@ -766,18 +764,16 @@ class RealityStreamSettings extends XrayCommonClass {
}
RealityStreamSettings.Settings = class extends XrayCommonClass {
constructor(publicKey = '', fingerprint = UTLS_FINGERPRINT.UTLS_FIREFOX, serverName = '', spiderX= '/') {
constructor(publicKey = '', fingerprint = UTLS_FINGERPRINT.UTLS_FIREFOX, spiderX= '/') {
super();
this.publicKey = publicKey;
this.fingerprint = fingerprint;
this.serverName = serverName;
this.spiderX = spiderX;
}
static fromJson(json = {}) {
return new RealityStreamSettings.Settings(
json.publicKey,
json.fingerprint,
json.serverName,
json.spiderX,
);
}
@@ -785,7 +781,6 @@ RealityStreamSettings.Settings = class extends XrayCommonClass {
return {
publicKey: this.publicKey,
fingerprint: this.fingerprint,
serverName: this.serverName,
spiderX: this.spiderX,
};
}
@@ -799,7 +794,6 @@ class SockoptStreamSettings extends XrayCommonClass {
this.mark = mark;
this.tproxy = tproxy;
}
static fromJson(json = {}) {
if (Object.keys(json).length === 0) return undefined;
return new SockoptStreamSettings(
@@ -1001,6 +995,18 @@ class Inbound extends XrayCommonClass {
}
}
get tls() {
return this.stream.security === 'tls';
}
set tls(isTls) {
if (isTls) {
this.stream.security = 'tls';
} else {
this.stream.security = 'none';
}
}
get xtls() {
return this.stream.security === 'xtls';
}
@@ -1012,7 +1018,19 @@ class Inbound extends XrayCommonClass {
this.stream.security = 'none';
}
}
get reality() {
return this.stream.security === 'reality';
}
set reality(isReality) {
if (isReality) {
this.stream.security = 'reality';
} else {
this.stream.security = 'none';
}
}
get network() {
return this.stream.network;
}
@@ -1124,6 +1142,11 @@ class Inbound extends XrayCommonClass {
return ["tcp", "ws", "http", "quic", "grpc"].includes(this.network);
}
canEnableReality() {
if(![Protocols.VLESS, Protocols.TROJAN].includes(this.protocol)) return false;
return ["tcp", "http", "grpc"].includes(this.network);
}
//this is used for xtls-rprx-vision
canEnableTlsFlow() {
if (((this.stream.security === 'tls') || (this.stream.security === 'reality')) && (this.network === "tcp")) {
@@ -1132,11 +1155,6 @@ class Inbound extends XrayCommonClass {
return false;
}
canEnableReality() {
if(![Protocols.VLESS, Protocols.TROJAN].includes(this.protocol)) return false;
return ["tcp", "http", "grpc"].includes(this.network);
}
canEnableXtls() {
if(![Protocols.VLESS, Protocols.TROJAN].includes(this.protocol)) return false;
return this.network === "tcp";
@@ -1146,6 +1164,10 @@ class Inbound extends XrayCommonClass {
return [Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(this.protocol);
}
canSniffing() {
return [Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(this.protocol);
}
reset() {
this.port = RandomUtil.randomIntRange(10000, 60000);
this.listen = '';
@@ -1530,28 +1552,6 @@ class Inbound extends XrayCommonClass {
return url.toString();
}
getWireguardLink(address, port, remark, peerId) {
let txt = `[Interface]\n`
txt += `PrivateKey = ${this.settings.peers[peerId].privateKey}\n`
txt += `Address = ${this.settings.peers[peerId].allowedIPs[0]}\n`
txt += `DNS = 1.1.1.1, 1.0.0.1\n`
if (this.settings.mtu) {
txt += `MTU = ${this.settings.mtu}\n`
}
txt += `\n# ${remark}\n`
txt += `[Peer]\n`
txt += `PublicKey = ${this.settings.pubKey}\n`
txt += `AllowedIPs = 0.0.0.0/0, ::/0\n`
txt += `Endpoint = ${address}:${port}`
if (this.settings.peers[peerId].psk) {
txt += `\nPresharedKey = ${this.settings.peers[peerId].psk}`
}
if (this.settings.peers[peerId].keepAlive) {
txt += `\nPersistentKeepalive = ${this.settings.peers[peerId].keepAlive}\n`
}
return txt;
}
genLink(address='', port=this.port, forceTls='same', remark='', client) {
switch (this.protocol) {
case Protocols.VMESS:
@@ -1575,7 +1575,7 @@ class Inbound extends XrayCommonClass {
const orderChars = remarkModel.slice(1);
let orders = {
'i': remark,
'e': email,
'e': client ? client.email : '',
'o': '',
};
if(ObjectUtil.isArrEmpty(this.stream.externalProxy)){
@@ -1598,7 +1598,6 @@ class Inbound extends XrayCommonClass {
}
genInboundLinks(remark = '', remarkModel = '-ieo') {
let addr = !ObjectUtil.isEmpty(this.listen) && this.listen !== "0.0.0.0" ? this.listen : location.hostname;
if(this.clients){
let links = [];
this.clients.forEach((client) => {
@@ -1608,14 +1607,7 @@ class Inbound extends XrayCommonClass {
});
return links.join('\r\n');
} else {
if(this.protocol == Protocols.SHADOWSOCKS && !this.isSSMultiUser) return this.genSSLink(addr, this.port, 'same', remark);
if(this.protocol == Protocols.WIREGUARD) {
let links = [];
this.settings.peers.forEach((p,index) => {
links.push(this.getWireguardLink(addr,this.port,remark + remarkModel.charAt(0) + (index+1),index));
});
return links.join('\r\n');
}
if(this.protocol == Protocols.SHADOWSOCKS && !this.isSSMultiUser) return this.genSSLink(this.listen, this.port, remark);
return '';
}
}
@@ -1665,8 +1657,7 @@ Inbound.Settings = class extends XrayCommonClass {
case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings(protocol);
case Protocols.DOKODEMO: return new Inbound.DokodemoSettings(protocol);
case Protocols.SOCKS: return new Inbound.SocksSettings(protocol);
case Protocols.HTTP: return new Inbound.HttpSettings(protocol);
case Protocols.WIREGUARD: return new Inbound.WireguardSettings(protocol);
case Protocols.HTTP: return new Inbound.HttpSettings(protocol);
default: return null;
}
}
@@ -1680,7 +1671,6 @@ Inbound.Settings = class extends XrayCommonClass {
case Protocols.DOKODEMO: return Inbound.DokodemoSettings.fromJson(json);
case Protocols.SOCKS: return Inbound.SocksSettings.fromJson(json);
case Protocols.HTTP: return Inbound.HttpSettings.fromJson(json);
case Protocols.WIREGUARD: return Inbound.WireguardSettings.fromJson(json);
default: return null;
}
}
@@ -2283,81 +2273,3 @@ Inbound.HttpSettings.HttpAccount = class extends XrayCommonClass {
return new Inbound.HttpSettings.HttpAccount(json.user, json.pass);
}
};
Inbound.WireguardSettings = class extends XrayCommonClass {
constructor(protocol, mtu=1420, secretKey=Wireguard.generateKeypair().privateKey, peers=[new Inbound.WireguardSettings.Peer()], kernelMode=false) {
super(protocol);
this.mtu = mtu;
this.secretKey = secretKey;
this.pubKey = secretKey.length>0 ? Wireguard.generateKeypair(secretKey).publicKey : '';
this.peers = peers;
this.kernelMode = kernelMode;
}
addPeer() {
this.peers.push(new Inbound.WireguardSettings.Peer(null,null,'',['10.0.0.' + (this.peers.length+2)]));
}
delPeer(index) {
this.peers.splice(index, 1);
}
static fromJson(json={}){
return new Inbound.WireguardSettings(
Protocols.WIREGUARD,
json.mtu,
json.secretKey,
json.peers.map(peer => Inbound.WireguardSettings.Peer.fromJson(peer)),
json.kernelMode,
);
}
toJson() {
return {
mtu: this.mtu?? undefined,
secretKey: this.secretKey,
peers: Inbound.WireguardSettings.Peer.toJsonArray(this.peers),
kernelMode: this.kernelMode,
};
}
};
Inbound.WireguardSettings.Peer = class extends XrayCommonClass {
constructor(privateKey, publicKey, psk='', allowedIPs=['10.0.0.2/32'], keepAlive=0) {
super();
this.privateKey = privateKey
this.publicKey = publicKey;
if (!this.publicKey){
[this.publicKey, this.privateKey] = Object.values(Wireguard.generateKeypair())
}
this.psk = psk;
allowedIPs.forEach((a,index) => {
if (a.length>0 && !a.includes('/')) allowedIPs[index] += '/32';
})
this.allowedIPs = allowedIPs;
this.keepAlive = keepAlive;
}
static fromJson(json={}){
return new Inbound.WireguardSettings.Peer(
json.privateKey,
json.publicKey,
json.preSharedKey,
json.allowedIPs,
json.keepAlive
);
}
toJson() {
this.allowedIPs.forEach((a,index) => {
if (a.length>0 && !a.includes('/')) this.allowedIPs[index] += '/32';
});
return {
privateKey: this.privateKey,
publicKey: this.publicKey,
preSharedKey: this.psk.length>0 ? this.psk : undefined,
allowedIPs: this.allowedIPs,
keepAlive: this.keepAlive?? undefined,
};
}
};

View File

@@ -295,190 +295,3 @@ class ObjectUtil {
return true;
}
}
class Wireguard {
static gf(init) {
var r = new Float64Array(16);
if (init) {
for (var i = 0; i < init.length; ++i)
r[i] = init[i];
}
return r;
}
static pack(o, n) {
var b, m = this.gf(), t = this.gf();
for (var i = 0; i < 16; ++i)
t[i] = n[i];
this.carry(t);
this.carry(t);
this.carry(t);
for (var j = 0; j < 2; ++j) {
m[0] = t[0] - 0xffed;
for (var i = 1; i < 15; ++i) {
m[i] = t[i] - 0xffff - ((m[i - 1] >> 16) & 1);
m[i - 1] &= 0xffff;
}
m[15] = t[15] - 0x7fff - ((m[14] >> 16) & 1);
b = (m[15] >> 16) & 1;
m[14] &= 0xffff;
this.cswap(t, m, 1 - b);
}
for (var i = 0; i < 16; ++i) {
o[2 * i] = t[i] & 0xff;
o[2 * i + 1] = t[i] >> 8;
}
}
static carry(o) {
var c;
for (var i = 0; i < 16; ++i) {
o[(i + 1) % 16] += (i < 15 ? 1 : 38) * Math.floor(o[i] / 65536);
o[i] &= 0xffff;
}
}
static cswap(p, q, b) {
var t, c = ~(b - 1);
for (var i = 0; i < 16; ++i) {
t = c & (p[i] ^ q[i]);
p[i] ^= t;
q[i] ^= t;
}
}
static add(o, a, b) {
for (var i = 0; i < 16; ++i)
o[i] = (a[i] + b[i]) | 0;
}
static subtract(o, a, b) {
for (var i = 0; i < 16; ++i)
o[i] = (a[i] - b[i]) | 0;
}
static multmod(o, a, b) {
var t = new Float64Array(31);
for (var i = 0; i < 16; ++i) {
for (var j = 0; j < 16; ++j)
t[i + j] += a[i] * b[j];
}
for (var i = 0; i < 15; ++i)
t[i] += 38 * t[i + 16];
for (var i = 0; i < 16; ++i)
o[i] = t[i];
this.carry(o);
this.carry(o);
}
static invert(o, i) {
var c = this.gf();
for (var a = 0; a < 16; ++a)
c[a] = i[a];
for (var a = 253; a >= 0; --a) {
this.multmod(c, c, c);
if (a !== 2 && a !== 4)
this.multmod(c, c, i);
}
for (var a = 0; a < 16; ++a)
o[a] = c[a];
}
static clamp(z) {
z[31] = (z[31] & 127) | 64;
z[0] &= 248;
}
static generatePublicKey(privateKey) {
var r, z = new Uint8Array(32);
var a = this.gf([1]),
b = this.gf([9]),
c = this.gf(),
d = this.gf([1]),
e = this.gf(),
f = this.gf(),
_121665 = this.gf([0xdb41, 1]),
_9 = this.gf([9]);
for (var i = 0; i < 32; ++i)
z[i] = privateKey[i];
this.clamp(z);
for (var i = 254; i >= 0; --i) {
r = (z[i >>> 3] >>> (i & 7)) & 1;
this.cswap(a, b, r);
this.cswap(c, d, r);
this.add(e, a, c);
this.subtract(a, a, c);
this.add(c, b, d);
this.subtract(b, b, d);
this.multmod(d, e, e);
this.multmod(f, a, a);
this.multmod(a, c, a);
this.multmod(c, b, e);
this.add(e, a, c);
this.subtract(a, a, c);
this.multmod(b, a, a);
this.subtract(c, d, f);
this.multmod(a, c, _121665);
this.add(a, a, d);
this.multmod(c, c, a);
this.multmod(a, d, f);
this.multmod(d, b, _9);
this.multmod(b, e, e);
this.cswap(a, b, r);
this.cswap(c, d, r);
}
this.invert(c, c);
this.multmod(a, a, c);
this.pack(z, a);
return z;
}
static generatePresharedKey() {
var privateKey = new Uint8Array(32);
window.crypto.getRandomValues(privateKey);
return privateKey;
}
static generatePrivateKey() {
var privateKey = this.generatePresharedKey();
this.clamp(privateKey);
return privateKey;
}
static encodeBase64(dest, src) {
var input = Uint8Array.from([(src[0] >> 2) & 63, ((src[0] << 4) | (src[1] >> 4)) & 63, ((src[1] << 2) | (src[2] >> 6)) & 63, src[2] & 63]);
for (var i = 0; i < 4; ++i)
dest[i] = input[i] + 65 +
(((25 - input[i]) >> 8) & 6) -
(((51 - input[i]) >> 8) & 75) -
(((61 - input[i]) >> 8) & 15) +
(((62 - input[i]) >> 8) & 3);
}
static keyToBase64(key) {
var i, base64 = new Uint8Array(44);
for (i = 0; i < 32 / 3; ++i)
this.encodeBase64(base64.subarray(i * 4), key.subarray(i * 3));
this.encodeBase64(base64.subarray(i * 4), Uint8Array.from([key[i * 3 + 0], key[i * 3 + 1], 0]));
base64[43] = 61;
return String.fromCharCode.apply(null, base64);
}
static keyFromBase64(encoded) {
const binaryStr = atob(encoded);
const bytes = new Uint8Array(binaryStr.length);
for (let i = 0; i < binaryStr.length; i++) {
bytes[i] = binaryStr.charCodeAt(i);
}
return bytes;
}
static generateKeypair(secretKey='') {
var privateKey = secretKey.length>0 ? this.keyFromBase64(secretKey) : this.generatePrivateKey();
var publicKey = this.generatePublicKey(privateKey);
return {
publicKey: this.keyToBase64(publicKey),
privateKey: secretKey.length>0 ? secretKey : this.keyToBase64(privateKey)
};
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,455 +0,0 @@
jdp-overlay {
height: 0;
width: 0;
}
jdp-container {
-moz-animation: 0.3s cubic-bezier(0.23, 1, 0.32, 1) jdpOpenAnimation;
-webkit-animation: 0.3s cubic-bezier(0.23, 1, 0.32, 1) jdpOpenAnimation;
animation: 0.3s cubic-bezier(0.23, 1, 0.32, 1) jdpOpenAnimation;
background: #fff;
border-radius: 1rem;
box-shadow: 0 2px 8px rgba(0,0,0,.15);
direction: rtl;
display: none;
width: 280px;
overflow: hidden;
padding: 0.5rem 0;
position: absolute;
-ms-touch-action: manipulation;
touch-action: manipulation;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
transform-origin: bottom;
}
jdp-container,
jdp-container *,
jdp-container :after,
jdp-container :before {
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
jdp-container .jdp-icon-minus,
jdp-container .jdp-icon-plus {
outline: 1px solid rgb(232 244 242);
outline-offset: -1px;
border-radius: 6px;
cursor: pointer;
display: flex;
flex: none;
overflow: hidden;
text-align: center;
text-decoration: none;
vertical-align: middle;
transition: all 0.2s;
width: 24px;
height: 24px;
align-items: center;
justify-content: center;
}
jdp-container .jdp-icon-minus:hover,
jdp-container .jdp-icon-plus:hover {
background-color: rgb(232 244 242);
}
jdp-container .jdp-icon-minus svg,
jdp-container .jdp-icon-plus svg {
height: 1.5rem;
padding: 0.25rem;
vertical-align: middle;
width: 1.5rem;
}
jdp-container .jdp-icon-minus.not-in-range,
jdp-container .jdp-icon-plus.not-in-range {
cursor: not-allowed;
}
jdp-container .jdp-icon-minus.not-in-range svg,
jdp-container .jdp-icon-plus.not-in-range svg {
opacity: 0.3;
}
jdp-container .jdp-months,
jdp-container .jdp-years {
fill: rgba(0, 0, 0, 0.9);
color: rgba(0, 0, 0, 0.9);
display: -webkit-inline-box;
display: -webkit-flex;
display: -ms-inline-flexbox;
display: inline-flex;
font-size: 120%;
margin: 0 2.5%;
}
jdp-container .jdp-months {
width: 40%;
margin-right: 1rem;
}
jdp-container .jdp-years {
width: 40%;
margin: 0.5rem 1.2rem 0.8rem 0;
}
jdp-container .jdp-month,
jdp-container .jdp-month input,
jdp-container .jdp-month select,
jdp-container .jdp-time,
jdp-container .jdp-time input,
jdp-container .jdp-time select,
jdp-container .jdp-year,
jdp-container .jdp-year input,
jdp-container .jdp-year select {
background: #fff;
border: none;
border-radius: 0;
color: inherit;
display: inline-block;
font-family: inherit;
font-size: inherit;
font-weight: 300;
height: auto;
line-height: inherit;
margin: 0;
outline: 0;
padding: 0;
text-align: center;
vertical-align: initial;
width: 100%;
font-feature-settings: "ss01";
}
jdp-container .jdp-month input:active,
jdp-container .jdp-month input:focus,
jdp-container .jdp-month select:active,
jdp-container .jdp-month select:focus,
jdp-container .jdp-month:active,
jdp-container .jdp-month:focus,
jdp-container .jdp-time input:active,
jdp-container .jdp-time input:focus,
jdp-container .jdp-time select:active,
jdp-container .jdp-time select:focus,
jdp-container .jdp-time:active,
jdp-container .jdp-time:focus,
jdp-container .jdp-year input:active,
jdp-container .jdp-year input:focus,
jdp-container .jdp-year select:active,
jdp-container .jdp-year select:focus,
jdp-container .jdp-year:active,
jdp-container .jdp-year:focus {
outline: 0;
}
jdp-container .jdp-month input option,
jdp-container .jdp-month option,
jdp-container .jdp-month select option,
jdp-container .jdp-time input option,
jdp-container .jdp-time option,
jdp-container .jdp-time select option,
jdp-container .jdp-year input option,
jdp-container .jdp-year option,
jdp-container .jdp-year select option {
font-size: 95%;
min-height: 1.3rem;
outline: 0;
padding: 0;
}
jdp-container .jdp-month input,
jdp-container .jdp-time input,
jdp-container .jdp-year input {
-webkit-appearance: none;
-moz-appearance: textfield;
cursor: text;
}
jdp-container .jdp-month input::-webkit-inner-spin-button,
jdp-container .jdp-month input::-webkit-outer-spin-button,
jdp-container .jdp-time input::-webkit-inner-spin-button,
jdp-container .jdp-time input::-webkit-outer-spin-button,
jdp-container .jdp-year input::-webkit-inner-spin-button,
jdp-container .jdp-year input::-webkit-outer-spin-button {
-webkit-appearance: none;
}
jdp-container .jdp-month select,
jdp-container .jdp-time select,
jdp-container .jdp-year select {
-webkit-appearance: none;
-moz-appearance: none;
cursor: pointer;
appearance: none;
position: relative;
}
jdp-container .jdp-days {
-ms-flex-pack: justify;
display: inline-block;
display: -ms-flexbox;
display: flex;
flex-wrap: wrap;
-ms-flex-wrap: wrap;
justify-content: space-around;
outline: 0;
padding: 8px 12px;
text-align: left;
width: 100%;
border-top: 1px solid #e8e8e8;
}
jdp-container .jdp-day,
jdp-container .jdp-day-name {
background: 0 0;
border: 1px solid transparent;
color: rgba(0,0,0,.65);
display: block;
font-weight: 400;
height: 24px;
justify-content: center;
line-height: 22px;
margin: 2px 6px;
position: relative;
text-align: center;
width: 24px;
font-feature-settings: "ss01";
}
jdp-container .jdp-day-name.today,
jdp-container .jdp-day.today {
border-color: var(--color-primary-100);
color: var(--color-primary-100);
font-weight: 700;
}
.dark jdp-container .jdp-day-name.selected,
.dark jdp-container .jdp-day.selected,
jdp-container .jdp-day-name.selected,
jdp-container .jdp-day.selected {
background-color: var(--color-primary-100) !important;
color: #fff !important;
opacity: 1 !important;
}
.dark jdp-container .jdp-day-name.holly-day,
.dark jdp-container .jdp-day-name.last-week,
.dark jdp-container .jdp-day.holly-day,
.dark jdp-container .jdp-day.last-week,
jdp-container .jdp-day-name.holly-day,
jdp-container .jdp-day-name.last-week,
jdp-container .jdp-day.holly-day,
jdp-container .jdp-day.last-week {
color: #f44336;
}
.dark jdp-container .jdp-day.not-in-month,
jdp-container .jdp-day.not-in-month {
opacity: 0.4;
}
jdp-container .jdp-day.disabled-day {
cursor: not-allowed;
opacity: 0.15;
}
jdp-container .jdp-day:not(.disabled-day) {
border-radius: 6px;
cursor: pointer;
transition: 0.1s linear;
}
jdp-container .jdp-day:not(.disabled-day):hover {
background: rgb(232 244 242);
}
jdp-container .jdp-day-name {
background-color: rgb(0 0 0 / 0%);
border-radius: 6px;
cursor: default;
}
jdp-container .jdp-footer {
-ms-flex-pack: justify;
display: inline-block;
display: -ms-flexbox;
display: flex;
flex-wrap: nowrap;
-ms-flex-wrap: nowrap;
justify-content: space-between;
outline: 0;
padding: 6px 12px 0;
width: 100%;
border-top: 1px solid #e8e8e8;
}
jdp-container .jdp-btn-close,
jdp-container .jdp-btn-empty,
jdp-container .jdp-btn-today {
background: #00877000;
border-radius: 5px;
color: var(--color-primary-100);
cursor: pointer;
display: inline-block;
font-size: 90%;
font-weight: 400;
padding: 0.3em 0.6em;
text-align: center;
}
jdp-container .jdp-btn-close.disabled-btn,
jdp-container .jdp-btn-empty.disabled-btn,
jdp-container .jdp-btn-today.disabled-btn {
cursor: not-allowed;
opacity: 0.2;
}
jdp-container .jdp-time-container {
display: flex;
padding: 6px 12px 12px 12px;
}
jdp-container .jdp-time-container .jdp-time {
flex: auto;
margin: 0 0.5rem;
position: relative;
}
jdp-container .jdp-time-container .jdp-time select {
border: 1px solid rgb(232 244 242);
border-radius: 6px;
appearance: none;
transition: all 0.2s;
}
jdp-container .jdp-time-container .jdp-time select:hover {
background-color: rgb(232 244 242);
}
jdp-container .jdp-time-container .jdp-time:after {
content: ":";
font-size: 1.5rem;
height: 100%;
position: absolute;
right: -0.7rem;
transform: translateY(-50%);
}
jdp-container .jdp-time-container .jdp-time:first-child:after {
display: none;
}
jdp-container .jdp-time-container.jdp-only-time .jdp-time select {
font-size: 1.5rem;
padding: 0.8rem 1rem 0.8rem 7px;
}
jdp-container .jdp-time-container.jdp-only-time .jdp-time:after {
font-size: 2.3rem;
position: absolute;
right: -0.8rem;
}
@-webkit-keyframes jdpOpenAnimation {
0% {
transform: scaleY(.8);
transform-origin: 0% 0%;
opacity: 0
}
to {
transform: scaleY(1);
transform-origin: 0% 0%;
opacity: 1
}
}
@keyframes jdpOpenAnimation {
0% {
transform: scaleY(.8);
transform-origin: 0% 0%;
opacity: 0
}
to {
transform: scaleY(1);
transform-origin: 0% 0%;
opacity: 1
}
}
@-webkit-keyframes jdpOpenAnimationMobile {
0% {
bottom: -10%;
opacity: 0;
}
to {
bottom: 0;
opacity: 1;
}
}
@keyframes jdpOpenAnimationMobile {
0% {
margin-bottom: -20%;
opacity: 0;
}
to {
margin-bottom: 0;
opacity: 1;
}
}
.dark jdp-container .jdp-days {
border-color: var(--dark-color-surface-400);
}
.dark jdp-overlay {
background-color: #181f2c;
}
.dark jdp-container {
background: var(--dark-color-background);
border-color: #2c3950;
box-shadow: 0 2px 8px rgba(0,0,0,.15);
color: #fff;
}
.dark jdp-container .jdp-icon-minus,
.dark jdp-container .jdp-icon-plus {
outline-color: var(--dark-color-surface-600);
}
.dark jdp-container .jdp-icon-minus:hover,
.dark jdp-container .jdp-icon-plus:hover {
background-color: var(--dark-color-surface-600);
}
.dark jdp-container .jdp-months,
.dark jdp-container .jdp-years {
fill: rgba(255, 255, 255, 0.9);
color: rgba(255, 255, 255, 0.9);
}
.dark jdp-container .jdp-month,
.dark jdp-container .jdp-month input,
.dark jdp-container .jdp-month select,
.dark jdp-container .jdp-time,
.dark jdp-container .jdp-time input,
.dark jdp-container .jdp-time select,
.dark jdp-container .jdp-year,
.dark jdp-container .jdp-year input,
.dark jdp-container .jdp-year select {
background: var(--dark-color-background);
color: var(--dark-color-text-primary);
}
.dark jdp-container .jdp-day,
.dark jdp-container .jdp-day-name {
border: 1px solid transparent;
color: var(--dark-color-text-primary);
}
.dark jdp-container .jdp-day-name.today,
.dark jdp-container .jdp-day.today {
border-color: var(--color-primary-100);
}
.dark jdp-container .jdp-day.disabled-day {
opacity: 0.15;
}
.dark jdp-container .jdp-day:not(.disabled-day):hover {
background-color: var(--dark-color-surface-600);
color: #fff;
}
.dark jdp-container .jdp-footer {
border-color: var(--dark-color-surface-400);
}
.dark jdp-container .jdp-btn-close,
.dark jdp-container .jdp-btn-empty,
.dark jdp-container .jdp-btn-today {
color: rgb(255 255 255 / 65%);
}
.dark jdp-container .jdp-btn-close:hover,
.dark jdp-container .jdp-btn-empty:hover,
.dark jdp-container .jdp-btn-today:hover {
color: rgb(255, 255, 255);
}
.dark jdp-container .jdp-btn-close.disabled-btn,
.dark jdp-container .jdp-btn-empty.disabled-btn,
.dark jdp-container .jdp-btn-today.disabled-btn {
opacity: 0.2;
}
.dark jdp-container .jdp-time-container .jdp-time select:hover {
background-color: var(--dark-color-surface-600);
color: #fff;
}
.dark jdp-container .jdp-time-container .jdp-time select {
border: 1px solid var(--dark-color-surface-600);
}

File diff suppressed because one or more lines are too long

View File

@@ -85,11 +85,7 @@ func (a *InboundController) addInbound(c *gin.Context) {
}
user := session.GetLoginUser(c)
inbound.UserId = user.Id
if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" {
inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
} else {
inbound.Tag = fmt.Sprintf("inbound-%v:%v", inbound.Listen, inbound.Port)
}
inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
needRestart := false
inbound, needRestart, err = a.inboundService.AddInbound(inbound)
@@ -282,11 +278,7 @@ func (a *InboundController) importInbound(c *gin.Context) {
user := session.GetLoginUser(c)
inbound.Id = 0
inbound.UserId = user.Id
if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" {
inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
} else {
inbound.Tag = fmt.Sprintf("inbound-%v:%v", inbound.Listen, inbound.Port)
}
inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
for index := range inbound.ClientStats {
inbound.ClientStats[index].Id = 0

View File

@@ -10,7 +10,6 @@ type XraySettingController struct {
XraySettingService service.XraySettingService
SettingService service.SettingService
InboundService service.InboundService
OutboundService service.OutboundService
XrayService service.XrayService
}
@@ -27,9 +26,6 @@ func (a *XraySettingController) initRouter(g *gin.RouterGroup) {
g.POST("/update", a.updateSetting)
g.GET("/getXrayResult", a.getXrayResult)
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
g.POST("/warp/:action", a.warp)
g.GET("/getOutboundsTraffic", a.getOutboundsTraffic)
g.POST("/resetOutboundsTraffic", a.resetOutboundsTraffic)
}
func (a *XraySettingController) getXraySetting(c *gin.Context) {
@@ -65,43 +61,3 @@ func (a *XraySettingController) getDefaultXrayConfig(c *gin.Context) {
func (a *XraySettingController) getXrayResult(c *gin.Context) {
jsonObj(c, a.XrayService.GetXrayResult(), nil)
}
func (a *XraySettingController) warp(c *gin.Context) {
action := c.Param("action")
var resp string
var err error
switch action {
case "data":
resp, err = a.XraySettingService.GetWarp()
case "config":
resp, err = a.XraySettingService.GetWarpConfig()
case "reg":
skey := c.PostForm("privateKey")
pkey := c.PostForm("publicKey")
resp, err = a.XraySettingService.RegWarp(skey, pkey)
case "license":
license := c.PostForm("license")
resp, err = a.XraySettingService.SetWarpLicence(license)
}
jsonObj(c, resp, err)
}
func (a *XraySettingController) getOutboundsTraffic(c *gin.Context) {
outboundsTraffic, err := a.OutboundService.GetOutboundsTraffic()
if err != nil {
jsonMsg(c, "Error getting traffics", err)
return
}
jsonObj(c, outboundsTraffic, nil)
}
func (a *XraySettingController) resetOutboundsTraffic(c *gin.Context) {
tag := c.PostForm("tag")
err := a.OutboundService.ResetOutboundTraffic(tag)
if err != nil {
jsonMsg(c, "Error in reset outbound traffics", err)
return
}
jsonObj(c, "", nil)
}

View File

@@ -28,7 +28,6 @@ type AllSetting struct {
RemarkModel string `json:"remarkModel" form:"remarkModel"`
TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"`
TgBotToken string `json:"tgBotToken" form:"tgBotToken"`
TgBotProxy string `json:"tgBotProxy" form:"tgBotProxy"`
TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"`
TgRunTime string `json:"tgRunTime" form:"tgRunTime"`
TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"`
@@ -48,10 +47,6 @@ type AllSetting struct {
SubEncrypt bool `json:"subEncrypt" form:"subEncrypt"`
SubShowInfo bool `json:"subShowInfo" form:"subShowInfo"`
SubURI string `json:"subURI" form:"subURI"`
SubJsonPath string `json:"subJsonPath" form:"subJsonPath"`
SubJsonURI string `json:"subJsonURI" form:"subJsonURI"`
SubJsonFragment string `json:"subJsonFragment" form:"subJsonFragment"`
Datepicker string `json:"datepicker" form:"datepicker"`
}
func (s *AllSetting) CheckValid() error {
@@ -108,13 +103,6 @@ func (s *AllSetting) CheckValid() error {
s.SubPath += "/"
}
if !strings.HasPrefix(s.SubJsonPath, "/") {
s.SubJsonPath = "/" + s.SubJsonPath
}
if !strings.HasSuffix(s.SubJsonPath, "/") {
s.SubJsonPath += "/"
}
_, err := time.LoadLocation(s.TimeLocation)
if err != nil {
return common.NewError("time location not exist:", s.TimeLocation)

View File

@@ -7,6 +7,8 @@
<link rel="stylesheet" href="{{ .base_path }}assets/ant-design-vue@1.7.8/antd.min.css">
<link rel="stylesheet" href="{{ .base_path }}assets/element-ui@2.15.0/theme-chalk/display.css">
<link rel="stylesheet" href="{{ .base_path }}assets/css/custom.css?{{ .cur_ver }}">
<link rel=”icon” type=”image/x-icon” href="{{ .base_path }}assets/favicon.ico">
<link rel="shortcut icon" type="image/x-icon" href="{{ .base_path }}assets/favicon.ico">
<style>
[v-cloak] {
display: none;
@@ -28,5 +30,4 @@
</style>
<title>{{ .host }}-{{ i18n .title}}</title>
</head>
<div id="message"></div>
{{end}}

View File

@@ -1,7 +1,6 @@
{{define "promptModal"}}
<a-modal id="prompt-modal" v-model="promptModal.visible" :title="promptModal.title"
:closable="true" @ok="promptModal.ok" :mask-closable="false"
:confirm-loading="promptModal.confirmLoading"
:ok-text="promptModal.okText" cancel-text='{{ i18n "cancel" }}' :class="themeSwitcher.currentTheme">
<a-input id="prompt-modal-input" :type="promptModal.type"
v-model="promptModal.value"
@@ -18,7 +17,6 @@
value: '',
okText: '{{ i18n "sure"}}',
visible: false,
confirmLoading: false,
keyEnter(e) {
if (this.type !== 'textarea') {
e.preventDefault();
@@ -32,6 +30,7 @@
}
},
ok() {
promptModal.close();
promptModal.confirm(promptModal.value);
},
confirm() {},
@@ -54,10 +53,7 @@
},
close() {
this.visible = false;
},
loading(loading=true) {
this.confirmLoading = loading;
},
}
};
const promptModalApp = new Vue({

View File

@@ -8,23 +8,13 @@
{{ 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>
<a-divider>Subscription</a-divider>
<div class="qr-bg"><canvas @click="copyToClipboard('qrCode-sub',genSubLink(qrModal.client.subId))" id="qrCode-sub" style="width: 100%; height: 100%;"></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>
<canvas @click="copyToClipboard('qrCode-'+index, row.link)"
:id="'qrCode-'+index"
class="qr-bg"></canvas>
<div class="qr-bg"><canvas @click="copyToClipboard('qrCode-'+index, row.link)" :id="'qrCode-'+index" style="width: 100%; height: 100%;"></canvas></div>
</template>
</a-modal>
@@ -45,21 +35,12 @@
this.client = client;
this.subId = '';
this.qrcodes = [];
if (this.inbound.protocol == Protocols.WIREGUARD){
this.inbound.genInboundLinks(dbInbound.remark).split('\r\n').forEach((l,index) =>{
this.qrcodes.push({
remark: "Peer " + (index+1),
link: l
});
this.inbound.genAllLinks(this.dbInbound.remark, app.remarkModel, client).forEach(l => {
this.qrcodes.push({
remark: l.remark,
link: l.link
});
} else {
this.inbound.genAllLinks(this.dbInbound.remark, app.remarkModel, client).forEach(l => {
this.qrcodes.push({
remark: l.remark,
link: l.link
});
});
}
});
this.visible = true;
},
close: function () {
@@ -92,16 +73,12 @@
},
genSubLink(subID) {
return app.subSettings.subURI+subID;
},
genSubJsonLink(subID) {
return app.subSettings.subJsonURI+subID;
}
},
updated() {
if (qrModal.client && qrModal.client.subId) {
qrModal.subId = qrModal.client.subId;
this.setQrCode("qrCode-sub", this.genSubLink(qrModal.subId));
this.setQrCode("qrCode-subJson", this.genSubJsonLink(qrModal.subId));
}
qrModal.qrcodes.forEach((element, index) => {
this.setQrCode("qrCode-" + index, element.link);

View File

@@ -1,16 +1,15 @@
{{define "textModal"}}
<a-modal id="text-modal" v-model="txtModal.visible" :title="txtModal.title"
:closable="true"
:class="themeSwitcher.currentTheme">
<template slot="footer">
<a-button v-if="!ObjectUtil.isEmpty(txtModal.fileName)" icon="download"
:href="'data:application/text;charset=utf-8,' + encodeURIComponent(txtModal.content)"
:download="txtModal.fileName">[[ txtModal.fileName ]]
</a-button>
<a-button type="primary" id="copy-btn">{{ i18n "copy" }}</a-button>
</template>
<a-input style="overflow-y: auto;" type="textarea" v-model="txtModal.content"
:autosize="{ minRows: 10, maxRows: 20}"></a-input>
:closable="true" ok-text='{{ i18n "copy" }}' cancel-text='{{ i18n "close" }}'
:class="themeSwitcher.currentTheme"
:ok-button-props="{attrs:{id:'txt-modal-ok-btn'}}">
<a-button v-if="!ObjectUtil.isEmpty(txtModal.fileName)" type="primary" style="margin-bottom: 10px;"
:href="'data:application/text;charset=utf-8,' + encodeURIComponent(txtModal.content)"
:download="txtModal.fileName">
{{ i18n "download" }} [[ txtModal.fileName ]]
</a-button>
<a-input type="textarea" v-model="txtModal.content"
:autosize="{ minRows: 10, maxRows: 20}"></a-input>
</a-modal>
<script>
@@ -29,7 +28,7 @@
this.visible = true;
textModalApp.$nextTick(() => {
if (this.clipboard === null) {
this.clipboard = new ClipboardJS('#copy-btn', {
this.clipboard = new ClipboardJS('#txt-modal-ok-btn', {
text: () => this.content,
});
this.clipboard.on('success', () => {
@@ -53,4 +52,4 @@
});
</script>
{{end}}
{{end}}

View File

@@ -2,14 +2,9 @@
<html lang="en">
{{template "head" .}}
<style>
html * {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1 {
text-align: center;
/* margin: 20px 0 50px 0;*/
height: 110px;
margin: 20px 0 50px 0;
}
.ant-btn,
.ant-input {
@@ -36,9 +31,7 @@
}
.title {
font-size: 32px;
}
.title b {
font-weight: bold !important;
font-weight: bold;
}
#app {
overflow: hidden;
@@ -49,9 +42,6 @@
border-radius: 2rem;
padding: 3rem;
transition: all 0.3s;
user-select:none;
-webkit-user-select:none;
-moz-user-select: none;
}
#login:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
@@ -71,13 +61,13 @@
z-index: 0;
}
.dark .under {
background-color: var(--dark-color-login-wave);
background-color: #0f2d32;
}
.dark #login {
background-color: var(--dark-color-surface-100);
background-color: #151f31;
}
.dark h1 {
color: rgba(255, 255, 255);
color: rgba(255, 255, 255, 0.85);
}
.ant-form-item {
margin-bottom: 16px;
@@ -124,7 +114,6 @@
position: relative;
border-radius: 25px;
width: 100%;
transition: all 0.3s cubic-bezier(.645,.045,.355,1);
}
.dark .wave-btn-bg {
color: #fff;
@@ -134,6 +123,7 @@
background-origin: border-box;
background-clip: padding-box, border-box;
background-size: 300%;
transition: all 0.3s cubic-bezier(.645,.045,.355,1);
width: 100%;
z-index: 1;
}
@@ -202,7 +192,7 @@
z-index: -1;
}
.dark .waves-header {
background-color: var(--dark-color-login-background);
background-color: #101828;
}
.waves-inner-header {
height: 50vh;
@@ -214,7 +204,7 @@
position: relative;
width: 100%;
height: 15vh;
margin-bottom: -8px; /*Fix for safari gap*/
margin-bottom: -5px; /*Fix for safari gap*/
min-height: 100px;
max-height: 150px;
}
@@ -222,27 +212,23 @@
animation: move-forever 25s cubic-bezier(0.55, 0.5, 0.45, 0.5) infinite;
}
.dark .parallax > use {
fill: var(--dark-color-login-wave);
fill: rgb(10 117 87 / 20%);
}
.parallax > use:nth-child(1) {
animation-delay: -2s;
animation-duration: 4s;
animation-duration: 7s;
opacity: 0.2;
}
.parallax > use:nth-child(2) {
animation-delay: -3s;
animation-duration: 7s;
animation-duration: 10s;
opacity: 0.4;
}
.parallax > use:nth-child(3) {
animation-delay: -4s;
animation-duration: 10s;
animation-duration: 13s;
opacity: 0.6;
}
.parallax > use:nth-child(4) {
animation-delay: -5s;
animation-duration: 13s;
}
@keyframes move-forever {
0% {
transform: translate3d(-90px, 0, 0);
@@ -257,225 +243,91 @@
min-height: 40px;
}
}
.words-wrapper {
width: 100%;
display: inline-block;
position: relative;
text-align: center;
}
.words-wrapper b {
width: 100%;
display: inline-block;
position: absolute;
left: 0;
top: 0;
}
.words-wrapper b.is-visible {
position: relative;
}
.headline.zoom .words-wrapper {
-webkit-perspective: 300px;
-moz-perspective: 300px;
perspective: 300px;
}
.headline {
display: flex;
justify-content: center;
align-items: center;
}
.headline.zoom b {
opacity: 0;
}
.headline.zoom b.is-visible {
opacity: 1;
-webkit-animation: zoom-in 0.8s;
-moz-animation: zoom-in 0.8s;
animation: cubic-bezier(0.215, 0.610, 0.355, 1.000) zoom-in 0.8s;
}
.headline.zoom b.is-hidden {
-webkit-animation: zoom-out 0.8s;
-moz-animation: zoom-out 0.8s;
animation: cubic-bezier(0.215, 0.610, 0.355, 1.000) zoom-out 0.4s;
}
@-webkit-keyframes zoom-in {
0% {
opacity: 0;
-webkit-transform: translateZ(100px);
}
100% {
opacity: 1;
-webkit-transform: translateZ(0);
}
}
@-moz-keyframes zoom-in {
0% {
opacity: 0;
-moz-transform: translateZ(100px);
}
100% {
opacity: 1;
-moz-transform: translateZ(0);
}
}
@keyframes zoom-in {
0% {
opacity: 0;
-webkit-transform: translateZ(100px);
-moz-transform: translateZ(100px);
-ms-transform: translateZ(100px);
-o-transform: translateZ(100px);
transform: translateZ(100px);
}
100% {
opacity: 1;
-webkit-transform: translateZ(0);
-moz-transform: translateZ(0);
-ms-transform: translateZ(0);
-o-transform: translateZ(0);
transform: translateZ(0);
}
}
@-webkit-keyframes zoom-out {
0% {
opacity: 1;
-webkit-transform: translateZ(0);
}
100% {
opacity: 0;
-webkit-transform: translateZ(-100px);
}
}
@-moz-keyframes zoom-out {
0% {
opacity: 1;
-moz-transform: translateZ(0);
}
100% {
opacity: 0;
-moz-transform: translateZ(-100px);
}
}
@keyframes zoom-out {
0% {
opacity: 1;
-webkit-transform: translateZ(0);
-moz-transform: translateZ(0);
-ms-transform: translateZ(0);
-o-transform: translateZ(0);
transform: translateZ(0);
}
100% {
opacity: 0;
-webkit-transform: translateZ(-100px);
-moz-transform: translateZ(-100px);
-ms-transform: translateZ(-100px);
-o-transform: translateZ(-100px);
transform: translateZ(-100px);
}
}
</style>
<body>
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
<transition name="list" appear>
<a-layout-content class="under" style="min-height: 0;">
<div class="waves-header">
<div class="waves-inner-header"></div>
<svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 24 150 28" preserveAspectRatio="none" shape-rendering="auto">
<defs>
<path id="gentle-wave" d="M-160 44c30 0 58-18 88-18s 58 18 88 18 58-18 88-18 58 18 88 18 v44h-352z" />
</defs>
<g class="parallax">
<use xlink:href="#gentle-wave" x="48" y="0" fill="rgba(0, 135, 113, 0.08)" />
<use xlink:href="#gentle-wave" x="48" y="3" fill="rgba(0, 135, 113, 0.08)" />
<use xlink:href="#gentle-wave" x="48" y="5" fill="rgba(0, 135, 113, 0.08)" />
<use xlink:href="#gentle-wave" x="48" y="7" fill="#c7ebe2" />
</g>
</svg>
</div>
<a-row type="flex" justify="center" align="middle" style="height: 100%; overflow: auto;">
<a-col :xs="22" :sm="20" :md="14" :lg="10" :xl="8" :xxl="6" id="login" style="margin: 3rem 0;">
<a-layout-content class="under" style="min-height: 0;">
<div class="waves-header">
<div class="waves-inner-header"></div>
<svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 24 150 28" preserveAspectRatio="none" shape-rendering="auto">
<defs>
<path id="gentle-wave" d="M-160 44c30 0 58-18 88-18s 58 18 88 18 58-18 88-18 58 18 88 18 v44h-352z" />
</defs>
<g class="parallax">
<use xlink:href="#gentle-wave" x="48" y="3" fill="rgba(0, 135, 113, 0.08)" />
<use xlink:href="#gentle-wave" x="48" y="5" fill="rgba(0, 135, 113, 0.08)" />
<use xlink:href="#gentle-wave" x="48" y="7" fill="rgba(0, 135, 113, 0.08)" />
</g>
</svg>
</div>
<a-row type="flex" justify="center" align="middle" style="height: 100%; overflow: auto;">
<a-col :xs="22" :sm="20" :md="14" :lg="10" :xl="8" :xxl="6" id="login" style="margin: 3rem 0;">
<a-row type="flex" justify="center">
<a-col style="width: 100%;">
<h1 class="title headline zoom">
<span class="words-wrapper">
<b class="is-visible">{{ i18n "pages.login.hello" }}</b>
<b>{{ i18n "pages.login.title" }}</b>
</span>
</h1>
</a-col>
<a-col>
<h1 class="title">{{ i18n "pages.login.title" }}</h1>
</a-col>
</a-row>
<a-row type="flex" justify="center">
<a-col span="24">
<a-form>
<a-form-item>
<a-input 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"
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"
placeholder='{{ i18n "secretToken" }}'
@keydown.enter.native="login">
</password-input>
</a-form-item>
<a-form-item>
<a-row justify="center" class="centered">
<div style="height: 50px;" class="wave-btn-bg wave-btn-bg-cl"
:style="loading ? { width: '52px' } : { display: 'inline-block' }">
<a-button class="ant-btn-primary-login" type="primary"
:loading="loading" @click="login"
:icon="loading ? 'poweroff' : undefined">
[[ loading ? '' : '{{ i18n "login" }}' ]]
</a-button>
</div>
</a-row>
</a-form-item>
<a-form-item>
<a-row justify="center" class="centered">
<a-col :span="24">
<a-select ref="selectLang" v-model="lang"
@change="setLang(lang)" style="width: 150px;"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="l.value" label="English" v-for="l in supportLangs">
<span role="img" aria-label="l.name" v-text="l.icon"></span>
&nbsp;&nbsp;<span v-text="l.name"></span>
</a-select-option>
</a-select>
</a-col>
</a-row>
</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>&nbsp;
</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>
</a-row>
</a-form-item>
</a-form>
</a-col>
<a-col span="24">
<a-form>
<a-form-item>
<a-input v-model.trim="user.username" placeholder='{{ i18n "username" }}'
@keydown.enter.native="login" autofocus>
<a-icon slot="prefix" type="user" style="font-size: 16px;"/>
</a-input>
</a-form-item>
<a-form-item>
<password-input 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"
placeholder='{{ i18n "secretToken" }}' @keydown.enter.native="login">
</password-input>
</a-input>
</a-form-item>
<a-form-item>
<a-row justify="center" class="centered">
<div class="wave-btn-bg wave-btn-bg-cl" :style="loading ? { width: '54px' } : { display: 'inline-block' }">
<a-button class="ant-btn-primary-login" type="primary" :loading="loading" @click="login" :icon="loading ? 'poweroff' : undefined"
:style="loading ? { width: '50px' } : { display: 'inline-block' }">
[[ loading ? '' : '{{ i18n "login" }}' ]]
</a-button>
</div>
</a-row>
</a-form-item>
<a-form-item>
<a-row justify="center" class="centered">
<a-col :span="24">
<a-select ref="selectLang" v-model="lang" @change="setLang(lang)" style="width: 150px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="l.value" label="English" v-for="l in supportLangs">
<span role="img" aria-label="l.name" v-text="l.icon"></span>
&nbsp;&nbsp;<span v-text="l.name"></span>
</a-select-option>
</a-select>
</a-col>
</a-row>
</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>&nbsp;
</a-col>
<a-col>
<theme-switch />
</a-col>
</a-row>
</a-form-item>
</a-form>
</a-col>
</a-row>
</a-col>
</a-row>
</a-layout-content>
</a-col>
</a-row>
</a-layout-content>
</transition>
</a-layout>
</a-layout>
{{template "js" .}}
{{template "component/themeSwitcher" .}}
{{template "component/password" .}}
@@ -521,42 +373,6 @@
},
},
});
document.addEventListener("DOMContentLoaded", function() {
var animationDelay = 2000;
initHeadline();
function initHeadline() {
animateHeadline(document.querySelectorAll('.headline'));
}
function animateHeadline(headlines) {
var duration = animationDelay;
headlines.forEach(function(headline) {
setTimeout(function() {
hideWord(headline.querySelector('.is-visible'));
}, duration);
});
}
function hideWord(word) {
var nextWord = takeNext(word);
switchWord(word, nextWord);
setTimeout(function() {
hideWord(nextWord);
}, animationDelay);
}
function takeNext(word) {
return (word.nextElementSibling) ? word.nextElementSibling : word.parentElement.firstElementChild;
}
function switchWord(oldWord, newWord) {
oldWord.classList.remove('is-visible');
oldWord.classList.add('is-hidden');
newWord.classList.remove('is-hidden');
newWord.classList.add('is-visible');
}
});
</script>
</body>
</html>

View File

@@ -82,7 +82,7 @@
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
{{ i18n "pages.inbounds.totalFlow" }}
{{ i18n "pages.inbounds.totalFlow" }} (GB)
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
@@ -104,10 +104,8 @@
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-date-picker v-if="datepicker == 'gregorian'" :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.currentTheme" v-model="clientsBulkModal.expiryTime"></a-date-picker>
<persian-datepicker v-else placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}'
value="clientsBulkModal.expiryTime" v-model="clientsBulkModal.expiryTime"></persian-datepicker>
</a-form-item>
<a-form-item v-if="clientsBulkModal.expiryTime != 0">
<template slot="label">
@@ -220,7 +218,7 @@
clientsBulkModal.visible = false;
clientsBulkModal.loading(false);
},
loading(loading=true) {
loading(loading) {
clientsBulkModal.confirmLoading = loading;
},
};
@@ -236,9 +234,6 @@
get delayedExpireDays() {
return this.clientsBulkModal.expiryTime < 0 ? this.clientsBulkModal.expiryTime / -86400000 : 0;
},
get datepicker() {
return app.datepicker;
},
set delayedExpireDays(days) {
this.clientsBulkModal.expiryTime = -86400000 * days;
},

View File

@@ -72,7 +72,7 @@
clientModal.visible = false;
clientModal.loading(false);
},
loading(loading=true) {
loading(loading) {
clientModal.confirmLoading = loading;
},
};
@@ -94,9 +94,6 @@
get isEdit() {
return this.clientModal.isEdit;
},
get datepicker() {
return app.datepicker;
},
get isTrafficExhausted() {
if (!clientStats) return false
if (clientStats.total <= 0) return false

View File

@@ -1,23 +1,27 @@
{{define "menuItems"}}
<a-menu-item key="{{ .base_path }}panel/">
<a-icon type="dashboard"></a-icon>
<span><b>{{ i18n "menu.dashboard"}}</b></span>
<span>{{ i18n "menu.dashboard"}}</span>
</a-menu-item>
<a-menu-item key="{{ .base_path }}panel/inbounds">
<a-icon type="user"></a-icon>
<span><b>{{ i18n "menu.inbounds"}}</b></span>
<span>{{ i18n "menu.inbounds"}}</span>
</a-menu-item>
<a-menu-item key="{{ .base_path }}panel/settings">
<a-icon type="setting"></a-icon>
<span><b>{{ i18n "menu.settings"}}</b></span>
<span>{{ i18n "menu.settings"}}</span>
</a-menu-item>
<a-menu-item key="{{ .base_path }}panel/xray">
<a-icon type="tool"></a-icon>
<span><b>{{ i18n "menu.xray"}}</b></span>
<span>{{ i18n "menu.xray"}}</span>
</a-menu-item>
<!--<a-menu-item key="{{ .base_path }}panel/clients">-->
<!-- <a-icon type="laptop"></a-icon>-->
<!-- <span>Client</span>-->
<!--</a-menu-item>-->
<a-menu-item key="{{ .base_path }}logout">
<a-icon type="logout"></a-icon>
<span><b>{{ i18n "menu.logout"}}</b></span>
<span>{{ i18n "menu.logout"}}</span>
</a-menu-item>
{{end}}
@@ -27,13 +31,7 @@
<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>
<theme-switch />
</a-menu-item>
</a-menu>
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" :selected-keys="['{{ .request_uri }}']"
@@ -52,13 +50,7 @@
<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>
<theme-switch />
</a-menu-item>
</a-menu>
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" :selected-keys="['{{ .request_uri }}']"

View File

@@ -1,60 +0,0 @@
{{define "component/persianDatepickerTemplate"}}
<template>
<div>
<a-input :value="value" type="text" v-model="date" data-jdp class="persian-datepicker"
@input="$emit('input', convertToGregorian($event.target.value)); jalaliDatepicker.hide();"
:placeholder="placeholder">
<template #addonAfter>
<a-icon type="calendar" style="font-size: 14px; opacity: 0.5;"/>
</template>
</a-input>
</div>
</template>
{{end}}
{{define "component/persianDatepicker"}}
<link rel="stylesheet" href="{{ .base_path }}assets/persian-datepicker/persian-datepicker.min.css"/>
<script src="{{ .base_path }}assets/moment/moment-jalali.min.js"></script>
<script src="{{ .base_path }}assets/persian-datepicker/persian-datepicker.min.js"></script>
<script>
const persianDatepicker = {};
Vue.component('persian-datepicker', {
props: ['placeholder', 'format', 'value'],
template: `{{template "component/persianDatepickerTemplate"}}`,
data() {
return {
date: '',
persianDatepicker,
};
},
watch: {
value: function (date) {
this.date = this.convertToJalalian(date)
}
},
mounted() {
this.date = this.convertToJalalian(this.value)
this.listenToDatepicker()
},
methods: {
convertToGregorian(date) {
return date ? moment(moment(date, 'jYYYY/jMM/jDD HH:mm:ss').format('YYYY-MM-DD HH:mm:ss')) : null
},
convertToJalalian(date) {
return date && moment.isMoment(date) ? date.format('jYYYY/jMM/jDD HH:mm:ss') : null
},
listenToDatepicker() {
jalaliDatepicker.startWatch({
time: true,
zIndex: '9999',
hideAfterChange: true,
useDropDownYears: false,
changeMonthRotateYear: true,
});
},
}
});
</script>
{{end}}

View File

@@ -1,224 +0,0 @@
{{define "component/sortableTableTrigger"}}
<a-icon type="drag"
class="sortable-icon"
style="cursor: move;"
@mouseup="mouseUpHandler"
@mousedown="mouseDownHandler"
@click="clickHandler" />
{{end}}
{{define "component/sortableTable"}}
<script>
const DRAGGABLE_ROW_CLASS = 'draggable-row';
const findParentRowElement = (el) => {
if (!el || !el.tagName) {
return null;
} else if (el.classList.contains(DRAGGABLE_ROW_CLASS)) {
return el;
} else if (el.parentNode) {
return findParentRowElement(el.parentNode);
} else {
return null;
}
}
Vue.component('a-table-sortable', {
data() {
return {
sortingElementIndex: null,
newElementIndex: null,
};
},
props: ['data-source', 'customRow'],
inheritAttrs: false,
provide() {
const sortable = {}
Object.defineProperty(sortable, "setSortableIndex", {
enumerable: true,
get: () => this.setCurrentSortableIndex,
});
Object.defineProperty(sortable, "resetSortableIndex", {
enumerable: true,
get: () => this.resetSortableIndex,
});
return {
sortable,
}
},
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,
},
this.$slots.default,
)
},
created() {
this.$memoSort = {};
},
methods: {
isDragging() {
const currentIndex = this.sortingElementIndex;
return currentIndex !== null && currentIndex !== undefined;
},
resetSortableIndex(e, index) {
this.sortingElementIndex = null;
this.newElementIndex = null;
this.$memoSort = {};
},
setCurrentSortableIndex(e, index) {
this.sortingElementIndex = index;
},
dragStartHandler(e, index) {
if (!this.isDragging()) {
e.preventDefault();
return;
}
},
dragStopHandler(e, index) {
this.resetSortableIndex(e, index);
},
dragOverHandler(e, index) {
if (!this.isDragging()) {
return;
}
e.preventDefault();
const currentIndex = this.sortingElementIndex;
if (index === currentIndex) {
this.newElementIndex = null;
return;
}
const row = findParentRowElement(e.target);
if (!row) {
return;
}
const rect = row.getBoundingClientRect();
const offsetTop = e.pageY - rect.top;
if (offsetTop < rect.height / 2) {
this.newElementIndex = Math.max(index - 1, 0);
} else {
this.newElementIndex = index;
}
},
dropHandler(e) {
if (this.isDragging()) {
this.$emit('onsort', this.sortingElementIndex, this.newElementIndex);
}
},
customRowRender(record, index) {
const parentMethodResult = this.customRow?.(record, index) || {};
const newIndex = this.newElementIndex;
const currentIndex = this.sortingElementIndex;
return {
...parentMethodResult,
attrs: {
...(parentMethodResult?.attrs || {}),
draggable: true,
},
on: {
...(parentMethodResult?.on || {}),
dragstart: (e) => this.dragStartHandler(e, index),
dragend: (e) => this.dragStopHandler(e, index),
dragover: (e) => this.dragOverHandler(e, index),
},
class: {
...(parentMethodResult?.class || {}),
[DRAGGABLE_ROW_CLASS]: true,
['dragging']: this.isDragging()
? (newIndex === null ? index === currentIndex : index === newIndex)
: false,
},
};
}
},
computed: {
records() {
const newIndex = this.newElementIndex;
const currentIndex = this.sortingElementIndex;
if (!this.isDragging() || newIndex === null || currentIndex === newIndex) {
return this.dataSource;
}
if (this.$memoSort.newIndex === newIndex) {
return this.$memoSort.list;
}
let list = [...this.dataSource];
list.splice(newIndex, 0, list.splice(currentIndex, 1)[0]);
this.$memoSort = {
newIndex,
list,
};
return list;
}
}
});
Vue.component('table-sort-trigger', {
template: `{{template "component/sortableTableTrigger"}}`,
props: ['item-index'],
inject: ['sortable'],
methods: {
mouseDownHandler(e) {
if (this.sortable) {
this.sortable.setSortableIndex(e, this.itemIndex);
}
},
mouseUpHandler(e) {
if (this.sortable) {
this.sortable.resetSortableIndex(e, this.itemIndex);
}
},
clickHandler(e) {
e.preventDefault();
},
}
})
</script>
<style>
@media only screen and (max-width: 767px) {
.sortable-icon {
display: none;
}
}
.ant-table-is-sorting .draggable-row td {
background-color: white !important;
}
.dark .ant-table-is-sorting .draggable-row td {
background-color: var(--dark-color-surface-100) !important;
}
.ant-table-is-sorting .dragging {
opacity: 0.5;
}
.ant-table-is-sorting .dragging .ant-table-row-index {
opacity: 0;
}
</style>
{{end}}

View File

@@ -10,48 +10,25 @@
<script>
function createThemeSwitcher() {
const isDarkTheme = localStorage.getItem('dark-mode') === 'true';
const isUltra = localStorage.getItem('isUltraDarkThemeEnabled') === 'true';
if (isUltra) {
document.documentElement.setAttribute('data-theme', 'ultra-dark');
}
const theme = isDarkTheme ? 'dark' : 'light';
document.querySelector('body').setAttribute('class', theme);
return {
isDarkTheme,
isUltra,
get currentTheme() {
return this.isDarkTheme ? 'dark' : 'light';
},
toggleTheme() {
this.isDarkTheme = !this.isDarkTheme;
localStorage.setItem('dark-mode', this.isDarkTheme);
document.querySelector('body').setAttribute('class', this.isDarkTheme ? 'dark' : 'light');
document.getElementById('message').className = themeSwitcher.currentTheme;
},
toggleUltra() {
this.isUltra = !this.isUltra;
if (this.isUltra) {
document.documentElement.setAttribute('data-theme', 'ultra-dark');
} else {
document.documentElement.removeAttribute('data-theme');
}
localStorage.setItem('isUltraDarkThemeEnabled', this.isUltra.toString());
}
};
}
const themeSwitcher = createThemeSwitcher();
Vue.component('theme-switch', {
props: [],
template: `{{template "component/themeSwitchTemplate"}}`,
data: () => ({
themeSwitcher
}),
mounted() {
this.$message.config({
getContainer: () => document.getElementById('message')
});
document.getElementById('message').className = themeSwitcher.currentTheme;
}
data: () => ({ themeSwitcher }),
});
</script>
{{end}}
{{end}}

View File

@@ -1,86 +0,0 @@
{{define "dnsModal"}}
<a-modal id="dns-modal" v-model="dnsModal.visible" :title="dnsModal.title" @ok="dnsModal.ok"
:closable="true" :mask-closable="false"
:ok-text="dnsModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "pages.xray.outbound.address" }}'>
<a-input v-model.trim="dnsModal.dnsServer.address"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.dns.domains" }}'>
<a-button size="small" type="primary" @click="dnsModal.dnsServer.domains.push('')">+</a-button>
<template v-for="(domain, index) in dnsModal.dnsServer.domains">
<a-input v-model.trim="dnsModal.dnsServer.domains[index]">
<a-button size="small" slot="addonAfter" @click="dnsModal.dnsServer.domains.splice(index,1)">-</a-button>
</a-input>
</template>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.dns.strategy" }}' v-if="isAdvanced">
<a-select
v-model="dnsModal.dnsServer.queryStrategy"
style="width: 100%"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="l" :label="l" v-for="l in ['UseIP', 'UseIPv4', 'UseIPv6']">
[[ l ]]
</a-select-option>
</a-select>
</a-form-item>
</a-form>
</a-modal>
<script>
const dnsModal = {
title: '',
visible: false,
okText: '{{ i18n "confirm" }}',
isEdit: false,
confirm: null,
dnsServer: {
address: "localhost",
domains: [],
queryStrategy: 'UseIP',
},
ok() {
domains = dnsModal.dnsServer.domains.filter(d => d.length>0);
dnsModal.dnsServer.domains = domains;
newDnsServer = domains.length > 0 ? dnsModal.dnsServer : dnsModal.dnsServer.address;
ObjectUtil.execute(dnsModal.confirm, newDnsServer);
},
show({ title='', okText='{{ i18n "confirm" }}', dnsServer, confirm=(dnsServer)=>{}, isEdit=false }) {
this.title = title;
this.okText = okText;
this.confirm = confirm;
this.visible = true;
if(isEdit) {
if (typeof dnsServer == 'object'){
this.dnsServer = dnsServer;
} else {
this.dnsServer.address = dnsServer?? '';
}
} else {
this.dnsServer = {
address: "localhost",
domains: [],
queryStrategy: 'UseIP',
}
}
this.isEdit = isEdit;
},
close() {
dnsModal.visible = false;
},
};
new Vue({
delimiters: ['[[', ']]'],
el: '#dns-modal',
data: {
dnsModal: dnsModal,
},
computed: {
isAdvanced: {
get: function () { return dnsModal.dnsServer.domains.length>0 }
}
}
});
</script>
{{end}}

View File

@@ -1,57 +0,0 @@
{{define "fakednsModal"}}
<a-modal id="fakedns-modal" v-model="fakednsModal.visible" :title="fakednsModal.title" @ok="fakednsModal.ok"
:closable="true" :mask-closable="false"
:ok-text="fakednsModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "pages.xray.fakedns.ipPool" }}'>
<a-input v-model.trim="fakednsModal.fakeDns.ipPool"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.fakedns.poolSize" }}'>
<a-input-number style="width: 100%;" type="number" min="1" v-model.trim="fakednsModal.fakeDns.poolSize"></a-input-number>
</a-form-item>
</a-form>
</a-modal>
<script>
const fakednsModal = {
title: '',
visible: false,
okText: '{{ i18n "confirm" }}',
isEdit: false,
confirm: null,
fakeDns: {
ipPool: "198.18.0.0/16",
poolSize: 65535,
},
ok() {
ObjectUtil.execute(fakednsModal.confirm, fakednsModal.fakeDns);
},
show({ title='', okText='{{ i18n "confirm" }}', fakeDns, confirm=(fakeDns)=>{}, isEdit=false }) {
this.title = title;
this.okText = okText;
this.confirm = confirm;
this.visible = true;
if(isEdit) {
this.fakeDns = fakeDns;
} else {
this.fakeDns = {
ipPool: "198.18.0.0/16",
poolSize: 65535,
}
}
this.isEdit = isEdit;
},
close() {
fakednsModal.visible = false;
},
};
new Vue({
delimiters: ['[[', ']]'],
el: '#fakedns-modal',
data: {
fakednsModal: fakednsModal,
}
});
</script>
{{end}}

View File

@@ -19,7 +19,7 @@
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "reset" }}</span>
<span>{{ i18n "pages.client.renew" }}</span>
</template>
{{ i18n "password" }}
<a-icon v-if="inbound.protocol === Protocols.SHADOWSOCKS"@click="client.password = RandomUtil.randomShadowsocksPassword()" type="sync"></a-icon>
@@ -32,7 +32,7 @@
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "reset" }}</span>
<span>{{ i18n "pages.client.renew" }}</span>
</template>
ID <a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"></a-icon>
</a-tooltip>
@@ -117,7 +117,7 @@
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
{{ i18n "pages.inbounds.totalFlow" }}
{{ i18n "pages.inbounds.totalFlow" }}(GB)
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
@@ -150,10 +150,8 @@
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-date-picker v-if="datepicker == 'gregorian'" :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.currentTheme" v-model="client._expiryTime"></a-date-picker>
<persian-datepicker v-else placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}'
value="client._expiryTime" v-model="client._expiryTime"></persian-datepicker>
<a-tag color="red" v-if="isEdit && isExpiry">Expired</a-tag>
</a-form-item>
<a-form-item v-if="client.expiryTime != 0">

View File

@@ -1,6 +1,6 @@
{{define "form/inbound"}}
<!-- base -->
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "enable" }}'>
<a-switch v-model="dbInbound.enable"></a-switch>
</a-form-item>
@@ -37,7 +37,7 @@
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
{{ i18n "pages.inbounds.totalFlow" }}
{{ i18n "pages.inbounds.totalFlow" }}(GB)
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
@@ -54,11 +54,9 @@
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-date-picker style="width: 100%;" v-if="datepicker == 'gregorian'" :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.currentTheme"
v-model="dbInbound._expiryTime"></a-date-picker>
<persian-datepicker v-else placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}'
value="dbInbound._expiryTime" v-model="dbInbound._expiryTime"></persian-datepicker>
</a-form-item>
</a-form>
@@ -97,11 +95,6 @@
{{template "form/http"}}
</template>
<!-- wireguard -->
<template v-if="inbound.protocol === Protocols.WIREGUARD">
{{template "form/wireguard"}}
</template>
<!-- stream settings -->
<template v-if="inbound.canEnableStream()">
{{template "form/streamSettings"}}
@@ -114,7 +107,7 @@
</template>
<!-- sniffing -->
<template>
<template v-if="inbound.canSniffing()">
{{template "form/sniffing"}}
</template>
{{end}}
{{end}}

View File

@@ -18,7 +18,7 @@
<a-select
v-model="outbound.settings.domainStrategy"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in OutboundDomainStrategies" :value="s">[[ s ]]</a-select-option>
<a-select-option v-for="s in outboundDomainStrategies" :value="s">[[ s ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='Fragment'>
@@ -46,114 +46,24 @@
<!-- blackhole settings -->
<template v-if="outbound.protocol === Protocols.Blackhole">
<a-form-item label='Response Type'>
<a-select
v-model="outbound.settings.type"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['', 'none','http']" :value="s">[[ s ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='Response Type'>
<a-select
v-model="outbound.settings.type"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['', 'none','http']" :value="s">[[ s ]]</a-select-option>
</a-select>
</a-form-item>
</template>
<!-- dns settings -->
<template v-if="outbound.protocol === Protocols.DNS">
<a-form-item label='{{ i18n "pages.inbounds.network" }}'>
<a-select
v-model="outbound.settings.network"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['udp','tcp']" :value="s">[[ s ]]</a-select-option>
</a-select>
</a-form-item>
</template>
<!-- wireguard settings -->
<template v-if="outbound.protocol === Protocols.Wireguard">
<a-form-item>
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
</template>
{{ i18n "pages.xray.outbound.address" }} <a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="outbound.settings.address"></a-input>
</a-form-item>
<a-form-item>
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "reset" }}</span>
</template>
{{ i18n "pages.xray.wireguard.secretKey" }}
<a-icon type="sync"
@click="[outbound.settings.pubKey, outbound.settings.secretKey] = Object.values(Wireguard.generateKeypair())">
</a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="outbound.settings.secretKey"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.wireguard.publicKey" }}'>
<a-input disabled v-model="outbound.settings.pubKey"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.wireguard.domainStrategy" }}'>
<a-select v-model="outbound.settings.domainStrategy" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="wds in ['', ...WireguardDomainStrategy]" :value="wds">[[ wds ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='MTU'>
<a-input-number v-model.number="outbound.settings.mtu"></a-input-number>
</a-form-item>
<a-form-item label='Workers'>
<a-input-number min="0" v-model.number="outbound.settings.workers"></a-input>
</a-form-item>
<a-form-item label='Kernel Mode'>
<a-switch v-model="outbound.settings.kernelMode"></a-switch>
</a-form-item>
<a-form-item>
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
</template>
Reserved <a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input v-model="outbound.settings.reserved"></a-input>
</a-form-item>
<a-form-item label="Peers">
<a-button type="primary" size="small" @click="outbound.settings.addPeer()">+</a-button>
</a-form-item>
<a-form v-for="(peer, index) in outbound.settings.peers" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-divider style="margin:0;">
Peer [[ index + 1 ]]
<a-icon v-if="outbound.settings.peers.length>1" type="delete" @click="() => outbound.settings.delPeer(index)"
style="color: rgb(255, 77, 79);cursor: pointer;"/>
</a-divider>
<a-form-item label='{{ i18n "pages.xray.wireguard.endpoint" }}'>
<a-input v-model.trim="peer.endpoint"></a-input>
<a-form-item label='{{ i18n "pages.inbounds.network" }}'>
<a-select
v-model="outbound.settings.network"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['udp','tcp']" :value="s">[[ s ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.wireguard.publicKey" }}'>
<a-input v-model.trim="peer.publicKey"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.wireguard.psk" }}'>
<a-input v-model.trim="peer.psk"></a-input>
</a-form-item>
<a-form-item>
<template slot="label">
{{ i18n "pages.xray.wireguard.allowedIPs" }} <a-button type="primary" size="small" @click="peer.allowedIPs.push('')">+</a-button>
</template>
<template v-for="(aip, index) in peer.allowedIPs" style="margin-bottom: 10px;">
<a-input v-model.trim="peer.allowedIPs[index]">
<a-button v-if="peer.allowedIPs.length>1" slot="addonAfter" size="small" @click="peer.allowedIPs.splice(index, 1)">-</a-button>
</a-input>
</template>
</a-form-item>
<a-form-item label='Keep Alive'>
<a-input-number v-model.number="peer.keepAlive" :min="0"></a-input>
</a-form-item>
</a-form>
</template>
<!-- Address + Port -->
@@ -171,8 +81,8 @@
<a-form-item label='ID'>
<a-input v-model.trim="outbound.settings.id"></a-input>
</a-form-item>
<!-- vless settings -->
<template v-if="outbound.canEnableTlsFlow()">
<!-- vless settings -->
<template v-if="outbound.canEnableTlsFlow()">
<a-form-item label='Flow'>
<a-select v-model="outbound.settings.flow" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
@@ -184,32 +94,25 @@
<!-- Servers (trojan/shadowsocks/socks/http) settings -->
<template v-if="outbound.hasServers()">
<!-- http / socks -->
<template v-if="outbound.hasUsername()">
<a-form-item label='{{ i18n "username" }}'>
<a-input v-model.trim="outbound.settings.user"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "password" }}'>
<a-input v-model.trim="outbound.settings.pass"></a-input>
</a-form-item>
</template>
<!-- trojan/shadowsocks -->
<template v-if="[Protocols.Trojan, Protocols.Shadowsocks].includes(outbound.protocol)">
<a-form-item label='{{ i18n "password" }}'>
<a-input v-model.trim="outbound.settings.password"></a-input>
<a-input v-model.trim="outbound.settings.password"></a-input>
</a-form-item>
</template>
<!-- shadowsocks -->
<template v-if="outbound.protocol === Protocols.Shadowsocks">
<a-form-item label='{{ i18n "encryption" }}'>
<a-select v-model="outbound.settings.method" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="(method, method_name) in SSMethods" :value="method">[[ method_name ]]</a-select-option>
<a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='UDP over TCP'>
<a-switch v-model="outbound.settings.uot"></a-switch>
</a-form-item>
</template>
</template>
</template>
<!-- stream settings -->
@@ -218,9 +121,9 @@
<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="kcp">KCP</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="http">HTTP2</a-select-option>
<a-select-option value="quic">QUIC</a-select-option>
<a-select-option value="grpc">gRPC</a-select-option>
</a-select>
@@ -246,12 +149,12 @@
<template v-if="outbound.stream.network === 'kcp'">
<a-form-item label='{{ i18n "camouflage" }}'>
<a-select v-model="outbound.stream.kcp.type" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="none">None</a-select-option>
<a-select-option value="srtp">SRTP</a-select-option>
<a-select-option value="utp">uTP</a-select-option>
<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="none">none (not camouflage)</a-select-option>
<a-select-option value="srtp">srtp (video call)</a-select-option>
<a-select-option value="utp">utp (BT download)</a-select-option>
<a-select-option value="wechat-video">wechat-video (WeChat video)</a-select-option>
<a-select-option value="dtls">dtls (DTLS 1.2 packages)</a-select-option>
<a-select-option value="wireguard">wireguard (wireguard packages)</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='{{ i18n "password" }}'>
@@ -263,19 +166,19 @@
<a-form-item label='TTI (ms)'>
<a-input-number v-model.number="outbound.stream.kcp.tti"></a-input-number>
</a-form-item>
<a-form-item label='Uplink (MB/s)'>
<a-form-item label='Uplink Capacity (MB/s)'>
<a-input-number v-model.number="outbound.stream.kcp.upCap"></a-input-number>
</a-form-item>
<a-form-item label='Downlink (MB/s)'>
<a-form-item label='Downlink Capacity (MB/s)'>
<a-input-number v-model.number="outbound.stream.kcp.downCap"></a-input-number>
</a-form-item>
<a-form-item label='Congestion'>
<a-switch v-model="outbound.stream.kcp.congestion"></a-switch>
</a-form-item>
<a-form-item label='Read Buffer (MB)'>
<a-form-item label='Read Buffer Size (MB)'>
<a-input-number v-model.number="outbound.stream.kcp.readBuffer"></a-input-number>
</a-form-item>
<a-form-item label='Write Buffer (MB)'>
<a-form-item label='Write Buffer Size (MB)'>
<a-input-number v-model.number="outbound.stream.kcp.writeBuffer"></a-input-number>
</a-form-item>
</template>
@@ -304,9 +207,9 @@
<template v-if="outbound.stream.network === 'quic'">
<a-form-item label='{{ i18n "pages.inbounds.stream.quic.encryption" }}'>
<a-select v-model="outbound.stream.quic.security" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="none">None</a-select-option>
<a-select-option value="aes-128-gcm">AES-128-GCM</a-select-option>
<a-select-option value="chacha20-poly1305">CHACHA20-POLY1305</a-select-option>
<a-select-option value="none">none</a-select-option>
<a-select-option value="aes-128-gcm">aes-128-gcm</a-select-option>
<a-select-option value="chacha20-poly1305">chacha20-poly1305</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='{{ i18n "password" }}'>
@@ -314,12 +217,12 @@
</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>
<a-select-option value="srtp">SRTP</a-select-option>
<a-select-option value="utp">uTP</a-select-option>
<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="none">none (No Obfuscation)</a-select-option>
<a-select-option value="srtp">SRTP (Video Call)</a-select-option>
<a-select-option value="utp">uTP (Bittorrent)</a-select-option>
<a-select-option value="wechat-video">WeChat Video</a-select-option>
<a-select-option value="dtls">DTLS (DTLS 1.2 packages)</a-select-option>
<a-select-option value="wireguard">WireGuard (WireGuard Packages)</a-select-option>
</a-select>
</a-form-item>
</template>
@@ -337,57 +240,59 @@
<!-- tls settings -->
<template v-if="outbound.canEnableTls()">
<a-form-item label='{{ i18n "security" }}'>
<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-group>
</a-form-item>
<template v-if="outbound.stream.isTls">
<a-form-item label="SNI" placeholder="Server Name Indication">
<a-input v-model.trim="outbound.stream.tls.serverName"></a-input>
<a-form-item label='{{ i18n "security" }}'>
<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-group>
</a-form-item>
<a-form-item label="uTLS">
<a-select v-model="outbound.stream.tls.fingerprint"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value=''>None</a-select-option>
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="ALPN">
<a-select mode="multiple"
:dropdown-class-name="themeSwitcher.currentTheme"
v-model="outbound.stream.tls.alpn">
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Allow Insecure">
<a-switch v-model="outbound.stream.tls.allowInsecure"></a-switch>
</a-form-item>
</template>
<!-- reality settings -->
<template v-if="outbound.stream.isReality">
<a-form-item label="SNI">
<a-input v-model.trim="outbound.stream.reality.serverName"></a-input>
</a-form-item>
<a-form-item label="uTLS">
<a-select v-model="outbound.stream.reality.fingerprint"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Short ID">
<a-input v-model.trim="outbound.stream.reality.shortId" style="width:250px"></a-input>
</a-form-item>
<a-form-item label="SpiderX">
<a-input v-model.trim="outbound.stream.reality.spiderX" style="width:250px"></a-input>
</a-form-item>
<a-form-item label="Public Key">
<a-input v-model.trim="outbound.stream.reality.publicKey"></a-input>
</a-form-item>
</template>
<template v-if="outbound.stream.isTls">
<a-form-item label="SNI" placeholder="Server Name Indication">
<a-input v-model.trim="outbound.stream.tls.serverName"></a-input>
</a-form-item>
<a-form-item label="uTLS">
<a-select v-model="outbound.stream.tls.fingerprint"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value=''>None</a-select-option>
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="ALPN">
<a-select
mode="multiple"
:dropdown-class-name="themeSwitcher.currentTheme"
v-model="outbound.stream.tls.alpn">
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Allow Insecure">
<a-switch v-model="outbound.stream.tls.allowInsecure"></a-switch>
</a-form-item>
</template>
<!-- reality settings -->
<template v-if="outbound.stream.isReality">
<a-form-item label='{{ i18n "domainName" }}'>
<a-input v-model.trim="outbound.stream.reality.serverName"></a-input>
</a-form-item>
<a-form-item label="uTLS">
<a-select v-model="outbound.stream.reality.fingerprint"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Short IDs">
<a-input v-model.trim="outbound.stream.reality.shortId" style="width:250px"></a-input>
</a-form-item>
<a-form-item label="SpiderX">
<a-input v-model.trim="outbound.stream.reality.spiderX" style="width:250px"></a-input>
</a-form-item>
<a-form-item label="Public Key">
<a-input v-model.trim="outbound.stream.reality.publicKey"></a-input>
</a-form-item>
</template>
</template>
</a-form>
</a-tab-pane>

View File

@@ -1,5 +1,5 @@
{{define "form/dokodemo"}}
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "pages.inbounds.targetAddress"}}'>
<a-input v-model.trim="inbound.settings.address"></a-input>
</a-form-item>

View File

@@ -1,6 +1,6 @@
{{define "form/http"}}
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<table style="width: 100%; text-align: center; margin: 1rem 0;">
<a-form>
<table style="width: 100%; text-align: center; margin-bottom: 10px;">
<tr>
<td width="45%">{{ i18n "username" }}</td>
<td width="45%">{{ i18n "password" }}</td>
@@ -18,4 +18,4 @@
</a-input>
</a-input-group>
</a-form>
{{end}}
{{end}}

View File

@@ -20,17 +20,17 @@
</a-collapse-panel>
</a-collapse>
</template>
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "encryption" }}'>
<a-select v-model="inbound.settings.method" @change="SSMethodChange" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="(method,method_name) in SSMethods" :value="method">[[ method_name ]]</a-select-option>
<a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item v-if="inbound.isSS2022">
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "reset" }}</span>
<span>{{ i18n "password" }}</span>
</template> Password <a-icon @click="inbound.settings.password = RandomUtil.randomShadowsocksPassword()" type="sync"></a-icon>
</a-tooltip>
</template>

View File

@@ -1,5 +1,5 @@
{{define "form/socks"}}
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "pages.inbounds.enable" }} UDP'>
<a-switch v-model="inbound.settings.udp"></a-switch>
</a-form-item>
@@ -11,7 +11,7 @@
@change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"></a-switch>
</a-form-item>
<template v-if="inbound.settings.auth === 'password'">
<table style="width: 100%; text-align: center; margin: 1rem 0;">
<table style="width: 100%; text-align: center; margin-bottom: 10px;">
<tr>
<td width="45%">{{ i18n "username" }}</td>
<td width="45%">{{ i18n "password" }}</td>

View File

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

View File

@@ -21,23 +21,27 @@
</a-collapse-panel>
</a-collapse>
<template v-if="inbound.isTcp && !inbound.stream.isReality">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form layout="inline">
<a-form-item label="Fallbacks">
<a-button type="primary" size="small" @click="inbound.settings.addFallback()">+</a-button>
<a-row>
<a-button type="primary" size="small"
@click="inbound.settings.addFallback()">
+
</a-button>
</a-row>
</a-form-item>
</a-form>
<!-- vless fallbacks -->
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }">
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:14} }">
<a-divider style="margin:0;">
Fallback [[ index + 1 ]]
<a-icon type="delete" @click="() => inbound.settings.delFallback(index)"
style="color: rgb(255, 77, 79);cursor: pointer;" />
style="color: rgb(255, 77, 79);cursor: pointer;"/>
</a-divider>
<a-form-item label='SNI'>
<a-form-item label='Name'>
<a-input v-model="fallback.name"></a-input>
</a-form-item>
</a-form-item>
<a-form-item label='ALPN'>
<a-input v-model="fallback.alpn"></a-input>
</a-form-item>

View File

@@ -1,80 +0,0 @@
{{define "form/wireguard"}}
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item>
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "reset" }}</span>
</template>
{{ i18n "pages.xray.wireguard.secretKey" }}
<a-icon type="sync"
@click="[inbound.settings.pubKey, inbound.settings.secretKey] = Object.values(Wireguard.generateKeypair())">
</a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="inbound.settings.secretKey"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.wireguard.publicKey" }}'>
<a-input disabled v-model="inbound.settings.pubKey"></a-input>
</a-form-item>
<a-form-item label='MTU'>
<a-input-number v-model.number="inbound.settings.mtu"></a-input-number>
</a-form-item>
<a-form-item label='Kernel Mode'>
<a-switch v-model="inbound.settings.kernelMode"></a-switch>
</a-form-item>
<a-form-item label="Peers">
<a-button type="primary" size="small" @click="inbound.settings.addPeer()">+</a-button>
</a-form-item>
<a-form v-for="(peer, index) in inbound.settings.peers" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-divider style="margin:0;">
Peer [[ index + 1 ]]
<a-icon v-if="inbound.settings.peers.length>1" type="delete" @click="() => inbound.settings.delPeer(index)"
style="color: rgb(255, 77, 79);cursor: pointer;"/>
</a-divider>
<a-form-item>
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "reset" }}</span>
</template>
{{ i18n "pages.xray.wireguard.secretKey" }}
<a-icon @click="[peer.publicKey, peer.privateKey] = Object.values(Wireguard.generateKeypair())"type="sync"> </a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="peer.privateKey"></a-input>
</a-form-item>
<a-form-item>
<template slot="label">
{{ i18n "pages.xray.wireguard.publicKey" }}
</template>
<a-input v-model.trim="peer.publicKey"></a-input>
</a-form-item>
<a-form-item>
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "reset" }}</span>
</template>
{{ i18n "pages.xray.wireguard.psk" }}
<a-icon @click="peer.psk = Wireguard.keyToBase64(Wireguard.generatePresharedKey())"type="sync"> </a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="peer.psk"></a-input>
</a-form-item>
<a-form-item>
<template slot="label">
{{ i18n "pages.xray.wireguard.allowedIPs" }} <a-button type="primary" size="small" @click="peer.allowedIPs.push('')">+</a-button>
</template>
<template v-for="(aip, index) in peer.allowedIPs" style="margin-bottom: 10px;">
<a-input v-model.trim="peer.allowedIPs[index]">
<a-button v-if="peer.allowedIPs.length>1" slot="addonAfter" size="small" @click="peer.allowedIPs.splice(index, 1)">-</a-button>
</a-input>
</template>
</a-form-item>
<a-form-item label='Keep Alive'>
<a-input-number v-model.number="peer.keepAlive" :min="0"></a-input>
</a-form-item>
</a-form>
</a-form>
{{end}}

View File

@@ -20,7 +20,7 @@
<a-input-number style="width: 15%;" v-model.number="row.port" min="1" max="65531"></a-input-number>
</a-tooltip>
<a-input style="width: 20%" v-model.trim="row.remark" placeholder='{{ i18n "remark" }}'></a-input>
<a-button style="width: 10%; margin: 0px; border-radius: 0 1rem 1rem 0;" @click="inbound.stream.externalProxy.splice(index, 1)">-</a-button>
<a-button style="width: 10%; margin: 0px" @click="inbound.stream.externalProxy.splice(index, 1)">-</a-button>
</a-input-group>
</a-form>
{{end}}

View File

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

View File

@@ -1,5 +1,5 @@
{{define "form/streamHTTP"}}
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "path" }}'>
<a-input v-model.trim="inbound.stream.http.path"></a-input>
</a-form-item>

View File

@@ -1,26 +1,17 @@
{{define "form/streamKCP"}}
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "camouflage" }}'>
<a-select v-model="inbound.stream.kcp.type" style="width: 50%" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="none">None</a-select-option>
<a-select-option value="srtp">SRTP</a-select-option>
<a-select-option value="utp">uTP</a-select-option>
<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 v-model="inbound.stream.kcp.type" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="none">none (No Obfuscation)</a-select-option>
<a-select-option value="srtp">SRTP (Video Call)</a-select-option>
<a-select-option value="utp">uTP (Bittorrent)</a-select-option>
<a-select-option value="wechat-video">WeChat Video</a-select-option>
<a-select-option value="dtls">DTLS (DTLS 1.2 packages)</a-select-option>
<a-select-option value="wireguard">WireGuard (WireGuard packages)</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "reset" }}</span>
</template>
{{ i18n "password" }}
<a-icon @click="inbound.stream.kcp.seed = RandomUtil.randomSeq(10)"type="sync"> </a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="inbound.stream.kcp.seed"></a-input>
<a-form-item label='{{ i18n "password" }}'>
<a-input v-model="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>

View File

@@ -1,32 +1,23 @@
{{define "form/streamQUIC"}}
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "pages.inbounds.stream.quic.encryption" }}'>
<a-select v-model="inbound.stream.quic.security" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="none">None</a-select-option>
<a-select-option value="aes-128-gcm">AES-128-GCM</a-select-option>
<a-select-option value="chacha20-poly1305">CHACHA20-POLY1305</a-select-option>
<a-select-option value="none">none</a-select-option>
<a-select-option value="aes-128-gcm">aes-128-gcm</a-select-option>
<a-select-option value="chacha20-poly1305">chacha20-poly1305</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "reset" }}</span>
</template>
{{ i18n "password" }}
<a-icon @click="inbound.stream.quic.key = RandomUtil.randomSeq(10)"type="sync"> </a-icon>
</a-tooltip>
</template>
<a-form-item label='{{ i18n "password" }}'>
<a-input v-model.trim="inbound.stream.quic.key"></a-input>
</a-form-item>
</a-form-item>
<a-form-item label='{{ i18n "camouflage" }}'>
<a-select v-model="inbound.stream.quic.type" style="width: 50%" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="none">None</a-select-option>
<a-select-option value="srtp">SRTP</a-select-option>
<a-select-option value="utp">uTP</a-select-option>
<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 v-model="inbound.stream.quic.type" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="none">none (No Obfuscation)</a-select-option>
<a-select-option value="srtp">SRTP (Video Call)</a-select-option>
<a-select-option value="utp">uTP (Bittorrent)</a-select-option>
<a-select-option value="wechat-video">WeChat Video</a-select-option>
<a-select-option value="dtls">DTLS (DTLS 1.2 packages)</a-select-option>
<a-select-option value="wireguard">WireGuard (WireGuard Packages)</a-select-option>
</a-select>
</a-form-item>
</a-form>

View File

@@ -1,11 +1,11 @@
{{define "form/streamSettings"}}
<!-- 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-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="{{ i18n "transmission" }}">
<a-select v-model="inbound.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="kcp">KCP</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>
@@ -47,4 +47,4 @@
<template>
{{template "form/streamSockopt"}}
</template>
{{end}}
{{end}}

View File

@@ -1,24 +1,24 @@
{{define "form/streamSockopt"}}
<a-divider style="margin:5px 0 0;"></a-divider>
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="TPROXY">
<a-form-item label="Transparent Proxy">
<a-switch v-model="inbound.stream.sockoptSwitch"></a-switch>
</a-form-item>
<template v-if="inbound.stream.sockoptSwitch">
<a-form-item label="PROXY Protocol">
<a-form-item label="Accept Proxy Protocol">
<a-switch v-model="inbound.stream.sockopt.acceptProxyProtocol"></a-switch>
</a-form-item>
<a-form-item label="TCP Fast Open">
<a-form-item label="TCP FastOpen">
<a-switch v-model.trim="inbound.stream.sockopt.tcpFastOpen"></a-switch>
</a-form-item>
<a-form-item label="Route Mark">
<a-input-number v-model="inbound.stream.sockopt.mark" :min="0"></a-input-number>
</a-form-item>
<a-form-item label="TPROXY">
<a-form-item label="T-Proxy">
<a-select v-model="inbound.stream.sockopt.tproxy" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="off">Off</a-select-option>
<a-select-option value="off">OFF</a-select-option>
<a-select-option value="redirect">Redirect</a-select-option>
<a-select-option value="tproxy">TPROXY</a-select-option>
<a-select-option value="tproxy">T-Proxy</a-select-option>
</a-select>
</a-form-item>
</template>

View File

@@ -1,63 +1,58 @@
{{define "form/streamTCP"}}
<!-- tcp type -->
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="PROXY Protocol" v-if="inbound.canEnableTls()">
<a-form-item label="Accept Proxy Protocol" v-if="inbound.canEnableTls()">
<a-switch v-model="inbound.stream.tcp.acceptProxyProtocol"></a-switch>
</a-form-item>
<a-form-item label='HTTP {{ i18n "camouflage" }}'>
<a-switch :checked="inbound.stream.tcp.type === 'http'"
<a-switch
:checked="inbound.stream.tcp.type === 'http'"
@change="checked => inbound.stream.tcp.type = checked ? 'http' : 'none'">
</a-switch>
</a-form-item>
</a-form>
<a-form v-if="inbound.stream.tcp.type === 'http'" :colon="false" :label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }">
<!-- tcp request -->
<a-divider style="margin:0;">{{ i18n "pages.inbounds.stream.general.request" }}</a-divider>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.version" }}'>
<!-- tcp request -->
<a-form v-if="inbound.stream.tcp.type === 'http'" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestVersion" }}'>
<a-input v-model.trim="inbound.stream.tcp.request.version"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.method" }}'>
<a-input v-model.trim="inbound.stream.tcp.request.method"></a-input>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestMethod" }}'>
<a-input v-model.trim="inbound.stream.tcp.request.method"></a-input>
</a-form-item>
<a-form-item>
<template slot="label">{{ i18n "pages.inbounds.stream.tcp.path" }}
<template slot="label">{{ i18n "pages.inbounds.stream.tcp.requestPath" }}
<a-button size="small" @click="inbound.stream.tcp.request.addPath('/')">+</a-button>
</template>
<template v-for="(path, index) in inbound.stream.tcp.request.path">
<a-input v-model.trim="inbound.stream.tcp.request.path[index]">
<a-button size="small" slot="addonAfter" @click="inbound.stream.tcp.request.removePath(index)"
v-if="inbound.stream.tcp.request.path.length>1">-</a-button>
<a-button size="small" slot="addonAfter"
@click="inbound.stream.tcp.request.removePath(index)"
v-if="inbound.stream.tcp.request.path.length>1">-</a-button>
</a-input>
</template>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
<a-button size="small" @click="inbound.stream.tcp.request.addHeader('Host', '')">+</a-button>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.general.requestHeader" }}'>
<a-button size="small" @click="inbound.stream.tcp.request.addHeader('', '')">+</a-button>
</a-form-item>
<a-form-item :wrapper-col="{span:24}">
<a-input-group compact v-for="(header, index) in inbound.stream.tcp.request.headers">
<a-input style="width: 50%" v-model.trim="header.name"
placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
<a-input style="width: 50%" v-model.trim="header.name" placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
</a-input>
<a-input style="width: 50%" v-model.trim="header.value"
placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
<a-button slot="addonAfter" size="small"
@click="inbound.stream.tcp.request.removeHeader(index)">-</a-button>
<a-input style="width: 50%" v-model.trim="header.value" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
<a-button slot="addonAfter" size="small" @click="inbound.stream.tcp.request.removeHeader(index)">-</a-button>
</a-input>
</a-input-group>
</a-form-item>
<!-- tcp response -->
<a-divider style="margin:0;">{{ i18n "pages.inbounds.stream.general.response" }}</a-divider>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.version" }}'>
<!-- tcp response -->
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseVersion" }}'>
<a-input v-model.trim="inbound.stream.tcp.response.version"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.status" }}'>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseStatus" }}'>
<a-input v-model.trim="inbound.stream.tcp.response.status"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.statusDescription" }}'>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseStatusDescription" }}'>
<a-input v-model.trim="inbound.stream.tcp.response.reason"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseHeader" }}'>
@@ -66,12 +61,11 @@
</a-form-item>
<a-form-item :wrapper-col="{span:24}">
<a-input-group compact v-for="(header, index) in inbound.stream.tcp.response.headers">
<a-input style="width: 50%" v-model.trim="header.name"
placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
<a-input style="width: 50%" v-model.trim="header.name" placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
</a-input>
<a-input style="width: 50%" v-model.trim="header.value"
placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
<template slot="addonAfter">
<a-button size="small" @click="inbound.stream.tcp.response.removeHeader(index)">-</a-button>
</template>
@@ -79,4 +73,4 @@
</a-input-group>
</a-form-item>
</a-form>
{{end}}
{{end}}

View File

@@ -1,13 +1,13 @@
{{define "form/streamWS"}}
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="PROXY Protocol">
<a-form-item label="Accept Proxy Protocol">
<a-switch v-model="inbound.stream.ws.acceptProxyProtocol"></a-switch>
</a-form-item>
<a-form-item label='{{ i18n "path" }}'>
<a-input v-model.trim="inbound.stream.ws.path"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
<a-button size="small" @click="inbound.stream.ws.addHeader('Host', '')">+</a-button>
<a-form-item label='{{ i18n "pages.inbounds.stream.general.requestHeader" }}'>
<a-button size="small" @click="inbound.stream.ws.addHeader()">+</a-button>
</a-form-item>
<a-form-item :wrapper-col="{span:24}">
<a-input-group compact v-for="(header, index) in inbound.stream.ws.headers">

View File

@@ -15,7 +15,7 @@
<template slot="title">
<span>{{ i18n "pages.inbounds.realityDesc" }}</span>
</template>
<a-radio-button v-if="inbound.canEnableReality()" value="reality">REALITY</a-radio-button>
<a-radio-button v-if="inbound.canEnableReality()" value="reality">Reality</a-radio-button>
</a-tooltip>
<a-radio-button value="tls">TLS</a-radio-button>
</a-radio-group>
@@ -26,24 +26,24 @@
<a-form-item label="SNI" placeholder="Server Name Indication">
<a-input v-model.trim="inbound.stream.tls.sni"></a-input>
</a-form-item>
<a-form-item label="Cipher Suites">
<a-form-item label="CipherSuites">
<a-select v-model="inbound.stream.tls.cipherSuites" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="">Auto</a-select-option>
<a-select-option value="">auto</a-select-option>
<a-select-option v-for="key,value in TLS_CIPHER_OPTION" :value="key">[[ value ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Min/Max Version">
<a-input-group compact>
<a-select v-model="inbound.stream.tls.minVersion" style="width: 50%" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select v-model="inbound.stream.tls.minVersion" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select>
<a-select v-model="inbound.stream.tls.maxVersion" style="width: 50%" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select v-model="inbound.stream.tls.maxVersion" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-input-group>
</a-form-item>
<a-form-item label="uTLS">
<a-select v-model="inbound.stream.tls.settings.fingerprint" style="width: 50%"
<a-select v-model="inbound.stream.tls.settings.fingerprint"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value=''>None</a-select-option>
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
@@ -73,10 +73,10 @@
@click="inbound.stream.tls.removeCert(index)" style="margin-left: 10px">-</a-button>
</a-form-item>
<template v-if="cert.useFile">
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
<a-form-item label='{{ i18n "pages.inbounds.publicKeyPath" }}'>
<a-input v-model.trim="cert.certFile"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
<a-input v-model.trim="cert.keyFile"></a-input>
</a-form-item>
<a-form-item label=" ">
@@ -85,14 +85,14 @@
</a-form-item>
</template>
<template v-else>
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
<a-input type="textarea" :rows="3" v-model="cert.cert"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
<a-form-item label='{{ i18n "pages.inbounds.keyContent" }}'>
<a-input type="textarea" :rows="3" v-model="cert.key"></a-input>
</a-form-item>
</template>
<a-form-item label='OCSP stapling'>
<a-form-item label='ocspStapling'>
<a-input-number v-model.number="cert.ocspStapling" :min="0"></a-input-number>
</a-form-item>
</template>
@@ -124,10 +124,10 @@
@click="inbound.stream.xtls.removeCert(index)" style="margin-left: 10px">-</a-button>
</a-form-item>
<template v-if="cert.useFile">
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
<a-form-item label='{{ i18n "pages.inbounds.publicKeyPath" }}'>
<a-input v-model.trim="cert.certFile"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
<a-input v-model.trim="cert.keyFile"></a-input>
</a-form-item>
<a-form-item label=" ">
@@ -136,10 +136,10 @@
</a-form-item>
</template>
<template v-else>
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
<a-input type="textarea" :rows="3" v-model="cert.cert"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
<a-form-item label='{{ i18n "pages.inbounds.keyContent" }}'>
<a-input type="textarea" :rows="3" v-model="cert.key"></a-input>
</a-form-item>
</template>
@@ -154,7 +154,7 @@
<a-input-number v-model.number="inbound.stream.reality.xver" :min="0"></a-input-number>
</a-form-item>
<a-form-item label='uTLS'>
<a-select v-model="inbound.stream.reality.settings.fingerprint" style="width: 50%"
<a-select v-model="inbound.stream.reality.settings.fingerprint"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select>
@@ -162,16 +162,16 @@
<a-form-item label='Dest'>
<a-input v-model.trim="inbound.stream.reality.dest"></a-input>
</a-form-item>
<a-form-item label='SNI'>
<a-form-item label='Server Names'>
<a-input v-model.trim="inbound.stream.reality.serverNames"></a-input>
</a-form-item>
<a-form-item>
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "reset" }}</span>
<span>{{ i18n "pages.client.renew" }}</span>
</template>
Short ID
Short IDs
<a-icon @click="inbound.stream.reality.shortIds = RandomUtil.randomShortId()" type="sync"> </a-icon>
</a-icon>
</template>
@@ -180,14 +180,14 @@
<a-form-item label='SpiderX'>
<a-input v-model.trim="inbound.stream.reality.settings.spiderX"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
<a-form-item label='Private Key'>
<a-input v-model.trim="inbound.stream.reality.privateKey"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
<a-form-item label='Public Key'>
<a-input v-model.trim="inbound.stream.reality.settings.publicKey"></a-input>
</a-form-item>
<a-form-item label=" ">
<a-button type="primary" icon="import" @click="getNewX25519Cert">Get New Cert</a-button>
<a-button type="primary" icon="import" @click="getNewX25519Cert">Get new cert</a-button>
</a-form-item>
</template>
</a-form>

View File

@@ -40,7 +40,7 @@
<a-switch v-model="client.enable" @change="switchEnableClient(record.id,client)"></a-switch>
</template>
<template slot="online" slot-scope="text, client, index">
<template v-if="client.enable && isClientOnline(client.email)">
<template v-if="isClientOnline(client.email)">
<a-tag color="green">{{ i18n "online" }}</a-tag>
</template>
<template v-else>
@@ -52,7 +52,7 @@
<template slot="title">
<template v-if="!isClientEnabled(record, client.email)">{{ i18n "depleted" }}</template>
<template v-else-if="!client.enable">{{ i18n "disabled" }}</template>
<template v-else-if="client.enable && isClientOnline(client.email)">{{ i18n "online" }}</template>
<template v-else-if="isClientOnline(client.email)">{{ i18n "online" }}</template>
</template>
<a-badge :class="isClientOnline(client.email)? 'online-animation' : ''" :color="client.enable ? statsExpColor(record, client.email) : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'">
</a-badge>

View File

@@ -164,9 +164,9 @@
</tr>
</table>
<template v-if="app.subSettings.enable && infoModal.clientSettings.subId">
<a-divider>Subscription URL</a-divider>
<a-divider>Subscription link</a-divider>
<a-row>
<a-col :sx="24" :md="22">SUB: <a :href="[[ infoModal.subLink ]]" target="_blank">[[ infoModal.subLink ]]</a></a-col>
<a-col :sx="24" :md="22"><a :href="[[ infoModal.subLink ]]" target="_blank">[[ infoModal.subLink ]]</a></a-col>
<a-col :sx="24" :md="2" style="text-align: right;">
<a-tooltip title='{{ i18n "copy" }}'>
<button class="ant-btn ant-btn-primary" id="copy-sub-link" @click="copyToClipboard('copy-sub-link', infoModal.subLink)">
@@ -175,24 +175,14 @@
</a-tooltip>
</a-col>
</a-row>
<a-row>
<a-col :sx="24" :md="22">JSON: <a :href="[[ infoModal.subJsonLink ]]" target="_blank">[[ infoModal.subJsonLink ]]</a></a-col>
<a-col :sx="24" :md="2" style="text-align: right; margin-top: 5px;">
<a-tooltip title='{{ i18n "copy" }}'>
<button class="ant-btn ant-btn-primary" id="copy-subJson-link" @click="copyToClipboard('copy-subJson-link', infoModal.subJsonLink)">
<a-icon type="snippets"></a-icon>
</button>
</a-tooltip>
</a-col>
</a-row>
</template>
<template v-if="app.tgBotEnable && infoModal.clientSettings.tgId">
<a-divider>Telegram ID</a-divider>
<a-row>
<a-col :sx="24" :md="22">[[ infoModal.clientSettings.tgId ]]</a-col>
<a-col :sx="24" :md="22"><a :href="[[ infoModal.tgLink ]]" target="_blank">@[[ infoModal.clientSettings.tgId ]]</a></a-col>
<a-col :sx="24" :md="2" style="text-align: right;">
<a-tooltip title='{{ i18n "copy" }}'>
<button class="ant-btn ant-btn-primary" id="copy-tg-link" @click="copyToClipboard('copy-tg-link', infoModal.clientSettings.tgId)">
<button class="ant-btn ant-btn-primary" id="copy-tg-link" @click="copyToClipboard('copy-tg-link', '@' + infoModal.clientSettings.tgId)">
<a-icon type="snippets"></a-icon>
</button>
</a-tooltip>
@@ -274,71 +264,6 @@
<td><a-tag color="green">[[ account.pass ]]</a-tag></td>
</tr>
</table>
<table v-if="dbInbound.isWireguard" style="margin-bottom: 10px; width: 100%;">
<tr class="client-table-odd-row">
<td>{{ i18n "pages.xray.wireguard.secretKey" }}</td>
<td>[[ inbound.settings.secretKey ]]</td>
</tr>
<tr>
<td>{{ i18n "pages.xray.wireguard.publicKey" }}</td>
<td>[[ inbound.settings.pubKey ]]</td>
</tr>
<tr class="client-table-odd-row">
<td>MTU</td>
<td>[[ inbound.settings.mtu ]]</td>
</tr>
<tr>
<td>Kernel Mode</td>
<td>[[ inbound.settings.kernelMode ]]</td>
</tr>
<template v-for="(peer, index) in inbound.settings.peers">
<tr>
<td colspan="2"><a-divider>Peer [[ index + 1 ]]</a-divider></td>
</tr>
<tr class="client-table-odd-row">
<td>{{ i18n "pages.xray.wireguard.secretKey" }}</td>
<td>[[ peer.privateKey ]]</td>
</tr>
<tr>
<td>{{ i18n "pages.xray.wireguard.publicKey" }}</td>
<td>[[ peer.publicKey ]]</td>
</tr>
<tr class="client-table-odd-row">
<td>{{ i18n "pages.xray.wireguard.psk" }}</td>
<td>[[ peer.psk ]]</td>
</tr>
<tr>
<td>{{ i18n "pages.xray.wireguard.allowedIPs" }}</td>
<td>[[ peer.allowedIPs.join(",") ]]</td>
</tr>
<tr class="client-table-odd-row">
<td>Keep Alive</td>
<td>[[ peer.keepAlive ]]</td>
</tr>
<tr>
<td colspan="2">
<a-row>
<a-col :span="22" style="overflow-wrap: anywhere;">
<a-tag color="blue">Config</a-tag>
<div
v-html="infoModal.links[index].replaceAll(`\n`,`<br />`)"
style="border-radius: 1rem; padding: 0.5rem;"
class="client-table-odd-row"></div>
</a-col>
<a-col :span="2" style="text-align: right;">
<a-tooltip title='{{ i18n "copy" }}'>
<button class="ant-btn ant-btn-primary"
:id="'copy-url-link-'+index"
@click="copyToClipboard('copy-url-link-'+index, infoModal.links[index])">
<a-icon type="snippets"></a-icon>
</button>
</a-tooltip>
</a-col>
</a-row>
</td>
</tr>
</table>
</template>
</template>
</a-modal>
<script>
@@ -355,7 +280,7 @@
index: null,
isExpired: false,
subLink: '',
subJsonLink: '',
tgLink: '',
show(dbInbound, index) {
this.index = index;
this.inbound = dbInbound.toInbound();
@@ -363,15 +288,13 @@
this.clientSettings = this.inbound.clients ? this.inbound.clients[index] : null;
this.isExpired = this.inbound.clients ? this.inbound.isExpiry(index): this.dbInbound.isExpiry;
this.clientStats = this.inbound.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : [];
if (this.inbound.protocol == Protocols.WIREGUARD){
this.links = this.inbound.genInboundLinks(dbInbound.remark).split('\r\n')
} else {
this.links = this.inbound.genAllLinks(this.dbInbound.remark, app.remarkModel, this.clientSettings);
}
this.links = this.inbound.genAllLinks(this.dbInbound.remark, app.remarkModel, this.clientSettings);
if (this.clientSettings) {
if (this.clientSettings.subId) {
this.subLink = this.genSubLink(this.clientSettings.subId);
this.subJsonLink = this.genSubJsonLink(this.clientSettings.subId);
}
if (this.clientSettings.tgId) {
this.tgLink = "https://t.me/" + this.clientSettings.tgId;
}
}
this.visible = true;
@@ -381,9 +304,6 @@
},
genSubLink(subID) {
return app.subSettings.subURI+subID;
},
genSubJsonLink(subID) {
return app.subSettings.subJsonURI+subID;
}
};
@@ -429,4 +349,4 @@
});
</script>
{{end}}
{{end}}

View File

@@ -40,7 +40,7 @@
inModal.visible = false;
inModal.loading(false);
},
loading(loading=true) {
loading(loading) {
inModal.confirmLoading = loading;
},
};
@@ -63,9 +63,6 @@
get client() {
return inModal.inbound.clients[0];
},
get datepicker() {
return app.datepicker;
},
get delayedExpireDays() {
return this.client && this.client.expiryTime < 0 ? this.client.expiryTime / -86400000 : 0;
},

View File

@@ -56,13 +56,9 @@
<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>
<a-tag v-if="false" color="red" style="margin-bottom: 10px">
Please go to the panel settings as soon as possible to modify the username and password, otherwise there may be a risk of leaking account information
</a-tag>
</transition>
<transition name="list" appear>
<a-card hoverable>
@@ -137,10 +133,6 @@
<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" }}
@@ -149,7 +141,7 @@
<a-icon type="file-done"></a-icon>
{{ i18n "pages.inbounds.resetAllClientTraffics" }}
</a-menu-item>
<a-menu-item key="delDepletedClients" style="color: #FF4D4F;">
<a-menu-item key="delDepletedClients">
<a-icon type="rest"></a-icon>
{{ i18n "pages.inbounds.delDepletedClients" }}
</a-menu-item>
@@ -186,7 +178,7 @@
</a-radio-group>
</div>
<a-back-top></a-back-top>
<a-table :columns="isMobile ? mobileColumns : columns" :row-key="dbInbound => dbInbound.id"
<a-table :columns="isMobile ? mobileColums : columns" :row-key="dbInbound => dbInbound.id"
:data-source="searchedInbounds"
:scroll="isMobile ? {} : { x: 1000 }"
:pagination=pagination(searchedInbounds)
@@ -204,7 +196,7 @@
<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-menu-item key="qrcode" v-if="dbInbound.isSS && !dbInbound.toInbound().isSSMultiUser">
<a-icon type="qrcode"></a-icon>
{{ i18n "qrCode" }}
</a-menu-item>
@@ -225,11 +217,7 @@
<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-menu-item key="delDepletedClients">
<a-icon type="rest"></a-icon>
{{ i18n "pages.inbounds.delDepletedClients" }}
</a-menu-item>
@@ -242,7 +230,7 @@
</template>
<a-menu-item key="clipboard">
<a-icon type="copy"></a-icon>
{{ i18n "pages.inbounds.exportInbound" }}
{{ i18n "pages.inbounds.copyToClipboard" }}
</a-menu-item>
<a-menu-item key="resetTraffic">
<a-icon type="retweet"></a-icon> {{ i18n "pages.inbounds.resetTraffic" }}
@@ -256,7 +244,7 @@
</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>
<a-switch size="small" v-model="dbInbound.enable" @change="switchEnable(dbInbound.id)"></a-switch>
{{ i18n "pages.inbounds.enable" }}
</a-menu-item>
</a-menu>
@@ -326,7 +314,7 @@
</a-popover>
</template>
<template slot="enable" slot-scope="text, dbInbound">
<a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound.id,dbInbound.enable)"></a-switch>
<a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound.id)"></a-switch>
</template>
<template slot="expiryTime" slot-scope="text, dbInbound">
<a-popover v-if="dbInbound.expiryTime > 0" :overlay-class-name="themeSwitcher.currentTheme">
@@ -458,28 +446,27 @@
<script src="{{ .base_path }}assets/js/model/xray.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/model/dbinbound.js?{{ .cur_ver }}"></script>
{{template "component/themeSwitcher" .}}
{{template "component/persianDatepicker" .}}
<script>
const columns = [{
title: "ID",
align: 'right',
dataIndex: "id",
width: 30,
width: 40,
responsive: ["xs"],
}, {
title: '{{ i18n "pages.inbounds.operate" }}',
align: 'center',
width: 30,
width: 40,
scopedSlots: { customRender: 'action' },
}, {
title: '{{ i18n "pages.inbounds.enable" }}',
align: 'center',
width: 30,
width: 40,
scopedSlots: { customRender: 'enable' },
}, {
title: '{{ i18n "pages.inbounds.remark" }}',
align: 'center',
width: 60,
width: 80,
dataIndex: "remark",
}, {
title: '{{ i18n "pages.inbounds.port" }}',
@@ -489,7 +476,7 @@
}, {
title: '{{ i18n "pages.inbounds.protocol" }}',
align: 'left',
width: 70,
width: 90,
scopedSlots: { customRender: 'protocol' },
}, {
title: '{{ i18n "clients" }}',
@@ -504,11 +491,11 @@
}, {
title: '{{ i18n "pages.inbounds.expireDate" }}',
align: 'center',
width: 40,
width: 60,
scopedSlots: { customRender: 'expiryTime' },
}];
const mobileColumns = [{
const mobileColums = [{
title: "ID",
align: 'right',
dataIndex: "id",
@@ -552,7 +539,6 @@
data: {
siderDrawer,
themeSwitcher,
persianDatepicker,
spinning: false,
inbounds: [],
dbInbounds: [],
@@ -571,13 +557,10 @@
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
subSettings: {
enable : false,
subURI : '',
subJsonURI : '',
subURI : ''
},
remarkModel: '-ieo',
datepicker: 'gregorian',
tgBotEnable: false,
showAlert: false,
pageSize: 0,
isMobile: window.innerWidth <= 768,
},
@@ -592,7 +575,6 @@
this.refreshing = false;
return;
}
await this.getOnlineUsers();
this.setInbounds(msg.obj);
setTimeout(() => {
@@ -619,12 +601,10 @@
this.tgBotEnable = tgBotEnable;
this.subSettings = {
enable : subEnable,
subURI: subURI,
subJsonURI: subJsonURI
subURI: subURI
};
this.pageSize = pageSize;
this.remarkModel = remarkModel;
this.datepicker = datepicker;
}
},
setInbounds(dbInbounds) {
@@ -658,12 +638,8 @@
clientCount = clients.length;
if (dbInbound.enable) {
clients.forEach(client => {
if (client.enable) {
active.push(client.email);
if (this.isClientOnline(client.email)) online.push(client.email);
} else {
deactive.push(client.email);
}
client.enable ? active.push(client.email) : deactive.push(client.email);
if(this.isClientOnline(client.email)) online.push(client.email);
});
clientStats.forEach(client => {
if (!client.enable) {
@@ -688,7 +664,6 @@
online: online,
};
},
searchInbounds(key) {
if (ObjectUtil.isEmpty(key)) {
this.searchedInbounds = this.dbInbounds.slice();
@@ -752,9 +727,6 @@
case "export":
this.exportAllLinks();
break;
case "subs":
this.exportAllSubs();
break;
case "resetInbounds":
this.resetAllTraffic();
break;
@@ -786,9 +758,6 @@
case "export":
this.inboundLinks(dbInbound.id);
break;
case "subs":
this.exportSubs(dbInbound.id);
break;
case "clipboard":
this.copyToClipboard(dbInbound.id);
break;
@@ -838,7 +807,7 @@
protocol: baseInbound.protocol,
settings: Inbound.Settings.getSettings(baseInbound.protocol).toString(),
streamSettings: baseInbound.stream.toString(),
sniffing: baseInbound.sniffing.toString(),
sniffing: baseInbound.canSniffing() ? baseInbound.sniffing.toString() : '{}',
};
await this.submit('/panel/inbound/add', data, inModal);
},
@@ -848,7 +817,9 @@
okText: '{{ i18n "pages.inbounds.create"}}',
cancelText: '{{ i18n "close" }}',
confirm: async (inbound, dbInbound) => {
await this.addInbound(inbound, dbInbound, inModal);
inModal.loading();
await this.addInbound(inbound, dbInbound);
inModal.close();
},
isEdit: false
});
@@ -863,7 +834,9 @@
inbound: inbound,
dbInbound: dbInbound,
confirm: async (inbound, dbInbound) => {
inModal.loading();
await this.updateInbound(inbound, dbInbound);
inModal.close();
},
isEdit: true
});
@@ -883,7 +856,7 @@
settings: inbound.settings.toString(),
};
if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString();
data.sniffing = inbound.sniffing.toString();
if (inbound.canSniffing()) data.sniffing = inbound.sniffing.toString();
await this.submit('/panel/inbound/add', data, inModal);
},
@@ -902,7 +875,7 @@
settings: inbound.settings.toString(),
};
if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString();
data.sniffing = inbound.sniffing.toString();
if (inbound.canSniffing()) data.sniffing = inbound.sniffing.toString();
await this.submit(`/panel/inbound/update/${dbInbound.id}`, data, inModal);
},
@@ -913,7 +886,9 @@
okText: '{{ i18n "pages.client.submitAdd"}}',
dbInbound: dbInbound,
confirm: async (clients, dbInboundId) => {
await this.addClient(clients, dbInboundId, clientModal);
clientModal.loading();
await this.addClient(clients, dbInboundId);
clientModal.close();
},
isEdit: false
});
@@ -925,7 +900,9 @@
okText: '{{ i18n "pages.client.bulk"}}',
dbInbound: dbInbound,
confirm: async (clients, dbInboundId) => {
await this.addClient(clients, dbInboundId, clientsBulkModal);
clientsBulkModal.loading();
await this.addClient(clients, dbInboundId);
clientsBulkModal.close();
},
});
},
@@ -954,19 +931,19 @@
default: return clients.findIndex(item => item.id === client.id && item.email === client.email);
}
},
async addClient(clients, dbInboundId, modal) {
async addClient(clients, dbInboundId) {
const data = {
id: dbInboundId,
settings: '{"clients": [' + clients.toString() + ']}',
};
await this.submit(`/panel/inbound/addClient`, data, modal);
await this.submit(`/panel/inbound/addClient`, data);
},
async updateClient(client, dbInboundId, clientId) {
const data = {
id: dbInboundId,
settings: '{"clients": [' + client.toString() + ']}',
};
await this.submit(`/panel/inbound/updateClient/${clientId}`, data, clientModal);
await this.submit(`/panel/inbound/updateClient/${clientId}`, data);
},
resetTraffic(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
@@ -986,7 +963,7 @@
},
delInbound(dbInboundId) {
this.$confirm({
title: '{{ i18n "pages.inbounds.deleteInbound"}}' + ' #' + dbInboundId,
title: '{{ i18n "pages.inbounds.deleteInbound"}}',
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
class: themeSwitcher.currentTheme,
okText: '{{ i18n "delete"}}',
@@ -999,7 +976,7 @@
clientId = this.getClientId(dbInbound.protocol, client);
if (confirmation){
this.$confirm({
title: '{{ i18n "pages.inbounds.deleteClient"}}' + ' ' + client.email,
title: '{{ i18n "pages.inbounds.deleteClient"}}',
content: '{{ i18n "pages.inbounds.deleteClientContent"}}',
class: themeSwitcher.currentTheme,
okText: '{{ i18n "delete"}}',
@@ -1053,9 +1030,8 @@
newDbInbound = this.checkFallback(dbInbound);
infoModal.show(newDbInbound, index);
},
switchEnable(dbInboundId,state) {
switchEnable(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
dbInbound.enable = state;
this.submit(`/panel/inbound/update/${dbInboundId}`, dbInbound);
},
async switchEnableClient(dbInboundId, client) {
@@ -1069,8 +1045,8 @@
await this.updateClient(clients[index], dbInboundId, clientId);
this.loading(false);
},
async submit(url, data, modal) {
const msg = await HttpUtil.postWithModal(url, data, modal);
async submit(url, data) {
const msg = await HttpUtil.postWithModal(url, data);
if (msg.success) {
await this.getDBInbounds();
}
@@ -1205,22 +1181,6 @@
newDbInbound = this.checkFallback(dbInbound);
txtModal.show('{{ i18n "pages.inbounds.export"}}', newDbInbound.genInboundLinks(), newDbInbound.remark);
},
exportSubs(dbInboundId) {
const dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
const clients = this.getInboundClients(dbInbound);
let subLinks = []
if (clients != null){
clients.forEach(c => {
if (c.subId && c.subId.length>0){
subLinks.push(this.subSettings.subURI + c.subId + "?name=" + c.subId)
}
})
}
txtModal.show(
'{{ i18n "pages.inbounds.export"}} - {{ i18n "pages.settings.subSettings" }}',
[...new Set(subLinks)].join('\n'),
dbInbound.remark + "-Subs");
},
importInbound() {
promptModal.open({
title: '{{ i18n "pages.inbounds.importInbound" }}',
@@ -1229,26 +1189,10 @@
okText: '{{ i18n "pages.inbounds.import" }}',
confirm: async (dbInboundText) => {
await this.submit('/panel/inbound/import', {data: dbInboundText}, promptModal);
promptModal.close();
},
});
},
exportAllSubs() {
let subLinks = []
for (const dbInbound of this.dbInbounds) {
const clients = this.getInboundClients(dbInbound);
if (clients != null){
clients.forEach(c => {
if (c.subId && c.subId.length>0){
subLinks.push(this.subSettings.subURI + c.subId + "?name=" + c.subId)
}
})
}
}
txtModal.show(
'{{ i18n "pages.inbounds.export"}} - {{ i18n "pages.settings.subSettings" }}',
[...new Set(subLinks)].join('\r\n'),
'All-Inbounds-Subs');
},
exportAllLinks() {
let copyText = [];
for (const dbInbound of this.dbInbounds) {
@@ -1289,7 +1233,7 @@
pagination(obj){
if (this.pageSize > 0 && obj.length>this.pageSize) {
// Set page options based on object size
sizeOptions = [];
sizeOptions = []
for (i=this.pageSize;i<=obj.length;i=i+this.pageSize) {
sizeOptions.push(i.toString());
}
@@ -1302,8 +1246,8 @@
position: 'bottom',
pageSize: this.pageSize,
pageSizeOptions: sizeOptions
};
return p;
}
return p
}
return false
},
@@ -1317,9 +1261,6 @@
}, 500)
},
mounted() {
if (window.location.protocol !== "https:") {
this.showAlert = true;
}
window.addEventListener('resize', this.onResize);
this.onResize();
this.loading();
@@ -1357,6 +1298,7 @@
}
},
});
</script>
{{template "inboundModal"}}
@@ -1366,5 +1308,6 @@
{{template "inboundInfoModal"}}
{{template "clientsModal"}}
{{template "clientsBulkModal"}}
</body>
</html>

View File

@@ -10,11 +10,13 @@
margin-inline: 0.3rem;
}
}
.ant-col-sm-24 {
margin-top: 10px;
}
.ant-card-dark h2 {
color: var(--dark-color-text-primary);
color: hsla(0, 0%, 100%, .65);
}
</style>
@@ -24,15 +26,6 @@
<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-row>
<a-card hoverable>
@@ -43,15 +36,15 @@
<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>
<div>CPU: [[ cpuCoreFormat(status.cpuCores) ]]</div>
<div>Speed: [[ 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) ]]
{{ i18n "pages.index.memory"}}: [[ sizeFormat(status.mem.current) ]] / [[ sizeFormat(status.mem.total) ]]
</div>
</a-col>
</a-row>
@@ -63,7 +56,7 @@
:stroke-color="status.swap.color"
:percent="status.swap.percent"></a-progress>
<div>
<b>Swap:</b> [[ sizeFormat(status.swap.current) ]] / [[ sizeFormat(status.swap.total) ]]
Swap: [[ sizeFormat(status.swap.current) ]] / [[ sizeFormat(status.swap.total) ]]
</div>
</a-col>
<a-col :span="12" style="text-align: center">
@@ -71,7 +64,7 @@
: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) ]]
{{ i18n "pages.index.hard"}}: [[ sizeFormat(status.disk.current) ]] / [[ sizeFormat(status.disk.total) ]]
</div>
</a-col>
</a-row>
@@ -82,28 +75,28 @@
</transition>
<transition name="list" appear>
<a-row>
<a-col :sm="24" :lg="12">
<a-col :sm="24" :md="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>
3X: <a href="https://github.com/MHSanaei/3x-ui/releases" target="_blank"><a-tag color="green">v{{ .cur_ver }}</a-tag></a>
Xray: <a-tag color="green" style="cursor: pointer;" @click="openSelectV2rayVersion">v[[ status.xray.version ]]</a-tag>
<a 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-col :sm="24" :md="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>
{{ i18n "menu.link" }}:
<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-col :sm="24" :md="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>
{{ i18n "pages.index.xrayStatus" }}:
<a-tag :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
<span slot="title" style="font-size: 12pt">Error in running xray-core
<a-tag color="purple" style="cursor: pointer; float: right;" @click="openLogs()">{{ i18n "pages.index.logs" }}</a-tag>
</span>
<template slot="content">
@@ -113,143 +106,133 @@
</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-tag color="purple" style="cursor: pointer;" @click="openSelectV2rayVersion">{{ i18n "pages.index.xraySwitch" }}</a-tag>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-col :sm="24" :md="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>
{{ i18n "pages.index.operationHours" }}:
Xray:
<a-tag color="green">[[ formatSecond(status.appStats.uptime) ]]</a-tag>
OS:
<a-tag color="green">[[ formatSecond(status.uptime) ]]</a-tag>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-col :sm="24" :md="12">
<a-card hoverable>
<b>{{ i18n "pages.index.systemLoad" }}:</b>
<a-tag color="green">
{{ i18n "pages.index.systemLoad" }}: [[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
<a-tooltip>
[[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
<template slot="title">
{{ i18n "pages.index.systemLoadDesc" }}
</template>
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</a-tag>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-col :sm="24" :md="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>
{{ i18n "usage"}}:
RAM [[ sizeFormat(status.appStats.mem) ]] -
Threads [[ status.appStats.threads ]]
</a-tooltip>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-col :sm="24" :md="12">
<a-card hoverable>
<a-row>
<a-col :span="12">
<a-tag>
IPv4:
<a-tooltip>
<a-icon type="global"></a-icon> IPv4
<template slot="title">
[[ status.publicIP.ipv4 ]]
</template>
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</a-tag>
</a-col>
<a-col :span="12">
<a-tag>
</a-col>
<a-col :span="12">
IPv6:
<a-tooltip>
<a-icon type="global"></a-icon> IPv6
<template slot="title">
[[ status.publicIP.ipv6 ]]
</template>
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</a-tag>
</a-col>
</a-row>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-col :sm="24" :md="12">
<a-card hoverable>
<a-row>
<a-col :span="12">
<a-tag>
TCP: [[ status.tcpCount ]]
<a-tooltip>
<a-icon type="swap"></a-icon> TCP: [[ status.tcpCount ]]
<template slot="title">
{{ i18n "pages.index.connectionTcpCountDesc" }}
</template>
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</a-tag>
</a-col>
<a-col :span="12">
<a-tag>
UDP: [[ status.udpCount ]]
<a-tooltip>
<a-icon type="swap"></a-icon> UDP: [[ status.udpCount ]]
<template slot="title">
{{ i18n "pages.index.connectionUdpCountDesc" }}
</template>
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</a-tag>
</a-col>
</a-row>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-col :sm="24" :md="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
[[ sizeFormat(status.netIO.up) ]]/s
<a-tooltip>
<template slot="title">
{{ i18n "pages.index.upSpeed" }}
</template>
<a-icon type="question-circle"></a-icon>
</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
[[ sizeFormat(status.netIO.down) ]]/s
<a-tooltip>
<template slot="title">
{{ i18n "pages.index.downSpeed" }}
</template>
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</a-tag>
</a-col>
</a-row>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-col :sm="24" :md="12">
<a-card hoverable>
<a-row>
<a-col :span="12">
<a-tag>
<a-icon type="cloud-upload"></a-icon>
[[ sizeFormat(status.netTraffic.sent) ]]
<a-tooltip>
<a-icon type="cloud-upload"></a-icon>
<template slot="title">
{{ i18n "pages.index.totalSent" }}
</template> Out: [[ sizeFormat(status.netTraffic.sent) ]]
</template>
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</a-tag>
</a-col>
<a-col :span="12">
<a-tag>
<a-icon type="cloud-download"></a-icon>
[[ sizeFormat(status.netTraffic.recv) ]]
<a-tooltip>
<a-icon type="cloud-download"></a-icon>
<template slot="title">
{{ i18n "pages.index.totalReceive" }}
</template> In: [[ sizeFormat(status.netTraffic.recv) ]]
</template>
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</a-tag>
</a-col>
</a-row>
</a-card>
@@ -263,66 +246,64 @@
: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>
<h2>{{ i18n "pages.index.xraySwitchClick"}}</h2>
<h2>{{ i18n "pages.index.xraySwitchClickDesk"}}</h2>
<template v-for="version, index in versionModal.versions">
<a-tag :color="index % 2 == 0 ? 'purple' : 'green'"
style="margin-right: 10px" @click="switchV2rayVersion(version)">
style="margin: 10px" @click="switchV2rayVersion(version)">
[[ version ]]
</a-tag>
</template>
</a-modal>
<a-modal id="log-modal" v-model="logModal.visible"
:closable="true" @cancel="() => logModal.visible = false"
<a-modal id="log-modal" v-model="logModal.visible" title="Logs"
:closable="true" @ok="() => logModal.visible = false" @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>
width="800px"
footer="">
<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 label="Count">
<a-select v-model="logModal.rows"
style="width: 80px"
@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-form-item>
<a-form-item label="Log Level">
<a-select v-model="logModal.level"
style="width: 120px"
@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-form-item>
<a-form-item label="SysLog">
<a-checkbox v-model="logModal.syslog" @change="openLogs()"></a-checkbox>
</a-form-item>
<a-form-item>
<a-checkbox v-model="logModal.syslog" @change="openLogs()">SysLog</a-checkbox>
<a-button class="ant-btn ant-btn-primary" :loading="logModal.loading" @click="openLogs()"><a-icon :spin="logModal.loading" type="sync"></a-icon> Reload</a-button>
</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-form-item>
<a-button type="primary" style="margin-bottom: 10px;"
:href="'data:application/text;charset=utf-8,' + encodeURIComponent(logModal.logs)" download="x-ui.log">
{{ i18n "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>
<div class="ant-input" style="height: auto; max-height: 500px; overflow: auto;" v-html="logModal.logs"></div>
</a-modal>
<a-modal id="backup-modal" v-model="backupModal.visible" :title="backupModal.title"
:closable="true" footer=""
:class="themeSwitcher.currentTheme">
:closable="true" :class="themeSwitcher.currentTheme"
@ok="() => backupModal.hide()" @cancel="() => backupModal.hide()">
<a-alert type="warning" style="margin-bottom: 10px; width: fit-content"
:message="backupModal.description"
show-icon
@@ -445,15 +426,14 @@
const logModal = {
visible: false,
logs: [],
logs: '',
rows: 20,
level: 'info',
syslog: false,
loading: false,
show(logs) {
this.visible = true;
this.logs = logs;
this.formattedLogs = this.logs?.length > 0 ? this.formatLogs(this.logs) : "No Record...";
this.logs = logs? this.formatLogs(logs) : "No Record...";
},
formatLogs(logs) {
let formattedLogs = '';
@@ -531,7 +511,6 @@
backupModal,
spinning: false,
loadingTip: '{{ i18n "loading"}}',
showAlert: false,
},
methods: {
loading(spinning, tip = '{{ i18n "loading"}}') {
@@ -655,14 +634,14 @@
},
},
async mounted() {
if (window.location.protocol !== "https:") {
this.showAlert = true;
}
while (true) {
let retries = 0;
while (retries < 5) {
try {
await this.getStatus();
retries = 0;
} catch (e) {
console.error(e);
console.error("Error occurred while fetching status:", e);
retries++;
}
await PromiseUtil.sleep(2000);
}

View File

@@ -75,37 +75,28 @@
<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-card hoverable style="margin-bottom: .5rem;">
<a-row>
<a-col :xs="24" :sm="8" 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">
<a-col :xs="24" :sm="16">
<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
>
<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>
</div>
</template>
</a-col>
@@ -147,32 +138,12 @@
<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
@@ -195,22 +166,42 @@
</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 style="padding: 20px;" layout="inline">
<table cellpadding="2">
<tr>
<td>{{ i18n "pages.settings.oldUsername"}}:</td>
<td>
<a-form-item>
<a-input v-model="user.oldUsername" style="width: 200px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.settings.currentPassword"}}:</td>
<td>
<a-form-item>
<password-input v-model="user.oldPassword" style="width: 200px"></password-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.settings.newUsername"}}:</td>
<td>
<a-form-item>
<a-input v-model="user.newUsername" style="width: 200px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.settings.newPassword"}}:</td>
<td>
<a-form-item>
<password-input v-model="user.newPassword" style="width: 200px"></password-input>
</a-form-item>
</td>
</tr>
</table>
<a-button type="primary" @click="updateUser">{{ i18n "confirm" }}</a-button>
</a-form>
<a-divider>{{ i18n "pages.settings.security.secret"}}</a-divider>
<a-form style="padding: 20px;">
@@ -242,6 +233,7 @@
<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>
@@ -251,19 +243,20 @@
<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%">
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>
&nbsp;&nbsp;<span v-text="l.name"></span>
@@ -290,17 +283,6 @@
<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>
@@ -326,28 +308,10 @@
saveBtnDisable: true,
user: {},
lang: getLang(),
showAlert: false,
remarkModels: {i:'Inbound',e:'Email',o:'Other'},
remarkSeparators: [' ','-','_','@',':','~','|',',','.','/'],
datepickerList: [{name:'Gregorian (Standard)', value: 'gregorian'}, {name:'Jalalian (شمسی)', value: 'jalalian'}],
remarkSample: '',
defaultFragment: {
tag: "fragment",
protocol: "freedom",
settings: {
domainStrategy: "AsIs",
fragment: {
packets: "tlshello",
length: "100-200",
interval: "10-20"
}
},
streamSettings: {
sockopt: {
tcpKeepAliveIdle: 100,
tcpNoDelay: true
}
}
},
get remarkModel() {
rm = this.allSetting.remarkModel;
return rm.length>1 ? rm.substring(1).split('') : [];
@@ -364,12 +328,6 @@
this.allSetting.remarkModel = value + this.allSetting.remarkModel.substring(1);
this.changeRemarkSample();
},
get datepicker() {
return this.allSetting.datepicker ? this.allSetting.datepicker : 'gregorian';
},
set datepicker(value) {
this.allSetting.datepicker = value;
},
changeRemarkSample(){
sample = []
this.remarkModel.forEach(r => sample.push(this.remarkModels[r]));
@@ -426,7 +384,9 @@
if (msg.success) {
this.loading(true);
await PromiseUtil.sleep(5000);
let { webCertFile, webKeyFile, webDomain: host, webPort: port, webBasePath: base } = this.allSetting;
var { webCertFile, webKeyFile, webDomain: host, webPort: port, webBasePath: base } = this.allSetting;
if (host == this.oldAllSetting.webDomain) host = null;
if (port == this.oldAllSetting.webPort) port = null;
const isTLS = webCertFile !== "" || webKeyFile !== "";
const url = buildURL({ host, port, isTLS, base, path: "panel/settings" });
window.location.replace(url);
@@ -476,59 +436,14 @@
}
},
},
computed: {
fragment: {
get: function() { return this.allSetting?.subJsonFragment != ""; },
set: function (v) {
this.allSetting.subJsonFragment = v ? JSON.stringify(this.defaultFragment) : "";
}
},
fragmentLength: {
get: function() { return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).settings.fragment.length : ""; },
set: function(v) {
if (v != ""){
newFragment = JSON.parse(this.allSetting.subJsonFragment);
newFragment.settings.fragment.length = v;
this.allSetting.subJsonFragment = JSON.stringify(newFragment);
}
}
},
fragmentInterval: {
get: function() { return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).settings.fragment.interval : ""; },
set: function(v) {
if (v != ""){
newFragment = JSON.parse(this.allSetting.subJsonFragment);
newFragment.settings.fragment.interval = v;
this.allSetting.subJsonFragment = JSON.stringify(newFragment);
}
}
},
confAlerts: {
get: function() {
if (!this.allSetting) return [];
var alerts = []
if (window.location.protocol !== "https:") alerts.push('{{ i18n "secAlertSSL" }}');
if (this.allSetting.webPort == 54321) alerts.push('{{ i18n "secAlertPanelPort" }}');
panelPath = window.location.pathname.split('/').length<4
if (panelPath && this.allSetting.webBasePath == '/') alerts.push('{{ i18n "secAlertPanelURI" }}');
if (this.allSetting.subEnable) {
subPath = this.allSetting.subURI.length >0 ? new URL(this.allSetting.subURI).pathname : this.allSetting.subPath;
if (subPath == '/sub/') alerts.push('{{ i18n "secAlertSubURI" }}');
subJsonPath = this.allSetting.subJsonURI.length >0 ? new URL(this.allSetting.subJsonURI).pathname : this.allSetting.subJsonPath;
if (subJsonPath == '/json/') alerts.push('{{ i18n "secAlertSubJsonURI" }}');
}
return alerts
}
}
},
async mounted() {
await this.getAllSetting();
while (true) {
await PromiseUtil.sleep(1000);
await PromiseUtil.sleep(600);
this.saveBtnDisable = this.oldAllSetting.equals(this.allSetting);
}
}
},
});
</script>
</body>
</html>
</html>

View File

@@ -1,205 +0,0 @@
{{define "warpModal"}}
<a-modal id="warp-modal" v-model="warpModal.visible" title="Cloudflare WARP"
:confirm-loading="warpModal.confirmLoading" :closable="true" :mask-closable="true"
:footer="null" :class="themeSwitcher.currentTheme">
<template v-if="ObjectUtil.isEmpty(warpModal.warpData)">
<a-button icon="api" @click="register" :loading="warpModal.confirmLoading">{{ i18n "pages.inbounds.create" }}</a-button>
</template>
<template v-else>
<table style="margin: 5px 0; width: 100%;">
<tr class="client-table-odd-row">
<td>Access Token</td>
<td>[[ warpModal.warpData.access_token ]]</td>
</tr>
<tr>
<td>Devide ID</td>
<td>[[ warpModal.warpData.device_id ]]</td>
</tr>
<tr class="client-table-odd-row">
<td>License Key</td>
<td>[[ warpModal.warpData.license_key ]]</td>
</tr>
<tr>
<td>Private Key</td>
<td>[[ warpModal.warpData.private_key ]]</td>
</tr>
</table>
<a-divider style="margin: 0;">{{ i18n "pages.settings.toasts.modifySettings" }}</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-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>
<template v-if="!ObjectUtil.isEmpty(warpModal.warpConfig)">
<table style="width: 100%">
<tr class="client-table-odd-row">
<td>Device Name</td>
<td>[[ warpModal.warpConfig.name ]]</td>
</tr>
<tr>
<td>Device Model</td>
<td>[[ warpModal.warpConfig.model ]]</td>
</tr>
<tr class="client-table-odd-row">
<td>Device Active</td>
<td>[[ warpModal.warpConfig.enabled ]]</td>
</tr>
<template v-if="!ObjectUtil.isEmpty(warpModal.warpConfig.account)">
<tr>
<td>Account Type</td>
<td>[[ warpModal.warpConfig.account.account_type ]]</td>
</tr>
<tr class="client-table-odd-row">
<td>Role</td>
<td>[[ warpModal.warpConfig.account.role ]]</td>
</tr>
<tr>
<td>Premium Data</td>
<td>[[ sizeFormat(warpModal.warpConfig.account.premium_data) ]]</td>
</tr>
<tr class="client-table-odd-row">
<td>Quota</td>
<td>[[ sizeFormat(warpModal.warpConfig.account.quota) ]]</td>
</tr>
<tr v-if="!ObjectUtil.isEmpty(warpModal.warpConfig.account.usage)">
<td>Usage</td>
<td>[[ sizeFormat(warpModal.warpConfig.account.usage) ]]</td>
</tr>
</template>
</table>
<a-divider style="margin: 10px 0;">WARP {{ i18n "pages.xray.rules.outbound" }}</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>
</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>
</template>
</a-form-item>
</a-form>
</template>
</template>
</a-modal>
<script>
const warpModal = {
visible: false,
confirmLoading: false,
warpData: null,
warpConfig: null,
warpOutbound: null,
show() {
this.visible = true;
this.warpConfig = null;
this.getData();
},
close() {
this.visible = false;
this.loading(false);
},
loading(loading=true) {
this.confirmLoading = loading;
},
async getData(){
this.loading(true);
const msg = await HttpUtil.post('/panel/xray/warp/data');
this.loading(false);
if (msg.success) {
this.warpData = msg.obj.length>0 ? JSON.parse(msg.obj): null;
}
},
};
new Vue({
delimiters: ['[[', ']]'],
el: '#warp-modal',
data: {
warpModal: warpModal,
warpPlus: '',
},
methods: {
collectConfig() {
config = warpModal.warpConfig.config;
peer = config.peers[0];
if(config){
warpModal.warpOutbound = Outbound.fromJson({
tag: 'warp',
protocol: Protocols.Wireguard,
settings: {
mtu: 1420,
secretKey: warpModal.warpData.private_key,
address: Object.values(config.interface.addresses),
domainStrategy: 'ForceIP',
peers: [{
publicKey: peer.public_key,
endpoint: peer.endpoint.host,
}],
kernelMode: false
}
});
}
},
async register(){
warpModal.loading(true);
keys = Wireguard.generateKeypair();
const msg = await HttpUtil.post('/panel/xray/warp/reg',keys);
if (msg.success) {
resp = JSON.parse(msg.obj);
warpModal.warpData = resp.data;
warpModal.warpConfig = resp.config;
this.collectConfig();
}
warpModal.loading(false);
},
async updateLicense(l){
warpModal.loading(true);
const msg = await HttpUtil.post('/panel/xray/warp/license',{license: l});
if (msg.success) {
warpModal.warpData = JSON.parse(msg.obj);
warpModal.warpConfig = null;
this.warpPlus = '';
}
warpModal.loading(false);
},
async getConfig(){
warpModal.loading(true);
const msg = await HttpUtil.post('/panel/xray/warp/config');
warpModal.loading(false);
if (msg.success) {
warpModal.warpConfig = JSON.parse(msg.obj);
this.collectConfig();
}
},
addOutbound(){
app.templateSettings.outbounds.push(warpModal.warpOutbound.toJson());
app.outboundSettings = JSON.stringify(app.templateSettings.outbounds);
warpModal.close();
},
resetOutbound(){
app.templateSettings.outbounds[this.warpOutboundIndex] = warpModal.warpOutbound.toJson();
app.outboundSettings = JSON.stringify(app.templateSettings.outbounds);
warpModal.close();
}
},
computed: {
warpOutboundIndex: {
get: function() {
return app.templateSettings ? app.templateSettings.outbounds.findIndex((o) => o.tag == 'warp') : -1;
}
}
}
});
</script>
{{end}}

File diff suppressed because it is too large Load Diff

View File

@@ -1,113 +0,0 @@
{{define "balancerModal"}}
<a-modal
id="balancer-modal"
v-model="balancerModal.visible"
:title="balancerModal.title"
@ok="balancerModal.ok"
:confirm-loading="balancerModal.confirmLoading"
:ok-button-props="{ props: { disabled: !balancerModal.isValid } }"
:closable="true"
:mask-closable="false"
:ok-text="balancerModal.okText"
cancel-text='{{ i18n "close" }}'
:class="themeSwitcher.currentTheme">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "pages.xray.balancer.tag" }}' has-feedback
:validate-status="balancerModal.duplicateTag? 'warning' : 'success'">
<a-input v-model.trim="balancerModal.balancer.tag" @change="balancerModal.check()"
placeholder='{{ i18n "pages.xray.balancer.tagDesc" }}'></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.balancer.balancerStrategy" }}'>
<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>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.balancer.balancerSelectors" }}' has-feedback
:validate-status="balancerModal.emptySelector? 'warning' : 'success'">
<a-select v-model="balancerModal.balancer.selector" mode="tags" @change="balancerModal.checkSelector()"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="tag in balancerModal.outboundTags" :value="tag">[[ tag ]]</a-select-option>
</a-select>
</a-form-item>
</table>
</a-form>
</a-modal>
<script>
const balancerModal = {
title: '',
visible: false,
confirmLoading: false,
okText: '{{ i18n "sure" }}',
isEdit: false,
confirm: null,
duplicateTag: false,
emptySelector: false,
balancer: {
tag: '',
strategy: 'random',
selector: []
},
outboundTags: [],
balancerTags:[],
ok() {
if (balancerModal.balancer.selector.length == 0) {
balancerModal.emptySelector = true;
return;
}
balancerModal.emptySelector = false;
ObjectUtil.execute(balancerModal.confirm, balancerModal.balancer);
},
show({ title = '', okText = '{{ i18n "sure" }}', balancerTags = [], balancer, confirm = (balancer) => { }, isEdit = false }) {
this.title = title;
this.okText = okText;
this.confirm = confirm;
this.visible = true;
if (isEdit) {
balancerModal.balancer = balancer;
} else {
balancerModal.balancer = {
tag: '',
strategy: 'random',
selector: []
};
}
this.balancerTags = balancerTags.filter((tag) => tag != balancer.tag);
this.outboundTags = app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag);
this.isEdit = isEdit;
this.check();
this.checkSelector();
},
close() {
this.visible = false;
this.loading(false);
},
loading(loading=true) {
this.confirmLoading = loading;
},
check() {
if (this.balancer.tag == '' || this.balancerTags.includes(this.balancer.tag)) {
this.duplicateTag = true;
this.isValid = false;
} else {
this.duplicateTag = false;
this.isValid = true;
}
},
checkSelector() {
this.emptySelector = this.balancer.selector.length == 0;
}
};
new Vue({
delimiters: ['[[', ']]'],
el: '#balancer-modal',
data: {
balancerModal: balancerModal
},
methods: {
}
});
</script>
{{end}}

View File

@@ -42,7 +42,7 @@
outModal.visible = false;
outModal.loading(false);
},
loading(loading=true) {
loading(loading) {
outModal.confirmLoading = loading;
},
check(){

View File

@@ -2,7 +2,7 @@
<a-modal id="reverse-modal" v-model="reverseModal.visible" :title="reverseModal.title" @ok="reverseModal.ok"
:confirm-loading="reverseModal.confirmLoading" :closable="true" :mask-closable="false"
:ok-text="reverseModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
<a-select v-model="reverseModal.reverse.type" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="x,y in reverseTypes" :value="y">[[ x ]]</a-select-option>
@@ -120,7 +120,7 @@
reverseModal.visible = false;
reverseModal.loading(false);
},
loading(loading=true) {
loading(loading) {
reverseModal.confirmLoading = loading;
},
};
@@ -132,6 +132,8 @@
reverseModal: reverseModal,
reverseTypes: { bridge: '{{ i18n "pages.xray.outbound.bridge" }}', portal:'{{ i18n "pages.xray.outbound.portal" }}'},
},
methods: {
}
});
</script>

View File

@@ -2,7 +2,7 @@
<a-modal id="rule-modal" v-model="ruleModal.visible" :title="ruleModal.title" @ok="ruleModal.ok"
:confirm-loading="ruleModal.confirmLoading" :closable="true" :mask-closable="false"
:ok-text="ruleModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='Domain Matcher'>
<a-select v-model="ruleModal.rule.domainMatcher" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="dm in ['','hybrid','linear']" :value="dm">[[ dm ]]</a-select-option>
@@ -20,7 +20,7 @@
<a-input v-model.trim="ruleModal.rule.source"></a-input>
</a-form-item>
<a-form-item>
<template slot="label">
<template slot="label">Source Port
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
@@ -36,8 +36,8 @@
</a-select>
</a-form-item>
<a-form-item label='Protocol'>
<a-select v-model="ruleModal.rule.protocol" mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="x in ['http','tls','bittorrent']" :value="x">[[ x ]]</a-select-option>
<a-select v-model="ruleModal.rule.protocol" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="x in ['','http','tls','bittorrent']" :value="x">[[ x ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='Attributes'>
@@ -107,19 +107,6 @@
<a-select-option v-for="tag in ruleModal.outboundTags" :value="tag">[[ tag ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.xray.balancer.balancerDesc" }}</span>
</template>
Balancer Tag <a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-select v-model="ruleModal.rule.balancerTag" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="tag in ruleModal.balancerTags" :value="tag">[[ tag ]]</a-select-option>
</a-select>
</a-form-item>
</table>
</a-form>
</a-modal>
@@ -146,12 +133,11 @@
protocol: [],
attrs: [],
outboundTag: "",
balancerTag: "",
},
inboundTags: [],
outboundTags: [],
users: [],
balancerTags: [],
balancerTag: [],
ok() {
newRule = ruleModal.getResult();
ObjectUtil.execute(ruleModal.confirm, newRule);
@@ -174,7 +160,6 @@
this.rule.protocol = rule.protocol;
this.rule.attrs = rule.attrs ? Object.entries(rule.attrs) : [];
this.rule.outboundTag = rule.outboundTag;
this.rule.balancerTag = rule.balancerTag ? rule.balancerTag : "";
} else {
this.rule = {
domainMatcher: "",
@@ -189,29 +174,24 @@
protocol: [],
attrs: [],
outboundTag: "",
balancerTag: "",
}
}
this.isEdit = isEdit;
this.inboundTags = app.templateSettings.inbounds.filter((i) => !ObjectUtil.isEmpty(i.tag)).map(obj => obj.tag);
this.inboundTags.push(...app.inboundTags);
this.outboundTags = ["", ...app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag)];
this.outboundTags = app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag);
if(app.templateSettings.reverse){
if(app.templateSettings.reverse.bridges) {
this.inboundTags.push(...app.templateSettings.reverse.bridges.map(b => b.tag));
}
if(app.templateSettings.reverse.portals) this.outboundTags.push(...app.templateSettings.reverse.portals.map(b => b.tag));
}
if (app.templateSettings.routing && app.templateSettings.routing.balancers) {
this.balancerTags = [ "", ...app.templateSettings.routing.balancers.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag)];
}
},
close() {
ruleModal.visible = false;
ruleModal.loading(false);
},
loading(loading=true) {
loading(loading) {
ruleModal.confirmLoading = loading;
},
getResult() {
@@ -230,8 +210,7 @@
rule.inboundTag = value.inboundTag;
rule.protocol = value.protocol;
rule.attrs = Object.fromEntries(value.attrs);
rule.outboundTag = value.outboundTag == "" ? undefined : value.outboundTag;
rule.balancerTag = value.balancerTag == "" ? undefined : value.balancerTag;
rule.outboundTag = value.outboundTag;
for (const [key, value] of Object.entries(rule)) {
if (

View File

@@ -1,9 +1,7 @@
package job
import (
"bufio"
"encoding/json"
"io"
"log"
"os"
"os/exec"
@@ -14,22 +12,18 @@ import (
"x-ui/database"
"x-ui/database/model"
"x-ui/config"
"x-ui/logger"
"x-ui/xray"
)
type CheckClientIpJob struct {
disAllowedIps []string
}
type CheckClientIpJob struct{}
var job *CheckClientIpJob
var disAllowedIps []string
var ipFiles = []string{
xray.GetIPLimitLogPath(),
xray.GetIPLimitBannedLogPath(),
xray.GetIPLimitBannedPrevLogPath(),
xray.GetAccessPersistentLogPath(),
xray.GetAccessPersistentPrevLogPath(),
}
func NewCheckClientIpJob() *CheckClientIpJob {
@@ -39,10 +33,8 @@ func NewCheckClientIpJob() *CheckClientIpJob {
func (j *CheckClientIpJob) Run() {
// create files and dirs required for iplimit if not exists
// create files 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()
@@ -53,37 +45,6 @@ func (j *CheckClientIpJob) Run() {
j.checkFail2BanInstalled()
j.processLogFile()
}
if !j.hasLimitIp() && xray.GetAccessLogPath() == "./access.log" {
go j.clearLogTime()
}
}
func (j *CheckClientIpJob) clearLogTime() {
for {
time.Sleep(time.Hour)
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)
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)
// clean access log
err = os.Truncate(accessLogPath, 0)
j.checkError(err)
}
func (j *CheckClientIpJob) hasLimitIp() bool {
@@ -127,34 +88,24 @@ func (j *CheckClientIpJob) checkFail2BanInstalled() {
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")
logger.Warning("access.log doesn't exist in your config.json")
return
}
file, err := os.Open(accessLogPath)
j.checkError(err)
defer file.Close()
data, err := os.ReadFile(accessLogPath)
InboundClientIps := make(map[string][]string)
j.checkError(err)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
ipRegx, _ := regexp.Compile(`(\d+\.\d+\.\d+\.\d+).* accepted`)
lines := strings.Split(string(data), "\n")
for _, line := range lines {
ipRegx, _ := regexp.Compile(`[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+`)
emailRegx, _ := regexp.Compile(`email:.+`)
matches := ipRegx.FindStringSubmatch(line)
if len(matches) > 1 {
ip := matches[1]
if ip == "127.0.0.1" {
matchesIp := ipRegx.FindString(line)
if len(matchesIp) > 0 {
ip := string(matchesIp)
if ip == "127.0.0.1" || ip == "1.1.1.1" {
continue
}
@@ -169,14 +120,14 @@ func (j *CheckClientIpJob) processLogFile() {
continue
}
InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail], ip)
} else {
InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail], ip)
}
}
}
j.checkError(scanner.Err())
disAllowedIps = []string{}
shouldCleanLog := false
for clientEmail, ips := range InboundClientIps {
@@ -187,13 +138,27 @@ func (j *CheckClientIpJob) processLogFile() {
} else {
shouldCleanLog = j.updateInboundClientIps(inboundClientIps, clientEmail, ips)
}
}
// added delay before cleaning logs to reduce chance of logging IP that already has been banned
time.Sleep(time.Second * 2)
if shouldCleanLog {
j.clearAccessLog()
// copy access log to persistent file
logAccessP, err := os.OpenFile(xray.GetAccessPersistentLogPath(), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
j.checkError(err)
input, err := os.ReadFile(accessLogPath)
j.checkError(err)
if _, err := logAccessP.Write(input); err != nil {
j.checkError(err)
}
defer logAccessP.Close()
// clean access log
if err := os.Truncate(xray.GetAccessLogPath(), 0); err != nil {
j.checkError(err)
}
}
}
@@ -269,7 +234,6 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun
json.Unmarshal([]byte(inbound.Settings), &settings)
clients := settings["clients"]
shouldCleanLog := false
j.disAllowedIps = []string{}
// create iplimit log file channel
logIpFile, err := os.OpenFile(xray.GetIPLimitLogPath(), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
@@ -288,7 +252,7 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun
shouldCleanLog = true
if limitIp < len(ips) && inbound.Enable {
j.disAllowedIps = append(j.disAllowedIps, ips[limitIp:]...)
disAllowedIps = append(disAllowedIps, ips[limitIp:]...)
for i := limitIp; i < len(ips); i++ {
log.Printf("[LIMIT_IP] Email = %s || SRC = %s", clientEmail, ips[i])
}
@@ -296,12 +260,8 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun
}
}
}
sort.Strings(j.disAllowedIps)
if len(j.disAllowedIps) > 0 {
logger.Debug("disAllowedIps ", j.disAllowedIps)
}
logger.Debug("disAllowedIps ", disAllowedIps)
sort.Strings(disAllowedIps)
db := database.GetDB()
err = db.Save(inboundClientIps).Error

View File

@@ -1,9 +1,6 @@
package job
import (
"x-ui/logger"
"x-ui/web/service"
)
import "x-ui/web/service"
type CheckXrayRunningJob struct {
xrayService service.XrayService
@@ -18,15 +15,11 @@ func NewCheckXrayRunningJob() *CheckXrayRunningJob {
func (j *CheckXrayRunningJob) Run() {
if j.xrayService.IsXrayRunning() {
j.checkTime = 0
} else {
j.checkTime++
//only restart if it's down 2 times in a row
if j.checkTime > 1 {
err := j.xrayService.RestartXray(false)
j.checkTime = 0
if err != nil {
logger.Error("Restart xray failed:", err)
}
}
return
}
j.checkTime++
if j.checkTime < 2 {
return
}
j.xrayService.SetToNeedRestart()
}

View File

@@ -15,38 +15,10 @@ func NewClearLogsJob() *ClearLogsJob {
// Here Run is an interface method of the Job interface
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
// clear log files
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)
if err != nil {
logger.Warning("clear logs job err:", err)
}
logFile, err := os.ReadFile(logFiles[i])
if err != nil {
logger.Warning("clear logs job err:", err)
}
_, err = logFilePrev.Write(logFile)
if err != nil {
logger.Warning("clear logs job err:", err)
}
defer logFilePrev.Close()
}
err := os.Truncate(logFiles[i], 0)
if err != nil {
if err := os.Truncate(logFiles[i], 0); err != nil {
logger.Warning("clear logs job err:", err)
}
}

View File

@@ -6,9 +6,8 @@ import (
)
type XrayTrafficJob struct {
xrayService service.XrayService
inboundService service.InboundService
outboundService service.OutboundService
xrayService service.XrayService
inboundService service.InboundService
}
func NewXrayTrafficJob() *XrayTrafficJob {
@@ -25,15 +24,11 @@ func (j *XrayTrafficJob) Run() {
logger.Warning("get xray traffic failed:", err)
return
}
err, needRestart0 := j.inboundService.AddTraffic(traffics, clientTraffics)
err, needRestart := j.inboundService.AddTraffic(traffics, clientTraffics)
if err != nil {
logger.Warning("add inbound traffic failed:", err)
logger.Warning("add traffic failed:", err)
}
err, needRestart1 := j.outboundService.AddTraffic(traffics, clientTraffics)
if err != nil {
logger.Warning("add outbound traffic failed:", err)
}
if needRestart0 || needRestart1 {
if needRestart {
j.xrayService.SetToNeedRestart()
}

View File

@@ -1,9 +1,7 @@
{
"log": {
"access": "none",
"dnsLog": false,
"error": "./error.log",
"loglevel": "warning"
"loglevel": "warning",
"error": "./error.log"
},
"api": {
"tag": "api",
@@ -26,7 +24,6 @@
],
"outbounds": [
{
"tag": "direct",
"protocol": "freedom",
"settings": {}
},
@@ -45,9 +42,7 @@
},
"system": {
"statsInboundDownlink": true,
"statsInboundUplink": true,
"statsOutboundDownlink": true,
"statsOutboundUplink": true
"statsInboundUplink": true
}
},
"routing": {

View File

@@ -38,25 +38,9 @@ func (s *InboundService) GetAllInbounds() ([]*model.Inbound, error) {
return inbounds, nil
}
func (s *InboundService) checkPortExist(listen string, port int, ignoreId int) (bool, error) {
func (s *InboundService) checkPortExist(port int, ignoreId int) (bool, error) {
db := database.GetDB()
if listen == "" || listen == "0.0.0.0" || listen == "::" || listen == "::0" {
db = db.Model(model.Inbound{}).Where("port = ?", port)
} else {
db = db.Model(model.Inbound{}).
Where("port = ?", port).
Where(
db.Model(model.Inbound{}).Where(
"listen = ?", listen,
).Or(
"listen = \"\"",
).Or(
"listen = \"0.0.0.0\"",
).Or(
"listen = \"::\"",
).Or(
"listen = \"::0\""))
}
db = db.Model(model.Inbound{}).Where("port = ?", port)
if ignoreId > 0 {
db = db.Where("id != ?", ignoreId)
}
@@ -151,7 +135,7 @@ func (s *InboundService) checkEmailExistForInbound(inbound *model.Inbound) (stri
}
func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, bool, error) {
exist, err := s.checkPortExist(inbound.Listen, inbound.Port, 0)
exist, err := s.checkPortExist(inbound.Port, 0)
if err != nil {
return inbound, false, err
}
@@ -268,7 +252,7 @@ func (s *InboundService) GetInbound(id int) (*model.Inbound, error) {
}
func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound, bool, error) {
exist, err := s.checkPortExist(inbound.Listen, inbound.Port, inbound.Id)
exist, err := s.checkPortExist(inbound.Port, inbound.Id)
if err != nil {
return inbound, false, err
}
@@ -311,11 +295,7 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
oldInbound.Settings = inbound.Settings
oldInbound.StreamSettings = inbound.StreamSettings
oldInbound.Sniffing = inbound.Sniffing
if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" {
oldInbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
} else {
oldInbound.Tag = fmt.Sprintf("inbound-%v:%v", inbound.Listen, inbound.Port)
}
oldInbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
needRestart := false
s.xrayApi.Init(p.GetAPIPort())
@@ -508,10 +488,6 @@ func (s *InboundService) DelInboundClient(inboundId int, clientId string) (bool,
}
}
if len(newClients) == 0 {
return false, common.NewError("no client remained in Inbound")
}
settings["clients"] = newClients
newSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil {
@@ -682,7 +658,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 +670,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
}
@@ -1170,8 +1146,6 @@ func (s *InboundService) SetClientTelegramUserID(trafficId int, tgId string) err
if oldClient.Email == clientEmail {
if inbound.Protocol == "trojan" {
clientId = oldClient.Password
} else if inbound.Protocol == "shadowsocks" {
clientId = oldClient.Email
} else {
clientId = oldClient.ID
}
@@ -1210,32 +1184,6 @@ func (s *InboundService) SetClientTelegramUserID(trafficId int, tgId string) err
return nil
}
func (s *InboundService) checkIsEnabledByEmail(clientEmail string) (bool, error) {
_, inbound, err := s.GetClientInboundByEmail(clientEmail)
if err != nil {
return false, err
}
if inbound == nil {
return false, common.NewError("Inbound Not Found For Email:", clientEmail)
}
clients, err := s.GetClients(inbound)
if err != nil {
return false, err
}
isEnable := false
for _, client := range clients {
if client.Email == clientEmail {
isEnable = client.Enable
break
}
}
return isEnable, err
}
func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, error) {
_, inbound, err := s.GetClientInboundByEmail(clientEmail)
if err != nil {
@@ -1257,8 +1205,6 @@ func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, er
if oldClient.Email == clientEmail {
if inbound.Protocol == "trojan" {
clientId = oldClient.Password
} else if inbound.Protocol == "shadowsocks" {
clientId = oldClient.Email
} else {
clientId = oldClient.ID
}
@@ -1320,8 +1266,6 @@ func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int
if oldClient.Email == clientEmail {
if inbound.Protocol == "trojan" {
clientId = oldClient.Password
} else if inbound.Protocol == "shadowsocks" {
clientId = oldClient.Email
} else {
clientId = oldClient.ID
}
@@ -1380,8 +1324,6 @@ func (s *InboundService) ResetClientExpiryTimeByEmail(clientEmail string, expiry
if oldClient.Email == clientEmail {
if inbound.Protocol == "trojan" {
clientId = oldClient.Password
} else if inbound.Protocol == "shadowsocks" {
clientId = oldClient.Email
} else {
clientId = oldClient.ID
}
@@ -1443,8 +1385,6 @@ func (s *InboundService) ResetClientTrafficLimitByEmail(clientEmail string, tota
if oldClient.Email == clientEmail {
if inbound.Protocol == "trojan" {
clientId = oldClient.Password
} else if inbound.Protocol == "shadowsocks" {
clientId = oldClient.Email
} else {
clientId = oldClient.ID
}
@@ -1673,10 +1613,10 @@ func (s *InboundService) DelDepletedClients(id int) (err error) {
return nil
}
func (s *InboundService) GetClientTrafficTgBot(tgId string) ([]*xray.ClientTraffic, error) {
func (s *InboundService) GetClientTrafficTgBot(tguname string) ([]*xray.ClientTraffic, error) {
db := database.GetDB()
var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Where("settings like ?", fmt.Sprintf(`%%"tgId": "%s"%%`, tgId)).Find(&inbounds).Error
err := db.Model(model.Inbound{}).Where("settings like ?", fmt.Sprintf(`%%"tgId": "%s"%%`, tguname)).Find(&inbounds).Error
if err != nil && err != gorm.ErrRecordNotFound {
return nil, err
}
@@ -1687,7 +1627,7 @@ func (s *InboundService) GetClientTrafficTgBot(tgId string) ([]*xray.ClientTraff
logger.Error("Unable to get clients from inbound")
}
for _, client := range clients {
if client.TgID == tgId {
if client.TgID == tguname {
emails = append(emails, client.Email)
}
}
@@ -1814,7 +1754,7 @@ func (s *InboundService) MigrationRequirements() {
json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings)
clients, ok := settings["clients"].([]interface{})
if ok {
// Fix Client configuration problems
// Fix Clinet configuration problems
var newClients []interface{}
for client_index := range clients {
c := clients[client_index].(map[string]interface{})
@@ -1900,13 +1840,6 @@ func (s *InboundService) MigrationRequirements() {
newStream, _ := json.MarshalIndent(stream, " ", " ")
tx.Model(model.Inbound{}).Where("id = ?", ep.Id).Update("stream_settings", newStream)
}
err = tx.Raw(`UPDATE inbounds
SET tag = REPLACE(tag, '0.0.0.0:', '')
WHERE INSTR(tag, '0.0.0.0:') > 0;`).Error
if err != nil {
return
}
}
func (s *InboundService) MigrateDB() {

View File

@@ -1,101 +0,0 @@
package service
import (
"x-ui/database"
"x-ui/database/model"
"x-ui/logger"
"x-ui/xray"
"gorm.io/gorm"
)
type OutboundService struct {
}
func (s *OutboundService) AddTraffic(traffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) {
var err error
db := database.GetDB()
tx := db.Begin()
defer func() {
if err != nil {
tx.Rollback()
} else {
tx.Commit()
}
}()
err = s.addOutboundTraffic(tx, traffics)
if err != nil {
return err, false
}
return nil, false
}
func (s *OutboundService) addOutboundTraffic(tx *gorm.DB, traffics []*xray.Traffic) error {
if len(traffics) == 0 {
return nil
}
var err error
for _, traffic := range traffics {
if traffic.IsOutbound {
var outbound model.OutboundTraffics
err = tx.Model(&model.OutboundTraffics{}).Where("tag = ?", traffic.Tag).
FirstOrCreate(&outbound).Error
if err != nil {
return err
}
outbound.Tag = traffic.Tag
outbound.Up = outbound.Up + traffic.Up
outbound.Down = outbound.Down + traffic.Down
outbound.Total = outbound.Up + outbound.Down
err = tx.Save(&outbound).Error
if err != nil {
return err
}
}
}
return nil
}
func (s *OutboundService) GetOutboundsTraffic() ([]*model.OutboundTraffics, error) {
db := database.GetDB()
var traffics []*model.OutboundTraffics
err := db.Model(model.OutboundTraffics{}).Find(&traffics).Error
if err != nil {
logger.Warning(err)
return nil, err
}
return traffics, nil
}
func (s *OutboundService) ResetOutboundTraffic(tag string) error {
db := database.GetDB()
whereText := "tag "
if tag == "-alltags-" {
whereText += " <> ?"
} else {
whereText += " = ?"
}
result := db.Model(model.OutboundTraffics{}).
Where(whereText, tag).
Updates(map[string]interface{}{"up": 0, "down": 0, "total": 0})
err := result.Error
if err != nil {
return err
}
return nil
}

View File

@@ -38,7 +38,6 @@ var defaultValueMap = map[string]string{
"timeLocation": "Asia/Tehran",
"tgBotEnable": "false",
"tgBotToken": "",
"tgBotProxy": "",
"tgBotChatId": "",
"tgRunTime": "@daily",
"tgBotBackup": "false",
@@ -57,11 +56,6 @@ var defaultValueMap = map[string]string{
"subEncrypt": "true",
"subShowInfo": "true",
"subURI": "",
"subJsonPath": "/json/",
"subJsonURI": "",
"subJsonFragment": "",
"datepicker": "gregorian",
"warp": "",
}
type SettingService struct {
@@ -250,14 +244,6 @@ func (s *SettingService) SetTgBotToken(token string) error {
return s.setString("tgBotToken", token)
}
func (s *SettingService) GetTgBotProxy() (string, error) {
return s.getString("tgBotProxy")
}
func (s *SettingService) SetTgBotProxy(token string) error {
return s.setString("tgBotProxy", token)
}
func (s *SettingService) GetTgBotChatId() (string, error) {
return s.getString("tgBotChatId")
}
@@ -390,11 +376,17 @@ func (s *SettingService) GetSubPort() (int, error) {
}
func (s *SettingService) GetSubPath() (string, error) {
return s.getString("subPath")
}
func (s *SettingService) GetSubJsonPath() (string, error) {
return s.getString("subJsonPath")
subPath, err := s.getString("subPath")
if err != nil {
return "", err
}
if !strings.HasPrefix(subPath, "/") {
subPath = "/" + subPath
}
if !strings.HasSuffix(subPath, "/") {
subPath += "/"
}
return subPath, nil
}
func (s *SettingService) GetSubDomain() (string, error) {
@@ -409,8 +401,8 @@ func (s *SettingService) GetSubKeyFile() (string, error) {
return s.getString("subKeyFile")
}
func (s *SettingService) GetSubUpdates() (string, error) {
return s.getString("subUpdates")
func (s *SettingService) GetSubUpdates() (int, error) {
return s.getInt("subUpdates")
}
func (s *SettingService) GetSubEncrypt() (bool, error) {
@@ -421,31 +413,12 @@ func (s *SettingService) GetSubShowInfo() (bool, error) {
return s.getBool("subShowInfo")
}
func (s *SettingService) GetPageSize() (int, error) {
return s.getInt("pageSize")
}
func (s *SettingService) GetSubURI() (string, error) {
return s.getString("subURI")
}
func (s *SettingService) GetSubJsonURI() (string, error) {
return s.getString("subJsonURI")
}
func (s *SettingService) GetSubJsonFragment() (string, error) {
return s.getString("subJsonFragment")
}
func (s *SettingService) GetDatepicker() (string, error) {
return s.getString("datepicker")
}
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) GetPageSize() (int, error) {
return s.getInt("pageSize")
}
func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error {
@@ -489,9 +462,7 @@ func (s *SettingService) GetDefaultSettings(host string) (interface{}, error) {
"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() },
}
result := make(map[string]interface{})
@@ -504,11 +475,10 @@ func (s *SettingService) GetDefaultSettings(host string) (interface{}, error) {
result[key] = value
}
if result["subEnable"].(bool) && (result["subURI"].(string) == "" || result["subJsonURI"].(string) == "") {
if result["subEnable"].(bool) && result["subURI"].(string) == "" {
subURI := ""
subPort, _ := s.GetSubPort()
subPath, _ := s.GetSubPath()
subJsonPath, _ := s.GetSubJsonPath()
subDomain, _ := s.GetSubDomain()
subKeyFile, _ := s.GetSubKeyFile()
subCertFile, _ := s.GetSubCertFile()
@@ -529,12 +499,12 @@ func (s *SettingService) GetDefaultSettings(host string) (interface{}, error) {
} else {
subURI += fmt.Sprintf("%s:%d", subDomain, subPort)
}
if result["subURI"].(string) == "" {
result["subURI"] = subURI + subPath
}
if result["subJsonURI"].(string) == "" {
result["subJsonURI"] = subURI + subJsonPath
if subPath[0] == byte('/') {
subURI += subPath
} else {
subURI += "/" + subPath
}
result["subURI"] = subURI
}
return result, nil

File diff suppressed because it is too large Load Diff

View File

@@ -95,7 +95,7 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
if !clientTraffic.Enable {
clients = RemoveIndex(clients, index-indexDecrease)
indexDecrease++
logger.Info("Remove Inbound User ", c["email"], " due the expire or traffic limit")
logger.Info("Remove Inbound User", c["email"], "due the expire or traffic limit")
}
@@ -185,7 +185,7 @@ func (s *XrayService) RestartXray(isForce bool) error {
return err
}
if s.IsXrayRunning() {
if p != nil && p.IsRunning() {
if !isForce && p.GetConfig().Equals(xrayConfig) {
logger.Debug("It does not need to restart xray")
return nil

View File

@@ -1,13 +1,8 @@
package service
import (
"bytes"
_ "embed"
"encoding/json"
"fmt"
"net/http"
"os"
"time"
"x-ui/util/common"
"x-ui/xray"
)
@@ -31,142 +26,3 @@ func (s *XraySettingService) CheckXrayConfig(XrayTemplateConfig string) error {
}
return nil
}
func (s *XraySettingService) GetWarpData() (string, error) {
warp, err := s.SettingService.GetWarp()
if err != nil {
return "", err
}
return warp, nil
}
func (s *XraySettingService) GetWarpConfig() (string, error) {
var warpData map[string]string
warp, err := s.SettingService.GetWarp()
if err != nil {
return "", err
}
err = json.Unmarshal([]byte(warp), &warpData)
if err != nil {
return "", err
}
url := fmt.Sprintf("https://api.cloudflareclient.com/v0a2158/reg/%s", warpData["device_id"])
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return "", err
}
req.Header.Set("Authorization", "Bearer "+warpData["access_token"])
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
buffer := bytes.NewBuffer(make([]byte, 8192))
buffer.Reset()
_, err = buffer.ReadFrom(resp.Body)
if err != nil {
return "", err
}
return buffer.String(), nil
}
func (s *XraySettingService) RegWarp(secretKey string, publicKey string) (string, error) {
tos := time.Now().UTC().Format("2006-01-02T15:04:05.000Z")
hostName, _ := os.Hostname()
data := fmt.Sprintf(`{"key":"%s","tos":"%s","type": "PC","model": "x-ui", "name": "%s"}`, publicKey, tos, hostName)
url := "https://api.cloudflareclient.com/v0a2158/reg"
req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte(data)))
if err != nil {
return "", err
}
req.Header.Add("CF-Client-Version", "a-7.21-0721")
req.Header.Add("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
buffer := bytes.NewBuffer(make([]byte, 8192))
buffer.Reset()
_, err = buffer.ReadFrom(resp.Body)
if err != nil {
return "", err
}
var rspData map[string]interface{}
err = json.Unmarshal(buffer.Bytes(), &rspData)
if err != nil {
return "", err
}
deviceId := rspData["id"].(string)
token := rspData["token"].(string)
license, ok := rspData["account"].(map[string]interface{})["license"].(string)
if !ok {
fmt.Println("Error accessing license value.")
return "", err
}
warpData := fmt.Sprintf("{\n \"access_token\": \"%s\",\n \"device_id\": \"%s\",", token, deviceId)
warpData += fmt.Sprintf("\n \"license_key\": \"%s\",\n \"private_key\": \"%s\"\n}", license, secretKey)
s.SettingService.SetWarp(warpData)
result := fmt.Sprintf("{\n \"data\": %s,\n \"config\": %s\n}", warpData, buffer.String())
return result, nil
}
func (s *XraySettingService) SetWarpLicence(license string) (string, error) {
var warpData map[string]string
warp, err := s.SettingService.GetWarp()
if err != nil {
return "", err
}
err = json.Unmarshal([]byte(warp), &warpData)
if err != nil {
return "", err
}
url := fmt.Sprintf("https://api.cloudflareclient.com/v0a2158/reg/%s/account", warpData["device_id"])
data := fmt.Sprintf(`{"license": "%s"}`, license)
req, err := http.NewRequest("PUT", url, bytes.NewBuffer([]byte(data)))
if err != nil {
return "", err
}
req.Header.Set("Authorization", "Bearer "+warpData["access_token"])
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
buffer := bytes.NewBuffer(make([]byte, 8192))
buffer.Reset()
_, err = buffer.ReadFrom(resp.Body)
if err != nil {
return "", err
}
warpData["license_key"] = license
newWarpData, err := json.MarshalIndent(warpData, "", " ")
if err != nil {
return "", err
}
s.SettingService.SetWarp(string(newWarpData))
println(string(newWarpData))
return string(newWarpData), nil
}

View File

@@ -1,6 +1,6 @@
"username" = "Username"
"password" = "Password"
"login" = "Log In"
"login" = "Login"
"confirm" = "Confirm"
"cancel" = "Cancel"
"close" = "Close"
@@ -8,7 +8,7 @@
"copied" = "Copied"
"download" = "Download"
"remark" = "Remark"
"enable" = "Enabled"
"enable" = "Enable"
"protocol" = "Protocol"
"search" = "Search"
"filter" = "Filter"
@@ -26,13 +26,13 @@
"edit" = "Edit"
"delete" = "Delete"
"reset" = "Reset"
"copySuccess" = "Copied Successful"
"copySuccess" = "Copied Successfully"
"sure" = "Sure"
"encryption" = "Encryption"
"transmission" = "Transmission"
"host" = "Host"
"path" = "Path"
"camouflage" = "Obfuscation"
"camouflage" = "Camouflage"
"status" = "Status"
"enabled" = "Enabled"
"disabled" = "Disabled"
@@ -41,10 +41,10 @@
"offline" = "Offline"
"online" = "Online"
"domainName" = "Domain Name"
"monitor" = "Listen IP"
"certificate" = "Digital Certificate"
"fail" = " Failed"
"success" = " Successful"
"monitor" = "Listening IP"
"certificate" = "Certificate"
"fail" = "Fail"
"success" = "Success"
"getVersion" = "Get Version"
"install" = "Install"
"clients" = "Clients"
@@ -52,80 +52,71 @@
"secretToken" = "Secret Token"
"remained" = "Remained"
"security" = "Security"
"secAlertTitle" = "Security Alert"
"secAlertSsl" = "This connection is not secure. Please avoid entering sensitive information until TLS is activated for data protection."
"secAlertConf" = "Certain settings are vulnerable to attacks. It is recommended to reinforce security protocols to prevent potential breaches."
"secAlertSSL" = "Panel lacks secure connection. Please install TLS certificate for data protection."
"secAlertPanelPort" = "Panel default port is vulnerable. Please configure a random or specific port."
"secAlertPanelURI" = "Panel default URI path is insecure. Please configure a complex URI path."
"secAlertSubURI" = "Subscription default URI path is insecure. Please configure a complex URI path."
"secAlertSubJsonURI" = "Subscription JSON default URI path is insecure. Please configure a complex URI path."
[menu]
"dashboard" = "Overview"
"dashboard" = "System Status"
"inbounds" = "Inbounds"
"settings" = "Panel Settings"
"xray" = "Xray Configs"
"logout" = "Log Out"
"link" = "Manage"
"xray" = "Xray Settings"
"logout" = "Logout"
"link" = "Management"
[pages.login]
"hello" = "Hello"
"title" = "Welcome"
"title" = "Login"
"loginAgain" = "Your session has expired, please log in again"
[pages.login.toasts]
"invalidFormData" = "The Input data format is invalid."
"emptyUsername" = "Username is required"
"emptyPassword" = "Password is required"
"invalidFormData" = "Input data format is invalid."
"emptyUsername" = "Please enter username."
"emptyPassword" = "Please enter password."
"wrongUsernameOrPassword" = "Invalid username or password."
"successLogin" = "Login"
[pages.index]
"title" = "Overview"
"title" = "System Status"
"memory" = "RAM"
"hard" = "Disk"
"xrayStatus" = "Xray"
"xrayStatus" = "Status"
"stopXray" = "Stop"
"restartXray" = "Restart"
"xraySwitch" = "Version"
"xraySwitch" = "SwitchV"
"xraySwitchClick" = "Choose the version you want to switch to."
"xraySwitchClickDesk" = "Choose carefully, as older versions may not be compatible with current configurations."
"xraySwitchClickDesk" = "Choose wisely, as older versions may not be compatible with current configurations."
"operationHours" = "Uptime"
"systemLoad" = "System Load"
"systemLoadDesc" = "System load average for the past 1, 5, and 15 minutes"
"connectionTcpCountDesc" = "Total TCP connections across the system"
"connectionUdpCountDesc" = "Total UDP connections across the system"
"connectionCount" = "Connection Stats"
"upSpeed" = "Overall upload speed across the system"
"downSpeed" = "Overall download speed across the system"
"totalSent" = "Total data sent across the system since OS startup"
"totalReceive" = "Total data received across the system since OS startup"
"xraySwitchVersionDialog" = "Change Xray Version"
"xraySwitchVersionDialogDesc" = "Are you sure you want to change the Xray version to"
"systemLoadDesc" = "system load average for the past 1, 5, and 15 minutes"
"connectionTcpCountDesc" = "Total TCP connections across all network cards"
"connectionUdpCountDesc" = "Total UDP connections across all network cards"
"connectionCount" = "Number of Connections"
"upSpeed" = "Total upload speed for all network cards"
"downSpeed" = "Total download speed for all network cards"
"totalSent" = "Total upload data across all network cards since OS startup"
"totalReceive" = "Total download data across all network cards since OS startup"
"xraySwitchVersionDialog" = "Switch Xray Version"
"xraySwitchVersionDialogDesc" = "Are you sure you want to switch the Xray version to"
"dontRefresh" = "Installation is in progress, please do not refresh this page"
"logs" = "Logs"
"config" = "Config"
"backup" = "Backup & Restore"
"backupTitle" = "Database Backup & Restore"
"backupDescription" = "It is recommended to make a backup before restoring a database."
"exportDatabase" = "Back Up"
"importDatabase" = "Restore"
"backupTitle" = "Backup & Restore Database"
"backupDescription" = "It is recommended to backup before importing a new database."
"exportDatabase" = "Download Database"
"importDatabase" = "Upload Database"
[pages.inbounds]
"title" = "Inbounds"
"totalDownUp" = "Total Sent/Received"
"totalDownUp" = "Total Uploads/Downloads"
"totalUsage" = "Total Usage"
"inboundCount" = "Total Inbounds"
"inboundCount" = "Number of Inbounds"
"operate" = "Menu"
"enable" = "Enabled"
"enable" = "Enable"
"remark" = "Remark"
"protocol" = "Protocol"
"port" = "Port"
"traffic" = "Traffic"
"details" = "Details"
"transportConfig" = "Transport"
"expireDate" = "Duration"
"expireDate" = "Expire Date"
"resetTraffic" = "Reset Traffic"
"addInbound" = "Add Inbound"
"generalActions" = "General Actions"
@@ -133,59 +124,61 @@
"update" = "Update"
"modifyInbound" = "Modify Inbound"
"deleteInbound" = "Delete Inbound"
"deleteInboundContent" = "Are you sure you want to delete inbound?"
"deleteInboundContent" = "Confirm deletion of inbound?"
"deleteClient" = "Delete Client"
"deleteClientContent" = "Are you sure you want to delete client?"
"resetTrafficContent" = "Are you sure you want to reset traffic?"
"copyLink" = "Copy URL"
"resetTrafficContent" = "Confirm traffic reset?"
"copyLink" = "Copy Link"
"address" = "Address"
"network" = "Network"
"destinationPort" = "Destination Port"
"targetAddress" = "Target Address"
"monitorDesc" = "Leave blank to listen on all IPs"
"meansNoLimit" = " = Unlimited. (unit: GB)"
"monitorDesc" = "Leave blank by default"
"meansNoLimit" = "Means no limit"
"totalFlow" = "Total Flow"
"leaveBlankToNeverExpire" = "Leave blank to never expire"
"noRecommendKeepDefault" = "It is recommended to keep the default"
"leaveBlankToNeverExpire" = "Leave Blank to Never Expire"
"noRecommendKeepDefault" = "No special requirements to maintain default settings"
"certificatePath" = "File Path"
"certificateContent" = "File Content"
"publicKey" = "Public Key"
"privatekey" = "Private Key"
"publicKeyPath" = "Public Key Path"
"publicKeyContent" = "Public Key Content"
"keyPath" = "Private Key Path"
"keyContent" = "Private Key Content"
"clickOnQRcode" = "Click on QR Code to Copy"
"client" = "Client"
"export" = "Export All URLs"
"export" = "Export Links"
"clone" = "Clone"
"cloneInbound" = "Clone"
"cloneInboundContent" = "All settings of this inbound, except Port, Listening IP, and Clients, will be applied to the clone."
"cloneInboundContent" = "All settings of this inbound, except for Port, Listening IP, and Clients, will be applied to the clone."
"cloneInboundOk" = "Clone"
"resetAllTraffic" = "Reset All Inbounds Traffic"
"resetAllTrafficTitle" = "Reset All Inbounds Traffic"
"resetAllTrafficContent" = "Are you sure you want to reset the traffic of all inbounds?"
"resetAllTrafficContent" = "Are you sure you want to reset all inbounds traffic?"
"resetInboundClientTraffics" = "Reset Clients Traffic"
"resetInboundClientTrafficTitle" = "Reset Clients Traffic"
"resetInboundClientTrafficContent" = "Are you sure you want to reset the traffic of this inbound's clients?"
"resetInboundClientTrafficContent" = "Are you sure you want to reset all traffic for this inbound's clients?"
"resetAllClientTraffics" = "Reset All Clients Traffic"
"resetAllClientTrafficTitle" = "Reset All Clients Traffic"
"resetAllClientTrafficContent" = "Are you sure you want to reset the traffic of all clients?"
"resetAllClientTrafficTitle" = "Reset all clients traffic"
"resetAllClientTrafficContent" = "Are you sure you want to reset all traffics for all clients?"
"delDepletedClients" = "Delete Depleted Clients"
"delDepletedClientsTitle" = "Delete Depleted Clients"
"delDepletedClientsContent" = "Are you sure you want to delete all the depleted clients?"
"delDepletedClientsContent" = "Are you sure you want to delete all depleted clients?"
"email" = "Email"
"emailDesc" = "Please provide a unique email address."
"IPLimit" = "IP Limit"
"IPLimitDesc" = "Disables inbound if the count exceeds the set value. (0 = disable)"
"IPLimitDesc" = "Disable inbound if the count exceeds the entered value (enter 0 to disable IP limit)."
"IPLimitlog" = "IP Log"
"IPLimitlogDesc" = "The IPs history log. (to enable inbound after disabling, clear the log)"
"IPLimitlogDesc" = "IPs history log (before enabling inbound after it has been disabled by IP limit, you should clear the log)."
"IPLimitlogclear" = "Clear The Log"
"setDefaultCert" = "Set Cert from Panel"
"xtlsDesc" = "Xray must be v1.7.5"
"realityDesc" = "Xray must be v1.8.0+"
"telegramDesc" = "Please provide Telegram or chat ID(s) without using the '@'. (get it here @userinfobot) or (use '/id' command in the bot)"
"subscriptionDesc" = "To find your subscription URL, navigate to the 'Details'. Additionally, you can use the same name for several clients."
"xtlsDesc" = "Xray core needs to be 1.7.5"
"realityDesc" = "Xray core needs to be 1.8.0 or higher."
"telegramDesc" = "use Telegram ID without @ or chat IDs ( you can get it here @userinfobot or use '/id' command in bot )"
"subscriptionDesc" = "you can find your sub link on Details, also you can use the same name for several configurations"
"info" = "Info"
"same" = "Same"
"inboundData" = "Inbound's Data"
"exportInbound" = "Export Inbound"
"copyToClipboard" = "Copy to Clipboard"
"import" = "Import"
"importInbound" = "Import an Inbound"
@@ -201,221 +194,196 @@
"last" = "Last"
"prefix" = "Prefix"
"postfix" = "Postfix"
"delayedStart" = "Start on Initial Use"
"expireDays" = "Duration"
"delayedStart" = "Start After First Use"
"expireDays" = "Expire Days"
"days" = "Day(s)"
"renew" = "Auto Renew"
"renewDesc" = "Auto-renewal after expiration. (0 = disable)(unit: day)"
"renewDesc" = "Auto renew days after expiration. 0 = disable"
[pages.inbounds.toasts]
"obtain" = "Obtain"
[pages.inbounds.stream.general]
"request" = "Request"
"response" = "Response"
"requestHeader" = "Request Header"
"name" = "Name"
"value" = "Value"
[pages.inbounds.stream.tcp]
"version" = "Version"
"method" = "Method"
"path" = "Path"
"status" = "Status"
"statusDescription" = "Status Desc"
"requestHeader" = "Request Header"
"requestVersion" = "Request Version"
"requestMethod" = "Request Method"
"requestPath" = "Request Path"
"responseVersion" = "Response Version"
"responseStatus" = "Response Status"
"responseStatusDescription" = "Response Status Description"
"responseHeader" = "Response Header"
[pages.inbounds.stream.quic]
"encryption" = "Encryption"
[pages.settings]
"title" = "Panel Settings"
"title" = "Settings"
"save" = "Save"
"infoDesc" = "Every change made here needs to be saved. Please restart the panel to apply changes."
"restartPanel" = "Restart Panel"
"restartPanelDesc" = "Are you sure you want to restart the panel? If you cannot access the panel after restarting, please view the panel log info on the server."
"restartPanelDesc" = "Are you sure you want to restart the panel? click OK to restart after 3 seconds. If you cannot access the panel after restarting, please view the panel log info on the server."
"actions" = "Actions"
"resetDefaultConfig" = "Reset to Default"
"panelSettings" = "General"
"securitySettings" = "Authentication"
"TGBotSettings" = "Telegram Bot"
"panelListeningIP" = "Listen IP"
"panelListeningIPDesc" = "The IP address for the web panel. (leave blank to listen on all IPs)"
"panelListeningDomain" = "Listen Domain"
"panelListeningDomainDesc" = "The domain name for the web panel. (leave blank to listen on all domains and IPs)"
"panelPort" = "Listen Port"
"panelPortDesc" = "The port number for the web panel. (must be an unused port)"
"publicKeyPath" = "Public Key Path"
"publicKeyPathDesc" = "The public key file path for the web panel. (begins with /)"
"privateKeyPath" = "Private Key Path"
"privateKeyPathDesc" = "The private key file path for the web panel. (begins with /)"
"panelUrlPath" = "URI Path"
"panelUrlPathDesc" = "The URI path for the web panel. (begins with / and concludes with /)"
"resetDefaultConfig" = "Reset to Default Configuration"
"panelSettings" = "Panel Settings"
"securitySettings" = "Security Settings"
"TGBotSettings" = "Telegram Bot Settings"
"panelListeningIP" = "Panel Listening IP"
"panelListeningIPDesc" = "Leave blank by default to monitor all IPs."
"panelListeningDomain" = "Panel Listening Domain"
"panelListeningDomainDesc" = "Leave blank by default to monitor all domains and IPs."
"panelPort" = "Panel Port"
"panelPortDesc" = "Port number for serving the panel."
"publicKeyPath" = "Panel Certificate Public Key Path"
"publicKeyPathDesc" = "Fill in an absolute path starting with."
"privateKeyPath" = "Panel Certificate Private Key Path"
"privateKeyPathDesc" = "Fill in an absolute path starting with."
"panelUrlPath" = "Panel URL Root Path"
"panelUrlPathDesc" = "Must start with '/' and end with."
"pageSize" = "Pagination Size"
"pageSizeDesc" = "Define page size for inbounds table. (0 = disable)"
"remarkModel" = "Remark Model & Separation Character"
"datepicker" = "Calendar Type"
"datepickerPlaceholder" = "Select date"
"datepickerDescription" = "Scheduled tasks will run based on this calendar."
"pageSizeDesc" = "Define page size for inbounds table. Set 0 to disable"
"remarkModel" = "Remark Model and Seperation Charachter"
"sampleRemark" = "Sample Remark"
"oldUsername" = "Current Username"
"currentPassword" = "Current Password"
"newUsername" = "New Username"
"newPassword" = "New Password"
"telegramBotEnable" = "Enable Telegram Bot"
"telegramBotEnableDesc" = "Enables the Telegram bot."
"telegramBotEnableDesc" = "Connect to the features of this panel through the Telegram bot."
"telegramToken" = "Telegram Token"
"telegramTokenDesc" = "The Telegram bot token obtained from '@BotFather'."
"telegramProxy" = "SOCKS Proxy"
"telegramProxyDesc" = "Enables SOCKS5 proxy for connecting to Telegram. (adjust settings as per guide)"
"telegramChatId" = "Admin Chat ID"
"telegramChatIdDesc" = "The Telegram Admin Chat ID(s). (comma-separated)(get it here @userinfobot) or (use '/id' command in the bot)"
"telegramNotifyTime" = "Notification Time"
"telegramNotifyTimeDesc" = "The Telegram bot notification time set for periodic reports. (use the crontab time format)"
"telegramTokenDesc" = "The token you have got from @BotFather."
"telegramChatId" = "Telegram Admin Chat IDs"
"telegramChatIdDesc" = "Multiple chat IDs separated by comma. use @userinfobot or use '/id' command in bot to get your Chat IDs."
"telegramNotifyTime" = "Telegram bot notification time"
"telegramNotifyTimeDesc" = "Use crontab timing format."
"tgNotifyBackup" = "Database Backup"
"tgNotifyBackupDesc" = "Send a database backup file with a report."
"tgNotifyBackupDesc" = "Include database backup file with report notification."
"tgNotifyLogin" = "Login Notification"
"tgNotifyLoginDesc" = "Get notified about the username, IP address, and time whenever someone attempts to log into your web panel."
"tgNotifyLoginDesc" = "Displays the username, IP address, and time when someone tries to log into your panel."
"sessionMaxAge" = "Session Duration"
"sessionMaxAgeDesc" = "The duration for which you can stay logged in. (unit: minute)"
"expireTimeDiff" = "Expiration Date Notification"
"expireTimeDiffDesc" = "Get notified about expiration date when reaching this threshold. (unit: day)"
"trafficDiff" = "Traffic Cap Notification"
"trafficDiffDesc" = "Get notified about traffic cap when reaching this threshold. (unit: GB)"
"tgNotifyCpu" = "CPU Load Notification"
"tgNotifyCpuDesc" = "Get notified if CPU load exceeds this threshold. (unit: %)"
"timeZone" = "Time Zone"
"timeZoneDesc" = "Scheduled tasks will run based on this time zone."
"sessionMaxAgeDesc" = "The duration of a login session. (unit: minute)"
"expireTimeDiff" = "Client Expiration Threshold Notification"
"expireTimeDiffDesc" = "Get notified about client expiration before the threshold. (unit: day)"
"trafficDiff" = "Traffic Limit Threshold Notification"
"trafficDiffDesc" = "Get notified about traffic exhaustion before reaching the threshold. (unit: GB)"
"tgNotifyCpu" = "CPU Load Threshold Notification"
"tgNotifyCpuDesc" = "Get notified if CPU usage exceeds this threshold. (unit: %)"
"timeZone" = "Time zone"
"timeZoneDesc" = "Scheduled tasks run according to the time in this time zone."
"subSettings" = "Subscription"
"subEnable" = "Enable Subscription Service"
"subEnableDesc" = "Enables the subscription service."
"subListen" = "Listen IP"
"subListenDesc" = "The IP address for the subscription service. (leave blank to listen on all IPs)"
"subPort" = "Listen Port"
"subPortDesc" = "The port number for the subscription service. (must be an unused port)"
"subCertPath" = "Public Key Path"
"subCertPathDesc" = "The public key file path for the subscription service. (begins with /)"
"subKeyPath" = "Private Key Path"
"subKeyPathDesc" = "The private key file path for the subscription service. (begins with /)"
"subPath" = "URI Path"
"subPathDesc" = "The URI path for the subscription service. (begins with / and concludes with /)"
"subDomain" = "Listen Domain"
"subDomainDesc" = "The domain name for the subscription service. (leave blank to listen on all domains and IPs)"
"subUpdates" = "Update Intervals"
"subUpdatesDesc" = "The update intervals of the subscription URL in the client apps. (unit: hour)"
"subEncrypt" = "Encode"
"subEncryptDesc" = "The returned content of subscription service will be Base64 encoded."
"subEnable" = "Enable Service"
"subEnableDesc" = "Subscription feature with separate configuration."
"subListen" = "Listening IP"
"subListenDesc" = "Leave blank by default to monitor all IPs."
"subPort" = "Subscription Port"
"subPortDesc" = "Port number for serving the subscription service. Must be unused in server."
"subCertPath" = "Subscription Certificate Public Key Path"
"subCertPathDesc" = "Fill in an absolute path starting with '/'"
"subKeyPath" = "Subscription Certificate Private Key Path"
"subKeyPathDesc" = "Fill in an absolute path starting with '/'"
"subPath" = "Subscription URL Root Path"
"subPathDesc" = "Must start with '/' and end with '/'"
"subDomain" = "Listening Domain"
"subDomainDesc" = "Leave blank by default to monitor all domains and IPs."
"subUpdates" = "Subscription update intervals"
"subUpdatesDesc" = "Interval hours between updates in client application."
"subEncrypt" = "Encode Configs"
"subEncryptDesc" = "Encode the returned configs in subscription."
"subShowInfo" = "Show Usage Info"
"subShowInfoDesc" = "The remaining traffic and date will be displayed in the client apps."
"subShowInfoDesc" = "Show remained traffic and date after config name."
"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"
"subURIDesc" = "Change base URI of subscription URL for using on behind of proxies."
[pages.xray]
"title" = "Xray Configs"
"save" = "Save"
"title" = "Xray Settings"
"save" = "Save Settings"
"restart" = "Restart Xray"
"basicTemplate" = "Basics"
"advancedTemplate" = "Advanced"
"generalConfigs" = "General"
"generalConfigsDesc" = "These options will determine general adjustments."
"logConfigs" = "Log"
"logConfigsDesc" = "Logs may affect your server's efficiency. It is recommended to enable it wisely only in case of your needs"
"blockConfigs" = "Protection Shield"
"blockConfigsDesc" = "These options will block traffic based on specific requested protocols and websites."
"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."
"ipv4Configs" = "IPv4 Routing"
"ipv4ConfigsDesc" = "These options will route traffic based on a specific destination via IPv4."
"warpConfigs" = "WARP Routing"
"warpConfigsDesc" = "These options will route traffic based on a specific destination via WARP."
"Template" = "Advanced Xray Configuration Template"
"TemplateDesc" = "The final Xray config file will be generated based on this template."
"FreedomStrategy" = "Freedom Protocol Strategy"
"FreedomStrategyDesc" = "Set the output strategy for the network in the Freedom Protocol."
"RoutingStrategy" = "Overall Routing Strategy"
"RoutingStrategyDesc" = "Set the overall traffic routing strategy for resolving all requests."
"Torrent" = "Block BitTorrent Protocol"
"TorrentDesc" = "Blocks BitTorrent protocol."
"PrivateIp" = "Block Connection to Private IPs"
"PrivateIpDesc" = "Blocks establishing connections to private IP ranges."
"basicTemplate" = "Basic Template"
"advancedTemplate" = "Advanced Template"
"generalConfigs" = "General Configs"
"generalConfigsDesc" = "These options will provide general adjustments."
"blockConfigs" = "Blocking Configs"
"blockConfigsDesc" = "These options will prevent users from connecting to specific protocols and websites."
"blockCountryConfigs" = "Block Country Configs"
"blockCountryConfigsDesc" = "These options will prevent users from connecting to specific country domains."
"directCountryConfigs" = "Direct Country Configs"
"directCountryConfigsDesc" = "These options will connect users directly to specific country domains."
"ipv4Configs" = "IPv4 Configs"
"ipv4ConfigsDesc" = "These options will route to target domains only via IPv4."
"warpConfigs" = "WARP Configs"
"warpConfigsDesc" = "Caution: Before using these options, install WARP in socks5 proxy mode on your server by following the steps on the panel's GitHub. WARP will route traffic to websites through Cloudflare servers."
"Template" = "Xray Configuration Template"
"TemplateDesc" = "Generate the final Xray configuration file based on this template."
"FreedomStrategy" = "Configure Strategy for Freedom Protocol"
"FreedomStrategyDesc" = "Set the output strategy of the network in the Freedom Protocol."
"RoutingStrategy" = "Configure Domains Routing Strategy"
"RoutingStrategyDesc" = "Set the overall routing strategy for DNS resolving."
"Torrent" = "Ban BitTorrent Protocol"
"TorrentDesc" = "Change the configuration template to avoid using BitTorrent protocol."
"PrivateIp" = "Ban Private IPs to Connect"
"PrivateIpDesc" = "Change the configuration template to avoid connecting to private IP ranges."
"Ads" = "Block Ads"
"AdsDesc" = "Blocks advertising websites."
"Family" = "Family Protection"
"FamilyDesc" = "Blocks adult content, and malware websites."
"Security" = "Security Shield"
"SecurityDesc" = "Blocks malware, phishing, and cryptominers websites."
"Speedtest" = "Block Speedtest"
"SpeedtestDesc" = "Blocks establishing connectins to speedtest websites."
"IRIp" = "Block Connection to Iran IPs"
"IRIpDesc" = "Blocks establishing connections to Iran IP ranges."
"IRDomain" = "Block Connection to Iran Domains"
"IRDomainDesc" = "Blocks establishing connections to Iran domains."
"ChinaIp" = "Block Connection to China IPs"
"ChinaIpDesc" = "Blocks establishing connections to China IP ranges."
"ChinaDomain" = "Block Connection to China Domains"
"ChinaDomainDesc" = "Blocks establishing connections to China domains."
"RussiaIp" = "Block Connection to Russia IPs"
"RussiaIpDesc" = "Blocks establishing connections to Russia IP ranges."
"RussiaDomain" = "Block Connection to Russia Domains"
"RussiaDomainDesc" = "Blocks establishing connections to Russia domains."
"VNIp" = "Block Connection to Vietnam IPs"
"VNIpDesc" = "Blocks establishing connections to Vietnam IP ranges."
"VNDomain" = "Block Connection to Vietnam Domains"
"VNDomainDesc" = "Blocks establishing connections to Vietnam domains."
"AdsDesc" = "Change the configuration template to block ads."
"Family" = "Block Malware and Adult Content"
"FamilyDesc" = "DNS resolvers to block malware and adult content for family protection."
"Speedtest" = "Block Speedtest Websites"
"SpeedtestDesc" = "Change the configuration template to avoid connecting to speedtest websites."
"IRIp" = "Disable Connection to Iran IPs"
"IRIpDesc" = "Change the configuration template to avoid connecting to Iran IP ranges."
"IRDomain" = "Disable Connection to Iran Domains"
"IRDomainDesc" = "Change the configuration template to avoid connecting to Iran domains."
"ChinaIp" = "Disable Connection to China IPs"
"ChinaIpDesc" = "Change the configuration template to avoid connecting to China IP ranges."
"ChinaDomain" = "Disable Connection to China Domains"
"ChinaDomainDesc" = "Change the configuration template to avoid connecting to China domains."
"RussiaIp" = "Disable Connection to Russia IPs"
"RussiaIpDesc" = "Change the configuration template to avoid connecting to Russia IP ranges."
"RussiaDomain" = "Disable Connection to Russia Domains"
"RussiaDomainDesc" = "Change the configuration template to avoid connecting to Russia domains."
"VNIp" = "Disable Connection to Vietnam IPs"
"VNIpDesc" = "Change the configuration template to avoid connecting to Vietnam IP ranges."
"VNDomain" = "Disable Connection to Vietnam Domains"
"VNDomainDesc" = "Change the configuration template to avoid connecting to Vietnam domains."
"DirectIRIp" = "Direct Connection to Iran IPs"
"DirectIRIpDesc" = "Directly establishes connections to Iran IP ranges."
"DirectIRIpDesc" = "Change the configuration template for direct connecting to Iran IP ranges."
"DirectIRDomain" = "Direct Connection to Iran Domains"
"DirectIRDomainDesc" = "Directly establishes connections to Iran domains."
"DirectIRDomainDesc" = "Change the configuration template for direct connecting to Iran domains."
"DirectChinaIp" = "Direct Connection to China IPs"
"DirectChinaIpDesc" = "Directly establishes connections to China IP ranges."
"DirectChinaIpDesc" = "Change the configuration template for direct connecting to China IP ranges."
"DirectChinaDomain" = "Direct Connection to China Domains"
"DirectChinaDomainDesc" = "Directly establishes connections to China domains."
"DirectChinaDomainDesc" = "Change the configuration template for direct connecting to China domains."
"DirectRussiaIp" = "Direct Connection to Russia IPs"
"DirectRussiaIpDesc" = "Directly establishes connections to Russia IP ranges."
"DirectRussiaIpDesc" = "Change the configuration template for direct connecting to Russia IP ranges."
"DirectRussiaDomain" = "Direct Connection to Russia Domains"
"DirectRussiaDomainDesc" = "Directly establishes connections to Russia domains."
"DirectRussiaDomainDesc" = "Change the configuration template for direct connecting to Russia domains."
"DirectVNIp" = "Direct Connection to Vietnam IPs"
"DirectVNIpDesc" = "Directly establishes connections to Vietnam IP ranges."
"DirectVNIpDesc" = "Change the configuration template for direct connecting to Vietnam IP ranges."
"DirectVNDomain" = "Direct Connection to Vietnam Domains"
"DirectVNDomainDesc" = "Directly establishes connections to Vietnam domains."
"GoogleIPv4" = "Google"
"GoogleIPv4Desc" = "Routes traffic to Google via IPv4."
"NetflixIPv4" = "Netflix"
"NetflixIPv4Desc" = "Routes traffic to Netflix via IPv4."
"GoogleWARP" = "Google"
"DirectVNDomainDesc" = "Change the configuration template for direct connecting to Vietnam domains."
"GoogleIPv4" = "Use IPv4 for Google"
"GoogleIPv4Desc" = "Add routing for Google to connect with IPv4."
"NetflixIPv4" = "Use IPv4 for Netflix"
"NetflixIPv4Desc" = "Add routing for Netflix to connect with IPv4."
"GoogleWARP" = "Route Google through WARP."
"GoogleWARPDesc" = "Add routing for Google via WARP."
"OpenAIWARP" = "ChatGPT"
"OpenAIWARPDesc" = "Routes traffic to ChatGPT via WARP."
"NetflixWARP" = "Netflix"
"NetflixWARPDesc" = "Routes traffic to Netflix via WARP."
"MetaWARP" = "Meta"
"MetaWARPDesc" = "Routes traffic to Meta (Instagram, Facebook, WhatsApp, Threads,...) via WARP."
"AppleWARP" = "Apple"
"AppleWARPDesc" = "Routes traffic to Apple via WARP."
"RedditWARP" = "Reddit"
"RedditWARPDesc" = "Routes traffic to Reddit via WARP."
"SpotifyWARP" = "Spotify"
"SpotifyWARPDesc" = "Routes traffic to Spotify via WARP."
"IRWARP" = "Iran domains"
"IRWARPDesc" = "Routes traffic to Iran domains via WARP."
"OpenAIWARP" = "Route OpenAI (ChatGPT) through WARP."
"OpenAIWARPDesc" = "Add routing for OpenAI (ChatGPT) via WARP."
"NetflixWARP" = "Route Netflix through WARP."
"NetflixWARPDesc" = "Add routing for Netflix via WARP."
"SpotifyWARP" = "Route Spotify through WARP."
"SpotifyWARPDesc" = "Add routing for Spotify via WARP."
"IRWARP" = "Route Iran domains through WARP."
"IRWARPDesc" = "Add routing for Iran domains via WARP."
"Inbounds" = "Inbounds"
"InboundsDesc" = "Accepting the specific clients."
"InboundsDesc" = "Change the configuration template to accept specific clients."
"Outbounds" = "Outbounds"
"Balancers" = "Balancers"
"OutboundsDesc" = "Set the outgoing traffic pathway."
"OutboundsDesc" = "Change the configuration template to define outgoing ways for this server."
"Routings" = "Routing Rules"
"RoutingsDesc" = "The priority of each rule is important!"
"completeTemplate" = "All"
"logLevel" = "Log Level"
"logLevelDesc" = "The log level for error logs, indicating the information that needs to be recorded."
"accessLog" = "Access Log"
"accessLogDesc" = "The file path for the access log. The special value 'none' disabled access logs"
"errorLog" = "Error Log"
"errorLogDesc" = "The file path for the error log. The special value 'none' disabled error logs"
[pages.xray.rules]
"first" = "First"
@@ -426,11 +394,10 @@
"dest" = "Destination"
"inbound" = "Inbound"
"outbound" = "Outbound"
"balancer" = "Balancer"
"info" = "Info"
"add" = "Add Rule"
"edit" = "Edit Rule"
"useComma" = "Comma-separated items"
"useComma" = "Comma separated items"
[pages.xray.outbound]
"addOutbound" = "Add Outbound"
@@ -447,52 +414,20 @@
"portal" = "Portal"
"intercon" = "Interconnection"
[pages.xray.balancer]
"addBalancer" = "Add Balancer"
"editBalancer" = "Edit Balancer"
"balancerStrategy" = "Strategy"
"balancerSelectors" = "Selectors"
"tag" = "Tag"
"tagDesc" = "Unique Tag"
"balancerDesc" = "It is not possible to use balancerTag and outboundTag at the same time. If used at the same time, only outboundTag will work."
[pages.xray.wireguard]
"secretKey" = "Secret Key"
"publicKey" = "Public Key"
"allowedIPs" = "Allowed IPs"
"endpoint" = "Endpoint"
"psk" = "PreShared Key"
"domainStrategy" = "Domain Strategy"
[pages.xray.dns]
"enable" = "Enable DNS"
"enableDesc" = "Enable built-in DNS server"
"strategy" = "Query Strategy"
"strategyDesc" = "Overall strategy to resolve domain names"
"add" = "Add Server"
"edit" = "Edit Server"
"domains" = "Domains"
[pages.xray.fakedns]
"add" = "Add Fake DNS"
"edit" = "Edit Fake DNS"
"ipPool" = "IP Pool Subnet"
"poolSize" = "Pool Size"
[pages.settings.security]
"admin" = "Admin"
"secret" = "Secret Token"
"loginSecurity" = "Secure Login"
"loginSecurityDesc" = "Adds an additional layer of authentication to provide more security."
"loginSecurity" = "Login security"
"loginSecurityDesc" = "Enable additional user login security step"
"secretToken" = "Secret Token"
"secretTokenDesc" = "Please securely store this token in a safe place. This token is required for login and cannot be recovered."
"secretTokenDesc" = "Please copy and securely store this token in a safe place. This token is required for login and cannot be recovered from the x-ui command tool."
[pages.settings.toasts]
"modifySettings" = "Modify Settings"
"getSettings" = "Get Settings"
"modifyUser" = "Modify Admin"
"originalUserPassIncorrect" = "The Current username or password is invalid"
"userPassMustBeNotEmpty" = "The new username and password is empty"
"modifyUser" = "Modify User"
"originalUserPassIncorrect" = "Incorrect original username or password"
"userPassMustBeNotEmpty" = "New username and password cannot be empty"
[tgbot]
"keyboardClosed" = "❌ Custom keyboard closed!"
@@ -501,8 +436,7 @@
"wentWrong" = "❌ Something went wrong!"
"noIpRecord" = "❗ No IP Record!"
"noInbounds" = "❗ No inbound found!"
"unlimited" = "♾ Unlimited(Reset)"
"add" = "Add"
"unlimited" = "♾ Unlimited"
"month" = "Month"
"months" = "Months"
"day" = "Day"
@@ -511,51 +445,48 @@
"unknown" = "Unknown"
"inbounds" = "Inbounds"
"clients" = "Clients"
"offline" = "🔴 Offline"
"online" = "🟢 Online"
[tgbot.commands]
"unknown" = "❗ Unknown command."
"unknown" = "❗ Unknown command"
"pleaseChoose" = "👇 Please choose:\r\n"
"help" = "🤖 Welcome to this bot! It's designed to offer specific data from the web panel and allows you to make modifications as needed.\r\n\r\n"
"help" = "🤖 Welcome to this bot! It's designed to offer you specific data from the server, and it allows you to make modifications as needed.\r\n\r\n"
"start" = "👋 Hello <i>{{ .Firstname }}</i>.\r\n"
"welcome" = "🤖 Welcome to <b>{{ .Hostname }}</b> management bot.\r\n"
"status" = "✅ Bot is OK!"
"usage" = "❗ Please provide a text to search!"
"getID" = "🆔 Your ID: <code>{{ .ID }}</code>"
"helpAdminCommands" = "To search for a client email:\r\n<code>/usage [Email]</code>\r\n\r\nTo search for inbounds (with client stats):\r\n<code>/inbound [Remark]</code>"
"helpClientCommands" = "To search for statistics, use the following command:\r\n\r\n<code>/usage [Email]</code>"
"helpAdminCommands" = "Search for a client email:\r\n<code>/usage [Email]</code>\r\n \r\nSearch for inbounds (with client stats):\r\n<code>/inbound [Remark]</code>"
"helpClientCommands" = "To search for statistics, just use the following command:\r\n \r\n<code>/usage [UUID|Password]</code>\r\n \r\nUse UUID for vmess/vless and Password for Trojan."
[tgbot.messages]
"cpuThreshold" = "🔴 CPU Load {{ .Percent }}% exceeds the threshold of {{ .Threshold }}%"
"cpuThreshold" = "🔴 CPU Load {{ .Percent }}% is more than threshold {{ .Threshold }}%"
"selectUserFailed" = "❌ Error in user selection!"
"userSaved" = "✅ Telegram User saved."
"loginSuccess" = "✅ Logged in to the panel successfully.\r\n"
"loginFailed" = "❗️ Log in to the panel failed.\r\n"
"loginSuccess" = "✅ Successfully logged-in to the panel.\r\n"
"loginFailed" = "❗️ Login to the panel failed.\r\n"
"report" = "🕰 Scheduled Reports: {{ .RunTime }}\r\n"
"datetime" = "⏰ Date&Time: {{ .DateTime }}\r\n"
"hostname" = "💻 Host: {{ .Hostname }}\r\n"
"version" = "🚀 3X-UI Version: {{ .Version }}\r\n"
"datetime" = "⏰ Date-Time: {{ .DateTime }}\r\n"
"hostname" = "💻 Hostname: {{ .Hostname }}\r\n"
"version" = "🚀 X-UI Version: {{ .Version }}\r\n"
"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n"
"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n"
"ip" = "🌐 IP: {{ .IP }}\r\n"
"ips" = "🔢 IPs:\r\n{{ .IPs }}\r\n"
"serverUpTime" = "⏳ Uptime: {{ .UpTime }} {{ .Unit }}\r\n"
"serverLoad" = "📈 System Load: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
"serverMemory" = "📋 RAM: {{ .Current }}/{{ .Total }}\r\n"
"ips" = "🔢 IPs: \r\n{{ .IPs }}\r\n"
"serverUpTime" = "⏳ Server Uptime: {{ .UpTime }} {{ .Unit }}\r\n"
"serverLoad" = "📈 Server Load: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
"serverMemory" = "📋 Server RAM: {{ .Current }}/{{ .Total }}\r\n"
"tcpCount" = "🔹 TCP: {{ .Count }}\r\n"
"udpCount" = "🔸 UDP: {{ .Count }}\r\n"
"traffic" = "🚦 Traffic: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
"xrayStatus" = " Status: {{ .State }}\r\n"
"xrayStatus" = " Xray Status: {{ .State }}\r\n"
"username" = "👤 Username: {{ .Username }}\r\n"
"time" = "⏰ Time: {{ .Time }}\r\n"
"inbound" = "📍 Inbound: {{ .Remark }}\r\n"
"port" = "🔌 Port: {{ .Port }}\r\n"
"expire" = "📅 Expire Date: {{ .Time }}\r\n"
"expireIn" = "📅 Expire In: {{ .Time }}\r\n"
"active" = "💡 Active: {{ .Enable }}\r\n"
"enabled" = "🚨 Enabled: {{ .Enable }}\r\n"
"online" = "🌐 Connection status: {{ .Status }}\r\n"
"active" = "💡 Active: ✅ Yes\r\n"
"inactive" = "💡 Active: ❌ No\r\n"
"email" = "📧 Email: {{ .Email }}\r\n"
"upload" = "🔼 Upload: ↑{{ .Upload }}\r\n"
"download" = "🔽 Download: ↓{{ .Download }}\r\n"
@@ -563,13 +494,10 @@
"TGUser" = "👤 Telegram User: {{ .TelegramID }}\r\n"
"exhaustedMsg" = "🚨 Exhausted {{ .Type }}:\r\n"
"exhaustedCount" = "🚨 Exhausted {{ .Type }} count:\r\n"
"onlinesCount" = "🌐 Online Clients: {{ .Count }}\r\n"
"disabled" = "🛑 Disabled: {{ .Disabled }}\r\n"
"depleteSoon" = "🔜 Deplete Soon: {{ .Deplete }}\r\n\r\n"
"depleteSoon" = "🔜 Deplete Soon: {{ .Deplete }}\r\n \r\n"
"backupTime" = "🗄 Backup Time: {{ .Time }}\r\n"
"refreshedOn" = "\r\n📋🔄 Refreshed On: {{ .Time }}\r\n\r\n"
"yes" = "✅ Yes"
"no" = "❌ No"
"refreshedOn" = "\r\n📋🔄 Refreshed On: {{ .Time }}\r\n \r\n"
[tgbot.buttons]
"closeKeyboard" = "❌ Close Keyboard"
@@ -579,47 +507,44 @@
"confirmResetTraffic" = "✅ Confirm Reset Traffic?"
"confirmClearIps" = "✅ Confirm Clear IPs?"
"confirmRemoveTGUser" = "✅ Confirm Remove Telegram User?"
"confirmToggle" = "✅ Confirm Enable/Disable User?"
"dbBackup" = "Get DB Backup"
"serverUsage" = "Server Usage"
"getInbounds" = "Get Inbounds"
"depleteSoon" = "Deplete Soon"
"depleteSoon" = "Deplete soon"
"clientUsage" = "Get Usage"
"onlines" = "Online Clients"
"commands" = "Commands"
"refresh" = "🔄 Refresh"
"clearIPs" = "❌ Clear IPs"
"removeTGUser" = "❌ Remove Telegram User"
"selectTGUser" = "👤 Select Telegram User"
"selectOneTGUser" = "👤 Select a Telegram User:"
"selectOneTGUser" = "👤 Select a telegram user:"
"resetTraffic" = "📈 Reset Traffic"
"resetExpire" = "📅 Change Expiry Date"
"resetExpire" = "📅 Reset Expire Days"
"ipLog" = "🔢 IP Log"
"ipLimit" = "🔢 IP Limit"
"setTGUser" = "👤 Set Telegram User"
"toggle" = "🔘 Enable / Disable"
"custom" = "🔢 Custom"
"confirmNumber" = "✅ Confirm: {{ .Num }}"
"confirmNumberAdd" = "✅ Confirm adding: {{ .Num }}"
"confirmNumber" = "✅ Confirm : {{ .Num }}"
"limitTraffic" = "🚧 Traffic Limit"
"getBanLogs" = "Get Ban Logs"
[tgbot.answers]
"successfulOperation" = "✅ Operation successful!"
"errorOperation" = "❗ Error in operation."
"getInboundsFailed" = "❌ Failed to get inbounds."
"canceled" = "❌ {{ .Email }}: Operation canceled."
"clientRefreshSuccess" = "✅ {{ .Email }}: Client refreshed successfully."
"IpRefreshSuccess" = "✅ {{ .Email }}: IPs refreshed successfully."
"TGIdRefreshSuccess" = "✅ {{ .Email }}: Client's Telegram User refreshed successfully."
"resetTrafficSuccess" = "✅ {{ .Email }}: Traffic reset successfully."
"setTrafficLimitSuccess" = "✅ {{ .Email }}: Traffic limit saved successfully."
"expireResetSuccess" = "✅ {{ .Email }}: Expire days reset successfully."
"resetIpSuccess" = "✅ {{ .Email }}: IP limit {{ .Count }} saved successfully."
"clearIpSuccess" = "✅ {{ .Email }}: IPs cleared successfully."
"getIpLog" = "✅ {{ .Email }}: Get IP Log."
"getUserInfo" = "✅ {{ .Email }}: Get Telegram User Info."
"removedTGUserSuccess" = "✅ {{ .Email }}: Telegram User removed successfully."
"enableSuccess" = "✅ {{ .Email }}: Enabled successfully."
"disableSuccess" = "✅ {{ .Email }}: Disabled successfully."
"askToAddUserId" = "Your configuration is not found!\r\nPlease ask your admin to use your Telegram ID in your configuration(s).\r\n\r\nYour User ID: <code>{{ .TgUserID }}</code>"
"successfulOperation" = "✅ Successful!"
"errorOperation" = "❗ Error in Operation."
"getInboundsFailed" = "❌ Failed to get inbounds"
"canceled" = "❌ {{ .Email }} : Operation canceled."
"clientRefreshSuccess" = "✅ {{ .Email }} : Client refreshed successfully."
"IpRefreshSuccess" = "✅ {{ .Email }} : IPs refreshed successfully."
"TGIdRefreshSuccess" = "✅ {{ .Email }} : Client's Telegram User refreshed successfully."
"resetTrafficSuccess" = "✅ {{ .Email }} : Traffic reset successfully."
"setTrafficLimitSuccess" = "✅ {{ .Email }} : Traffic limit saved successfully."
"expireResetSuccess" = "✅ {{ .Email }} : Expire days reset successfully."
"resetIpSuccess" = "✅ {{ .Email }} : IP limit {{ .Count }} saved successfully."
"clearIpSuccess" = "✅ {{ .Email }} : IPs cleared successfully."
"getIpLog" = "✅ {{ .Email }} : Get IP Log."
"getUserInfo" = "✅ {{ .Email }} : Get Telegram User Info."
"removedTGUserSuccess" = "✅ {{ .Email }} : Telegram User removed successfully."
"enableSuccess" = "✅ {{ .Email }} : Enabled successfully."
"disableSuccess" = "✅ {{ .Email }} : Disabled successfully."
"askToAddUserId" = "Your configuration is not found!\r\nPlease ask your Admin to use your telegram user id in your configuration(s).\r\n\r\nYour user id: <b>{{ .TgUserID }}</b>"
"askToAddUserName" = "Your configuration is not found!\r\nPlease ask your Admin to use your telegram username or user id in your configuration(s).\r\n\r\nYour username: <b>@{{ .TgUserName }}</b>\r\n\r\nYour user id: <b>{{ .TgUserID }}</b>"

View File

@@ -1,6 +1,6 @@
"username" = "Nombre de Usuario"
"password" = "Contraseña"
"login" = "Acceder"
"login" = "Acceso"
"confirm" = "Confirmar"
"cancel" = "Cancelar"
"close" = "Cerrar"
@@ -20,7 +20,7 @@
"check" = "Verificar"
"indefinite" = "Indefinido"
"unlimited" = "Ilimitado"
"none" = "None"
"none" = "Ninguno"
"qrCode" = "Código QR"
"info" = "Más Información"
"edit" = "Editar"
@@ -42,7 +42,7 @@
"online" = "en línea"
"domainName" = "Nombre de dominio"
"monitor" = "Listening IP"
"certificate" = "Certificado Digital"
"certificate" = "Certificado"
"fail" = "Falló"
"success" = "Éxito"
"getVersion" = "Obtener versión"
@@ -52,26 +52,17 @@
"secretToken" = "Token Secreto"
"remained" = "Restante"
"security" = "Seguridad"
"secAlertTitle" = "Alerta de seguridad"
"secAlertSsl" = "Esta conexión no es segura. Evite ingresar información confidencial hasta que TLS esté activado para la protección de datos."
"secAlertConf" = "Certae occasus vulnerabiles sunt impetus. Commendatur ad securitatem protocolla roboranda ne interrupta potentiale."
"secAlertSSL" = "La panel carece de conexión segura. Por favor, instale un certificado TLS para la protección de datos."
"secAlertPanelPort" = "La puerto predeterminado del panel es vulnerable. Por favor, configure un puerto aleatorio o específico."
"secAlertPanelURI" = "La ruta URI predeterminada del panel no es segura. Por favor, configure una ruta URI compleja."
"secAlertSubURI" = "La ruta URI predeterminada de la suscripción no es segura. Por favor, configure una ruta URI compleja."
"secAlertSubJsonURI" = "La ruta URI predeterminada de la suscripción JSON no es segura. Por favor, configure una ruta URI compleja."
[menu]
"dashboard" = "Estado del Sistema"
"inbounds" = "Entradas"
"settings" = "Configuraciones"
"xray" = "Ajustes Xray"
"xray" = "Configuración Xray"
"logout" = "Cerrar Sesión"
"link" = "Gestionar"
"link" = "Otro"
[pages.login]
"hello" = "Hola"
"title" = "Bienvenido"
"title" = "Iniciar Sesión"
"loginAgain" = "El límite de tiempo de inicio de sesión ha expirado. Por favor, inicia sesión nuevamente."
[pages.login.toasts]
@@ -85,10 +76,10 @@
"title" = "Estado del Sistema"
"memory" = "Memoria"
"hard" = "Disco Duro"
"xrayStatus" = "Xray"
"stopXray" = "Detener"
"xrayStatus" = "Estado de Xray"
"stopXray" = "Detener Xray"
"restartXray" = "Reiniciar"
"xraySwitch" = "Versión"
"xraySwitch" = "Cambiar Versión"
"xraySwitchClick" = "Elige la versión a la que deseas cambiar."
"xraySwitchClickDesk" = "Elige sabiamente, ya que las versiones anteriores pueden no ser compatibles con las configuraciones actuales."
"operationHours" = "Tiempo de Funcionamiento"
@@ -143,14 +134,16 @@
"destinationPort" = "Puerto de Destino"
"targetAddress" = "Dirección de Destino"
"monitorDesc" = "Dejar en blanco por defecto"
"meansNoLimit" = " = illimitata. (unidad: GB)"
"meansNoLimit" = "Significa Sin Límite"
"totalFlow" = "Flujo Total"
"leaveBlankToNeverExpire" = "Dejar en Blanco para Nunca Expirar"
"noRecommendKeepDefault" = "No hay requisitos especiales para mantener la configuración predeterminada"
"certificatePath" = "Ruta del Archivo"
"certificateContent" = "Contenido del Archivo"
"publicKey" = "llave Pública"
"privatekey" = "llave Privada"
"publicKeyPath" = "Ruta de la Clave Pública"
"publicKeyContent" = "Contenido de la Clave Pública"
"keyPath" = "Ruta de la Clave Privada"
"keyContent" = "Contenido de la Clave Privada"
"clickOnQRcode" = "Haz clic en el Código QR para Copiar"
"client" = "Cliente"
"export" = "Exportar Enlaces"
@@ -180,12 +173,12 @@
"setDefaultCert" = "Establecer certificado desde el panel"
"xtlsDesc" = "La versión del núcleo de Xray debe ser 1.7.5"
"realityDesc" = "La versión del núcleo de Xray debe ser 1.8.0 o superior."
"telegramDesc" = "Utiliza únicamente IDs de chat (puedes obtenerlo aquí @userinfobot o usando el comando '/id' en el bot)."
"telegramDesc" = "Utiliza el ID de Telegram sin @ o los IDs de chat (puedes obtenerlo aquí @userinfobot o usando el comando '/id' en el bot)."
"subscriptionDesc" = "Puedes encontrar tu enlace de suscripción en Detalles, también puedes usar el mismo nombre para varias configuraciones."
"info" = "Info"
"same" = "misma"
"inboundData" = "Datos de entrada"
"exportInbound" = "Exportación entrante"
"copyToClipboard" = "Copiar al portapapeles"
"import" = "Importar"
"importInbound" = "Importar un entrante"
@@ -201,29 +194,28 @@
"last" = "Último"
"prefix" = "Prefijo"
"postfix" = "Sufijo"
"delayedStart" = "Inicio Inicial"
"expireDays" = "Duratio"
"delayedStart" = "Iniciar después del primer uso"
"expireDays" = "Días de Expiración"
"days" = "día(s)"
"renew" = "Renovación automática"
"renewDesc" = "Auto-renovatio post tutelam receptam. (0 = disable) (unitas: dies)"
"renewDesc" = "Renovación automática días después del vencimiento. 0 = deshabilitar"
[pages.inbounds.toasts]
"obtain" = "Recibir"
[pages.inbounds.stream.general]
"request" = "Pedido"
"response" = "Respuesta"
"requestHeader" = "Encabezado de la Petición"
"name" = "Nombre"
"value" = "Valor"
[pages.inbounds.stream.tcp]
"version" = "Versión"
"method" = "Método"
"path" = "Camino"
"status" = "Estado"
"statusDescription" = "Descripción de la Situación"
"requestHeader" = "Encabezado de solicitud"
"responseHeader" = "Encabezado de respuesta"
"requestVersion" = "Versión de la Petición"
"requestMethod" = "Método de la Petición"
"requestPath" = "Ruta de la Petición"
"responseVersion" = "Versión de la Respuesta"
"responseStatus" = "Estado de la Respuesta"
"responseStatusDescription" = "Descripción del Estado de la Respuesta"
"responseHeader" = "Encabezado de la Respuesta"
[pages.inbounds.stream.quic]
"encryption" = "Cifrado"
@@ -254,9 +246,6 @@
"pageSize" = "Tamaño de paginación"
"pageSizeDesc" = "Defina el tamaño de página para la tabla de entradas. Establezca 0 para desactivar"
"remarkModel" = "Modelo de observación y carácter de separación"
"datepicker" = "selector de fechas"
"datepickerPlaceholder" = "Seleccionar fecha"
"datepickerDescription" = "El tipo de calendario selector especifica la fecha de vencimiento"
"sampleRemark" = "Observación de muestra"
"oldUsername" = "Nombre de Usuario Actual"
"currentPassword" = "Contraseña Actual"
@@ -266,8 +255,6 @@
"telegramBotEnableDesc" = "Conéctese a las funciones de este panel a través del bot de Telegram."
"telegramToken" = "Token de Telegram"
"telegramTokenDesc" = "Debe obtener el token del administrador de bots de Telegram @botfather."
"telegramProxy" = "Socks5 Proxy"
"telegramProxyDesc" = "Si necesita el proxy Socks5 para conectarse a Telegram. Ajuste su configuración según la guía."
"telegramChatId" = "IDs de Chat de Telegram para Administradores"
"telegramChatIdDesc" = "IDs de Chat múltiples separados por comas. Use @userinfobot o use el comando '/id' en el bot para obtener sus IDs de Chat."
"telegramNotifyTime" = "Hora de Notificación del Bot de Telegram"
@@ -309,8 +296,6 @@
"subShowInfoDesc" = "Mostrar tráfico restante y fecha después del nombre de configuración."
"subURI" = "URI de proxy inverso"
"subURIDesc" = "Cambiar el URI base de la URL de suscripción para usar detrás de los servidores proxy"
"fragment" = "Fragmentación"
"fragmentDesc" = "Habilitar la fragmentación para el paquete de saludo TLS"
[pages.xray]
"title" = "Xray Configuración"
@@ -320,8 +305,6 @@
"advancedTemplate" = "Plantilla Avanzada"
"generalConfigs" = "Configuraciones Generales"
"generalConfigsDesc" = "Estas opciones proporcionarán ajustes generales."
"logConfigs" = "Registro"
"logConfigsDesc" = "Los registros pueden afectar la eficiencia de su servidor. Se recomienda habilitarlo sabiamente solo en caso de sus necesidades."
"blockConfigs" = "Configuraciones de Bloqueo"
"blockConfigsDesc" = "Estas opciones evitarán que los usuarios se conecten a protocolos y sitios web específicos."
"blockCountryConfigs" = "Configuraciones de Bloqueo por País"
@@ -344,10 +327,8 @@
"PrivateIpDesc" = "Cambia la plantilla de configuración para evitar la conexión a rangos de IP privadas."
"Ads" = "Bloquear Anuncios"
"AdsDesc" = "Cambia la plantilla de configuración para bloquear anuncios."
"Family" = "Bloquee malware y contenido para adultos"
"FamilyDesc" = "Resolutores de DNS de Cloudflare para bloquear malware y contenido para adultos para protección familiar."
"Security" = "Bloquee sitios web de malware, phishing y criptomineros"
"SecurityDesc" = "Cambiar la plantilla de configuración para la protección de seguridad."
"Family" = "Bloquear Malware y Contenido para Adultos"
"FamilyDesc" = "Resolvedores de DNS para bloquear malware y contenido para adultos para protección familiar."
"Speedtest" = "Bloquear Sitios Web de Pruebas de Velocidad"
"SpeedtestDesc" = "Cambia la plantilla de configuración para evitar la conexión a sitios web de pruebas de velocidad."
"IRIp" = "Desactivar Conexión a Rangos de IP de Irán"
@@ -386,36 +367,23 @@
"GoogleIPv4Desc" = "Agregar enrutamiento para que Google se conecte con IPv4."
"NetflixIPv4" = "Usar IPv4 para Netflix"
"NetflixIPv4Desc" = "Agregar enrutamiento para que Netflix se conecte con IPv4."
"GoogleWARP" = "Google"
"GoogleWARPDesc" = "Enruta el tráfico a Apple a través de WARP."
"OpenAIWARP" = "OpenAI (ChatGPT)"
"OpenAIWARPDesc" = "Enruta el tráfico a OpenAI (ChatGPT) a través de WARP."
"NetflixWARP" = "Netflix"
"NetflixWARPDesc" = "Enruta el tráfico a Netflix a través de WARP."
"MetaWARP" = "Meta"
"MetaWARPDesc" = "Enruta el tráfico a Meta (Instagram, Facebook, WhatsApp, Threads,...) a través de WARP."
"AppleWARP" = "Apple"
"AppleWARPDesc" = "Enruta el tráfico a Apple a través de WARP."
"RedditWARP" = "Reddit"
"RedditWARPDesc" = "Enruta el tráfico a Reddit a través de WARP."
"SpotifyWARP" = "Spotify"
"SpotifyWARPDesc" = "Enruta el tráfico a Spotify a través de WARP."
"GoogleWARP" = "Rutear Google a través de WARP."
"GoogleWARPDesc" = "Agregar enrutamiento para Google a través de WARP."
"OpenAIWARP" = "Rutear OpenAI (ChatGPT) a través de WARP."
"OpenAIWARPDesc" = "Agregar enrutamiento para OpenAI (ChatGPT) a través de WARP."
"NetflixWARP" = "Rutear Netflix a través de WARP."
"NetflixWARPDesc" = "Agregar enrutamiento para Netflix a través de WARP."
"SpotifyWARP" = "Rutear Spotify a través de WARP."
"SpotifyWARPDesc" = "Agregar enrutamiento para Spotify a través de WARP."
"IRWARP" = "Rutear dominios de Irán a través de WARP."
"IRWARPDesc" = "Agregar enrutamiento para dominios de Irán a través de WARP."
"Inbounds" = "Entrante"
"InboundsDesc" = "Cambia la plantilla de configuración para aceptar clientes específicos."
"Outbounds" = "Salidas"
"Balancers" = "Equilibradores"
"OutboundsDesc" = "Cambia la plantilla de configuración para definir formas de salida para este servidor."
"Routings" = "Reglas de enrutamiento"
"RoutingsDesc" = "¡La prioridad de cada regla es importante!"
"completeTemplate" = "Todos"
"logLevel" = "Nivel de registro"
"logLevelDesc" = "El nivel de registro para registros de errores, que indica la información que debe registrarse."
"accessLog" = "Registro de acceso"
"accessLogDesc" = "La ruta del archivo para el registro de acceso. El valor especial 'ninguno' deshabilita los registros de acceso"
"errorLog" = "Registro de errores"
"errorLogDesc" = "La ruta del archivo para el registro de errores. El valor especial 'ninguno' deshabilitó los registros de errores"
[pages.xray.rules]
"first" = "Primero"
@@ -426,7 +394,6 @@
"dest" = "Destino"
"inbound" = "Entrante"
"outbound" = "saliente"
"balancer" = "Balancín"
"info" = "Información"
"add" = "Agregar regla"
"edit" = "Editar regla"
@@ -447,38 +414,6 @@
"portal" = "portal"
"intercon" = "Interconexión"
[pages.xray.balancer]
"addBalancer" = "Agregar equilibrador"
"editBalancer" = "Editar balanceador"
"balancerStrategy" = "Estrategia"
"balancerSelectors" = "Selectores"
"tag" = "Etiqueta"
"tagDesc" = "etiqueta única"
"balancerDesc" = "No es posible utilizar balancerTag y outboundTag al mismo tiempo. Si se utilizan al mismo tiempo, sólo funcionará outboundTag."
[pages.xray.wireguard]
"secretKey" = "Llave secreta"
"publicKey" = "Llave pública"
"allowedIPs" = "IP permitidas"
"endpoint" = "Punto final"
"psk" = "Clave precompartida"
"domainStrategy" = "Estrategia de dominio"
[pages.xray.dns]
"enable" = "Habilitar DNS"
"enableDesc" = "Habilitar servidor DNS integrado"
"strategy" = "Estrategia de consulta"
"strategyDesc" = "Estrategia general para resolver nombres de dominio"
"add" = "Agregar servidor"
"edit" = "Editar servidor"
"domains" = "Dominios"
[pages.xray.fakedns]
"add" = "Agregar DNS falso"
"edit" = "Editar DNS falso"
"ipPool" = "Subred del grupo de IP"
"poolSize" = "Tamaño del grupo"
[pages.settings.security]
"admin" = "Administrador"
"secret" = "Token Secreto"
@@ -502,7 +437,6 @@
"noIpRecord" = "❗ ¡Sin Registro de IP!"
"noInbounds" = "❗ ¡No se encontraron entradas!"
"unlimited" = "♾ Ilimitado"
"add" = "Agregar"
"month" = "Mes"
"months" = "Meses"
"day" = "Día"
@@ -511,8 +445,6 @@
"unknown" = "Desconocido"
"inbounds" = "Entradas"
"clients" = "Clientes"
"offline" = "🔴 Sin conexión"
"online" = "🟢 En línea"
[tgbot.commands]
"unknown" = "❗ Comando desconocido"
@@ -523,8 +455,8 @@
"status" = "✅ ¡El bot está bien!"
"usage" = "❗ ¡Por favor proporciona un texto para buscar!"
"getID" = "🆔 Tu ID: <code>{{ .ID }}</code>"
"helpAdminCommands" = "Buscar un correo electrónico de cliente:\r\n<code>/usage [Email]</code>\r\n\r\nBuscar entradas (con estadísticas de cliente):\r\n<code>/inbound [Nota]</code>"
"helpClientCommands" = "Para buscar estadísticas, simplemente usa el siguiente comando:\r\n\r\n<code>/usage [UUID|Contraseña]</code>"
"helpAdminCommands" = "Buscar un correo electrónico de cliente:\r\n<code>/usage [Email]</code>\r\n \r\nBuscar entradas (con estadísticas de cliente):\r\n<code>/inbound [Nota]</code>"
"helpClientCommands" = "Para buscar estadísticas, simplemente usa el siguiente comando:\r\n \r\n<code>/usage [UUID|Contraseña]</code>\r\n \r\nUsa UUID para vmess/vless y Contraseña para Trojan."
[tgbot.messages]
"cpuThreshold" = "🔴 El uso de CPU {{ .Percent }}% es mayor que el umbral {{ .Threshold }}%"
@@ -539,7 +471,7 @@
"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n"
"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n"
"ip" = "🌐 IP: {{ .IP }}\r\n"
"ips" = "🔢 IPs:\r\n{{ .IPs }}\r\n"
"ips" = "🔢 IPs: \r\n{{ .IPs }}\r\n"
"serverUpTime" = "⏳ Tiempo de actividad del servidor: {{ .UpTime }} {{ .Unit }}\r\n"
"serverLoad" = "📈 Carga del servidor: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
"serverMemory" = "📋 Memoria del servidor: {{ .Current }}/{{ .Total }}\r\n"
@@ -553,23 +485,19 @@
"port" = "🔌 Puerto: {{ .Port }}\r\n"
"expire" = "📅 Fecha de Vencimiento: {{ .Time }}\r\n"
"expireIn" = "📅 Vence en: {{ .Time }}\r\n"
"active" = "💡 Activo: {{ .Enable }}\r\n"
"enabled" = "🚨 Habilitado: {{ .Enable }}\r\n"
"online" = "🌐 Estado de conexión: {{ .Status }}\r\n"
"active" = "💡 Activo: ✅ Sí\r\n"
"inactive" = "💡 Activo: ❌ No\r\n"
"email" = "📧 Email: {{ .Email }}\r\n"
"upload" = "🔼 Subida: ↑{{ .Upload }}\r\n"
"download" = "🔽 Bajada: ↓{{ .Download }}\r\n"
"total" = "📊 Total: ↑↓{{ .UpDown }} / {{ .Total }}\r\n"
"TGUser" = "👤 Usuario de Telegram: {{ .TelegramID }}\r\n"
"exhaustedMsg" = "🚨 Agotado {{ .Type }}:\r\n"
"exhaustedCount" = "🚨 Cantidad de Agotados {{ .Type }}:\r\n"
"onlinesCount" = "🌐 Clientes en línea: {{ .Count }}\r\n"
"exhaustedMsg" = "🚨 Agotado {{ .Type }}: \r\n"
"exhaustedCount" = "🚨 Cantidad de Agotados {{ .Type }}: \r\n"
"disabled" = "🛑 Desactivado: {{ .Disabled }}\r\n"
"depleteSoon" = "🔜 Se agotará pronto: {{ .Deplete }}\r\n\r\n"
"depleteSoon" = "🔜 Se agotará pronto: {{ .Deplete }}\r\n \r\n"
"backupTime" = "🗄 Hora de la Copia de Seguridad: {{ .Time }}\r\n"
"refreshedOn" = "\r\n📋🔄 Actualizado en: {{ .Time }}\r\n\r\n"
"yes" = "✅ Sí"
"no" = "❌ No"
"refreshedOn" = "\r\n📋🔄 Actualizado en: {{ .Time }}\r\n \r\n"
[tgbot.buttons]
"closeKeyboard" = "❌ Cerrar Teclado"
@@ -579,13 +507,11 @@
"confirmResetTraffic" = "✅ ¿Confirmar Reinicio de Tráfico?"
"confirmClearIps" = "✅ ¿Confirmar Limpiar IPs?"
"confirmRemoveTGUser" = "✅ ¿Confirmar Eliminar Usuario de Telegram?"
"confirmToggle" = " ✅ ¿Confirmar habilitar/deshabilitar usuario?"
"dbBackup" = "Obtener Copia de Seguridad de BD"
"serverUsage" = "Uso del Servidor"
"getInbounds" = "Obtener Entradas"
"depleteSoon" = "Pronto se Agotará"
"clientUsage" = "Obtener Uso"
"onlines" = "Clientes en línea"
"commands" = "Comandos"
"refresh" = "🔄 Actualizar"
"clearIPs" = "❌ Limpiar IPs"
@@ -593,16 +519,14 @@
"selectTGUser" = "👤 Seleccionar Usuario de Telegram"
"selectOneTGUser" = "👤 Selecciona un usuario de telegram:"
"resetTraffic" = "📈 Reiniciar Tráfico"
"resetExpire" = "📅 Cambiar fecha de Vencimiento"
"resetExpire" = "📅 Reiniciar Días de Vencimiento"
"ipLog" = "🔢 Registro de IP"
"ipLimit" = "🔢 Límite de IP"
"setTGUser" = "👤 Establecer Usuario de Telegram"
"toggle" = "🔘 Habilitar / Deshabilitar"
"custom" = "🔢 Costumbre"
"confirmNumber" = "✅ Confirmar: {{ .Num }}"
"confirmNumberAdd" = "✅ Confirmar agregando: {{ .Num }}"
"confirmNumber" = "✅ Confirmar : {{ .Num }}"
"limitTraffic" = "🚧 Límite de tráfico"
"getBanLogs" = "Registros de prohibición"
[tgbot.answers]
"successfulOperation" = "✅ ¡Exitosa!"
@@ -622,4 +546,5 @@
"removedTGUserSuccess" = "✅ {{ .Email }} : Usuario de Telegram eliminado exitosamente."
"enableSuccess" = "✅ {{ .Email }} : Habilitado exitosamente."
"disableSuccess" = "✅ {{ .Email }} : Deshabilitado exitosamente."
"askToAddUserId" = "¡No se encuentra su configuración!\r\nPor favor, pídale a su administrador que use su ID de usuario de Telegram en su(s) configuración(es).\r\n\r\nSu ID de usuario: <code>{{ .TgUserID }}</code>"
"askToAddUserId" = "¡No se encuentra su configuración!\r\nPor favor, pídale a su administrador que use su ID de usuario de Telegram en su(s) configuración(es).\r\n\r\nSu ID de usuario: <b>{{ .TgUserID }}</b>"
"askToAddUserName" = "¡No se encuentra su configuración!\r\nPor favor, pídale a su administrador que use su nombre de usuario o ID de usuario de Telegram en su(s) configuración(es).\r\n\r\nSu nombre de usuario: <b>@{{ .TgUserName }}</b>\r\n\r\nSu ID de usuario: <b>{{ .TgUserID }}</b>"

View File

@@ -1,5 +1,5 @@
"username" = "نامکاربری"
"password" = "رمزعبور"
"username" = "نام کاربری"
"password" = "رمز عبور"
"login" = "ورود"
"confirm" = "تایید"
"cancel" = "انصراف"
@@ -12,7 +12,7 @@
"protocol" = "پروتکل"
"search" = "جستجو"
"filter" = "فیلتر"
"loading" = "...در حال بارگذاری"
"loading" = "در حال بروزرسانی..."
"second" = "ثانیه"
"minute" = "دقیقه"
"hour" = "ساعت"
@@ -21,103 +21,94 @@
"indefinite" = "نامحدود"
"unlimited" = "نامحدود"
"none" = "هیچ"
"qrCode" = "QRکد"
"qrCode" = "QR کد"
"info" = "اطلاعات بیشتر"
"edit" = "ویرایش"
"delete" = "حذف"
"reset" = "ریست"
"copySuccess" = "باموفقیت کپیشد"
"copySuccess" = "با موفقیت کپی شد"
"sure" = "مطمئن"
"encryption" = "رمزگذاری"
"transmission" = "راهاتصال"
"transmission" = "راه اتصال"
"host" = "آدرس"
"path" = "مسیر"
"camouflage" = "مبهم‌سازی"
"camouflage" = "استتار"
"status" = "وضعیت"
"enabled" = "فعال"
"disabled" = "غیرفعال"
"depleted" = "منقضی"
"depletingSoon" = "درحالانقضا"
"depletingSoon" = "در حال انقضا"
"offline" = "آفلاین"
"online" = "آنلاین"
"domainName" = "آدرس دامنه"
"monitor" = "آیپی اتصال"
"monitor" = "آی پی اتصال"
"certificate" = "گواهی دیجیتال"
"fail" = "ناموفق"
"fail" = "خطا"
"success" = " موفق"
"getVersion" = "دریافت نسخه"
"getVersion" = "دریافت ورژن"
"install" = "نصب"
"clients" = "کاربران"
"usage" = "استفاده"
"secretToken" = "توکن امنیتی"
"remained" = "باقیمانده"
"remained" = "باقیمانده"
"security" = "امنیت"
"secAlertTitle" = "هشدار‌امنیتی"
"secAlertSsl" = "این‌اتصال‌امن نیست. لطفا‌ تازمانی‌که تی‌ال‌اس برای محافظت از‌ داده‌ها فعال نشده‌است، از وارد کردن اطلاعات حساس خودداری کنید"
"secAlertConf" = "تنظیمات خاصی در برابر حملات آسیب پذیر هستند. توصیه می‌شود پروتکل‌های امنیتی را برای جلوگیری از نفوذ احتمالی تقویت کنید"
"secAlertSSL" = "پنل فاقد ارتباط امن است. لطفاً یک گواهینامه تی‌ال‌اس برای محافظت از داده‌ها نصب کنید"
"secAlertPanelPort" = "استفاده از پورت پیش‌فرض پنل ناامن است. لطفاً یک پورت تصادفی یا خاص تنظیم کنید"
"secAlertPanelURI" = "مسیر پیش‌فرض لینک پنل ناامن است. لطفاً یک مسیر پیچیده تنظیم کنید"
"secAlertSubURI" = "مسیر پیش‌فرض لینک سابسکریپشن ناامن است. لطفاً یک مسیر پیچیده تنظیم کنید"
"secAlertSubJsonURI" = "مسیر پیش‌فرض لینک سابسکریپشن جیسون ناامن است. لطفاً یک مسیر پیچیده تنظیم کنید"
[menu]
"dashboard" = "نمای کلی"
"inbounds" = "ورودی‌ها"
"dashboard" = "وضعیت سیستم"
"inbounds" = "سرویس ها"
"settings" = "تنظیمات پنل"
"xray" = "پیکربندی ایکس‌ری"
"xray" = "الگوی ایکس‌ری"
"logout" = "خروج"
"link" = "مدیریت"
[pages.login]
"hello" = "سلام"
"title" = "خوش‌آمدید"
"loginAgain" = "مدت زمان استفاده به‌اتمام‌رسیده، لطفا دوباره وارد شوید"
"title" = "ورود به سیستم"
"loginAgain" = "مدت زمان استفاده به اتمام رسیده ، لطفا دوباره وارد شوید"
[pages.login.toasts]
"invalidFormData" = "اطلاعات به‌درستی وارد نشدهاست"
"emptyUsername" = "لطفا یک نامکاربری وارد کنید‌"
"emptyPassword" = "لطفا یک رمزعبور وارد کنید"
"wrongUsernameOrPassword" = "نامکاربری یا رمزعبوراشتباه‌است"
"successLogin" = "ورود"
"invalidFormData" = "اطلاعات وارد شده به صورت درست وارد نشده است"
"emptyUsername" = "نام کاربری خالی میباشد"
"emptyPassword" = "رمز عبور خالی میباشد"
"wrongUsernameOrPassword" = "نام کاربری و رمز عبور اشتباه میباشد"
"successLogin" = "خوش آمدید"
[pages.index]
"title" = "نمای کلی"
"memory" = "RAM"
"hard" = "Disk"
"xrayStatus" = "ایکس‌ری"
"title" = "وضعیت سیستم"
"memory" = "حافظه رم"
"hard" = "حافظه دیسک"
"xrayStatus" = "وضعیت"
"stopXray" = "توقف"
"restartXray" = "شروعمجدد"
"xraySwitch" = "‌نسخه"
"xraySwitchClick" = سخه مورد نظر را انتخاب کنید"
"xraySwitchClickDesk" = "لطفا بادقت انتخاب کنید. درصورت انتخاب نسخه قدیمی‌تر، امکان ناهماهنگی با پیکربندی فعلی وجود دارد"
"operationHours" = "مدت‌کارکرد"
"systemLoad" = "بارسیستم"
"restartXray" = "شروع مجدد"
"xraySwitch" = "تغییر ورژن"
"xraySwitchClick" = "ورژن مورد نظر را انتخاب کنید"
"xraySwitchClickDesk" = "لطفا با دقت انتخاب کنید ، در صورت انتخاب اشتباه امکان قطعی سیستم وجود دارد "
"operationHours" = "آپ تایم سیستم"
"systemLoad" = "بار سیستم"
"systemLoadDesc" = "میانگین بار سیستم برای 1، 5 و 15 دقیقه گذشته"
"connectionTcpCountDesc" = "در تمام‌شبکه‌ها TCP مجموع‌اتصالات"
"connectionUdpCountDesc" = "در تمام‌شبکه‌ها UDP مجموع‌اتصالات"
"connectionTcpCountDesc" = "مجموع اتصالات TCP در تمام کارت های شبکه"
"connectionUdpCountDesc" = "مجموع اتصالات UDP در تمام کارت های شبکه"
"connectionCount" = "تعداد کانکشن ها"
"upSpeed" = "سرعت کلی آپلود در تمام‌شبکه‌ها"
"downSpeed" = "سرعت کلی دانلود در تمام‌شبکه‌ها"
"totalSent" = "مجموع ترافیک ارسال‌‌شده پس‌از شروع‌به‌کار سیستم‌عامل"
"totalReceive" = "مجموع ترافیک دریافت‌شده پس‌از شروع‌به‌کار سیستم‌عامل"
"xraySwitchVersionDialog" = "تغییر نسخه ایکس‌ری"
"xraySwitchVersionDialogDesc" = "آیا از تغییر نسخه‌ مطمئن هستید؟"
"dontRefresh" = "در حال نصب، لطفا صفحه را رفرش نکنید"
"logs" = "گزارشها"
"config" = "پیکربندی"
"backup" = "پشتیبانگیری"
"backupTitle" = "پشتیبانگیری دیتابیس"
"backupDescription" = "توصیه‌می‌شود قبلاز واردکردن یک دیتابیس جدید، نسخه پشتیبان تهیه کنید"
"exportDatabase" = " پشتیبان‌گیری"
"importDatabase" = "بازگرداندن"
"upSpeed" = "سرعت آپلود در حال حاضر سیستم"
"downSpeed" = "سرعت دانلود در حال حاضر سیستم"
"totalSent" = "جمع کل ترافیک آپلود مصرفی"
"totalReceive" = "جمع کل ترافیک دانلود مصرفی"
"xraySwitchVersionDialog" = "تغییر ورژن"
"xraySwitchVersionDialogDesc" = "آیا از تغییر ورژن مطمئن هستین"
"dontRefresh" = "در حال نصب ، لطفا رفرش نکنید "
"logs" = "گزارش ها"
"config" = "تنظیمات"
"backup" = "پشتیبان گیری و بازیابی"
"backupTitle" = "پشتیبان گیری و بازیابی دیتابیس"
"backupDescription" = "به یاد داشته باشید که قبل از وارد کردن یک دیتابیس جدید، نسخه پشتیبان تهیه کنید"
"exportDatabase" = "دانلود دیتابیس"
"importDatabase" = "آپلود دیتابیس"
[pages.inbounds]
"title" = "کاربران"
"totalDownUp" = "دریافت/ارسال کل"
"totalUsage" = "‌‌‌مصرف کل"
"inboundCount" = "کل ورودی‌ها"
"operate" = "عملیات"
"totalDownUp" = "جمع آپلود/دانلود"
"totalUsage" = "جمع کل"
"inboundCount" = "تعداد سرویس ها"
"operate" = "فهرست"
"enable" = "فعال"
"remark" = "نام"
"protocol" = "پروتکل"
@@ -125,69 +116,71 @@
"traffic" = "ترافیک"
"details" = "توضیحات"
"transportConfig" = "نحوه اتصال"
"expireDate" = "مدت زمان"
"expireDate" = "تاریخ انقضا"
"resetTraffic" = "ریست ترافیک"
"addInbound" = "افزودن ورودی"
"addInbound" = "اضافه کردن سرویس"
"generalActions" = "عملیات کلی"
"create" = "افزودن"
"create" = "اضافه کردن"
"update" = "ویرایش"
"modifyInbound" = "ویرایش ورودی"
"deleteInbound" = "حذف ورودی"
"deleteInboundContent" = "آیا مطمئن به حذف ورودی هستید؟"
"modifyInbound" = "ویرایش سرویس"
"deleteInbound" = "حذف سرویس"
"deleteInboundContent" = "آیا مطمئن به حذف سرویس هستید ؟"
"deleteClient" = "حذف کاربر"
"deleteClientContent" = "آیا مطمئن به حذف کاربر هستید؟"
"resetTrafficContent" = "آیا مطمئن به ریست ترافیک هستید؟"
"deleteClientContent" = "آیا مطمئن به حذف کاربر هستید ؟"
"resetTrafficContent" = "آیا مطمئن به ریست ترافیک هستید ؟"
"copyLink" = "کپی لینک"
"address" = "آدرس"
"network" = "شبکه"
"destinationPort" = "پورت مقصد"
"targetAddress" = "آدرس مقصد"
"monitorDesc" = "بهطور پیشفرض خالیبگذارید"
"meansNoLimit" = "0 = واحد: گیگابایت) نامحدود)"
"totalFlow" = "ترافیک کل"
"leaveBlankToNeverExpire" = "برای منقضینشدن خالی‌بگذارید"
"noRecommendKeepDefault" = "توصیهمیشود به‌طور پیشفرض حفظشود"
"certificatePath" = "مسیر فایل"
"certificateContent" = "محتوای فایل"
"publicKey" = "کلید عمومی"
"privatekey" = "کلید خصوصی"
"clickOnQRcode" = "برای کپی بر روی کدتصویری کلیک کنید"
"monitorDesc" = "به طور پیش فرض خالی بگذارید"
"meansNoLimit" = "یعنی بدون محدودیت"
"totalFlow" = "کل ترافیک"
"leaveBlankToNeverExpire" = "خالی بگذارید تا هرگز منقضی نشود"
"noRecommendKeepDefault" = "توصیه می شود به عنوان پیش فرض حفظ شود"
"certificatePath" = "مسیر فایل گواهی"
"certificateContent" = "محتوای فایل گواهی"
"publicKeyPath" = "مسیر کلید عمومی"
"publicKeyContent" = "محتوای کلید عمومی"
"keyPath" = "مسیر کلید خصوصی"
"keyContent" = "محتوای کلید خصوصی"
"clickOnQRcode" = "برای کپی بر روی کد تصویری کلیک کنید"
"client" = "کاربر"
"export" = "استخراج لینکها"
"clone" = "شبیهسازی"
"cloneInbound" = "شبیه‌سازی ورودی"
"cloneInboundContent" = "همه موارد این ورودی بجز پورت، آی‌پی و کاربر‌ها شبیهسازی خواهند شد"
"export" = "استخراج لینکها"
"clone" = "شبیه سازی"
"cloneInbound" = "ایجاد"
"cloneInboundContent" = "همه موارد این ورودی بجز پورت ، ای پی و کلاینت ها شبیه سازی خواهند شد"
"cloneInboundOk" = "ساختن شبیه ساز"
"resetAllTraffic" = "ریست ترافیک کل ورودی‌ها"
"resetAllTrafficTitle" = "ریست ترافیک کل ورودی‌ها"
"resetAllTrafficContent" = "آیا مطمئن به ریست ترافیک تمام ورودی‌ها هستید؟"
"resetAllTraffic" = "ریست ترافیک کل سرویس ها"
"resetAllTrafficTitle" = "ریست ترافیک کل سرویس ها"
"resetAllTrafficContent" = "آیا مطمئن هستید که میخواهید تمام ترافیک سرویس ها را ریست کنید؟"
"resetInboundClientTraffics" = "ریست ترافیک کاربران"
"resetInboundClientTrafficTitle" = "ریست ترافیک کاربران"
"resetInboundClientTrafficContent" = "آیا مطمئن به ریست ترافیک تمام کاربران این‌ ورودی هستید؟"
"resetAllClientTraffics" = "ریست ترافیک کل کاربران"
"resetInboundClientTrafficTitle" = "ریست ترافیک کل کاربران"
"resetInboundClientTrafficContent" = "آیا مطمئن هستید که میخواهید تمام ترافیک کاربران این سرویس را ریست کنید؟"
"resetAllClientTraffics" = "ریست ترافیک کاربران"
"resetAllClientTrafficTitle" = "ریست ترافیک کل کاربران"
"resetAllClientTrafficContent" = "آیا مطمئن به ریست ترافیک تمام کاربران هستید؟"
"resetAllClientTrafficContent" = "آیا مطمئن هستید که میخواهید تمام ترافیک کاربران را ریست کنید؟"
"delDepletedClients" = "حذف کاربران منقضی"
"delDepletedClientsTitle" = "حذف کاربران منقضی"
"delDepletedClientsContent" = "آیا مطمئن به حذف تمام کاربران منقضیشده ‌هستید؟"
"delDepletedClientsContent" = "آیا مطمئن هستید مه میخواهید تمامی کاربران منقضی شده را حذف کنید؟"
"email" = "ایمیل"
"emailDesc" = "باید یک ایمیل یکتا باشد"
"IPLimit" = "محدودیت آی‌پی"
"IPLimitDesc" = "(اگر تعداد از مقدار تنظیم شده بیشتر شود، ورودی را غیرفعال می کند. (0 = غیرفعال"
"IPLimitlog" = "گزارشها"
"IPLimitlogDesc" = "گزارش تاریخچه آی‌پی. برای فعال کردن ورودی پس از غیرفعال شدن، گزارش را پاک کنید"
"IPLimitlogclear" = "پاک کردن گزارشها"
"emailDesc" = "ایمیل باید کاملا منحصر به فرد باشد"
"IPLimit" = "محدودیت ای پی"
"IPLimitDesc" = "غیرفعال کردن ورودی در صورت بیش از تعداد وارد شده (0 برای غیرفعال کردن محدودیت ای پی )"
"IPLimitlog" = "گزارش ها"
"IPLimitlogDesc" = "گزارش سابقه ای پی (قبل از فعال کردن ورودی پس از غیرفعال شدن توسط محدودیت ای پی، باید گزارش را پاک کنید)"
"IPLimitlogclear" = "پاک کردن گزارش ها"
"setDefaultCert" = "استفاده از گواهی پنل"
"xtlsDesc" = "ایکس‌ری باید 1.7.5 باشد"
"realityDesc" = "ایکس‌ری باید +1.8.0 باشد"
"telegramDesc" = "دریافت کنید ('/id'یا (دستور (@userinfobot) آی‌دی(های) چت تلگرام مدیر، از"
"subscriptionDesc" = "شما میتوانید لینک سابسکربپشن خودرا در 'جزئیات' پیدا کنید، همچنین میتوانید از همین نام برای چندین کاربر استفادهکنید"
"xtlsDesc" = "هسته Xray باید 1.7.5 باشد"
"realityDesc" = "هسته Xray باید 1.8.0 و بالاتر باشد"
"telegramDesc" = "از آیدی تلگرام بدون @ یا آیدی چت استفاده کنید (می توانید آن را از اینجا دریافت کنید @userinfobot یا در ربات دستور '/id' را وارد کنید)"
"subscriptionDesc" = "می توانید ساب لینک خود را در جزئیات پیدا کنید، همچنین می توانید از همین نام برای چندین کانفیگ استفاده کنید"
"info" = "اطلاعات"
"same" = "همسان"
"inboundData" = "داده‌های ورودی"
"exportInbound" = "استخراج ورودی"
"import" = "افزودن"
"importInbound" = "افزودن یک ورودی"
"inboundData" = "داده‌های سرویس"
"copyToClipboard" = "کپی در حافظه"
"import" = ارد کردن"
"importInbound" = ارد کردن یک سرویس"
[pages.client]
"add" = "کاربر جدید"
@@ -195,227 +188,202 @@
"submitAdd" = "اضافه کردن"
"submitEdit" = "ذخیره تغییرات"
"clientCount" = "تعداد کاربران"
"bulk" = "انبوهسازی"
"bulk" = "انبوه سازی"
"method" = "روش"
"first" = "از"
"last" = "تا"
"prefix" = "پیشوند"
"postfix" = "پسوند"
"delayedStart" = "شروع‌پس‌ازاولیناستفاده"
"expireDays" = "مدت زمان"
"delayedStart" = "شروع بعد از اولین استفاده"
"expireDays" = "روزهای اعتبار"
"days" = "(روز)"
"renew" = "تمدید خودکار"
"renewDesc" = "(تمدید خودکار پساز انقضا. (0 = غیرفعال)(واحد: روز"
"renewDesc" = "روزهای تمدید خودکار پس از انقضا. 0 = غیرفعال"
[pages.inbounds.toasts]
"obtain" = "فراهم‌سازی"
"obtain" = "Obtain"
[pages.inbounds.stream.general]
"request" = "درخواست"
"response" = "پاسخ"
"requestHeader" = "درخواست سربرگ"
"name" = "نام"
"value" = "مقدار"
[pages.inbounds.stream.tcp]
"version" = "نسخه"
"method" = "متد"
"path" = "مسیر"
"status" = ضعیت"
"statusDescription" = "توضیحات وضعیت"
"requestHeader" = "سربرگ درخواست"
"requestVersion" = "ورژن درخواست"
"requestMethod" = "متد درخواست"
"requestPath" = "مسیر درخواست"
"responseVersion" = رژن پاسخ"
"responseStatus" = "وضعیت پاسخ"
"responseStatusDescription" = "توضیحات وضعیت پاسخ"
"responseHeader" = "سربرگ پاسخ"
[pages.inbounds.stream.quic]
"encryption" = "رمزنگاری"
[pages.settings]
"title" = "تنظیمات پنل"
"title" = "تنظیمات"
"save" = "ذخیره"
"infoDesc" = "برای اعمال تغییرات در این بخش باید پس از ذخیره کردن، پنل را ریستارت کنید"
"restartPanel" = "ریستارت پنل"
"restartPanelDesc" = "آیا مطمئن به ریستارت پنل هستید؟ اگر پس‌از ریستارت نمیتوانید به پنل دسترسی پیدا کنید، لطفاً گزارش‌های موجود در اسکریپت پنل را بررسی کنید"
"restartPanelDesc" = "آیا مطمئن هستید که می خواهید پنل را دوباره راه اندازی کنید؟ برای راه اندازی مجدد روی OK کلیک کنید. اگر بعد از 3 ثانیه نمی توانید به پنل دسترسی پیدا کنید، لطفاً برای مشاهده اطلاعات گزارش پانل به سرور برگردید"
"actions" = "عملیات ها"
"resetDefaultConfig" = "برگشت به پیشفرض"
"panelSettings" = "پیکربندی"
"securitySettings" = "احرازهویت"
"TGBotSettings" = "ربات تلگرام"
"panelListeningIP" = "آدرس آیپی"
"panelListeningIPDesc" = "آدرس آی‌پی برای وب پنل. برای گوش‌دادن به‌تمام آی‌پیها خالیبگذارید"
"panelListeningDomain" = "نام دامنه"
"panelListeningDomainDesc" = "آدرس دامنه برای وب پنل. برای گوش دادن به‌تمام دامنه‌ها و آی‌پی‌ها خالیبگذارید"
"panelPort" = "پورت"
"panelPortDesc" = "شماره پورت برای وب پنل. باید پورت استفاده نشده‌باشد"
"publicKeyPath" = "مسیر کلید عمومی"
"publicKeyPathDesc" = "مسیر فایل کلیدعمومی برای وب پنل. با '/' شروعمیشود"
"privateKeyPath" = "مسیر کلید خصوصی"
"privateKeyPathDesc" = "مسیر فایل کلیدخصوصی برای وب پنل. با '/' شروعمیشود"
"panelUrlPath" = "URI مسیر"
"panelUrlPathDesc" = رای وب پنل. با '/' شروع و با '/' خاتمه‌ می‌یابد URI مسیر"
"resetDefaultConfig" = "برگشت به تنظیمات پیشفرض"
"panelSettings" = "تنظیمات پنل"
"securitySettings" = "تنظیمات امنیتی"
"TGBotSettings" = "تنظیمات ربات تلگرام"
"panelListeningIP" = "محدودیت آی پی پنل"
"panelListeningIPDesc" = "برای استفاده از تمام آی‌پیها به طور پیش فرض خالی بگذارید"
"panelListeningDomain" = "محدودیت دامین پنل"
"panelListeningDomainDesc" = "برای استفاده از تمام دامنه‌ها و آی‌پی‌ها به طور پیش فرض خالی بگذارید"
"panelPort" = "پورت پنل"
"panelPortDesc" = "پورت مورد استفاده برای نمایش این پنل"
"publicKeyPath" = "مسیر فایل گواهی کلید عمومی پنل"
"publicKeyPathDesc" = "باید یک مسیر مطلق باشد که با / شروع می شود "
"privateKeyPath" = "مسیر فایل گواهی کلید خصوصی پنل"
"privateKeyPathDesc" = "باید یک مسیر مطلق باشد که با / شروع می شود "
"panelUrlPath" = "آدرس روت پنل"
"panelUrlPathDesc" = "باید با '/' شروع شود و با '/' تمام شود"
"pageSize" = "اندازه صفحه بندی جدول"
"pageSizeDesc" = "(اندازه صفحه برای جدول ورودی‌ها.(0 = غیرفعال"
"remarkModel" = "نامکانفیگ و جداکننده"
"datepicker" = وع تقویم"
"datepickerPlaceholder" = "انتخاب تاریخ"
"datepickerDescription" = "وظایف برنامه ریزی شده بر اساس این تقویم اجرا می‌شود"
"sampleRemark" = "نمونه‌نام"
"oldUsername" = "نام‌کاربری فعلی"
"currentPassword" = "رمز‌عبور فعلی"
"newUsername" = "نام‌کاربری جدید"
"newPassword" = "رمزعبور جدید"
"telegramBotEnable" = "فعال‌سازی ربات تلگرام"
"telegramBotEnableDesc" = "ربات تلگرام را فعال می‌کند"
"pageSizeDesc" = "اندازه صفحه را برای جدول سرویس ها تعریف کنید. 0: غیرفعال"
"remarkModel" = "نام کانفیگ و جداکننده"
"sampleRemark" = مونه نام"
"oldUsername" = "نام کاربری فعلی"
"currentPassword" = "رمز عبور فعلی"
"newUsername" = "نام کاربری جدید"
"newPassword" = "رمز عبور جدید"
"telegramBotEnable" = "فعالسازی ربات تلگرام"
"telegramBotEnableDesc" = "از طریق بات تلگرام به امکانات این پنل متصل شوید"
"telegramToken" = "توکن تلگرام"
"telegramTokenDesc" = "دریافت کنید @botfather توکن را می‌توانید از"
"telegramProxy" = "SOCKS پراکسی"
"telegramProxyDesc" = "را برای اتصال به تلگرام فعال می کند SOCKS5 پراکسی"
"telegramChatId" = "آی‌دی چت مدیر"
"telegramChatIdDesc" = "دریافت ‌کنید ('/id'یا (دستور (@userinfobot) آی‌دی(های) چت تلگرام مدیر، از"
"telegramNotifyTime" = "زمان نوتیفیکیشن"
"telegramNotifyTimeDesc" = "زمان‌اطلاع‌رسانی ربات تلگرام برای گزارش های دوره‌ای. از فرمت زمانبندی لینوکس استفاده‌کنید‌"
"tgNotifyBackup" = "پشتیبان‌گیری از دیتابیس"
"tgNotifyBackupDesc" = "فایل پشتیبان‌دیتابیس را به‌همراه گزارش ارسال می‌کند"
"telegramTokenDesc" = "توکن را باید از مدیر بات های تلگرام دریافت کنید @botfather"
"telegramChatId" = "آی دی تلگرام مدیریت"
"telegramChatIdDesc" = "از @userinfobot یا دستور '/id' در ربات برای دریافت شناسه های چت خود استفاده کنید. با استفاده از کاما میتونید چند آی دی را از هم جدا کنید. "
"telegramNotifyTime" = "مدت زمان نوتیفیکیشن ربات تلگرام"
"telegramNotifyTimeDesc" = "از فرمت زمان بندی لینوکس استفاده کنید "
"tgNotifyBackup" = "پشتیبان گیری از پایگاه داده"
"tgNotifyBackupDesc" = "ارسال کپی فایل پایگاه داده به همراه گزارش دوره ای"
"tgNotifyLogin" = "اعلان ورود"
"tgNotifyLoginDesc" = "نامکاربری، آدرس آی‌پی، و زمان ورود، فردی که سعی می‌کند وارد پنل شود را نمایش میدهد"
"tgNotifyLoginDesc" = "نام کاربری، آدرس ای پی، و زمان وقتی که فردی سعی می‌کند به پنل شما وارد شود نمایش میدهد"
"sessionMaxAge" = "بیشینه زمان جلسه وب"
"sessionMaxAgeDesc" = "(بیشینه زمانی که میتوانید لاگین بمانید. (واحد: دقیقه"
"sessionMaxAgeDesc" = "بیشینه زمانی که میتوانید لاگین بمانید (واحد: دقیقه)"
"expireTimeDiff" = "آستانه زمان باقی مانده"
"expireTimeDiffDesc" = "(فاصله زمانی هشدار تا رسیدن به زمان انقضا. (واحد: روز"
"expireTimeDiffDesc" = "فاصله زمانی هشدار تا رسیدن به زمان انقضا (واحد: روز)"
"trafficDiff" = "آستانه ترافیک باقی مانده"
"trafficDiffDesc" = "(فاصله زمانی هشدار تا رسیدن به اتمام ترافیک. (واحد: گیگابایت"
"tgNotifyCpu" = "آستانه هشدار بار پردازنده"
"tgNotifyCpuDesc" = "(اگر بار روی پردازنده ازاین آستانه فراتر رفت، برای شما پیام ارسال می‌شود. (واحد: درصد"
"timeZone" = "منطقه زمانی"
"timeZoneDesc" = "وظایف برنامه ریزی شده بر اساس این منطقهزمانی اجرا میشود"
"trafficDiffDesc" = "فاصله زمانی هشدار تا رسیدن به اتمام ترافیک (واحد: گیگابایت)"
"tgNotifyCpu" = "آستانه هشدار درصد پردازنده"
"tgNotifyCpuDesc" = "این ربات تلگرام در صورت استفاده پردازنده بیشتر از این درصد برای شما پیام ارسال می کند.(واحد: درصد)"
"timeZone" = "منظقه زمانی"
"timeZoneDesc" = "وظایف برنامه ریزی شده بر اساس این منطقه زمانی اجرا می شوند. پنل را مجدداً راه اندازی می کند تا اعمال شود"
"subSettings" = "سابسکریپشن"
"subEnable" = "فعال‌سازی سرویس سابسکریپشن"
"subEnableDesc" = " سرویس سابسکریپشن را فعال‌می‌کند"
"subListen" = "آدرس آی‌پی"
"subListenDesc" = "آدرس آی‌پی برای سرویس سابسکریپشن. برای گوش دادن به‌تمام آی‌پیها خالیبگذارید"
"subPort" = "پورت"
"subPortDesc" = "شماره پورت برای سرویس سابسکریپشن. باید پورت استفاده نشده‌باشد"
"subCertPath" = "مسیر کلید عمومی"
"subCertPathDesc" = "مسیر فایل کلیدعمومی برای سرویس سابیکریپشن. با '/' شروعمیشود"
"subKeyPath" = "مسیر کلید خصوصی"
"subKeyPathDesc" = "مسیر فایل کلیدخصوصی برای سرویس سابسکریپشن. با '/' شروعمیشود"
"subPath" = "URI مسیر"
"subPathDesc" = رای سرویس سابسکریپشن. با '/' شروع و با '/' خاتمه می‌یابد URI مسیر"
"subDomain" = "نام دامنه"
"subDomainDesc" = "آدرس دامنه برای سرویس سابسکریپشن. برای گوش دادن به تمام دامنهها و آی‌پیها خالیبگذارید"
"subUpdates" = "فاصله بروزرسانی سابسکریپشن"
"subUpdatesDesc" = "(فاصله مابین بروزرسانی در برنامه‌های کاربری. (واحد: ساعت"
"subEncrypt" = "کدگذاری"
"subEncryptDesc" = "کدگذاری خواهدشد Base64 محتوای برگشتی سرویس سابسکریپشن برپایه"
"subEnable" = "فعال کردن سرویس"
"subEnableDesc" = "ویژگی سابسکریپشن با پیکربندی جداگانه"
"subListen" = "محدودیت آی‌پی"
"subListenDesc" = "برای استفاده از همه آی‌پی ها به طور پیش فرض خالی بگذارید"
"subPort" = "پورت سرویس"
"subPortDesc" = "شماره پورت برای ارائه خدمات سابسکریپشن باید خالی باشد"
"subCertPath" = "مسیر فایل کلید عمومی گواهی سابسکریپشن"
"subCertPathDesc" = "یک مسیر مطلق که با '/' شروع می شود را پر کنید."
"subKeyPath" = "مسیر فایل کلید خصوصی گواهی سابسکریپشن"
"subKeyPathDesc" = "یک مسیر مطلق که با '/' شروع می شود را پر کنید."
"subPath" = "مسیر ریشه سابسکریپشن"
"subPathDesc" = "باید با '/' شروع شود و با '/' ختم شود."
"subDomain" = "دامنه مخصوص سابسکریپشن"
"subDomainDesc" = "برای نظارت بر همه دامنه ها و آی‌پی ها به طور پیش فرض خالی بگذارید"
"subUpdates" = "فاصله به روز رسانی های سابسکریپشن"
"subUpdatesDesc" = "ساعت های فاصله بین به روز رسانی در برنامه کاربر"
"subEncrypt" = "رمزگذاری کانفیگ ها"
"subEncryptDesc" = "رمزگذاری کانفیگ های بازگشتی سابسکریپشن"
"subShowInfo" = "نمایش اطلاعات مصرف"
"subShowInfoDesc" = "ترافیک و زمان باقیمانده را در برنامه‌های کاربری نمایش میدهد"
"subURI" = "پروکسی معکوس URI مسیر"
"subURIDesc" = "سابسکریپشن را برای استفاده در پشت پراکسیها تغییر میدهد URI مسیر"
"fragment" = "تکه‌تکه شدن"
"fragmentDesc" = "فعال کردن تکه تکه شدن برای بسته نخست تی‌ال‌اس"
"subShowInfoDesc" = "ترافیک و زمان باقیمانده را در هر کانفیگ نمایش میدهد"
"subURI" = "آدرس پایه پروکسی معکوس"
"subURIDesc" = "آدرس پایه سابسکریپشن را برای استفاده در پشت پراکسی ها تغییر میدهد"
[pages.xray]
"title" = "پیکربندی ایکس‌ری"
"save" = "ذخیره"
"title" = "الگوها"
"save" = "ذخیره تنظیمات"
"restart" = "ریستارت ایکس‌ری"
"basicTemplate" = "پایه"
"advancedTemplate" = "پیشرفته"
"generalConfigs" = "استراتژی‌ کلی"
"generalConfigsDesc" = "این گزینه‌ها استراتژی کلی ترافیک را تعیین می‌کنند"
"logConfigs" = "گزارش"
"logConfigsDesc" = "گزارش‌ها ممکن است بر کارایی سرور شما تأثیر بگذارد. توصیه می شود فقط در صورت نیاز آن را عاقلانه فعال کنید"
"blockConfigs" = "سپر محافظ"
"blockConfigsDesc" = "این گزینهها ترافیک را بر اساس پروتکل‌های درخواستی خاص، و وب سایت‌ها مسدود میکند"
"blockCountryConfigs" = "مسدودسازی کشور"
"blockCountryConfigsDesc" = "این گزینهها ترافیک را بر اساس کشور درخواستی خاص مسدود میکند"
"directCountryConfigs" = "اتصال مستقیم کشور"
"directCountryConfigsDesc" = "این گزینهها ترافیک را بر اساس کشور درخواستی خاص بصورت مستقیم ارسال می‌کند"
"ipv4Configs" = "IPv4 مسیریابی"
"ipv4ConfigsDesc" = "این گزینه‌ها ترافیک را از طریق آیپینسخه4 به مقصد هدایت میکند"
"warpConfigs" = "WARP مسیریابی"
"warpConfigsDesc" = "طبق راهنما نصب کنید SOCKS5 این گزینه‌ها ترافیک‌ را از طریق وارپ کلادفلر به مقصد هدایت می‌کند. ابتدا، وارپ را در حالت پراکسی"
"Template" = "‌پیکربندی پیشرفته الگو ایکس‌ری"
"TemplateDesc" = "فایل پیکربندی نهایی ایکس‌ری بر اساس این الگو ایجاد می‌شود"
"FreedomStrategy" = "Freedom استراتژی پروتکل"
"FreedomStrategyDesc" = "تعیین می‌کند Freedom استراتژی خروجی شبکه را برای پروتکل"
"RoutingStrategy" = "استراتژی کلی مسیریابی"
"RoutingStrategyDesc" = "استراتژی کلی مسیریابی برای حل تمام درخواست‌ها را تعیین می‌کند"
"Torrent" = "مسدودسازی پروتکل بیت‌تورنت"
"TorrentDesc" = "پروتکل بیت تورنت را مسدود می‌کند"
"PrivateIp" = "مسدودسازی اتصال آی‌پی‌های خصوصی"
"PrivateIpDesc" = "اتصال به آی‌پی‌های رنج خصوصی را مسدود می‌کند"
"Ads" = "مسدودسازی تبلیغات"
"AdsDesc" = "وب‌سایت‌های تبلیغاتی را مسدود می‌کند"
"Family" = "محافظت خانواده"
"FamilyDesc" = "محتوای مخصوص بزرگسالان، و وب‌سایتهای ناامن را مسدود می‌کند"
"Security" = "محافظت امنیتی"
"SecurityDesc" = "وب‌سایت‌های ناامن، بدافزار، فیشینگ، و کریپتوماینرها را مسدود می‌کند"
"Speedtest" = "مسدودسازی اسپیدتست"
"SpeedtestDesc" = "اتصال به وب‌سایت‌های تست سرعت را مسدود می‌کند"
"IRIp" = "مسدودسازی اتصال به آیپیهای ایران"
"IRIpDesc" = "اتصال به آیپیهای کشور ایران را مسدود می‌کند"
"IRDomain" = "مسدودسازی اتصال به دامنههای ایران"
"IRDomainDesc" = "اتصال به دامنههای کشور ایران را مسدود می‌کند"
"ChinaIp" = "مسدودسازی اتصال به آی‌‌پیهای چین"
"ChinaIpDesc" = "اتصال به آیپیهای کشور چین را مسدود می‌کند"
"ChinaDomain" = "مسدودسازی اتصال به دامنههای چین"
"ChinaDomainDesc" = "اتصال به دامنههای کشور چین را مسدود می‌کند"
"RussiaIp" = "مسدودسازی اتصال به آیپیهای روسیه"
"RussiaIpDesc" = "اتصال به آیپیهای کشور روسیه را مسدود می‌کند"
"RussiaDomain" = "مسدودسازی اتصال به دامنههای روسیه"
"RussiaDomainDesc" = "اتصال به دامنههای کشور روسیه را مسدود می‌کند"
"VNIp" = "مسدودسازی اتصال به آیپیهای ویتنام"
"VNIpDesc" = "اتصال به آیپیهای کشور ویتنام را مسدود می‌کند"
"VNDomain" = "مسدودسازی اتصال به دامنه های ویتنام"
"VNDomainDesc" = "اتصال به دامنههای کشور ویتنام را مسدود می‌کند"
"DirectIRIp" = "اتصال مستقیم آیپیهای ایران"
"DirectIRIpDesc" = "اتصال مستقیم به آیپیهای کشور ایران"
"DirectIRDomain" = "اتصال مستقیم دامنههای ایران"
"DirectIRDomainDesc" = "اتصال مستقیم به دامنههای کشور ایران"
"DirectChinaIp" = "اتصال مستقیم آیپیهای چین"
"DirectChinaIpDesc" = "اتصال مستقیم به آیپیهای کشور چین"
"DirectChinaDomain" = "اتصال مستقیم دامنههای چین"
"DirectChinaDomainDesc" = "اتصال مستقیم به دامنههای کشور چین"
"DirectRussiaIp" = "اتصال مستقیم آی‌پیهای روسیه"
"DirectRussiaIpDesc" = "اتصال مستقیم به آیپیهای کشور روسیه"
"DirectRussiaDomain" = "اتصال مستقیم دامنههای روسیه"
"DirectRussiaDomainDesc" = "اتصال مستقیم به دامنههای کشور روسیه"
"DirectVNIp" = "اتصال مستقیم آیپی‌های ویتنام"
"DirectVNIpDesc" = "اتصال مستقیم به آیپی‌های کشور ویتنام"
"DirectVNDomain" = "اتصال مستقیم دامنه‌های ویتنام"
"DirectVNDomainDesc" = "اتصال مستقیم به دامنه‌های کشور ویتنام"
"GoogleIPv4" = "گوگل"
"GoogleIPv4Desc" = "ترافیک را از طریق آیپینسخه4 به گوگل هدایت میکند"
"NetflixIPv4" = "نتفلیکس"
"NetflixIPv4Desc" = "ترافیک را از طریق آیپینسخه4 به نتفلیکس هدایت میکند"
"GoogleWARP" = "گوگل"
"GoogleWARPDesc" = "ترافیک را از طریق وارپ به گوگل هدایت میکند"
"OpenAIWARP" = "چت جی‌پی‌تی"
"OpenAIWARPDesc" = "ترافیک را از طریق وارپ به چت جی‌پی‌تی هدایت میکند"
"NetflixWARP" = "نتفلیکس"
"NetflixWARPDesc" = "ترافیک را از طریق وارپ به نتفلیکس هدایت میکند"
"MetaWARP" = "متا"
"MetaWARPDesc" = "ترافیک را از طریق وارپ به متا (اینستاگرام، فیس بوک، واتساپ، تردز و...) هدایت می کند."
"AppleWARP" = "اپل"
"AppleWARPDesc" = " ترافیک را از طریق وارپ به اپل هدایت می‌کند"
"RedditWARP" = "ردیت"
"RedditWARPDesc" = " ترافیک را از طریق وارپ به ردیت هدایت می‌کند"
"SpotifyWARP" = "اسپاتیفای"
"SpotifyWARPDesc" = " ترافیک را از طریق وارپ به اسپاتیفای هدایت می‌کند"
"IRWARP" = "دامنه‌های ایران"
"IRWARPDesc" = "ترافیک را از طریق وارپ به دامنه‌های کشور ایران هدایت می‌کند"
"basicTemplate" = "بخش الگو پایه"
"advancedTemplate" = "بخش الگو پیشرفته"
"generalConfigs" = "تنظیمات عمومی"
"generalConfigsDesc" = "این تنظیمات میتواند ترافیک کلی سرویس را متاثر کند"
"blockConfigs" = "مسدود سازی"
"blockConfigsDesc" = "این گزینه ها از اتصال کاربران به پروتکل ها و وب سایت های خاص جلوگیری می کند"
"blockCountryConfigs" = "تنظیمات برای مسدودسازی کشورها"
"blockCountryConfigsDesc" = "این گزینه اتصال کاربران به دامنه های کشوری خاص را مسدود می کند"
"directCountryConfigs" = "تنظیمات برای اتصال مستقیم کشورها"
"directCountryConfigsDesc" = "این گزینه کاربران را به دامنه های کشوری خاص را به طور مستقیم، متصل می کند"
"ipv4Configs" = "تنظیمات برای IPv4"
"ipv4ConfigsDesc" = "این گزینه فقط از طریق آیپی ورژن ۴ به دامنه های هدف هدایت می شود"
"warpConfigs" = "تنظیمات برای WARP"
"warpConfigsDesc" = "هشدار: قبل از استفاده از این گزینه، WARP را در حالت پراکسی socks5 با دنبال کردن مراحل در GitHub پنل روی سرور خود نصب کنید. WARP ترافیک را از طریق سرورهای Cloudflare به وب سایت ها هدایت می کند"
"Template" = "تنظیمات الگو ایکس ری"
"TemplateDesc" = "فایل پیکربندی ایکس ری نهایی بر اساس این الگو ایجاد میشود. لطفاً این را تغییر ندهید مگر اینکه دقیقاً بدانید که چه کاری انجام می دهید!"
"FreedomStrategy" = "روش استفاده از شبکه خروجی مستقیم"
"FreedomStrategyDesc" = "تعیین روش استفاده از خروجی برای پرتکل مستقیم"
"RoutingStrategy" = "پیکربندی استراتژی حل دامنه در مسیریابی"
"RoutingStrategyDesc" = "تعیین استراتژی مسیریابی کلی برای پیدا کردن دامنه"
"Torrent" = "فیلتر کردن بیت تورنت"
"TorrentDesc" = "الگوی تنظیمات را برای فیلتر کردن پروتکل بیت تورنت برای کاربران تغییر میدهد"
"PrivateIp" = "جلوگیری از اتصال آیپی های خصوصی یا محلی"
"PrivateIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های خصوصی یا محلی و بسته های سرگردان تغییر میدهد"
"Ads" = "مسدود کردن تبلیغات"
"AdsDesc" = "الگوی تنظیمات را برای مسدود کردن تبلیغات تغییر میدهد"
"Family" = "فعال کردن حالت خانواده"
"FamilyDesc" = "برای جلوگیری از ارتباط با وبسایت های ناامن"
"Speedtest" = "جلوگیری از اتصال به سایت های تست سرعت"
"SpeedtestDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال به سایت های تست سرعت تغییر میدهد"
"IRIp" = "جلوگیری از اتصال آیپی های ایران"
"IRIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های ایران تغییر میدهد"
"IRDomain" = "جلوگیری از اتصال دامنه های ایران"
"IRDomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های ایران تغییر میدهد"
"ChinaIp" = "جلوگیری از اتصال آیپی های چین"
"ChinaIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های چین تغییر میدهد"
"ChinaDomain" = "جلوگیری از اتصال دامنه های چین"
"ChinaDomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های چین تغییر میدهد"
"RussiaIp" = "جلوگیری از اتصال آیپی های روسیه"
"RussiaIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های روسیه تغییر میدهد"
"RussiaDomain" = "جلوگیری از اتصال دامنه های روسیه"
"RussiaDomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های روسیه تغییر میدهد"
"VNIp" = "جلوگیری از اتصال آیپی های ویتنام"
"VNIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های ویتنام تغییر میدهد"
"VNDomain" = "جلوگیری از اتصال دامنه های ویتنام"
"VNDomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های ویتنام تغییر میدهد"
"DirectIRIp" = "ارتباط مستقیم به آیپی های ایران"
"DirectIRIpDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به آیپی های ایران تغییر میدهد"
"DirectIRDomain" = "ارتباط مستقیم به دامنه های ایران"
"DirectIRDomainDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به دامنه های ایران تغییر میدهد"
"DirectChinaIp" = "ارتباط مستقیم به آیپی های چین"
"DirectChinaIpDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به آیپی های چین تغییر میدهد"
"DirectChinaDomain" = "ارتباط مستقیم به دامنه های چین"
"DirectChinaDomainDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به دامنه های چین تغییر میدهد"
"DirectRussiaIp" = "ارتباط مستقیم به آیپی های روسیه"
"DirectRussiaIpDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به آیپی های روسیه تغییر میدهد"
"DirectRussiaDomain" = "ارتباط مستقیم به دامنه های روسیه"
"DirectRussiaDomainDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به دامنه های روسیه تغییر میدهد"
"DirectVNIp" = "اتصال مستقیم به ای پی های ویتنام"
"DirectVNIpDesc" = "الگوی پیکربندی را برای اتصال مستقیم به محدوده آی پی های ویتنام تغییر میدهد"
"DirectVNDomain" = "اتصال مستقیم به دامنه های ویتنام"
"DirectVNDomainDesc" = "الگوی پیکربندی را برای اتصال مستقیم به دامنه های ویتنام تغییر میدهد"
"GoogleIPv4" = "استفاده از آیپی ورژن 4 برای اتصال به گوگل"
"GoogleIPv4Desc" = "مسیردهی جدید برای اتصال به گوگل با آیپی ورژن 4 اضافه میکند"
"NetflixIPv4" = "استفاده از آیپی ورژن 4 برای اتصال به نتفلیکس"
"NetflixIPv4Desc" = "مسیردهی جدید برای اتصال به نتفلیکس با آیپی ورژن 4 اضافه میکند"
"GoogleWARP" = "مسیردهی گوگل به WARP"
"GoogleWARPDesc" = "مسیردهی جدید برای اتصال به گوگل به WARP اضافه میکند"
"OpenAIWARP" = "مسیردهی OpenAI (ChatGPT) به WARP"
"OpenAIWARPDesc" = "مسیردهی جدید برای اتصال به OpenAI (ChatGPT) به WARP اضافه میکند"
"NetflixWARP" = "مسیردهی نتفلیکس به WARP"
"NetflixWARPDesc" = "مسیردهی جدید برای اتصال به نتفلیکس به WARP اضافه میکند"
"SpotifyWARP" = "مسیردهی اسپاتیفای به WARP"
"SpotifyWARPDesc" = "مسیردهی جدید برای اتصال به اسپاتیفای به WARP اضافه میکند"
"IRWARP" = "مسیردهی دامنه های ایران به WARP"
"IRWARPDesc" = "مسیردهی جدید برای اتصال به دامنه های ایران به WARP اضافه میکند"
"Inbounds" = "ورودی‌ها"
"InboundsDesc" = "پذیرش کلاینت خاص"
"InboundsDesc" = "میتوانید الگوی تنظیمات را برای ورودی های خاص تنظیم نمایید"
"Outbounds" = "خروجی‌ها"
"Balancers" = "بالانسرها"
"OutboundsDesc" = "مسیر ترافیک خروجی را تنظیم کنید"
"OutboundsDesc" = "میتوانید الگوی تنظیمات را برای خروجی اینترنت تنظیم نمایید"
"Routings" = "قوانین مسیریابی"
"RoutingsDesc" = "اولویت هر قانون مهم است"
"RoutingsDesc" = "اولویت هر قانون مهم است!"
"completeTemplate" = "کامل"
"logLevel" = "سطح گزارش"
"logLevelDesc" = "سطح گزارش برای گزارش های خطا، نشان دهنده اطلاعاتی است که باید ثبت شوند."
"accessLog" = "مسیر گزارش"
"accessLogDesc" = "مسیر فایل برای گزارش دسترسی. مقدار ویژه «هیچ» گزارش‌های دسترسی را غیرفعال میکند."
"errorLog" = "گزارش خطا"
"errorLogDesc" = "مسیر فایل برای ورود به سیستم خطا. مقدار ویژه «هیچ» گزارش های خطا را غیرفعال میکند"
[pages.xray.rules]
"first" = "اولین"
@@ -426,7 +394,6 @@
"dest" = "مقصد"
"inbound" = "ورودی"
"outbound" = "خروجی"
"balancer" = "بالانسر"
"info" = "اطلاعات"
"add" = "افزودن قانون"
"edit" = "ویرایش قانون"
@@ -444,55 +411,23 @@
"domain" = "دامنه"
"type" = "نوع"
"bridge" = "پل"
"portal" = ورتال"
"portal" = "پرتال"
"intercon" = "اتصال میانی"
[pages.xray.balancer]
"addBalancer" = "افزودن بالانسر"
"editBalancer" = "ویرایش بالانسر"
"balancerStrategy" = "استراتژی"
"balancerSelectors" = "انتخاب‌گرها"
"tag" = "برچسب"
"tagDesc" = "برچسب یگانه"
"balancerDesc" = "امکان استفاده همزمان balancerTag و outboundTag باهم وجود ندارد. درصورت استفاده همزمان فقط outboundTag عمل خواهد کرد."
[pages.xray.wireguard]
"secretKey" = "کلید شخصی"
"publicKey" = "کلید عمومی"
"allowedIPs" = "آی‌پی‌های مجاز"
"endpoint" = "نقطه پایانی"
"psk" = "کلید مشترک"
"domainStrategy" = "استراتژی حل دامنه"
[pages.xray.dns]
"enable" = "فعال کردن حل دامنه"
"enableDesc" = "سرور حل دامنه داخلی را فعال کنید"
"strategy" = "استراتژی پرس‌وجو"
"strategyDesc" = "استراتژی کلی برای حل نام دامنه"
"add" = "افزودن سرور"
"edit" = "ویرایش سرور"
"domains" = "دامنه‌ها"
[pages.xray.fakedns]
"add" = "افزودن دی‌ان‌اس جعلی"
"edit" = "ویرایش دی‌ان‌اس جعلی"
"ipPool" = "زیرشبکه استخر آی‌پی"
"poolSize" = "اندازه استخر"
[pages.settings.security]
"admin" = "مدیر"
"secret" = "توکن مخفی"
"loginSecurity" = "ورود ایمن"
"loginSecurityDesc" = "یک لایه اضافی از احراز هویت برای ایجاد امنیت بیشتر اضافه می کند"
"secretToken" = "توکن مخفی"
"secretTokenDesc" = "لطفاً این توکن را در مکانی امن ذخیره کنید. این توکن برای ورود به سیستم مورد نیاز است و قابل بازیابی نیست"
"secret" = "توکن امنیتی"
"loginSecurity" = "لاگین ایمن"
"loginSecurityDesc" = "افزودن یک مرحله دیگر به فرآیند لاگین"
"secretToken" = "توکن امنیتی"
"secretTokenDesc" = "این کد امنیتی را نزد خود در این جای امن نگه دارید، بدون این کد امکان ورود به پنل را نخواهید داشت. امکان بازیابی آن وجود ندارد!"
[pages.settings.toasts]
"modifySettings" = "ویرایش تنظیمات"
"getSettings" = "دریافت تنظیمات"
"modifyUser" = "ویرایش مدیر"
"originalUserPassIncorrect" = "نامکاربری یا رمزعبور فعلی اشتباه‌است"
"userPassMustBeNotEmpty" = "نامکاربری یا رمزعبور جدید خالی‌است"
"modifyUser" = "ویرایش کاربر"
"originalUserPassIncorrect" = "نام کاربری و رمز عبور فعلی اشتباه می باشد "
"userPassMustBeNotEmpty" = "نام کاربری و رمز عبور جدید نمیتواند خالی باشد "
[tgbot]
"keyboardClosed" = "❌ کیبورد سفارشی بسته شد!"
@@ -501,18 +436,15 @@
"wentWrong" = "❌ مشکلی رخ داده است!"
"noIpRecord" = "❗ رکورد IP یافت نشد!"
"noInbounds" = "❗ هیچ ورودی یافت نشد!"
"unlimited" = "♾ - نامحدود(ریست)"
"add" = "اضافه کردن"
"unlimited" = "♾ نامحدود"
"month" = "ماه"
"months" = "ماه‌ها"
"day" = "روز"
"days" = "روزها"
"hours" = "ساعتها"
"hours" = "ساعت ها"
"unknown" = "نامشخص"
"inbounds" = "ورودی‌ها"
"clients" = "کلاینت‌ها"
"offline" = "🔴 آفلاین"
"online" = "🟢 آنلاین"
[tgbot.commands]
"unknown" = "❗ دستور ناشناخته"
@@ -523,69 +455,63 @@
"status" = "✅ ربات در حالت عادی است!"
"usage" = "❗ لطفاً یک متن برای جستجو وارد کنید!"
"getID" = "🆔 شناسه شما: <code>{{ .ID }}</code>"
"helpAdminCommands" = "برای جستجوی ایمیل مشتری:\r\n<code>/usage [ایمیل]</code>\r\n\r\nبرای جستجوی ورودی‌ها (با آمار مشتری):\r\n<code>/inbound [توضیح]</code>"
"helpClientCommands" = "برای جستجوی آمار، فقط از دستور زیر استفاده کنید:\r\n\r\n<code>/usage [Email]</code>"
"helpAdminCommands" = "برای جستجوی ایمیل مشتری:\r\n<code>/usage [ایمیل]</code>\r\n \r\nبرای جستجوی ورودی‌ها (با آمار مشتری):\r\n<code>/inbound [توضیح]</code>"
"helpClientCommands" = "برای جستجوی آمار، فقط از دستور زیر استفاده کنید:\r\n \r\n<code>/usage [UUID|رمز عبور]</code>\r\n \r\nاز UUID برای vmess/vless و از رمز عبور برای Trojan استفاده کنید."
[tgbot.messages]
"cpuThreshold" = "🔴 بار ‌پردازنده {{ .Percent }}% بیشتر از آستانه است {{ .Threshold }}%"
"cpuThreshold" = "🔴 میزان استفاده از CPU {{ .Percent }}% بیشتر از آستانه {{ .Threshold }}% است."
"selectUserFailed" = "❌ خطا در انتخاب کاربر!"
"userSaved" = "✅ کاربر تلگرام ذخیره شد."
"loginSuccess" = "✅ با موفقیت به پنل وارد شدید.\r\n"
"loginFailed" = "❗️ ورود به پنل ناموفقبود \r\n"
"report" = "🕰 گزارشاتزمان‌بندیشده: {{ .RunTime }}\r\n"
"datetime" = "⏰ تاریخ‌وزمان: {{ .DateTime }}\r\n"
"hostname" = "💻 ناممیزبان: {{ .Hostname }}\r\n"
"version" = "🚀 نسخه‌پنل: {{ .Version }}\r\n"
"loginFailed" = "❗️ ورود به پنل ناموفق بود.\r\n"
"report" = "🕰 گزارشات زمان‌بندی شده: {{ .RunTime }}\r\n"
"datetime" = "⏰ تاریخ-زمان: {{ .DateTime }}\r\n"
"hostname" = "💻 نام میزبان: {{ .Hostname }}\r\n"
"version" = "🚀 نسخه X-UI: {{ .Version }}\r\n"
"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n"
"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n"
"ip" = "🌐 آدرس‌آی‌پی: {{ .IP }}\r\n"
"ips" = "🔢 آدرس‌های آی‌پی:\r\n{{ .IPs }}\r\n"
"serverUpTime" = "⏳ مدت‌کارکردسیستم: {{ .UpTime }} {{ .Unit }}\r\n"
"serverLoad" = "📈 بارسیستم: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
"serverMemory" = "📋 RAM: {{ .Current }}/{{ .Total }}\r\n"
"tcpCount" = "🔹 TCP: {{ .Count }}\r\n"
"udpCount" = "🔸 UDP: {{ .Count }}\r\n"
"ip" = "🌐 آدرس IP: {{ .IP }}\r\n"
"ips" = "🔢 آدرس‌های IP: \r\n{{ .IPs }}\r\n"
"serverUpTime" = "⏳ زمان کارکرد سرور: {{ .UpTime }} {{ .Unit }}\r\n"
"serverLoad" = "📈 بار سرور: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
"serverMemory" = "📋 حافظه سرور: {{ .Current }}/{{ .Total }}\r\n"
"tcpCount" = "🔹 تعداد ترافیک TCP: {{ .Count }}\r\n"
"udpCount" = "🔸 تعداد ترافیک UDP: {{ .Count }}\r\n"
"traffic" = "🚦 ترافیک: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
"xrayStatus" = " وضعیت‌ایکس‌ری: {{ .State }}\r\n"
"username" = "👤 نامکاربری: {{ .Username }}\r\n"
"xrayStatus" = " وضعیت Xray: {{ .State }}\r\n"
"username" = "👤 نام کاربری: {{ .Username }}\r\n"
"time" = "⏰ زمان: {{ .Time }}\r\n"
"inbound" = "📍 نام‌ورودی: {{ .Remark }}\r\n"
"inbound" = "📍 ورودی: {{ .Remark }}\r\n"
"port" = "🔌 پورت: {{ .Port }}\r\n"
"expire" = "📅 تاریخانقضا: {{ .Time }}\r\n\r\n"
"expireIn" = "📅 باقیمانده‌تاانقضا: {{ .Time }}\r\n\r\n"
"active" = "💡 فعال: {{ .Enable }}\r\n"
"enabled" = "🚨 وضعیت: {{ .Enable }}\r\n"
"online" = "🌐 وضعیت اتصال: {{ .Status }}\r\n"
"expire" = "📅 تاریخ انقضا: {{ .Time }}\r\n"
"expireIn" = "📅 باقیمانده از انقضا: {{ .Time }}\r\n"
"active" = "💡 فعال: \r\n"
"inactive" = "💡 فعال: ❌\r\n"
"email" = "📧 ایمیل: {{ .Email }}\r\n"
"upload" = "🔼 آپلود↑: {{ .Upload }}\r\n"
"download" = "🔽 دانلود↓: {{ .Download }}\r\n"
"total" = "🔄 کل: {{ .UpDown }} / {{ .Total }}\r\n"
"total" = "📊 کل: {{ .UpDown }} / {{ .Total }}\r\n"
"TGUser" = "👤 کاربر تلگرام: {{ .TelegramID }}\r\n"
"exhaustedMsg" = "🚨 {{ .Type }} بهاتمامرسیدهاست:\r\n"
"exhaustedCount" = "🚨 تعداد {{ .Type }} بهاتمامرسیده‌است:\r\n"
"onlinesCount" = "🌐 کاربران‌آنلاین: {{ .Count }}\r\n"
"exhaustedMsg" = "🚨 {{ .Type }} به اتمام رسیده است:\r\n"
"exhaustedCount" = "🚨 تعداد {{ .Type }} به اتمام رسیده:\r\n"
"disabled" = "🛑 غیرفعال: {{ .Disabled }}\r\n"
"depleteSoon" = "🔜 بهزودیبهپایانخواهدرسید: {{ .Deplete }}\r\n\r\n"
"backupTime" = "🗄 زمانپشتیبان‌گیری: {{ .Time }}\r\n"
"refreshedOn" = "\r\n📋🔄 تازه‌سازی شده در: {{ .Time }}\r\n\r\n"
"yes" = "✅ بله"
"no" = "❌ خیر"
"depleteSoon" = "🔜 به زودی به پایان خواهد رسید: {{ .Deplete }}\r\n \r\n"
"backupTime" = "🗄 زمان پشتیبان‌گیری: {{ .Time }}\r\n"
"refreshedOn" = "\r\n📋🔄 تازه‌سازی شده در: {{ .Time }}\r\n \r\n"
[tgbot.buttons]
"closeKeyboard" = "❌ بستن کیبورد"
"cancel" = "❌ لغو"
"cancelReset" = "❌ لغو تنظیم مجدد"
"cancelIpLimit" = "❌ لغو محدودیت آی‌پی"
"cancelIpLimit" = "❌ لغو محدودیت IP"
"confirmResetTraffic" = "✅ تأیید تنظیم مجدد ترافیک؟"
"confirmClearIps" = "✅ تأیید پاک‌سازی آدرس‌های آی‌پی؟"
"confirmClearIps" = "✅ تأیید پاک‌سازی آدرس‌های IP؟"
"confirmRemoveTGUser" = "✅ تأیید حذف کاربر تلگرام؟"
"confirmToggle" = "✅ تایید فعال/غیرفعال کردن کاربر؟"
"dbBackup" = "دریافت پشتیبان"
"serverUsage" = "استفاده از سیستم"
"dbBackup" = "دریافت پشتیبان پایگاه داده"
"serverUsage" = "استفاده از سرور"
"getInbounds" = "دریافت ورودی‌ها"
"depleteSoon" = "بهزودی به پایان خواهد رسید"
"depleteSoon" = "به زودی به پایان خواهد رسید"
"clientUsage" = "دریافت آمار کاربر"
"onlines" = "کاربران آنلاین"
"commands" = "دستورات"
"refresh" = "🔄 تازه‌سازی"
"clearIPs" = "❌ پاک‌سازی آدرس‌ها"
@@ -599,10 +525,8 @@
"setTGUser" = "👤 تنظیم کاربر تلگرام"
"toggle" = "🔘 فعال / غیرفعال"
"custom" = "🔢 سفارشی"
"confirmNumber" = "✅ تایید: {{ .Num }}"
"confirmNumberAdd" = "✅ تایید اضافه کردن: {{ .Num }}"
"confirmNumber" = "✅ تایید : {{ .Num }}"
"limitTraffic" = "🚧 محدودیت ترافیک"
"getBanLogs" = "گزارش های بلوک را دریافت کنید"
[tgbot.answers]
"successfulOperation" = "✅ انجام شد!"
@@ -622,4 +546,5 @@
"removedTGUserSuccess" = "✅ {{ .Email }} : کاربر تلگرام با موفقیت حذف شد."
"enableSuccess" = "✅ {{ .Email }} : با موفقیت فعال شد."
"disableSuccess" = "✅ {{ .Email }} : با موفقیت غیرفعال شد."
"askToAddUserId" = "پیکربندی شما یافت نشد!\r\nلطفاً از مدیر خود بخواهید که شناسه کاربر تلگرام خود را در پیکربندی (های) خود استفاده کند.\r\n\r\nشناسه کاربری شما: <code>{{ .TgUserID }}</code>"
"askToAddUserId" = "پیکربندی شما یافت نشد!\r\nلطفاً از مدیر خود بخواهید که شناسه کاربر تلگرام خود را در پیکربندی (های) خود استفاده کند.\r\n\r\nشناسه کاربری شما: <b>{{ .TgUserID }}</b>"
"askToAddUserName" = "پیکربندی شما یافت نشد!\r\nلطفاً از مدیر خود بخواهید که نام کاربری یا شناسه کاربر تلگرام خود را در پیکربندی (های) خود استفاده کند.\r\n\r\nنام کاربری شما: <b>@{{ .TgUserName }}</b>\r\n\r\nشناسه کاربری شما: <b>{{ .TgUserID }}</b>"

View File

@@ -1,625 +0,0 @@
"username" = "Nama Pengguna"
"password" = "Kata Sandi"
"login" = "Masuk"
"confirm" = "Konfirmasi"
"cancel" = "Batal"
"close" = "Tutup"
"copy" = "Salin"
"copied" = "Tersalin"
"download" = "Unduh"
"remark" = "Catatan"
"enable" = "Aktifkan"
"protocol" = "Protokol"
"search" = "Cari"
"filter" = "Filter"
"loading" = "Memuat..."
"second" = "Detik"
"minute" = "Menit"
"hour" = "Jam"
"day" = "Hari"
"check" = "Centang"
"indefinite" = "Tak Terbatas"
"unlimited" = "Tanpa Batas"
"none" = "None"
"qrCode" = "Kode QR"
"info" = "Informasi Lebih Lanjut"
"edit" = "Edit"
"delete" = "Hapus"
"reset" = "Reset"
"copySuccess" = "Berhasil Disalin"
"sure" = "Yakin"
"encryption" = "Enkripsi"
"transmission" = "Transmisi"
"host" = "Host"
"path" = "Jalur"
"camouflage" = "Obfuscation"
"status" = "Status"
"enabled" = "Aktif"
"disabled" = "Nonaktif"
"depleted" = "Habis"
"depletingSoon" = "Akan Habis"
"offline" = "Offline"
"online" = "Online"
"domainName" = "Nama Domain"
"monitor" = "IP Pemantauan"
"certificate" = "Sertifikat Digital"
"fail" = "Gagal"
"success" = "Berhasil"
"getVersion" = "Dapatkan Versi"
"install" = "Instal"
"clients" = "Klien"
"usage" = "Penggunaan"
"secretToken" = "Token Rahasia"
"remained" = "Tersisa"
"security" = "Keamanan"
"secAlertTitle" = "Peringatan keamanan"
"secAlertSsl" = "Koneksi ini tidak aman. Harap hindari memasukkan informasi sensitif sampai TLS diaktifkan untuk perlindungan data."
"secAlertConf" = "Beberapa pengaturan rentan terhadap serangan. Disarankan untuk memperkuat protokol keamanan guna mencegah pelanggaran potensial."
"secAlertSSL" = "Panel kekurangan koneksi yang aman. Harap instal sertifikat TLS untuk perlindungan data."
"secAlertPanelPort" = "Port default panel rentan. Harap konfigurasi port acak atau tertentu."
"secAlertPanelURI" = "Jalur URI default panel tidak aman. Harap konfigurasi jalur URI kompleks."
"secAlertSubURI" = "Jalur URI default langganan tidak aman. Harap konfigurasi jalur URI kompleks."
"secAlertSubJsonURI" = "Jalur URI default JSON langganan tidak aman. Harap konfigurasikan jalur URI kompleks."
[menu]
"dashboard" = "Ikhtisar"
"inbounds" = "Masuk"
"settings" = "Pengaturan Panel"
"xray" = "Konfigurasi Xray"
"logout" = "Keluar"
"link" = "Kelola"
[pages.login]
"hello" = "Halo"
"title" = "Selamat Datang"
"loginAgain" = "Sesi Anda telah berakhir, harap masuk kembali"
[pages.login.toasts]
"invalidFormData" = "Format data input tidak valid."
"emptyUsername" = "Nama Pengguna diperlukan"
"emptyPassword" = "Kata Sandi diperlukan"
"wrongUsernameOrPassword" = "Nama pengguna atau kata sandi tidak valid."
"successLogin" = "Login berhasil"
[pages.index]
"title" = "Ikhtisar"
"memory" = "RAM"
"hard" = "Disk"
"xrayStatus" = "Xray"
"stopXray" = "Stop"
"restartXray" = "Restart"
"xraySwitch" = "Versi"
"xraySwitchClick" = "Pilih versi yang ingin Anda pindah."
"xraySwitchClickDesk" = "Pilih dengan hati-hati, karena versi yang lebih lama mungkin tidak kompatibel dengan konfigurasi saat ini."
"operationHours" = "Waktu Aktif"
"systemLoad" = "Beban Sistem"
"systemLoadDesc" = "Rata-rata beban sistem selama 1, 5, dan 15 menit terakhir"
"connectionTcpCountDesc" = "Total koneksi TCP di seluruh sistem"
"connectionUdpCountDesc" = "Total koneksi UDP di seluruh sistem"
"connectionCount" = "Statistik Koneksi"
"upSpeed" = "Kecepatan unggah keseluruhan di seluruh sistem"
"downSpeed" = "Kecepatan unduh keseluruhan di seluruh sistem"
"totalSent" = "Total data terkirim di seluruh sistem sejak startup OS"
"totalReceive" = "Total data diterima di seluruh sistem sejak startup OS"
"xraySwitchVersionDialog" = "Ganti Versi Xray"
"xraySwitchVersionDialogDesc" = "Apakah Anda yakin ingin mengubah versi Xray menjadi"
"dontRefresh" = "Instalasi sedang berlangsung, harap jangan menyegarkan halaman ini"
"logs" = "Log"
"config" = "Konfigurasi"
"backup" = "Cadangan & Pulihkan"
"backupTitle" = "Cadangan & Pulihkan Database"
"backupDescription" = "Disarankan untuk membuat cadangan sebelum memulihkan database."
"exportDatabase" = "Cadangkan"
"importDatabase" = "Pulihkan"
[pages.inbounds]
"title" = "Masuk"
"totalDownUp" = "Total Terkirim/Diterima"
"totalUsage" = "Penggunaan Total"
"inboundCount" = "Total Masuk"
"operate" = "Menu"
"enable" = "Aktifkan"
"remark" = "Catatan"
"protocol" = "Protokol"
"port" = "Port"
"traffic" = "Traffic"
"details" = "Rincian"
"transportConfig" = "Transport"
"expireDate" = "Durasi"
"resetTraffic" = "Reset Traffic"
"addInbound" = "Tambahkan Masuk"
"generalActions" = "Tindakan Umum"
"create" = "Buat"
"update" = "Perbarui"
"modifyInbound" = "Ubah Masuk"
"deleteInbound" = "Hapus Masuk"
"deleteInboundContent" = "Apakah Anda yakin ingin menghapus masuk?"
"deleteClient" = "Hapus Klien"
"deleteClientContent" = "Apakah Anda yakin ingin menghapus klien?"
"resetTrafficContent" = "Apakah Anda yakin ingin mereset traffic?"
"copyLink" = "Salin URL"
"address" = "Alamat"
"network" = "Jaringan"
"destinationPort" = "Port Tujuan"
"targetAddress" = "Alamat Target"
"monitorDesc" = "Biarkan kosong untuk mendengarkan semua IP"
"meansNoLimit" = " = Unlimited. (unit: GB)"
"totalFlow" = "Total Aliran"
"leaveBlankToNeverExpire" = "Biarkan kosong untuk tidak pernah kedaluwarsa"
"noRecommendKeepDefault" = "Disarankan untuk tetap menggunakan pengaturan default"
"certificatePath" = "Path Berkas"
"certificateContent" = "Konten Berkas"
"publicKey" = "Kunci Publik"
"privatekey" = "Kunci Pribadi"
"clickOnQRcode" = "Klik pada Kode QR untuk Menyalin"
"client" = "Klien"
"export" = "Ekspor Semua URL"
"clone" = "Duplikat"
"cloneInbound" = "Duplikat"
"cloneInboundContent" = "Semua pengaturan masuk ini, kecuali Port, Listening IP, dan Klien, akan diterapkan pada duplikat."
"cloneInboundOk" = "Duplikat"
"resetAllTraffic" = "Reset Semua Traffic Masuk"
"resetAllTrafficTitle" = "Reset Semua Traffic Masuk"
"resetAllTrafficContent" = "Apakah Anda yakin ingin mereset traffic semua masuk?"
"resetInboundClientTraffics" = "Reset Traffic Klien Masuk"
"resetInboundClientTrafficTitle" = "Reset Traffic Klien Masuk"
"resetInboundClientTrafficContent" = "Apakah Anda yakin ingin mereset traffic klien masuk ini?"
"resetAllClientTraffics" = "Reset Traffic Semua Klien"
"resetAllClientTrafficTitle" = "Reset Traffic Semua Klien"
"resetAllClientTrafficContent" = "Apakah Anda yakin ingin mereset traffic semua klien?"
"delDepletedClients" = "Hapus Klien Habis"
"delDepletedClientsTitle" = "Hapus Klien Habis"
"delDepletedClientsContent" = "Apakah Anda yakin ingin menghapus semua klien yang habis?"
"email" = "Email"
"emailDesc" = "Harap berikan alamat email yang unik."
"IPLimit" = "Batas IP"
"IPLimitDesc" = "Menonaktifkan masuk jika jumlah melebihi nilai yang ditetapkan. (0 = nonaktif)"
"IPLimitlog" = "Log IP"
"IPLimitlogDesc" = "Log histori IP. (untuk mengaktifkan masuk setelah menonaktifkan, hapus log)"
"IPLimitlogclear" = "Hapus Log"
"setDefaultCert" = "Atur Sertifikat dari Panel"
"xtlsDesc" = "Xray harus versi 1.7.5"
"realityDesc" = "Xray harus versi 1.8.0+"
"telegramDesc" = "Harap berikan ID Telegram atau obrolan tanpa menggunakan '@'. (dapatkan di sini @userinfobot) atau (gunakan perintah '/id' di bot)"
"subscriptionDesc" = "Untuk menemukan URL langganan Anda, buka 'Rincian'. Selain itu, Anda dapat menggunakan nama yang sama untuk beberapa klien."
"info" = "Info"
"same" = "Sama"
"inboundData" = "Data Masuk"
"exportInbound" = "Ekspor Masuk"
"import" = "Impor"
"importInbound" = "Impor Masuk"
[pages.client]
"add" = "Tambah Klien"
"edit" = "Edit Klien"
"submitAdd" = "Tambah Klien"
"submitEdit" = "Simpan Perubahan"
"clientCount" = "Jumlah Klien"
"bulk" = "Tambahkan Massal"
"method" = "Metode"
"first" = "Pertama"
"last" = "Terakhir"
"prefix" = "Awalan"
"postfix" = "Akhiran"
"delayedStart" = "Mulai Awal"
"expireDays" = "Durasi"
"days" = "Hari"
"renew" = "Perpanjang Otomatis"
"renewDesc" = "Perpanjangan otomatis setelah kedaluwarsa. (0 = nonaktif)(unit: hari)"
[pages.inbounds.toasts]
"obtain" = "Dapatkan"
[pages.inbounds.stream.general]
"request" = "Permintaan"
"response" = "Respons"
"name" = "Nama"
"value" = "Nilai"
[pages.inbounds.stream.tcp]
"version" = "Versi"
"method" = "Metode"
"path" = "Path"
"status" = "Status"
"statusDescription" = "Deskripsi Status"
"requestHeader" = "Header Permintaan"
"responseHeader" = "Header Respons"
[pages.inbounds.stream.quic]
"encryption" = "Enkripsi"
[pages.settings]
"title" = "Pengaturan Panel"
"save" = "Simpan"
"infoDesc" = "Setiap perubahan yang dibuat di sini perlu disimpan. Harap restart panel untuk menerapkan perubahan."
"restartPanel" = "Restart Panel"
"restartPanelDesc" = "Apakah Anda yakin ingin merestart panel? Jika Anda tidak dapat mengakses panel setelah merestart, lihat info log panel di server."
"actions" = "Tindakan"
"resetDefaultConfig" = "Reset ke Default"
"panelSettings" = "Umum"
"securitySettings" = "Otentikasi"
"TGBotSettings" = "Bot Telegram"
"panelListeningIP" = "IP Pendengar"
"panelListeningIPDesc" = "Alamat IP untuk panel web. (biarkan kosong untuk mendengarkan semua IP)"
"panelListeningDomain" = "Domain Pendengar"
"panelListeningDomainDesc" = "Nama domain untuk panel web. (biarkan kosong untuk mendengarkan semua domain dan IP)"
"panelPort" = "Port Pendengar"
"panelPortDesc" = "Nomor port untuk panel web. (harus menjadi port yang tidak digunakan)"
"publicKeyPath" = "Path Kunci Publik"
"publicKeyPathDesc" = "Path berkas kunci publik untuk panel web. (dimulai dengan /)"
"privateKeyPath" = "Path Kunci Privat"
"privateKeyPathDesc" = "Path berkas kunci privat untuk panel web. (dimulai dengan /)"
"panelUrlPath" = "URI Path"
"panelUrlPathDesc" = "URI path untuk panel web. (dimulai dengan / dan diakhiri dengan /)"
"pageSize" = "Ukuran Halaman"
"pageSizeDesc" = "Tentukan ukuran halaman untuk tabel masuk. (0 = nonaktif)"
"remarkModel" = "Model Catatan & Karakter Pemisah"
"datepicker" = "Jenis Kalender"
"datepickerPlaceholder" = "Pilih tanggal"
"datepickerDescription" = "Tugas terjadwal akan berjalan berdasarkan kalender ini."
"sampleRemark" = "Contoh Catatan"
"oldUsername" = "Username Saat Ini"
"currentPassword" = "Kata Sandi Saat Ini"
"newUsername" = "Username Baru"
"newPassword" = "Kata Sandi Baru"
"telegramBotEnable" = "Aktifkan Bot Telegram"
"telegramBotEnableDesc" = "Mengaktifkan bot Telegram."
"telegramToken" = "Token Telegram"
"telegramTokenDesc" = "Token bot Telegram yang diperoleh dari '@BotFather'."
"telegramProxy" = "Proxy SOCKS"
"telegramProxyDesc" = "Mengaktifkan proxy SOCKS5 untuk terhubung ke Telegram. (sesuaikan pengaturan sesuai panduan)"
"telegramChatId" = "ID Obrolan Admin"
"telegramChatIdDesc" = "ID Obrolan Admin Telegram. (dipisahkan koma)(dapatkan di sini @userinfobot) atau (gunakan perintah '/id' di bot)"
"telegramNotifyTime" = "Waktu Notifikasi"
"telegramNotifyTimeDesc" = "Waktu notifikasi bot Telegram yang diatur untuk laporan berkala. (gunakan format waktu crontab)"
"tgNotifyBackup" = "Cadangan Database"
"tgNotifyBackupDesc" = "Kirim berkas cadangan database dengan laporan."
"tgNotifyLogin" = "Notifikasi Login"
"tgNotifyLoginDesc" = "Dapatkan notifikasi tentang username, alamat IP, dan waktu setiap kali seseorang mencoba masuk ke panel web Anda."
"sessionMaxAge" = "Durasi Sesi"
"sessionMaxAgeDesc" = "Durasi di mana Anda dapat tetap masuk. (unit: menit)"
"expireTimeDiff" = "Notifikasi Tanggal Kedaluwarsa"
"expireTimeDiffDesc" = "Dapatkan notifikasi tentang tanggal kedaluwarsa saat mencapai ambang batas ini. (unit: hari)"
"trafficDiff" = "Notifikasi Batas Traffic"
"trafficDiffDesc" = "Dapatkan notifikasi tentang batas traffic saat mencapai ambang batas ini. (unit: GB)"
"tgNotifyCpu" = "Notifikasi Beban CPU"
"tgNotifyCpuDesc" = "Dapatkan notifikasi jika beban CPU melebihi ambang batas ini. (unit: %)"
"timeZone" = "Zone Waktu"
"timeZoneDesc" = "Tugas terjadwal akan berjalan berdasarkan zona waktu ini."
"subSettings" = "Langganan"
"subEnable" = "Aktifkan Layanan Langganan"
"subEnableDesc" = "Mengaktifkan layanan langganan."
"subListen" = "IP Pendengar"
"subListenDesc" = "Alamat IP untuk layanan langganan. (biarkan kosong untuk mendengarkan semua IP)"
"subPort" = "Port Pendengar"
"subPortDesc" = "Nomor port untuk layanan langganan. (harus menjadi port yang tidak digunakan)"
"subCertPath" = "Path Kunci Publik"
"subCertPathDesc" = "Path berkas kunci publik untuk layanan langganan. (dimulai dengan /)"
"subKeyPath" = "Path Kunci Privat"
"subKeyPathDesc" = "Path berkas kunci privat untuk layanan langganan. (dimulai dengan /)"
"subPath" = "URI Path"
"subPathDesc" = "URI path untuk layanan langganan. (dimulai dengan / dan diakhiri dengan /)"
"subDomain" = "Domain Pendengar"
"subDomainDesc" = "Nama domain untuk layanan langganan. (biarkan kosong untuk mendengarkan semua domain dan IP)"
"subUpdates" = "Interval Pembaruan"
"subUpdatesDesc" = "Interval pembaruan URL langganan dalam aplikasi klien. (unit: jam)"
"subEncrypt" = "Encode"
"subEncryptDesc" = "Konten yang dikembalikan dari layanan langganan akan dienkripsi Base64."
"subShowInfo" = "Tampilkan Info Penggunaan"
"subShowInfoDesc" = "Sisa traffic dan tanggal akan ditampilkan di aplikasi klien."
"subURI" = "URI Proxy Terbalik"
"subURIDesc" = "URI path URL langganan untuk penggunaan di belakang proxy."
"fragment" = "Fragmentasi"
"fragmentDesc" = "Aktifkan fragmentasi untuk paket hello TLS"
[pages.xray]
"title" = "Konfigurasi Xray"
"save" = "Simpan"
"restart" = "Restart Xray"
"basicTemplate" = "Dasar"
"advancedTemplate" = "Lanjutan"
"generalConfigs" = "Strategi Umum"
"generalConfigsDesc" = "Opsi ini akan menentukan penyesuaian strategi umum."
"logConfigs" = "Catatan"
"logConfigsDesc" = "Log dapat mempengaruhi efisiensi server Anda. Disarankan untuk mengaktifkannya dengan bijak hanya jika diperlukan"
"blockConfigs" = "Pelindung"
"blockConfigsDesc" = "Opsi ini akan memblokir lalu lintas berdasarkan protokol dan situs web yang diminta."
"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."
"ipv4Configs" = "Pengalihan IPv4"
"ipv4ConfigsDesc" = "Opsi ini akan mengalihkan lalu lintas berdasarkan tujuan tertentu melalui IPv4."
"warpConfigs" = "Pengalihan WARP"
"warpConfigsDesc" = "Opsi ini akan mengalihkan lalu lintas berdasarkan tujuan tertentu melalui WARP."
"Template" = "Template Konfigurasi Xray Lanjutan"
"TemplateDesc" = "File konfigurasi Xray akhir akan dibuat berdasarkan template ini."
"FreedomStrategy" = "Strategi Protokol Freedom"
"FreedomStrategyDesc" = "Atur strategi output untuk jaringan dalam Protokol Freedom."
"RoutingStrategy" = "Strategi Pengalihan Keseluruhan"
"RoutingStrategyDesc" = "Atur strategi pengalihan lalu lintas keseluruhan untuk menyelesaikan semua permintaan."
"Torrent" = "Blokir Protokol BitTorrent"
"TorrentDesc" = "Memblokir protokol BitTorrent."
"PrivateIp" = "Blokir Koneksi ke IP Pribadi"
"PrivateIpDesc" = "Memblokir pembentukan koneksi ke rentang IP pribadi."
"Ads" = "Blokir Iklan"
"AdsDesc" = "Memblokir situs web periklanan."
"Family" = "Proteksi Keluarga"
"FamilyDesc" = "Memblokir konten dewasa dan situs web berbahaya."
"Security" = "Pelindung Keamanan"
"SecurityDesc" = "Memblokir situs web malware, phishing, dan penambang kripto."
"Speedtest" = "Blokir Speedtest"
"SpeedtestDesc" = "Memblokir pembentukan koneksi ke situs web speedtest."
"IRIp" = "Blokir Koneksi ke IP Iran"
"IRIpDesc" = "Memblokir pembentukan koneksi ke rentang IP Iran."
"IRDomain" = "Blokir Koneksi ke Domain Iran"
"IRDomainDesc" = "Memblokir pembentukan koneksi ke domain Iran."
"ChinaIp" = "Blokir Koneksi ke IP China"
"ChinaIpDesc" = "Memblokir pembentukan koneksi ke rentang IP China."
"ChinaDomain" = "Blokir Koneksi ke Domain China"
"ChinaDomainDesc" = "Memblokir pembentukan koneksi ke domain China."
"RussiaIp" = "Blokir Koneksi ke IP Rusia"
"RussiaIpDesc" = "Memblokir pembentukan koneksi ke rentang IP Rusia."
"RussiaDomain" = "Blokir Koneksi ke Domain Rusia"
"RussiaDomainDesc" = "Memblokir pembentukan koneksi ke domain Rusia."
"VNIp" = "Blokir Koneksi ke IP Vietnam"
"VNIpDesc" = "Memblokir pembentukan koneksi ke rentang IP Vietnam."
"VNDomain" = "Blokir Koneksi ke Domain Vietnam"
"VNDomainDesc" = "Memblokir pembentukan koneksi ke domain Vietnam."
"DirectIRIp" = "Koneksi Langsung ke IP Iran"
"DirectIRIpDesc" = "Membentuk koneksi langsung ke rentang IP Iran."
"DirectIRDomain" = "Koneksi Langsung ke Domain Iran"
"DirectIRDomainDesc" = "Membentuk koneksi langsung ke domain Iran."
"DirectChinaIp" = "Koneksi Langsung ke IP China"
"DirectChinaIpDesc" = "Membentuk koneksi langsung ke rentang IP China."
"DirectChinaDomain" = "Koneksi Langsung ke Domain China"
"DirectChinaDomainDesc" = "Membentuk koneksi langsung ke domain China."
"DirectRussiaIp" = "Koneksi Langsung ke IP Rusia"
"DirectRussiaIpDesc" = "Membentuk koneksi langsung ke rentang IP Rusia."
"DirectRussiaDomain" = "Koneksi Langsung ke Domain Rusia"
"DirectRussiaDomainDesc" = "Membentuk koneksi langsung ke domain Rusia."
"DirectVNIp" = "Koneksi Langsung ke IP Vietnam"
"DirectVNIpDesc" = "Membentuk koneksi langsung ke rentang IP Vietnam."
"DirectVNDomain" = "Koneksi Langsung ke Domain Vietnam"
"DirectVNDomainDesc" = "Membentuk koneksi langsung ke domain Vietnam."
"GoogleIPv4" = "Google"
"GoogleIPv4Desc" = "Rute lalu lintas ke Google melalui IPv4."
"NetflixIPv4" = "Netflix"
"NetflixIPv4Desc" = "Rute lalu lintas ke Netflix melalui IPv4."
"GoogleWARP" = "Google"
"GoogleWARPDesc" = "Tambahkan pengalihan untuk Google melalui WARP."
"OpenAIWARP" = "ChatGPT"
"OpenAIWARPDesc" = "Rute lalu lintas ke ChatGPT melalui WARP."
"NetflixWARP" = "Netflix"
"NetflixWARPDesc" = "Rute lalu lintas ke Netflix melalui WARP."
"MetaWARP" = "Meta"
"MetaWARPDesc" = "Merutekan lalu lintas ke Meta (Instagram, Facebook, WhatsApp, Threads,...) melalui WARP."
"AppleWARP" = "Apple"
"AppleWARPDesc" = "Merutekan lalu lintas ke Apple melalui WARP."
"RedditWARP" = "Reddit"
"RedditWARPDesc" = "Merutekan lalu lintas ke Reddit melalui WARP."
"SpotifyWARP" = "Spotify"
"SpotifyWARPDesc" = "Rute lalu lintas ke Spotify melalui WARP."
"IRWARP" = "Domain Iran"
"IRWARPDesc" = "Rute lalu lintas ke domain Iran melalui WARP."
"Inbounds" = "Masuk"
"InboundsDesc" = "Menerima klien tertentu."
"Outbounds" = "Keluar"
"Balancers" = "Penyeimbang"
"OutboundsDesc" = "Atur jalur lalu lintas keluar."
"Routings" = "Aturan Pengalihan"
"RoutingsDesc" = "Prioritas setiap aturan penting!"
"completeTemplate" = "Semua"
"logLevel" = "Tingkat Log"
"logLevelDesc" = "Tingkat log untuk log kesalahan, menunjukkan informasi yang perlu dicatat."
"accessLog" = "Log Akses"
"accessLogDesc" = "Jalur file untuk log akses. Nilai khusus 'tidak ada' menonaktifkan log akses"
"errorLog" = "Catatan eror"
"errorLogDesc" = "Jalur file untuk log kesalahan. Nilai khusus 'tidak ada' menonaktifkan log kesalahan"
[pages.xray.rules]
"first" = "Pertama"
"last" = "Terakhir"
"up" = "Naik"
"down" = "Turun"
"source" = "Sumber"
"dest" = "Tujuan"
"inbound" = "Masuk"
"outbound" = "Keluar"
"balancer" = "Pengimbang"
"info" = "Info"
"add" = "Tambahkan Aturan"
"edit" = "Edit Aturan"
"useComma" = "Item yang dipisahkan koma"
[pages.xray.outbound]
"addOutbound" = "Tambahkan Keluar"
"addReverse" = "Tambahkan Revers"
"editOutbound" = "Edit Keluar"
"editReverse" = "Edit Revers"
"tag" = "Tag"
"tagDesc" = "Tag Unik"
"address" = "Alamat"
"reverse" = "Revers"
"domain" = "Domain"
"type" = "Tipe"
"bridge" = "Jembatan"
"portal" = "Portal"
"intercon" = "Interkoneksi"
[pages.xray.balancer]
"addBalancer" = "Tambahkan Penyeimbang"
"editBalancer" = "Sunting Penyeimbang"
"balancerStrategy" = "Strategi"
"balancerSelectors" = "Penyeleksi"
"tag" = "Menandai"
"tagDesc" = "Label Unik"
"balancerDesc" = "BalancerTag dan outboundTag tidak dapat digunakan secara bersamaan. Jika digunakan secara bersamaan, hanya outboundTag yang akan berfungsi."
[pages.xray.wireguard]
"secretKey" = "Kunci Rahasia"
"publicKey" = "Kunci Publik"
"allowedIPs" = "IP yang Diizinkan"
"endpoint" = "Titik Akhir"
"psk" = "Kunci Pra-Bagi"
"domainStrategy" = "Strategi Domain"
[pages.xray.dns]
"enable" = "Aktifkan DNS"
"enableDesc" = "Aktifkan server DNS bawaan"
"strategy" = "Strategi Kueri"
"strategyDesc" = "Strategi keseluruhan untuk menyelesaikan nama domain"
"add" = "Tambahkan Server"
"edit" = "Sunting Server"
"domains" = "Domains"
[pages.xray.fakedns]
"add" = "Tambahkan DNS Palsu"
"edit" = "Edit DNS Palsu"
"ipPool" = "Subnet Kumpulan IP"
"poolSize" = "Ukuran Kolam"
[pages.settings.security]
"admin" = "Admin"
"secret" = "Token Rahasia"
"loginSecurity" = "Login Aman"
"loginSecurityDesc" = "Menambahkan lapisan otentikasi tambahan untuk memberikan keamanan lebih."
"secretToken" = "Token Rahasia"
"secretTokenDesc" = "Simpan token ini dengan aman di tempat yang aman. Token ini diperlukan untuk login dan tidak dapat dipulihkan."
[pages.settings.toasts]
"modifySettings" = "Ubah Pengaturan"
"getSettings" = "Dapatkan Pengaturan"
"modifyUser" = "Ubah Admin"
"originalUserPassIncorrect" = "Username atau password saat ini tidak valid"
"userPassMustBeNotEmpty" = "Username dan password baru tidak boleh kosong"
[tgbot]
"keyboardClosed" = "❌ Papan ketik kustom ditutup!"
"noResult" = "❗ Tidak ada hasil!"
"noQuery" = "❌ Permintaan tidak ditemukan! Harap gunakan perintah lagi!"
"wentWrong" = "❌ Ada yang salah!"
"noIpRecord" = "❗ Tidak ada Catatan IP!"
"noInbounds" = "❗ Tidak ada masuk ditemukan!"
"unlimited" = "♾ Tak terbatas"
"add" = "Tambah"
"month" = "Bulan"
"months" = "Bulan"
"day" = "Hari"
"days" = "Hari"
"hours" = "Jam"
"unknown" = "Tidak diketahui"
"inbounds" = "Masuk"
"clients" = "Klien"
"offline" = "🔴 Offline"
"online" = "🟢 Online"
[tgbot.commands]
"unknown" = "❗ Perintah tidak dikenal."
"pleaseChoose" = "👇 Harap pilih:\r\n"
"help" = "🤖 Selamat datang di bot ini! Ini dirancang untuk menyediakan data tertentu dari panel web dan memungkinkan Anda melakukan modifikasi sesuai kebutuhan.\r\n\r\n"
"start" = "👋 Halo <i>{{ .Firstname }}</i>.\r\n"
"welcome" = "🤖 Selamat datang di <b>{{.Hostname }}</b> bot managemen.\r\n"
"status" = "✅ Bot dalam keadaan baik!"
"usage" = "❗ Harap berikan teks untuk mencari!"
"getID" = "🆔 ID Anda:<code>{{.ID }}</code>"
"helpAdminCommands" = "Untuk mencari email klien:\r\n<code>/usage [Email]</code>\r\n\r\nUntuk mencari masuk (dengan statistik klien):\r\n<code>/inbound [Remark]</code>"
"helpClientCommands" = "Untuk mencari statistik, gunakan perintah berikut:\r\n\r\n<code>/usage [Email]</code>"
[tgbot.messages]
"cpuThreshold" = "🔴 Beban CPU {{ .Percent }}% melebihi batas {{ .Threshold }}%"
"selectUserFailed" = "❌ Kesalahan dalam pemilihan pengguna!"
"userSaved" = "✅ Pengguna Telegram tersimpan."
"loginSuccess" = "✅ Berhasil masuk ke panel.\r\n"
"loginFailed" = "❗️ Gagal masuk ke panel.\r\n"
"report" = "🕰 Laporan Terjadwal: {{ .RunTime }}\r\n"
"datetime" = "⏰ Tanggal & Waktu: {{ .DateTime }}\r\n"
"hostname" = "💻 Host: {{ .Hostname }}\r\n"
"version" = "🚀 Versi 3X-UI: {{ .Version }}\r\n"
"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n"
"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n"
"ip" = "🌐 IP: {{ .IP }}\r\n"
"ips" = "🔢 IP:\r\n{{ .IPs }}\r\n"
"serverUpTime" = "⏳ Waktu Aktif: {{ .UpTime }} {{ .Unit }}\r\n"
"serverLoad" = "📈 Beban Sistem: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
"serverMemory" = "📋 RAM: {{ .Current }}/{{ .Total }}\r\n"
"tcpCount" = "🔹 TCP: {{ .Count }}\r\n"
"udpCount" = "🔸 UDP: {{ .Count }}\r\n"
"traffic" = "🚦 Lalu Lintas: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
"xrayStatus" = " Status: {{ .State }}\r\n"
"username" = "👤 Nama Pengguna: {{ .Username }}\r\n"
"time" = "⏰ Waktu: {{ .Time }}\r\n"
"inbound" = "📍 Inbound: {{ .Remark }}\r\n"
"port" = "🔌 Port: {{ .Port }}\r\n"
"expire" = "📅 Tanggal Kadaluarsa: {{ .Time }}\r\n"
"expireIn" = "📅 Kadaluarsa Dalam: {{ .Time }}\r\n"
"active" = "💡 Aktif: {{ .Enable }}\r\n"
"enabled" = "🚨 Diaktifkan: {{ .Enable }}\r\n"
"online" = "🌐 Status Koneksi: {{ .Status }}\r\n"
"email" = "📧 Email: {{ .Email }}\r\n"
"upload" = "🔼 Unggah: ↑{{ .Upload }}\r\n"
"download" = "🔽 Unduh: ↓{{ .Download }}\r\n"
"total" = "📊 Total: ↑↓{{ .UpDown }} / {{ .Total }}\r\n"
"TGUser" = "👤 Pengguna Telegram: {{ .TelegramID }}\r\n"
"exhaustedMsg" = "🚨 Habis {{ .Type }}:\r\n"
"exhaustedCount" = "🚨 Jumlah Habis {{ .Type }}:\r\n"
"onlinesCount" = "🌐 Klien Online: {{ .Count }}\r\n"
"disabled" = "🛑 Dinonaktifkan: {{ .Disabled }}\r\n"
"depleteSoon" = "🔜 Habis Sebentar: {{ .Deplete }}\r\n\r\n"
"backupTime" = "🗄 Waktu Backup: {{ .Time }}\r\n"
"refreshedOn" = "\r\n📋🔄 Diperbarui Pada: {{ .Time }}\r\n\r\n"
"yes" = "✅ Ya"
"no" = "❌ Tidak"
[tgbot.buttons]
"closeKeyboard" = "❌ Tutup Papan Ketik"
"cancel" = "❌ Batal"
"cancelReset" = "❌ Batal Reset"
"cancelIpLimit" = "❌ Batal Batas IP"
"confirmResetTraffic" = "✅ Konfirmasi Reset Lalu Lintas?"
"confirmClearIps" = "✅ Konfirmasi Hapus IPs?"
"confirmRemoveTGUser" = "✅ Konfirmasi Hapus Pengguna Telegram?"
"confirmToggle" = "✅ Konfirmasi Aktifkan/Nonaktifkan Pengguna?"
"dbBackup" = "Dapatkan Cadangan DB"
"serverUsage" = "Penggunaan Server"
"getInbounds" = "Dapatkan Inbounds"
"depleteSoon" = "Habis Sebentar"
"clientUsage" = "Dapatkan Penggunaan"
"onlines" = "Klien Online"
"commands" = "Perintah"
"refresh" = "🔄 Perbarui"
"clearIPs" = "❌ Hapus IPs"
"removeTGUser" = "❌ Hapus Pengguna Telegram"
"selectTGUser" = "👤 Pilih Pengguna Telegram"
"selectOneTGUser" = "👤 Pilih Pengguna Telegram:"
"resetTraffic" = "📈 Reset Lalu Lintas"
"resetExpire" = "📅 Ubah Tanggal Kadaluarsa"
"ipLog" = "🔢 Log IP"
"ipLimit" = "🔢 Batas IP"
"setTGUser" = "👤 Set Pengguna Telegram"
"toggle" = "🔘 Aktifkan / Nonaktifkan"
"custom" = "🔢 Kustom"
"confirmNumber" = "✅ Konfirmasi: {{ .Num }}"
"confirmNumberAdd" = "✅ Konfirmasi menambahkan: {{ .Num }}"
"limitTraffic" = "🚧 Batas Lalu Lintas"
"getBanLogs" = "Dapatkan Log Pemblokiran"
[tgbot.answers]
"successfulOperation" = "✅ Operasi berhasil!"
"errorOperation" = "❗ Kesalahan dalam operasi."
"getInboundsFailed" = "❌ Gagal mendapatkan inbounds."
"canceled" = "❌ {{ .Email }}: Operasi dibatalkan."
"clientRefreshSuccess" = "✅ {{ .Email }}: Klien diperbarui dengan berhasil."
"IpRefreshSuccess" = "✅ {{ .Email }}: IP diperbarui dengan berhasil."
"TGIdRefreshSuccess" = "✅ {{ .Email }}: Pengguna Telegram Klien diperbarui dengan berhasil."
"resetTrafficSuccess" = "✅ {{ .Email }}: Lalu lintas direset dengan berhasil."
"setTrafficLimitSuccess" = "✅ {{ .Email }}: Batas lalu lintas disimpan dengan berhasil."
"expireResetSuccess" = "✅ {{ .Email }}: Hari kadaluarsa direset dengan berhasil."
"resetIpSuccess" = "✅ {{ .Email }}: Batas IP {{ .Count }} disimpan dengan berhasil."
"clearIpSuccess" = "✅ {{ .Email }}: IP dihapus dengan berhasil."
"getIpLog" = "✅ {{ .Email }}: Dapatkan Log IP."
"getUserInfo" = "✅ {{ .Email }}: Dapatkan Info Pengguna Telegram."
"removedTGUserSuccess" = "✅ {{ .Email }}: Pengguna Telegram dihapus dengan berhasil."
"enableSuccess" = "✅ {{ .Email }}: Diaktifkan dengan berhasil."
"disableSuccess" = "✅ {{ .Email }}: Dinonaktifkan dengan berhasil."
"askToAddUserId" = "Konfigurasi Anda tidak ditemukan!\r\nSilakan minta admin Anda untuk menggunakan ID Telegram Anda dalam konfigurasi Anda.\r\n\r\nID Pengguna Anda: <code>{{ .TgUserID }}</code>"

Some files were not shown because too many files have changed in this diff Show More