Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61e068ab40 |
10
.github/workflows/docker.yml
vendored
10
.github/workflows/docker.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Release 3X-UI for Docker
|
||||
name: Release 3X-UI dockerhub
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
|
||||
- name: Login to GHCR
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5.5.1
|
||||
uses: docker/metadata-action@v5.5.0
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}
|
||||
|
||||
@@ -36,6 +36,6 @@ jobs:
|
||||
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, linux/arm/v6
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
38
.github/workflows/release.yml
vendored
38
.github/workflows/release.yml
vendored
@@ -15,8 +15,6 @@ jobs:
|
||||
- arm64
|
||||
- armv7
|
||||
- armv6
|
||||
- 386
|
||||
- armv5
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -25,21 +23,15 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5.0.0
|
||||
with:
|
||||
go-version: '1.22'
|
||||
go-version: '1.21'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
if [ "${{ matrix.platform }}" == "arm64" ]; then
|
||||
sudo apt install gcc-aarch64-linux-gnu
|
||||
elif [ "${{ matrix.platform }}" == "armv7" ]; then
|
||||
elif [ "${{ matrix.platform }}" == "armv7" ] || [ "${{ matrix.platform }}" == "armv6" ]; 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
|
||||
@@ -50,21 +42,10 @@ jobs:
|
||||
if [ "${{ matrix.platform }}" == "arm64" ]; then
|
||||
export GOARCH=arm64
|
||||
export CC=aarch64-linux-gnu-gcc
|
||||
elif [ "${{ matrix.platform }}" == "armv7" ]; then
|
||||
elif [ "${{ matrix.platform }}" == "armv7" ] || [ "${{ matrix.platform }}" == "armv6" ]; then
|
||||
export GOARCH=arm
|
||||
export GOARM=7
|
||||
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
|
||||
|
||||
@@ -94,14 +75,6 @@ jobs:
|
||||
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
|
||||
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
|
||||
@@ -116,11 +89,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@2.9.0
|
||||
- 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
|
||||
|
||||
@@ -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"
|
||||
@@ -25,16 +22,19 @@ case $1 in
|
||||
FNAME="amd64"
|
||||
;;
|
||||
esac
|
||||
|
||||
|
||||
mkdir -p build/bin
|
||||
cd build/bin
|
||||
|
||||
wget "https://github.com/XTLS/Xray-core/releases/download/v1.8.7/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 ../../
|
||||
17
Dockerfile
17
Dockerfile
@@ -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" ]
|
||||
|
||||
179
README.md
179
README.md
@@ -25,39 +25,11 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
|
||||
|
||||
## Install Custom Version
|
||||
|
||||
To install your desired version, add the version to the end of the installation command. e.g., ver `v2.2.0`:
|
||||
To install your desired version, add the version to the end of the installation command. e.g., ver `v2.0.2`:
|
||||
|
||||
```
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v2.2.0
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v2.0.2
|
||||
```
|
||||
|
||||
## SSL Certificate
|
||||
|
||||
<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`.*
|
||||
|
||||
</details>
|
||||
|
||||
## Manual Install & Upgrade
|
||||
|
||||
<details>
|
||||
@@ -69,17 +41,7 @@ certbot renew --dry-run
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
@@ -87,16 +49,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
|
||||
@@ -150,24 +103,6 @@ systemctl restart x-ui
|
||||
ghcr.io/mhsanaei/3x-ui:latest
|
||||
```
|
||||
|
||||
update to latest version
|
||||
|
||||
```sh
|
||||
cd 3x-ui
|
||||
docker compose down
|
||||
docker compose pull 3x-ui
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
remove 3x-ui from docker
|
||||
|
||||
```sh
|
||||
docker stop 3x-ui
|
||||
docker rm 3x-ui
|
||||
cd --
|
||||
rm -r 3x-ui
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -183,25 +118,6 @@ remove 3x-ui from docker
|
||||
- AlmaLinux 9+
|
||||
- Rockylinux 9+
|
||||
|
||||
## Supported Architectures and Devices
|
||||
|
||||
<details>
|
||||
<summary>Click for Supported Architectures and devices 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:
|
||||
|
||||
- **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
|
||||
|
||||
@@ -211,7 +127,6 @@ Our platform offers compatibility with a diverse range of architectures and devi
|
||||
- Russian
|
||||
- Vietnamese
|
||||
- Spanish
|
||||
- Indonesian
|
||||
|
||||
|
||||
## Features
|
||||
@@ -253,6 +168,54 @@ Our platform offers compatibility with a diverse range of architectures and devi
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
## SSL Certificate
|
||||
|
||||
<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`.*
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
## Xray Configurations
|
||||
|
||||
<details>
|
||||
<summary>Click for Xray configurations details</summary>
|
||||
|
||||
#### Usage
|
||||
|
||||
**1.** Copy & paste into the Advanced Xray Configuration:
|
||||
|
||||
- [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)
|
||||
|
||||
***Tip:*** *You don't need to do this for a fresh install.*
|
||||
|
||||
</details>
|
||||
|
||||
## [WARP Configuration](https://gitlab.com/fscarmen/warp)
|
||||
|
||||
<details>
|
||||
@@ -260,21 +223,21 @@ Our platform offers compatibility with a diverse range of architectures and devi
|
||||
|
||||
#### Usage
|
||||
|
||||
If you want to use routing to WARP before v2.1.0 follow steps as below:
|
||||
If you want to use routing to WARP 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:
|
||||
|
||||
@@ -305,13 +268,13 @@ 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"
|
||||
},
|
||||
```
|
||||
|
||||
@@ -446,6 +409,20 @@ XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
## Supported Architectures and Devices
|
||||
|
||||
Supports a variety of different architectures and devices. Here are some of the main architectures that we support:
|
||||
|
||||
- **amd64**: This is the most common architecture for personal computers and servers. It supports most modern operating systems.
|
||||
|
||||
- **armv8 / arm64 / aarch64**: This is the architecture for modern mobile and embedded devices, including smartphones and tablets. (Ex: Raspberry Pi 4, Raspberry Pi 3, Raspberry Pi Zero 2/Zero 2 W, Orange Pi 3 LTS,...)
|
||||
|
||||
- **armv7 / arm / arm32**: This is the architecture for older mobile and embedded devices. It is still widely used in many devices. (Ex: Orange Pi Zero LTS, Orange Pi PC Plus, Raspberry Pi 2,...)
|
||||
|
||||
- **armv6 / arm / arm32**: This is the architecture for very old embedded devices. While not as common as before, there are still some devices using this architecture. (Ex: Raspberry Pi 1, Raspberry Pi Zero/Zero W,...)
|
||||
|
||||
|
||||
## Preview
|
||||
|
||||

|
||||
|
||||
@@ -1 +1 @@
|
||||
2.2.0
|
||||
2.1.0
|
||||
@@ -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{})
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
32
go.mod
32
go.mod
@@ -1,29 +1,29 @@
|
||||
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/shirou/gopsutil/v3 v3.23.12
|
||||
github.com/valyala/fasthttp v1.51.0
|
||||
github.com/xtls/xray-core v1.8.7
|
||||
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
|
||||
@@ -36,7 +36,7 @@ require (
|
||||
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.16.0 // 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
|
||||
@@ -45,11 +45,10 @@ require (
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/gorilla/sessions v1.2.2 // 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.6 // indirect
|
||||
github.com/klauspost/compress v1.17.4 // 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
|
||||
@@ -76,7 +75,6 @@ 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/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
|
||||
@@ -84,16 +82,16 @@ require (
|
||||
go.uber.org/mock v0.4.0 // indirect
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||
golang.org/x/arch v0.6.0 // indirect
|
||||
golang.org/x/crypto v0.19.0 // indirect
|
||||
golang.org/x/crypto v0.18.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc // indirect
|
||||
golang.org/x/mod v0.14.0 // indirect
|
||||
golang.org/x/net v0.21.0 // indirect
|
||||
golang.org/x/sys v0.17.0 // indirect
|
||||
golang.org/x/net v0.20.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.16.1 // 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-20240123012728-ef4313101c80 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 // indirect
|
||||
google.golang.org/protobuf v1.32.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20231104011432-48a6d7d5bd0b // indirect
|
||||
|
||||
60
go.sum
60
go.sum
@@ -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=
|
||||
@@ -76,8 +76,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.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE=
|
||||
github.com/go-playground/validator/v10 v10.16.0/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=
|
||||
@@ -124,8 +124,6 @@ github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTj
|
||||
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
|
||||
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,8 +136,8 @@ 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.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI=
|
||||
github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
|
||||
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
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=
|
||||
@@ -177,12 +175,12 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
|
||||
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/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.2 h1:Bi2gGVkfn6gQcjNjZJVO8Gf0FHzMPf2phUei9tejVMs=
|
||||
github.com/onsi/ginkgo/v2 v2.13.2/go.mod h1:XStQ8QcGwLyF4HdfcZB8SFOS/MWCgDuXMSBe6zrvLgM=
|
||||
github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=
|
||||
@@ -232,8 +230,8 @@ github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJ
|
||||
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.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4=
|
||||
github.com/shirou/gopsutil/v3 v3.23.12/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=
|
||||
@@ -290,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=
|
||||
@@ -323,8 +319,8 @@ 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.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc h1:ao2WRsKSzW6KuUY9IWPwWahcHCgR0s52IfwutMfEbdM=
|
||||
golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
|
||||
@@ -342,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.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
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=
|
||||
@@ -373,9 +369,9 @@ 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.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/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=
|
||||
@@ -411,14 +407,14 @@ 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-20240123012728-ef4313101c80 h1:AjyfHzEPEFp/NpvfN5g+KDla3EMojjhRVZc1i7cj+oM=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 h1:gphdwh0npgs8elJ4T6J+DQJHPVF7RsuJHCfwztUb4J4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA=
|
||||
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=
|
||||
@@ -439,10 +435,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=
|
||||
|
||||
46
install.sh
46
install.sh
@@ -26,15 +26,12 @@ 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' ;;
|
||||
armv6* | armv6 | arm) echo 'armv6' ;;
|
||||
*) echo -e "${green}Unsupported CPU architecture! ${plain}" && rm -f install.sh && exit 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
echo "arch: $(arch3xui)"
|
||||
|
||||
os_version=""
|
||||
@@ -81,21 +78,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|almalinux|rocky)
|
||||
yum -y update && yum install -y -q wget curl tar
|
||||
;;
|
||||
arch|manjaro)
|
||||
pacman -Syu && pacman -Syu --noconfirm wget curl tar
|
||||
;;
|
||||
*)
|
||||
apt-get update && 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 +118,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
|
||||
@@ -165,21 +160,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
|
||||
|
||||
@@ -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...))
|
||||
|
||||
97
media/configs/traffic+block-ads+ipv4-google.json
Normal file
97
media/configs/traffic+block-ads+ipv4-google.json
Normal 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": {}
|
||||
}
|
||||
105
media/configs/traffic+block-ads+warp.json
Normal file
105
media/configs/traffic+block-ads+warp.json
Normal 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": {}
|
||||
}
|
||||
84
media/configs/traffic+block-iran-domains.json
Normal file
84
media/configs/traffic+block-iran-domains.json
Normal 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": {}
|
||||
}
|
||||
76
media/configs/traffic+block-iran-ip.json
Normal file
76
media/configs/traffic+block-iran-ip.json
Normal 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": {}
|
||||
}
|
||||
65
media/configs/traffic.json
Normal file
65
media/configs/traffic.json
Normal 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": {}
|
||||
}
|
||||
105
sub/default.json
105
sub/default.json
@@ -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": {}
|
||||
}
|
||||
44
sub/sub.go
44
sub/sub.go
@@ -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
|
||||
}
|
||||
|
||||
@@ -3,57 +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) {
|
||||
println(c.Request.Header["User-Agent"][0])
|
||||
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 {
|
||||
@@ -63,32 +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) {
|
||||
println(c.Request.Header["User-Agent"][0])
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -25,42 +25,47 @@ type SubService struct {
|
||||
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"
|
||||
}
|
||||
s.datepicker, err = s.settingService.GetDatepicker()
|
||||
if err != nil {
|
||||
s.datepicker = "gregorian"
|
||||
}
|
||||
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 +76,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 +97,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 +130,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 +138,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 +578,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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -538,7 +538,7 @@
|
||||
|
||||
var on = function(emitter, type, f) {
|
||||
if (emitter.addEventListener) {
|
||||
emitter.addEventListener(type, f, { passive: true });
|
||||
emitter.addEventListener(type, f, false);
|
||||
} else if (emitter.attachEvent) {
|
||||
emitter.attachEvent("on" + type, f);
|
||||
} else {
|
||||
|
||||
@@ -45,8 +45,8 @@ 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: #000000; border-color: #25272a; color: rgb(255 255 255 / 85%); }
|
||||
.dark .cm-s-xq.CodeMirror:hover { background-color: rgb(0 50 42 / 20%); border-color: #008771; transition: all .3s; }
|
||||
.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: 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); }
|
||||
|
||||
@@ -1,18 +1,3 @@
|
||||
:root {
|
||||
--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-text-primary: rgb(255 255 255 / 85%);
|
||||
--dark-color-stroke: #202025;
|
||||
--dark-color-btn-danger: #cd3838;
|
||||
--dark-color-btn-danger-border: transparent;
|
||||
--dark-color-btn-danger-hover: #e94b4b;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100vh;
|
||||
@@ -517,13 +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 2px 8px transparent;
|
||||
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 80%);
|
||||
}
|
||||
|
||||
.dark > .ant-layout,
|
||||
@@ -533,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 {
|
||||
@@ -543,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,
|
||||
@@ -555,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,
|
||||
@@ -568,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,
|
||||
@@ -576,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,
|
||||
@@ -612,7 +597,7 @@ style attribute {
|
||||
.dark .ant-calendar-year-panel-year,
|
||||
.dark .ant-calendar-month-panel-month,
|
||||
.dark .ant-calendar-decade-panel-decade {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
}
|
||||
|
||||
.dark .ant-list-item-meta-description {
|
||||
@@ -638,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: rgba(255, 255, 255, 0.85);
|
||||
background-color: #222d42;
|
||||
border-color: #2c3950;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
}
|
||||
|
||||
.dark .ant-select-selection:hover,
|
||||
@@ -657,7 +643,7 @@ style attribute {
|
||||
}
|
||||
|
||||
.dark .ant-btn:not(.ant-btn-primary):not(.ant-btn-danger) {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
background-color: rgb(10 117 87 / 30%);
|
||||
border: 1px solid #008771;
|
||||
}
|
||||
@@ -680,7 +666,7 @@ style attribute {
|
||||
.dark .ant-btn-danger[disabled],
|
||||
.dark .ant-calendar-ok-btn-disabled {
|
||||
color: rgb(255 255 255 / 35%);
|
||||
background-color: var(--dark-color-surface-300);
|
||||
background-color: #2c3950;
|
||||
border-color: #42516c;
|
||||
}
|
||||
|
||||
@@ -689,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: rgb(89 89 89 / 15%);
|
||||
background-color: #00877122;
|
||||
}
|
||||
|
||||
.dark .ant-table-row-expand-icon {
|
||||
@@ -706,31 +692,31 @@ style attribute {
|
||||
|
||||
.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 {
|
||||
@@ -738,9 +724,9 @@ style attribute {
|
||||
}
|
||||
|
||||
.dark .ant-tag {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
background-color: #ffffff0a;
|
||||
border-color: #344461;
|
||||
}
|
||||
|
||||
.dark .ant-tag-blue {
|
||||
@@ -751,38 +737,38 @@ style attribute {
|
||||
|
||||
.dark .ant-tag-red,
|
||||
.dark .ant-alert-error {
|
||||
background-color: #2a1215;
|
||||
border-color: #58181c;
|
||||
color: #e84749;
|
||||
background-color: #291515;
|
||||
border-color: #5c2626;
|
||||
color: #e04141;
|
||||
}
|
||||
|
||||
.dark .ant-tag-orange,
|
||||
.dark .ant-alert-warning {
|
||||
background-color: #2b1d11;
|
||||
border-color: #593815;
|
||||
color: #e89a3c;
|
||||
background-color: #312313;
|
||||
border-color: #593914;
|
||||
color: #ffa031;
|
||||
}
|
||||
|
||||
.dark .ant-tag-green {
|
||||
background-color: #112421;
|
||||
border-color: #195544;
|
||||
color: #59cbac;
|
||||
border-color: #144840;
|
||||
color: #33bca5;
|
||||
}
|
||||
|
||||
.dark .ant-tag-purple {
|
||||
background-color: #241121;
|
||||
border-color: #5a2969;
|
||||
color: #d686ca;
|
||||
background-color: #2c1e32;
|
||||
border-color: #49394e;
|
||||
color: #cfb9cc;
|
||||
}
|
||||
|
||||
.dark .ant-modal-content,
|
||||
.dark .ant-modal-header {
|
||||
background-color: #101113;
|
||||
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 {
|
||||
@@ -792,7 +778,7 @@ style attribute {
|
||||
|
||||
.dark .ant-calendar-date:hover,
|
||||
.dark .ant-calendar-time-picker-select li:hover {
|
||||
background-color: var(--dark-color-surface-300);
|
||||
background-color: #313f5a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@@ -810,7 +796,7 @@ style attribute {
|
||||
}
|
||||
|
||||
.dark .ant-calendar-time-picker-select {
|
||||
border-right-color: var(--dark-color-surface-300);
|
||||
border-right-color: #2c3950;
|
||||
}
|
||||
|
||||
.has-warning .ant-input,
|
||||
@@ -971,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,
|
||||
@@ -982,7 +968,7 @@ li.ant-select-dropdown-menu-item:empty:after {
|
||||
.dark .ant-calendar-year-panel-year:hover,
|
||||
.dark .ant-calendar-month-panel-month:hover,
|
||||
.dark .ant-calendar-decade-panel-decade:hover {
|
||||
background-color: var(--dark-color-surface-600);
|
||||
background-color: #222d42;
|
||||
}
|
||||
|
||||
.dark .ant-calendar-header a:hover {
|
||||
@@ -990,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,
|
||||
@@ -1042,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 {
|
||||
@@ -1057,25 +1043,19 @@ li.ant-select-dropdown-menu-item:empty:after {
|
||||
}
|
||||
|
||||
.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 {
|
||||
@@ -1109,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-300);
|
||||
background-color: #313f5a;
|
||||
}
|
||||
|
||||
.ant-select-dropdown,
|
||||
@@ -1130,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;
|
||||
@@ -1143,35 +1121,3 @@ li.ant-select-dropdown-menu-item:empty:after {
|
||||
.ant-input-group-addon:not(:first-child):not(:last-child), .ant-input-group-wrap:not(:first-child):not(:last-child), .ant-input-group>.ant-input: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: #000000;
|
||||
border: 1px solid #303134;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
.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
BIN
web/assets/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -29,11 +29,6 @@ const supportLangs = [
|
||||
value: 'es-ES',
|
||||
icon: '🇪🇸',
|
||||
},
|
||||
{
|
||||
name: 'Indonesian',
|
||||
value: 'id-ID',
|
||||
icon: '🇮🇩',
|
||||
},
|
||||
];
|
||||
|
||||
function getLang() {
|
||||
|
||||
@@ -418,7 +418,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);
|
||||
}
|
||||
|
||||
@@ -861,13 +861,13 @@ Outbound.SocksSettings = class extends CommonClass {
|
||||
}
|
||||
|
||||
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].pass) ? '' : servers[0].users[0].pass,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -891,13 +891,13 @@ Outbound.HttpSettings = class extends CommonClass {
|
||||
}
|
||||
|
||||
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].pass) ? '' : servers[0].users[0].pass,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -914,8 +914,8 @@ Outbound.HttpSettings = class extends CommonClass {
|
||||
|
||||
Outbound.WireguardSettings = class extends CommonClass {
|
||||
constructor(
|
||||
mtu=1420, secretKey='',
|
||||
address=[''], workers=2, domainStrategy='', reserved='',
|
||||
mtu=1420, secretKey=Wireguard.generateKeypair().privateKey,
|
||||
address='', workers=2, domainStrategy='', reserved='',
|
||||
peers=[new Outbound.WireguardSettings.Peer()], kernelMode=false) {
|
||||
super();
|
||||
this.mtu = mtu;
|
||||
@@ -957,7 +957,7 @@ Outbound.WireguardSettings = class extends CommonClass {
|
||||
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,
|
||||
reserved: this.reserved ? this.reserved.split(",") : undefined,
|
||||
peers: Outbound.WireguardSettings.Peer.toJsonArray(this.peers),
|
||||
kernelMode: this.kernelMode,
|
||||
};
|
||||
|
||||
@@ -28,7 +28,6 @@ class AllSetting {
|
||||
this.subListen = "";
|
||||
this.subPort = "2096";
|
||||
this.subPath = "/sub/";
|
||||
this.subJsonPath = "/json/";
|
||||
this.subDomain = "";
|
||||
this.subCertFile = "";
|
||||
this.subKeyFile = "";
|
||||
@@ -36,8 +35,6 @@ class AllSetting {
|
||||
this.subEncrypt = true;
|
||||
this.subShowInfo = false;
|
||||
this.subURI = '';
|
||||
this.subJsonURI = '';
|
||||
this.subJsonFragment = '';
|
||||
|
||||
this.timeLocation = "Asia/Tehran";
|
||||
|
||||
|
||||
@@ -1146,6 +1146,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 +1534,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 +1557,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 +1580,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 +1589,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, 'same', remark);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -2295,7 +2269,7 @@ Inbound.WireguardSettings = class extends XrayCommonClass {
|
||||
}
|
||||
|
||||
addPeer() {
|
||||
this.peers.push(new Inbound.WireguardSettings.Peer(null,null,'',['10.0.0.' + (this.peers.length+2)]));
|
||||
this.peers.push(new Inbound.WireguardSettings.Peer());
|
||||
}
|
||||
|
||||
delPeer(index) {
|
||||
@@ -2323,24 +2297,16 @@ Inbound.WireguardSettings = class extends XrayCommonClass {
|
||||
};
|
||||
|
||||
Inbound.WireguardSettings.Peer = class extends XrayCommonClass {
|
||||
constructor(privateKey, publicKey, psk='', allowedIPs=['10.0.0.2/32'], keepAlive=0) {
|
||||
constructor(publicKey='', psk='', allowedIPs=['0.0.0.0/0','::/0'], 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,
|
||||
@@ -2349,11 +2315,7 @@ Inbound.WireguardSettings.Peer = class extends XrayCommonClass {
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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
|
||||
|
||||
@@ -10,7 +10,6 @@ type XraySettingController struct {
|
||||
XraySettingService service.XraySettingService
|
||||
SettingService service.SettingService
|
||||
InboundService service.InboundService
|
||||
OutboundService service.OutboundService
|
||||
XrayService service.XrayService
|
||||
}
|
||||
|
||||
@@ -28,8 +27,6 @@ func (a *XraySettingController) initRouter(g *gin.RouterGroup) {
|
||||
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) {
|
||||
@@ -81,27 +78,9 @@ func (a *XraySettingController) warp(c *gin.Context) {
|
||||
resp, err = a.XraySettingService.RegWarp(skey, pkey)
|
||||
case "license":
|
||||
license := c.PostForm("license")
|
||||
println(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)
|
||||
}
|
||||
|
||||
@@ -48,9 +48,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"`
|
||||
}
|
||||
|
||||
@@ -108,13 +105,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)
|
||||
|
||||
@@ -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}}
|
||||
@@ -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);
|
||||
|
||||
@@ -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}}
|
||||
@@ -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;
|
||||
@@ -71,10 +64,10 @@
|
||||
background-color: #0f2d32;
|
||||
}
|
||||
.dark #login {
|
||||
background-color: #101113;
|
||||
background-color: #151f31;
|
||||
}
|
||||
.dark h1 {
|
||||
color: rgba(255, 255, 255);
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
.ant-form-item {
|
||||
margin-bottom: 16px;
|
||||
@@ -199,7 +192,7 @@
|
||||
z-index: -1;
|
||||
}
|
||||
.dark .waves-header {
|
||||
background-color: #0a2227;
|
||||
background-color: #101828;
|
||||
}
|
||||
.waves-inner-header {
|
||||
height: 50vh;
|
||||
@@ -211,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;
|
||||
}
|
||||
@@ -219,27 +212,23 @@
|
||||
animation: move-forever 25s cubic-bezier(0.55, 0.5, 0.45, 0.5) infinite;
|
||||
}
|
||||
.dark .parallax > use {
|
||||
fill: #0f2d32;
|
||||
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);
|
||||
@@ -254,216 +243,89 @@
|
||||
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">
|
||||
<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-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.title" }}</b>
|
||||
<b>3X-UI</b>
|
||||
</span>
|
||||
</h1>
|
||||
<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="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>
|
||||
<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-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">
|
||||
[[ 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>
|
||||
<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>
|
||||
</a-col>
|
||||
<a-col>
|
||||
<theme-switch />
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</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-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: '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>
|
||||
<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>
|
||||
</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>
|
||||
</transition>
|
||||
</a-row>
|
||||
</a-layout-content>
|
||||
</transition>
|
||||
</a-layout>
|
||||
{{template "js" .}}
|
||||
{{template "component/themeSwitcher" .}}
|
||||
@@ -510,42 +372,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>
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
</template>
|
||||
<a-date-picker v-if="datepicker == 'gregorian'" :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" }}'
|
||||
<persian-datepicker v-else :dropdown-class-name="themeSwitcher.currentTheme"
|
||||
value="clientsBulkModal.expiryTime" v-model="clientsBulkModal.expiryTime"></persian-datepicker>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="clientsBulkModal.expiryTime != 0">
|
||||
@@ -220,7 +220,7 @@
|
||||
clientsBulkModal.visible = false;
|
||||
clientsBulkModal.loading(false);
|
||||
},
|
||||
loading(loading=true) {
|
||||
loading(loading) {
|
||||
clientsBulkModal.confirmLoading = loading;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
clientModal.visible = false;
|
||||
clientModal.loading(false);
|
||||
},
|
||||
loading(loading=true) {
|
||||
loading(loading) {
|
||||
clientModal.confirmLoading = loading;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
{{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>-->
|
||||
@@ -21,7 +21,7 @@
|
||||
<!--</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}}
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
<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">
|
||||
placeholder="انتخاب تاریخ">
|
||||
<template #addonAfter>
|
||||
<a-icon type="calendar" style="font-size: 14px; opacity: 0.5;"/>
|
||||
<a-icon type="calendar" style="font-size: 16px;"/>
|
||||
</template>
|
||||
</a-input>
|
||||
</div>
|
||||
@@ -21,7 +21,7 @@
|
||||
const persianDatepicker = {};
|
||||
|
||||
Vue.component('persian-datepicker', {
|
||||
props: ['placeholder', 'format', 'value'],
|
||||
props: ['dropdown-class-name', 'format', 'value'],
|
||||
template: `{{template "component/persianDatepickerTemplate"}}`,
|
||||
data() {
|
||||
return {
|
||||
@@ -48,7 +48,7 @@
|
||||
listenToDatepicker() {
|
||||
jalaliDatepicker.startWatch({
|
||||
time: true,
|
||||
zIndex: '9999',
|
||||
container: '.ant-modal-wrap',
|
||||
hideAfterChange: true,
|
||||
useDropDownYears: false,
|
||||
changeMonthRotateYear: true,
|
||||
@@ -57,4 +57,4 @@
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
{{end}}
|
||||
@@ -11,7 +11,6 @@
|
||||
function createThemeSwitcher() {
|
||||
const isDarkTheme = localStorage.getItem('dark-mode') === 'true';
|
||||
const theme = isDarkTheme ? 'dark' : 'light';
|
||||
document.querySelector('body').setAttribute('class', theme)
|
||||
return {
|
||||
isDarkTheme,
|
||||
get currentTheme() {
|
||||
@@ -20,8 +19,6 @@
|
||||
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;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -32,10 +29,6 @@
|
||||
props: [],
|
||||
template: `{{template "component/themeSwitchTemplate"}}`,
|
||||
data: () => ({ themeSwitcher }),
|
||||
mounted() {
|
||||
this.$message.config({getContainer: () => document.getElementById('message')});
|
||||
document.getElementById('message').className = themeSwitcher.currentTheme;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
@@ -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:6} }" :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}}
|
||||
@@ -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:6} }" :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}}
|
||||
@@ -152,7 +152,7 @@
|
||||
</template>
|
||||
<a-date-picker v-if="datepicker == 'gregorian'" :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" }}'
|
||||
<persian-datepicker v-else :dropdown-class-name="themeSwitcher.currentTheme"
|
||||
value="client._expiryTime" v-model="client._expiryTime"></persian-datepicker>
|
||||
<a-tag color="red" v-if="isEdit && isExpiry">Expired</a-tag>
|
||||
</a-form-item>
|
||||
|
||||
@@ -54,11 +54,11 @@
|
||||
<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 v-if="datepicker == 'gregorian'" :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>
|
||||
<persian-datepicker v-else :dropdown-class-name="themeSwitcher.currentTheme"
|
||||
value="dbInbound._expiryTime" v-model="dbInbound._expiryTime"></persian-datepicker>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
</template>
|
||||
|
||||
<!-- sniffing -->
|
||||
<template>
|
||||
<template v-if="inbound.canSniffing()">
|
||||
{{template "form/sniffing"}}
|
||||
</template>
|
||||
{{end}}
|
||||
{{end}}
|
||||
@@ -171,6 +171,7 @@
|
||||
<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()">
|
||||
<a-form-item label='Flow'>
|
||||
@@ -193,23 +194,18 @@
|
||||
<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-form-item>
|
||||
</template>
|
||||
<!-- shadowsocks -->
|
||||
<template v-if="outbound.protocol === Protocols.Shadowsocks">
|
||||
|
||||
<!-- 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,method_name) in SSMethods" :value="method">[[ method_name ]]</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 -->
|
||||
@@ -349,15 +345,13 @@
|
||||
<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 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"
|
||||
<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>
|
||||
@@ -369,12 +363,11 @@
|
||||
|
||||
<!-- reality settings -->
|
||||
<template v-if="outbound.stream.isReality">
|
||||
<a-form-item label="SNI">
|
||||
<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 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>
|
||||
|
||||
@@ -32,34 +32,10 @@
|
||||
<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-form-item label='{{ i18n "pages.xray.wireguard.publicKey" }}'>
|
||||
<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-form-item label='{{ i18n "pages.xray.wireguard.psk" }}'>
|
||||
<a-input v-model.trim="peer.psk"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{{define "form/sniffing"}}
|
||||
<a-divider style="margin:5px 0 0;"></a-divider>
|
||||
<a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
Sniffing
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
</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-button size="small" @click="inbound.stream.tcp.request.addHeader('host', '')">+</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">
|
||||
@@ -79,4 +79,4 @@
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
{{end}}
|
||||
@@ -7,7 +7,7 @@
|
||||
<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-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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -166,7 +166,7 @@
|
||||
<template v-if="app.subSettings.enable && infoModal.clientSettings.subId">
|
||||
<a-divider>Subscription URL</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>
|
||||
@@ -293,50 +283,24 @@
|
||||
</tr>
|
||||
<template v-for="(peer, index) in inbound.settings.peers">
|
||||
<tr>
|
||||
<td colspan="2"><a-divider>Peer [[ index + 1 ]]</a-divider></td>
|
||||
<td colspan="2"><a-tag>Peer [[ index + 1 ]]</a-tag></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">
|
||||
<tr>
|
||||
<td>{{ i18n "pages.xray.wireguard.psk" }}</td>
|
||||
<td>[[ peer.psk ]]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr class="client-table-odd-row">
|
||||
<td>{{ i18n "pages.xray.wireguard.allowedIPs" }}</td>
|
||||
<td>[[ peer.allowedIPs.join(",") ]]</td>
|
||||
</tr>
|
||||
<tr class="client-table-odd-row">
|
||||
<tr>
|
||||
<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>
|
||||
@@ -355,7 +319,7 @@
|
||||
index: null,
|
||||
isExpired: false,
|
||||
subLink: '',
|
||||
subJsonLink: '',
|
||||
tgLink: '',
|
||||
show(dbInbound, index) {
|
||||
this.index = index;
|
||||
this.inbound = dbInbound.toInbound();
|
||||
@@ -363,15 +327,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 +343,6 @@
|
||||
},
|
||||
genSubLink(subID) {
|
||||
return app.subSettings.subURI+subID;
|
||||
},
|
||||
genSubJsonLink(subID) {
|
||||
return app.subSettings.subJsonURI+subID;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
inModal.visible = false;
|
||||
inModal.loading(false);
|
||||
},
|
||||
loading(loading=true) {
|
||||
loading(loading) {
|
||||
inModal.confirmLoading = loading;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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" }}
|
||||
@@ -508,7 +496,7 @@
|
||||
scopedSlots: { customRender: 'expiryTime' },
|
||||
}];
|
||||
|
||||
const mobileColumns = [{
|
||||
const mobileColums = [{
|
||||
title: "ID",
|
||||
align: 'right',
|
||||
dataIndex: "id",
|
||||
@@ -571,13 +559,11 @@
|
||||
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 +578,6 @@
|
||||
this.refreshing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
await this.getOnlineUsers();
|
||||
this.setInbounds(msg.obj);
|
||||
setTimeout(() => {
|
||||
@@ -619,8 +604,7 @@
|
||||
this.tgBotEnable = tgBotEnable;
|
||||
this.subSettings = {
|
||||
enable : subEnable,
|
||||
subURI: subURI,
|
||||
subJsonURI: subJsonURI
|
||||
subURI: subURI
|
||||
};
|
||||
this.pageSize = pageSize;
|
||||
this.remarkModel = remarkModel;
|
||||
@@ -658,12 +642,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 +668,6 @@
|
||||
online: online,
|
||||
};
|
||||
},
|
||||
|
||||
searchInbounds(key) {
|
||||
if (ObjectUtil.isEmpty(key)) {
|
||||
this.searchedInbounds = this.dbInbounds.slice();
|
||||
@@ -752,9 +731,6 @@
|
||||
case "export":
|
||||
this.exportAllLinks();
|
||||
break;
|
||||
case "subs":
|
||||
this.exportAllSubs();
|
||||
break;
|
||||
case "resetInbounds":
|
||||
this.resetAllTraffic();
|
||||
break;
|
||||
@@ -786,9 +762,6 @@
|
||||
case "export":
|
||||
this.inboundLinks(dbInbound.id);
|
||||
break;
|
||||
case "subs":
|
||||
this.exportSubs(dbInbound.id);
|
||||
break;
|
||||
case "clipboard":
|
||||
this.copyToClipboard(dbInbound.id);
|
||||
break;
|
||||
@@ -838,7 +811,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);
|
||||
},
|
||||
@@ -887,7 +860,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);
|
||||
},
|
||||
@@ -906,7 +879,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);
|
||||
},
|
||||
@@ -994,7 +967,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"}}',
|
||||
@@ -1007,7 +980,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"}}',
|
||||
@@ -1213,22 +1186,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" }}',
|
||||
@@ -1241,23 +1198,6 @@
|
||||
},
|
||||
});
|
||||
},
|
||||
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) {
|
||||
@@ -1298,7 +1238,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());
|
||||
}
|
||||
@@ -1311,8 +1251,8 @@
|
||||
position: 'bottom',
|
||||
pageSize: this.pageSize,
|
||||
pageSizeOptions: sizeOptions
|
||||
};
|
||||
return p;
|
||||
}
|
||||
return p
|
||||
}
|
||||
return false
|
||||
},
|
||||
@@ -1326,9 +1266,6 @@
|
||||
}, 500)
|
||||
},
|
||||
mounted() {
|
||||
if (window.location.protocol !== "https:") {
|
||||
this.showAlert = true;
|
||||
}
|
||||
window.addEventListener('resize', this.onResize);
|
||||
this.onResize();
|
||||
this.loading();
|
||||
@@ -1366,6 +1303,7 @@
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
{{template "inboundModal"}}
|
||||
@@ -1375,5 +1313,6 @@
|
||||
{{template "inboundInfoModal"}}
|
||||
{{template "clientsModal"}}
|
||||
{{template "clientsBulkModal"}}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -18,16 +18,6 @@
|
||||
.ant-card-dark h2 {
|
||||
color: hsla(0, 0%, 100%, .65);
|
||||
}
|
||||
|
||||
.dark .ant-card-hoverable:hover,
|
||||
.dark .ant-space-item > .ant-tabs:hover {
|
||||
transform: scale(0.987);
|
||||
outline-color: #40434d;
|
||||
}
|
||||
|
||||
.dark .ant-card-bordered {
|
||||
outline: 2px solid var(--dark-color-background);
|
||||
}
|
||||
</style>
|
||||
|
||||
<body>
|
||||
@@ -36,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>
|
||||
@@ -55,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>
|
||||
@@ -75,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">
|
||||
@@ -83,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>
|
||||
@@ -94,25 +75,25 @@
|
||||
</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-UI <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="green">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.toUpperCase() ]]</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
|
||||
@@ -125,143 +106,137 @@
|
||||
</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>
|
||||
<a-icon type="global"></a-icon>
|
||||
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">
|
||||
<a-icon type="global"></a-icon>
|
||||
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>
|
||||
<a-icon type="swap"></a-icon>
|
||||
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>
|
||||
<a-icon type="swap"></a-icon>
|
||||
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>
|
||||
@@ -281,60 +256,60 @@
|
||||
></a-alert>
|
||||
<template v-for="version, index in versionModal.versions">
|
||||
<a-tag :color="index % 2 == 0 ? 'purple' : 'green'"
|
||||
style="margin-right: 10px" @click="switchV2rayVersion(version)">
|
||||
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
|
||||
@@ -457,15 +432,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 = '';
|
||||
@@ -543,7 +517,6 @@
|
||||
backupModal,
|
||||
spinning: false,
|
||||
loadingTip: '{{ i18n "loading"}}',
|
||||
showAlert: false,
|
||||
},
|
||||
methods: {
|
||||
loading(spinning, tip = '{{ i18n "loading"}}') {
|
||||
@@ -667,14 +640,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);
|
||||
}
|
||||
|
||||
@@ -75,43 +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="showAlert" style="margin-bottom: 10px"
|
||||
message='{{ i18n "secAlertTitle" }}'
|
||||
color="red"
|
||||
description='{{ i18n "secAlertSsl" }}'
|
||||
show-icon closable
|
||||
>
|
||||
</a-alert>
|
||||
<a-alert type="error" v-if="confAlerts.length>0" style="margin-bottom: 10px"
|
||||
message='{{ i18n "secAlertTitle" }}'
|
||||
color="red"
|
||||
show-icon closable
|
||||
>
|
||||
<template slot="description">
|
||||
{{ i18n "secAlertConf" }}
|
||||
<li v-for="a in confAlerts">- [[ a ]]</li>
|
||||
</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>
|
||||
@@ -179,6 +164,7 @@
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta title="Language" />
|
||||
</a-col>
|
||||
|
||||
<a-col :lg="24" :xl="12">
|
||||
<template>
|
||||
<a-select
|
||||
@@ -248,6 +234,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>
|
||||
@@ -257,19 +244,21 @@
|
||||
<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>
|
||||
<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>
|
||||
<span v-text="l.name"></span>
|
||||
@@ -296,17 +285,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>
|
||||
@@ -337,24 +315,6 @@
|
||||
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('') : [];
|
||||
@@ -483,60 +443,13 @@
|
||||
}
|
||||
},
|
||||
},
|
||||
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 (this.allSetting.port == 54321) alerts.push('{{ i18n "pages.settings.panelPort"}}');
|
||||
panelPath = window.location.pathname.split('/').length<4
|
||||
if (panelPath && this.allSetting.webBasePath == '/') alerts.push('{{ i18n "pages.settings.panelSettings"}} {{ i18n "pages.settings.panelUrlPath"}}');
|
||||
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 "pages.settings.subSettings"}} {{ i18n "pages.settings.subPath"}}');
|
||||
subJsonPath = this.allSetting.subJsonURI.length >0 ? new URL(this.allSetting.subJsonURI).pathname : this.allSetting.subJsonPath;
|
||||
if (subJsonPath == '/json/') alerts.push('JSON {{ i18n "pages.settings.subPath"}}');
|
||||
}
|
||||
return alerts
|
||||
}
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
if (window.location.protocol !== "https:") {
|
||||
this.showAlert = true;
|
||||
}
|
||||
await this.getAllSetting();
|
||||
while (true) {
|
||||
await PromiseUtil.sleep(1000);
|
||||
await PromiseUtil.sleep(600);
|
||||
this.saveBtnDisable = this.oldAllSetting.equals(this.allSetting);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -108,7 +108,7 @@
|
||||
this.visible = false;
|
||||
this.loading(false);
|
||||
},
|
||||
loading(loading=true) {
|
||||
loading(loading) {
|
||||
this.confirmLoading = loading;
|
||||
},
|
||||
async getData(){
|
||||
@@ -140,7 +140,6 @@
|
||||
mtu: 1420,
|
||||
secretKey: warpModal.warpData.private_key,
|
||||
address: Object.values(config.interface.addresses),
|
||||
domainStrategy: 'ForceIP',
|
||||
peers: [{
|
||||
publicKey: peer.public_key,
|
||||
endpoint: peer.endpoint.host,
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
{{template "head" .}}
|
||||
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/codemirror.css?{{ .cur_ver }}"></script>
|
||||
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/codemirror.css">
|
||||
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/fold/foldgutter.css">
|
||||
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/xq.css?{{ .cur_ver }}">
|
||||
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/lint/lint.css">
|
||||
|
||||
<script src="{{ .base_path }}assets/base64/base64.min.js"></script>
|
||||
<script src="{{ .base_path }}assets/js/model/outbound.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/codemirror/codemirror.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/codemirror/codemirror.js"></script>
|
||||
<script src="{{ .base_path }}assets/codemirror/javascript.js"></script>
|
||||
<script src="{{ .base_path }}assets/codemirror/jshint.js"></script>
|
||||
<script src="{{ .base_path }}assets/codemirror/jsonlint.js"></script>
|
||||
@@ -63,15 +63,6 @@
|
||||
<a-layout id="content-layout">
|
||||
<a-layout-content>
|
||||
<a-spin :spinning="spinning" :delay="500" tip='{{ i18n "loading"}}'>
|
||||
<transition name="list" appear>
|
||||
<a-alert type="error" v-if="showAlert" style="margin-bottom: 10px"
|
||||
message='{{ i18n "secAlertTitle" }}'
|
||||
color="red"
|
||||
description='{{ i18n "secAlertSsl" }}'
|
||||
show-icon closable
|
||||
>
|
||||
</a-alert>
|
||||
</transition>
|
||||
<a-space direction="vertical">
|
||||
<a-card hoverable style="margin-bottom: .5rem;">
|
||||
<a-row>
|
||||
@@ -108,7 +99,7 @@
|
||||
:class="themeSwitcher.currentTheme">
|
||||
<a-tab-pane key="tpl-1" tab='{{ i18n "pages.xray.basicTemplate"}}'>
|
||||
<a-space direction="horizontal" style="padding: 20px 20px">
|
||||
<a-button type="danger" @click="resetXrayConfigToDefault">{{ i18n "pages.settings.resetDefaultConfig" }}</a-button>
|
||||
<a-button type="primary" @click="resetXrayConfigToDefault">{{ i18n "pages.settings.resetDefaultConfig" }}</a-button>
|
||||
</a-space>
|
||||
<a-collapse>
|
||||
<a-collapse-panel header='{{ i18n "pages.xray.generalConfigs"}}'>
|
||||
@@ -123,12 +114,15 @@
|
||||
<a-list-item>
|
||||
<a-row style="padding: 20px">
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta title='{{ i18n "pages.xray.FreedomStrategy" }}'
|
||||
description='{{ i18n "pages.xray.FreedomStrategyDesc" }}' />
|
||||
<a-list-item-meta
|
||||
title='{{ i18n "pages.xray.FreedomStrategy" }}'
|
||||
description='{{ i18n "pages.xray.FreedomStrategyDesc" }}'/>
|
||||
</a-col>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<template>
|
||||
<a-select v-model="freedomStrategy" :dropdown-class-name="themeSwitcher.currentTheme"
|
||||
<a-select
|
||||
v-model="freedomStrategy"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||
style="width: 100%">
|
||||
<a-select-option v-for="s in OutboundDomainStrategies" :value="s">[[ s ]]</a-select-option>
|
||||
</a-select>
|
||||
@@ -138,67 +132,22 @@
|
||||
</a-list-item>
|
||||
<a-row style="padding: 20px">
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta title='{{ i18n "pages.xray.RoutingStrategy" }}'
|
||||
description='{{ i18n "pages.xray.RoutingStrategyDesc" }}' />
|
||||
<a-list-item-meta
|
||||
title='{{ i18n "pages.xray.RoutingStrategy" }}'
|
||||
description='{{ i18n "pages.xray.RoutingStrategyDesc" }}'/>
|
||||
</a-col>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-select v-model="routingStrategy" :dropdown-class-name="themeSwitcher.currentTheme"
|
||||
<template>
|
||||
<a-select
|
||||
v-model="routingStrategy"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||
style="width: 100%">
|
||||
<a-select-option v-for="s in routingDomainStrategies" :value="s">[[ s ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-list-item>
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel header='{{ i18n "pages.xray.logConfigs" }}'>
|
||||
<a-row :xs="24" :sm="24" :lg="12">
|
||||
<a-alert type="warning" style="text-align: center;">
|
||||
<template slot="message">
|
||||
<a-icon type="exclamation-circle" theme="filled" style="color: #FFA031"></a-icon>
|
||||
{{ i18n "pages.xray.logConfigsDesc" }}
|
||||
</template>
|
||||
</a-alert>
|
||||
</a-row>
|
||||
<a-row style="padding: 20px">
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta title='{{ i18n "pages.xray.logLevel" }}'
|
||||
description='{{ i18n "pages.xray.logLevelDesc" }}' />
|
||||
</a-col>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<template>
|
||||
<a-select v-model="setLogLevel" :dropdown-class-name="themeSwitcher.currentTheme" style="width: 100%">
|
||||
<a-select-option v-for="s in logLevel" :value="s">[[ s ]]</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row style="padding: 20px">
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta title='{{ i18n "pages.xray.accessLog" }}'
|
||||
description='{{ i18n "pages.xray.accessLogDesc" }}' />
|
||||
</a-col>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<template>
|
||||
<a-select v-model="accessLog" :dropdown-class-name="themeSwitcher.currentTheme" style="width: 100%">
|
||||
<a-select-option v-for="s in access" :value="s">[[ s ]]</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row style="padding: 20px">
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta title='{{ i18n "pages.xray.errorLog" }}'
|
||||
description='{{ i18n "pages.xray.errorLogDesc" }}' />
|
||||
</a-col>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<template>
|
||||
<a-select v-model="errorLog" :dropdown-class-name="themeSwitcher.currentTheme" style="width: 100%">
|
||||
<a-select-option v-for="s in error" :value="s">[[ s ]]</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-list-item>
|
||||
</a-list-item>
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel header='{{ i18n "pages.xray.blockConfigs"}}'>
|
||||
<a-row :xs="24" :sm="24" :lg="12">
|
||||
@@ -278,11 +227,8 @@
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.xray.OpenAIWARP"}}' desc='{{ i18n "pages.xray.OpenAIWARPDesc"}}' v-model="OpenAIWARPSettings"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.xray.NetflixWARP"}}' desc='{{ i18n "pages.xray.NetflixWARPDesc"}}' v-model="NetflixWARPSettings"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.xray.SpotifyWARP"}}' desc='{{ i18n "pages.xray.SpotifyWARPDesc"}}' v-model="SpotifyWARPSettings"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.xray.MetaWARP"}}' desc='{{ i18n "pages.xray.MetaWARPDesc"}}' v-model="MetaWARPSettings"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.xray.AppleWARP"}}' desc='{{ i18n "pages.xray.AppleWARPDesc"}}' v-model="AppleWARPSettings"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.xray.RedditWARP"}}' desc='{{ i18n "pages.xray.RedditWARPDesc"}}' v-model="RedditWARPSettings"></setting-list-item>
|
||||
</template>
|
||||
<a-button v-else type="primary" icon="cloud" style="margin: 15px 20px;" @click="showWarp()">WARP</a-button>
|
||||
<a-button v-else style="margin: 10px 0;" @click="showWarp">WARP {{ i18n "pages.xray.rules.outbound" }}</a-button>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</a-tab-pane>
|
||||
@@ -347,14 +293,6 @@
|
||||
[[ rule.outboundTag ]]
|
||||
</a-popover>
|
||||
</template>
|
||||
<template slot="balancer" slot-scope="text, rule, index">
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<p v-if="rule.balancerTag">Balancer Tag: [[ rule.balancerTag ]]</p>
|
||||
</template>
|
||||
[[ rule.balancerTag ]]
|
||||
</a-popover>
|
||||
</template>
|
||||
<template slot="info" slot-scope="text, rule, index">
|
||||
<a-popover placement="bottomRight"
|
||||
v-if="(rule.source+rule.sourcePort+rule.network+rule.protocol+rule.attrs+rule.ip+rule.domain+rule.port).length>0"
|
||||
@@ -393,10 +331,6 @@
|
||||
<td>Port</td>
|
||||
<td><a-tag color="green" v-for="r in rule.port.split(',')">[[ r ]]</a-tag></td>
|
||||
</tr>
|
||||
<tr v-if="rule.balancerTag">
|
||||
<td>Balancer Tag</td>
|
||||
<td><a-tag color="blue">[[ rule.balancerTag ]]</a-tag></td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
<a-button shape="round" size="small" style="font-size: 14px; padding: 0 10px;">
|
||||
@@ -407,17 +341,8 @@
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="tpl-3" tab='{{ i18n "pages.xray.Outbounds"}}' style="padding-top: 20px;" force-render="true">
|
||||
<a-row>
|
||||
<a-col :xs="12" :sm="12" :lg="12">
|
||||
<a-button type="primary" icon="plus" @click="addOutbound()" style="margin-bottom: 10px;">{{ i18n
|
||||
"pages.xray.outbound.addOutbound" }}</a-button>
|
||||
<a-button type="primary" icon="cloud" @click="showWarp()" style="margin-bottom: 10px;">WARP</a-button>
|
||||
</a-col>
|
||||
<a-col :xs="12" :sm="12" :lg="12" style="text-align: right;">
|
||||
<a-icon type="sync" :spin="refreshing" @click="refreshOutboundTraffic()" style="margin: 0 5px;"></a-icon>
|
||||
<a-icon type="retweet" @click="resetOutboundTraffic(-1)"></a-icon>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-button type="primary" icon="plus" @click="addOutbound()" style="margin-bottom: 10px;">{{ i18n "pages.xray.outbound.addOutbound" }}</a-button>
|
||||
<a-button type="primary" @click="showWarp()" style="margin-bottom: 10px;">WARP</a-button>
|
||||
<a-table :columns="outboundColumns" bordered
|
||||
:row-key="r => r.key"
|
||||
:data-source="outboundData"
|
||||
@@ -430,19 +355,10 @@
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-icon @click="e => e.preventDefault()" type="more" style="font-size: 16px; text-decoration: bold;"></a-icon>
|
||||
<a-menu slot="overlay" :theme="themeSwitcher.currentTheme">
|
||||
<a-menu-item v-if="index>0" @click="setFirstOutbound(index)">
|
||||
<a-icon type="vertical-align-top"></a-icon>
|
||||
{{ i18n "pages.xray.rules.first"}}
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="editOutbound(index)">
|
||||
<a-icon type="edit"></a-icon>
|
||||
{{ i18n "edit" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="resetOutboundTraffic(index)">
|
||||
<span>
|
||||
<a-icon type="retweet"></a-icon> {{ i18n "pages.inbounds.resetTraffic"}}
|
||||
</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="deleteOutbound(index)">
|
||||
<span style="color: #FF4D4F">
|
||||
<a-icon type="delete"></a-icon> {{ i18n "delete"}}
|
||||
@@ -462,14 +378,11 @@
|
||||
<a-tag style="margin:0;" v-if="outbound.streamSettings.security=='reality'" color="green">reality</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
<template slot="traffic" slot-scope="text, outbound, index">
|
||||
<a-tag color="green">[[ findOutboundTraffic(outbound) ]]</a-tag>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="tpl-4" tab='{{ i18n "pages.xray.outbound.reverse"}}' style="padding-top: 20px;" force-render="true">
|
||||
<a-button type="primary" icon="plus" @click="addReverse()" style="margin-bottom: 10px;">{{ i18n "pages.xray.outbound.addReverse" }}</a-button>
|
||||
<a-table :columns="reverseColumns" bordered v-if="reverseData.length>0"
|
||||
<a-table :columns="reverseColumns" bordered
|
||||
:row-key="r => r.key"
|
||||
:data-source="reverseData"
|
||||
:scroll="isMobile ? {} : { x: 200 }"
|
||||
@@ -495,123 +408,6 @@
|
||||
</template>
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="tpl-5" tab='{{ i18n "pages.xray.Balancers"}}' style="padding-top: 20px;" force-render="true">
|
||||
<a-alert type="warning" style="margin-bottom: 10px; width: fit-content"
|
||||
message='{{ i18n "pages.xray.balancer.balancerDesc" }}' show-icon></a-alert>
|
||||
<a-button type="primary" icon="plus" @click="addBalancer()" style="margin-bottom: 10px;">{{ i18n "pages.xray.balancer.addBalancer"}}</a-button>
|
||||
<a-table :columns="balancerColumns" bordered v-if="balancersData.length>0"
|
||||
:row-key="r => r.key"
|
||||
:data-source="balancersData"
|
||||
:scroll="isMobile ? {} : { x: 200 }"
|
||||
:pagination="false"
|
||||
:indent-size="0"
|
||||
:style="isMobile ? 'padding: 5px 0' : 'margin-left: 1px;'">
|
||||
<template slot="action" slot-scope="text, balancer, index">
|
||||
[[ index+1 ]]
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-icon @click="e => e.preventDefault()" type="more" style="font-size: 16px; text-decoration: bold;"></a-icon>
|
||||
<a-menu slot="overlay" :theme="themeSwitcher.currentTheme">
|
||||
<a-menu-item @click="editBalancer(index)">
|
||||
<a-icon type="edit"></a-icon>
|
||||
{{ i18n "edit" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="deleteBalancer(index)">
|
||||
<span style="color: #FF4D4F">
|
||||
<a-icon type="delete"></a-icon> {{ i18n "delete"}}
|
||||
</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
<template slot="strategy" slot-scope="text, balancer, index">
|
||||
<a-tag style="margin:0;" v-if="balancer.strategy=='random'" color="purple">Random</a-tag>
|
||||
<a-tag style="margin:0;" v-if="balancer.strategy=='roundRobin'" color="green">Round Robin</a-tag>
|
||||
</template>
|
||||
<template slot="selector" slot-scope="text, balancer, index">
|
||||
<a-tag class="info-large-tag" style="margin:1;" v-for="sel in balancer.selector">[[ sel ]]</a-tag>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="tpl-6" tab='DNS' style="padding-top: 20px;" force-render="true">
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.xray.dns.enable" }}' desc='{{ i18n "pages.xray.dns.enableDesc" }}' v-model="enableDNS"></setting-list-item>
|
||||
<template v-if="enableDNS">
|
||||
<a-list-item>
|
||||
<a-row style="padding: 20px">
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta title='{{ i18n "pages.xray.dns.strategy" }}' description='{{ i18n "pages.xray.dns.strategyDesc" }}' />
|
||||
</a-col>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-select
|
||||
v-model="dnsStrategy"
|
||||
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-col>
|
||||
</a-row>
|
||||
</a-list-item>
|
||||
<a-divider>DNS</a-divider>
|
||||
<a-button type="primary" icon="plus" @click="addDNSServer()" style="margin-bottom: 10px;">{{ i18n "pages.xray.dns.add" }}</a-button>
|
||||
<a-table :columns="dnsColumns" bordered v-if="dnsServers.length>0"
|
||||
:row-key="r => r.key"
|
||||
:data-source="dnsServers"
|
||||
:scroll="isMobile ? {} : { x: 200 }"
|
||||
:pagination="false"
|
||||
:indent-size="0"
|
||||
:style="isMobile ? 'padding: 5px 0' : 'margin-left: 1px;'">
|
||||
<template slot="action" slot-scope="text,dns,index">
|
||||
[[ index+1 ]]
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-icon @click="e => e.preventDefault()" type="more" style="font-size: 16px; text-decoration: bold;"></a-icon>
|
||||
<a-menu slot="overlay" :theme="themeSwitcher.currentTheme">
|
||||
<a-menu-item @click="editDNSServer(index)">
|
||||
<a-icon type="edit"></a-icon>
|
||||
{{ i18n "edit" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="deleteDNSServer(index)">
|
||||
<span style="color: #FF4D4F">
|
||||
<a-icon type="delete"></a-icon> {{ i18n "delete"}}
|
||||
</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
<template slot="address" slot-scope="dns,index">
|
||||
<span v-if="typeof dns == 'object'">[[ dns.address ]]</span>
|
||||
<span v-else>[[ dns ]]</span>
|
||||
</template>
|
||||
<template slot="domain" slot-scope="dns,index">
|
||||
<span v-if="typeof dns == 'object'">[[ dns.domains.join(",") ]]</span>
|
||||
</template>
|
||||
</a-table>
|
||||
<a-divider>Fake DNS</a-divider>
|
||||
<a-button type="primary" icon="plus" @click="addFakedns()" style="margin-bottom: 10px;">{{ i18n "pages.xray.fakedns.add" }}</a-button>
|
||||
<a-table :columns="fakednsColumns" bordered v-if="fakeDns && fakeDns.length>0" :row-key="r => r.key"
|
||||
:data-source="fakeDns" :scroll="isMobile ? {} : { x: 200 }" :pagination="false" :indent-size="0"
|
||||
:style="isMobile ? 'padding: 5px 0' : 'margin-left: 1px;'">
|
||||
<template slot="action" slot-scope="text,fakedns,index">
|
||||
[[ index+1 ]]
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-icon @click="e => e.preventDefault()" type="more"
|
||||
style="font-size: 16px; text-decoration: bold;"></a-icon>
|
||||
<a-menu slot="overlay" :theme="themeSwitcher.currentTheme">
|
||||
<a-menu-item @click="editFakedns(index)">
|
||||
<a-icon type="edit"></a-icon>
|
||||
{{ i18n "edit" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="deleteFakedns(index)">
|
||||
<span style="color: #FF4D4F">
|
||||
<a-icon type="delete"></a-icon> {{ i18n "delete"}}
|
||||
</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
</a-table>
|
||||
</template>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="tpl-advanced" tab='{{ i18n "pages.xray.advancedTemplate"}}' style="padding-top: 20px;" force-render="true">
|
||||
<a-list-item-meta title='{{ i18n "pages.xray.Template"}}' description='{{ i18n "pages.xray.TemplateDesc"}}'></a-list-item-meta>
|
||||
<a-radio-group v-model="advSettings" @change="changeCode" button-style="solid" style="margin: 10px 0;" :size="isMobile ? 'small' : ''">
|
||||
@@ -634,9 +430,6 @@
|
||||
{{template "ruleModal"}}
|
||||
{{template "outModal"}}
|
||||
{{template "reverseModal"}}
|
||||
{{template "balancerModal"}}
|
||||
{{template "dnsModal"}}
|
||||
{{template "fakednsModal"}}
|
||||
{{template "warpModal"}}
|
||||
<script>
|
||||
const rulesColumns = [
|
||||
@@ -653,10 +446,9 @@
|
||||
{ title: 'Domain', dataIndex: 'domain', align: 'center', width: 20, ellipsis: true },
|
||||
{ title: 'Port', dataIndex: 'port', align: 'center', width: 10, ellipsis: true }]},
|
||||
{ title: '{{ i18n "pages.xray.rules.inbound"}}', children: [
|
||||
{ title: 'Inbound Tag', dataIndex: 'inboundTag', align: 'center', width: 15, ellipsis: true },
|
||||
{ title: 'Inbound Tag', dataIndex: 'inboundTag', align: 'center', width: 20, ellipsis: true },
|
||||
{ title: 'Client Email', dataIndex: 'user', align: 'center', width: 20, ellipsis: true }]},
|
||||
{ title: '{{ i18n "pages.xray.rules.outbound"}}', dataIndex: 'outboundTag', align: 'center', width: 15 },
|
||||
{ title: '{{ i18n "pages.xray.rules.balancer"}}', dataIndex: 'balancerTag', align: 'center', width: 15 },
|
||||
{ title: '{{ i18n "pages.xray.rules.outbound"}}', dataIndex: 'outboundTag', align: 'center', width: 20 },
|
||||
];
|
||||
|
||||
const rulesMobileColumns = [
|
||||
@@ -671,7 +463,6 @@
|
||||
{ title: '{{ i18n "pages.xray.outbound.tag"}}', dataIndex: 'tag', align: 'center', width: 50 },
|
||||
{ title: '{{ i18n "protocol"}}', align: 'center', width: 50, scopedSlots: { customRender: 'protocol' } },
|
||||
{ title: '{{ i18n "pages.xray.outbound.address"}}', align: 'center', width: 50, scopedSlots: { customRender: 'address' } },
|
||||
{ title: '{{ i18n "pages.inbounds.traffic" }}', align: 'center', width: 50, scopedSlots: { customRender: 'traffic' } },
|
||||
];
|
||||
|
||||
const reverseColumns = [
|
||||
@@ -681,25 +472,6 @@
|
||||
{ title: '{{ i18n "pages.xray.outbound.domain"}}', dataIndex: 'domain', align: 'center', width: 50 },
|
||||
];
|
||||
|
||||
const dnsColumns = [
|
||||
{ title: "#", align: 'center', width: 20, scopedSlots: { customRender: 'action' } },
|
||||
{ title: '{{ i18n "pages.xray.outbound.address"}}', align: 'center', width: 50, scopedSlots: { customRender: 'address' } },
|
||||
{ title: '{{ i18n "pages.xray.dns.domains"}}', align: 'center', width: 50, scopedSlots: { customRender: 'domain' } },
|
||||
];
|
||||
|
||||
const fakednsColumns = [
|
||||
{ title: "#", align: 'center', width: 20, scopedSlots: { customRender: 'action' } },
|
||||
{ title: '{{ i18n "pages.xray.fakedns.ipPool"}}', dataIndex: 'ipPool', align: 'center', width: 50 },
|
||||
{ title: '{{ i18n "pages.xray.fakedns.poolSize"}}', dataIndex: 'poolSize', align: 'center', width: 50 },
|
||||
];
|
||||
|
||||
const balancerColumns = [
|
||||
{ title: "#", align: 'center', width: 20, scopedSlots: { customRender: 'action' } },
|
||||
{ title: '{{ i18n "pages.xray.balancer.tag"}}', dataIndex: 'tag', align: 'center', width: 50 },
|
||||
{ title: '{{ i18n "pages.xray.balancer.balancerStrategy"}}', align: 'center', width: 50, scopedSlots: { customRender: 'strategy' }},
|
||||
{ title: '{{ i18n "pages.xray.balancer.balancerSelectors"}}', align: 'center', width: 100, scopedSlots: { customRender: 'selector' }},
|
||||
];
|
||||
|
||||
const app = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#app',
|
||||
@@ -711,11 +483,8 @@
|
||||
oldXraySetting: '',
|
||||
xraySetting: '',
|
||||
inboundTags: [],
|
||||
outboundsTraffic: [],
|
||||
saveBtnDisable: true,
|
||||
refreshing: false,
|
||||
restartResult: '',
|
||||
showAlert: false,
|
||||
isMobile: window.innerWidth <= 768,
|
||||
advSettings: 'xraySetting',
|
||||
cm: null,
|
||||
@@ -752,9 +521,6 @@
|
||||
protocol: "freedom"
|
||||
},
|
||||
routingDomainStrategies: ["AsIs", "IPIfNonMatch", "IPOnDemand"],
|
||||
logLevel: ["none" , "debug" , "info" , "warning", "error"],
|
||||
access: ["none" , "./access.log" ],
|
||||
error: ["none" , "./error.log" ],
|
||||
settingsData: {
|
||||
protocols: {
|
||||
bittorrent: ["bittorrent"],
|
||||
@@ -781,9 +547,6 @@
|
||||
google: ["geosite:google"],
|
||||
spotify: ["geosite:spotify"],
|
||||
netflix: ["geosite:netflix"],
|
||||
meta: ["geosite:meta"],
|
||||
apple: ["geosite:apple"],
|
||||
reddit: ["geosite:reddit"],
|
||||
cn: [
|
||||
"geosite:cn",
|
||||
"regexp:.*\\.cn$"
|
||||
@@ -806,11 +569,9 @@
|
||||
familyProtectDNS: {
|
||||
"servers": [
|
||||
"1.1.1.3", // https://developers.cloudflare.com/1.1.1.1/setup/
|
||||
"1.0.0.3",
|
||||
"2606:4700:4700::1113",
|
||||
"2606:4700:4700::1003"
|
||||
"1.0.0.3"
|
||||
],
|
||||
"queryStrategy": "UseIP"
|
||||
"queryStrategy": "UseIPv4"
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -818,12 +579,6 @@
|
||||
loading(spinning = true) {
|
||||
this.spinning = spinning;
|
||||
},
|
||||
async getOutboundsTraffic() {
|
||||
const msg = await HttpUtil.get("/panel/xray/getOutboundsTraffic");
|
||||
if (msg.success) {
|
||||
this.outboundsTraffic = msg.obj;
|
||||
}
|
||||
},
|
||||
async getXraySetting() {
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.post("/panel/xray/");
|
||||
@@ -1002,14 +757,6 @@
|
||||
}
|
||||
return true;
|
||||
},
|
||||
findOutboundTraffic(o) {
|
||||
for (const otraffic of this.outboundsTraffic) {
|
||||
if (otraffic.tag == o.tag) {
|
||||
return sizeFormat(otraffic.up) + ' / ' + sizeFormat(otraffic.down);
|
||||
}
|
||||
}
|
||||
return sizeFormat(0) + ' / ' + sizeFormat(0);
|
||||
},
|
||||
findOutboundAddress(o) {
|
||||
serverObj = null;
|
||||
switch(o.protocol){
|
||||
@@ -1067,126 +814,6 @@
|
||||
outbounds.splice(index,1);
|
||||
this.outboundSettings = JSON.stringify(outbounds);
|
||||
},
|
||||
setFirstOutbound(index){
|
||||
outbounds = this.templateSettings.outbounds;
|
||||
outbounds.splice(0, 0, outbounds.splice(index, 1)[0]);
|
||||
this.outboundSettings = JSON.stringify(outbounds);
|
||||
},
|
||||
async refreshOutboundTraffic() {
|
||||
if (!this.refreshing) {
|
||||
this.refreshing = true;
|
||||
await this.getOutboundsTraffic();
|
||||
|
||||
data = []
|
||||
if (this.templateSettings != null) {
|
||||
this.templateSettings.outbounds.forEach((o, index) => {
|
||||
data.push({'key': index, ...o});
|
||||
});
|
||||
}
|
||||
|
||||
this.outboundData = data;
|
||||
this.refreshing = false;
|
||||
}
|
||||
},
|
||||
async resetOutboundTraffic(index) {
|
||||
let tag = "-alltags-";
|
||||
if (index >= 0) {
|
||||
tag = this.outboundData[index].tag ? this.outboundData[index].tag : ""
|
||||
}
|
||||
const msg = await HttpUtil.post("/panel/xray/resetOutboundsTraffic", { tag: tag });
|
||||
if (msg.success) {
|
||||
await this.refreshOutboundTraffic();
|
||||
}
|
||||
},
|
||||
addBalancer() {
|
||||
balancerModal.show({
|
||||
title: '{{ i18n "pages.xray.balancer.addBalancer"}}',
|
||||
okText: '{{ i18n "pages.xray.balancer.addBalancer"}}',
|
||||
balancerTags: this.balancersData.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag),
|
||||
balancer: {
|
||||
tag: '',
|
||||
strategy: 'random',
|
||||
selector: []
|
||||
},
|
||||
confirm: (balancer) => {
|
||||
balancerModal.loading();
|
||||
newTemplateSettings = this.templateSettings;
|
||||
if (newTemplateSettings.routing.balancers == undefined) {
|
||||
newTemplateSettings.routing.balancers = [];
|
||||
}
|
||||
let tmpBalancer = {
|
||||
'tag': balancer.tag,
|
||||
'selector': balancer.selector
|
||||
};
|
||||
if (balancer.strategy == 'roundRobin') {
|
||||
tmpBalancer.strategy = {
|
||||
'type': balancer.strategy
|
||||
};
|
||||
}
|
||||
newTemplateSettings.routing.balancers.push(tmpBalancer);
|
||||
this.templateSettings = newTemplateSettings;
|
||||
balancerModal.close();
|
||||
},
|
||||
isEdit: false
|
||||
});
|
||||
},
|
||||
editBalancer(index) {
|
||||
const oldTag = this.balancersData[index].tag;
|
||||
balancerModal.show({
|
||||
title: '{{ i18n "pages.xray.balancer.editBalancer"}}',
|
||||
okText: '{{ i18n "sure" }}',
|
||||
balancerTags: this.balancersData.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag),
|
||||
balancer: this.balancersData[index],
|
||||
confirm: (balancer) => {
|
||||
balancerModal.loading();
|
||||
newTemplateSettings = this.templateSettings;
|
||||
|
||||
let tmpBalancer = {
|
||||
'tag': balancer.tag,
|
||||
'selector': balancer.selector
|
||||
};
|
||||
if (balancer.strategy == 'roundRobin') {
|
||||
tmpBalancer.strategy = {
|
||||
'type': balancer.strategy
|
||||
};
|
||||
}
|
||||
|
||||
newTemplateSettings.routing.balancers[index] = tmpBalancer;
|
||||
// change edited tag if used in rule section
|
||||
if (oldTag != balancer.tag) {
|
||||
newTemplateSettings.routing.rules.forEach((rule) => {
|
||||
if (rule.balancerTag && rule.balancerTag == oldTag) {
|
||||
rule.balancerTag = balancer.tag;
|
||||
}
|
||||
});
|
||||
}
|
||||
this.templateSettings = newTemplateSettings;
|
||||
balancerModal.close();
|
||||
},
|
||||
isEdit: true
|
||||
});
|
||||
},
|
||||
deleteBalancer(index) {
|
||||
newTemplateSettings = this.templateSettings;
|
||||
|
||||
//remove from balancers
|
||||
const oldTag = this.balancersData[index].tag;
|
||||
this.balancersData.splice(index, 1);
|
||||
|
||||
// remove from settings
|
||||
let realIndex = newTemplateSettings.routing.balancers.findIndex((b) => b.tag == oldTag);
|
||||
newTemplateSettings.routing.balancers.splice(realIndex, 1);
|
||||
|
||||
// remove related routing rules
|
||||
let rules = [];
|
||||
newTemplateSettings.routing.rules.forEach((r) => {
|
||||
if (!r.balancerTag || r.balancerTag != oldTag) {
|
||||
rules.push(r);
|
||||
}
|
||||
});
|
||||
newTemplateSettings.routing.rules = rules;
|
||||
this.templateSettings = newTemplateSettings;
|
||||
},
|
||||
addReverse(){
|
||||
reverseModal.show({
|
||||
title: '{{ i18n "pages.xray.outbound.addReverse"}}',
|
||||
@@ -1272,66 +899,6 @@
|
||||
|
||||
this.templateSettings = newTemplateSettings;
|
||||
},
|
||||
addDNSServer(){
|
||||
dnsModal.show({
|
||||
title: '{{ i18n "pages.xray.dns.add" }}',
|
||||
confirm: (dnsServer) => {
|
||||
dnsServers = this.dnsServers;
|
||||
dnsServers.push(dnsServer);
|
||||
this.dnsServers = dnsServers;
|
||||
dnsModal.close();
|
||||
},
|
||||
isEdit: false
|
||||
});
|
||||
},
|
||||
editDNSServer(index){
|
||||
dnsModal.show({
|
||||
title: '{{ i18n "pages.xray.dns.edit" }} #' + (index+1),
|
||||
dnsServer: this.dnsServers[index],
|
||||
confirm: (dnsServer) => {
|
||||
dnsServers = this.dnsServers;
|
||||
dnsServers[index] = dnsServer;
|
||||
this.dnsServers = dnsServers;
|
||||
dnsModal.close();
|
||||
},
|
||||
isEdit: true
|
||||
});
|
||||
},
|
||||
deleteDNSServer(index){
|
||||
newDnsServers = this.dnsServers;
|
||||
newDnsServers.splice(index,1);
|
||||
this.dnsServers = newDnsServers;
|
||||
},
|
||||
addFakedns() {
|
||||
fakednsModal.show({
|
||||
title: '{{ i18n "pages.xray.fakedns.add" }}',
|
||||
confirm: (item) => {
|
||||
fakeDns = this.fakeDns?? [];
|
||||
fakeDns.push(item);
|
||||
this.fakeDns = fakeDns;
|
||||
fakednsModal.close();
|
||||
},
|
||||
isEdit: false
|
||||
});
|
||||
},
|
||||
editFakedns(index){
|
||||
fakednsModal.show({
|
||||
title: '{{ i18n "pages.xray.fakedns.edit" }} #' + (index+1),
|
||||
fakeDns: this.fakeDns[index],
|
||||
confirm: (item) => {
|
||||
fakeDns = this.fakeDns;
|
||||
fakeDns[index] = item;
|
||||
this.fakeDns = fakeDns;
|
||||
fakednsModal.close();
|
||||
},
|
||||
isEdit: true
|
||||
});
|
||||
},
|
||||
deleteFakedns(index){
|
||||
fakeDns = this.fakeDns;
|
||||
fakeDns.splice(index,1);
|
||||
this.fakeDns = fakeDns;
|
||||
},
|
||||
addRule(){
|
||||
ruleModal.show({
|
||||
title: '{{ i18n "pages.xray.rules.add"}}',
|
||||
@@ -1378,12 +945,8 @@
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
if (window.location.protocol !== "https:") {
|
||||
this.showAlert = true;
|
||||
}
|
||||
await this.getXraySetting();
|
||||
await this.getXrayResult();
|
||||
await this.getOutboundsTraffic();
|
||||
while (true) {
|
||||
await PromiseUtil.sleep(800);
|
||||
this.saveBtnDisable = this.oldXraySetting === this.xraySetting;
|
||||
@@ -1439,27 +1002,6 @@
|
||||
return data;
|
||||
},
|
||||
},
|
||||
balancersData: {
|
||||
get: function () {
|
||||
data = []
|
||||
if (this.templateSettings != null && this.templateSettings.routing != null && this.templateSettings.routing.balancers != null) {
|
||||
this.templateSettings.routing.balancers.forEach((o, index) => {
|
||||
let strategy = "random"
|
||||
if (o.strategy && o.strategy.type == "roundRobin") {
|
||||
strategy = o.strategy.type
|
||||
}
|
||||
|
||||
data.push({
|
||||
'key': index,
|
||||
'tag': o.tag ? o.tag : "",
|
||||
'strategy': strategy,
|
||||
'selector': o.selector ? o.selector : []
|
||||
});
|
||||
});
|
||||
}
|
||||
return data;
|
||||
}
|
||||
},
|
||||
routingRuleSettings: {
|
||||
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.routing.rules, null, 2) : null; },
|
||||
set: function (newValue) {
|
||||
@@ -1521,39 +1063,6 @@
|
||||
this.templateSettings = newTemplateSettings;
|
||||
}
|
||||
},
|
||||
setLogLevel: {
|
||||
get: function () {
|
||||
if (!this.templateSettings || !this.templateSettings.log || !this.templateSettings.log.loglevel) return "warning";
|
||||
return this.templateSettings.log.loglevel;
|
||||
},
|
||||
set: function (newValue) {
|
||||
newTemplateSettings = this.templateSettings;
|
||||
newTemplateSettings.log.loglevel = newValue;
|
||||
this.templateSettings = newTemplateSettings;
|
||||
}
|
||||
},
|
||||
accessLog: {
|
||||
get: function () {
|
||||
if (!this.templateSettings || !this.templateSettings.log || !this.templateSettings.log.access) return "";
|
||||
return this.templateSettings.log.access;
|
||||
},
|
||||
set: function (newValue) {
|
||||
newTemplateSettings = this.templateSettings;
|
||||
newTemplateSettings.log.access = newValue;
|
||||
this.templateSettings = newTemplateSettings;
|
||||
}
|
||||
},
|
||||
errorLog: {
|
||||
get: function () {
|
||||
if (!this.templateSettings || !this.templateSettings.log || !this.templateSettings.log.error) return "";
|
||||
return this.templateSettings.log.error;
|
||||
},
|
||||
set: function (newValue) {
|
||||
newTemplateSettings = this.templateSettings;
|
||||
newTemplateSettings.log.error = newValue;
|
||||
this.templateSettings = newTemplateSettings;
|
||||
}
|
||||
},
|
||||
blockedIPs: {
|
||||
get: function () {
|
||||
return this.templateRuleGetter({ outboundTag: "blocked", property: "ip" });
|
||||
@@ -1613,6 +1122,26 @@
|
||||
this.templateRuleSetter({ outboundTag: "warp", property: "domain", data: newValue });
|
||||
}
|
||||
},
|
||||
manualBlockedIPs: {
|
||||
get: function () { return JSON.stringify(this.blockedIPs, null, 2); },
|
||||
set: debounce(function (value) { this.blockedIPs = JSON.parse(value); }, 1000)
|
||||
},
|
||||
manualBlockedDomains: {
|
||||
get: function () { return JSON.stringify(this.blockedDomains, null, 2); },
|
||||
set: debounce(function (value) { this.blockedDomains = JSON.parse(value); }, 1000)
|
||||
},
|
||||
manualDirectIPs: {
|
||||
get: function () { return JSON.stringify(this.directIPs, null, 2); },
|
||||
set: debounce(function (value) { this.directIPs = JSON.parse(value); }, 1000)
|
||||
},
|
||||
manualDirectDomains: {
|
||||
get: function () { return JSON.stringify(this.directDomains, null, 2); },
|
||||
set: debounce(function (value) { this.directDomains = JSON.parse(value); }, 1000)
|
||||
},
|
||||
manualIPv4Domains: {
|
||||
get: function () { return JSON.stringify(this.ipv4Domains, null, 2); },
|
||||
set: debounce(function (value) { this.ipv4Domains = JSON.parse(value); }, 1000)
|
||||
},
|
||||
torrentSettings: {
|
||||
get: function () {
|
||||
return doAllItemsExist(this.settingsData.protocols.bittorrent, this.blockedProtocols);
|
||||
@@ -1676,14 +1205,14 @@
|
||||
familyProtectSettings: {
|
||||
get: function () {
|
||||
if (!this.templateSettings || !this.templateSettings.dns || !this.templateSettings.dns.servers) return false;
|
||||
return doAllItemsExist(this.settingsData.familyProtectDNS.servers, this.templateSettings.dns.servers);
|
||||
return doAllItemsExist(this.templateSettings.dns.servers, this.settingsData.familyProtectDNS.servers);
|
||||
},
|
||||
set: function (newValue) {
|
||||
newTemplateSettings = this.templateSettings;
|
||||
if (newValue) {
|
||||
newTemplateSettings.dns = this.settingsData.familyProtectDNS;
|
||||
} else {
|
||||
newTemplateSettings.dns.servers = newTemplateSettings.dns?.servers?.filter(data => !this.settingsData.familyProtectDNS.servers.includes(data))
|
||||
delete newTemplateSettings.dns;
|
||||
}
|
||||
this.templateSettings = newTemplateSettings;
|
||||
},
|
||||
@@ -1945,42 +1474,6 @@
|
||||
}
|
||||
},
|
||||
},
|
||||
MetaWARPSettings: {
|
||||
get: function () {
|
||||
return doAllItemsExist(this.settingsData.domains.meta, this.warpDomains);
|
||||
},
|
||||
set: function (newValue) {
|
||||
if (newValue) {
|
||||
this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.meta];
|
||||
} else {
|
||||
this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.meta.includes(data));
|
||||
}
|
||||
},
|
||||
},
|
||||
AppleWARPSettings: {
|
||||
get: function () {
|
||||
return doAllItemsExist(this.settingsData.domains.apple, this.warpDomains);
|
||||
},
|
||||
set: function (newValue) {
|
||||
if (newValue) {
|
||||
this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.apple];
|
||||
} else {
|
||||
this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.apple.includes(data));
|
||||
}
|
||||
},
|
||||
},
|
||||
RedditWARPSettings: {
|
||||
get: function () {
|
||||
return doAllItemsExist(this.settingsData.domains.reddit, this.warpDomains);
|
||||
},
|
||||
set: function (newValue) {
|
||||
if (newValue) {
|
||||
this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.reddit];
|
||||
} else {
|
||||
this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.reddit.includes(data));
|
||||
}
|
||||
},
|
||||
},
|
||||
SpotifyWARPSettings: {
|
||||
get: function () {
|
||||
return doAllItemsExist(this.settingsData.domains.spotify, this.warpDomains);
|
||||
@@ -1993,42 +1486,6 @@
|
||||
}
|
||||
},
|
||||
},
|
||||
enableDNS: {
|
||||
get: function () {
|
||||
return this.templateSettings ? this.templateSettings.dns != null : false;
|
||||
},
|
||||
set: function (newValue) {
|
||||
newTemplateSettings = this.templateSettings;
|
||||
newTemplateSettings.dns = newValue ? { servers: [], queryStrategy: "UseIP" } : null;
|
||||
this.templateSettings = newTemplateSettings;
|
||||
}
|
||||
},
|
||||
dnsStrategy: {
|
||||
get: function () {
|
||||
return this.enableDNS ? this.templateSettings.dns.queryStrategy : null;
|
||||
},
|
||||
set: function (newValue) {
|
||||
newTemplateSettings = this.templateSettings;
|
||||
newTemplateSettings.dns.queryStrategy = newValue;
|
||||
this.templateSettings = newTemplateSettings;
|
||||
}
|
||||
},
|
||||
dnsServers: {
|
||||
get: function () { return this.enableDNS ? this.templateSettings.dns.servers : []; },
|
||||
set: function (newValue) {
|
||||
newTemplateSettings = this.templateSettings;
|
||||
newTemplateSettings.dns.servers = newValue;
|
||||
this.templateSettings = newTemplateSettings;
|
||||
}
|
||||
},
|
||||
fakeDns: {
|
||||
get: function () { return this.templateSettings && this.templateSettings.fakedns ? this.templateSettings.fakedns : []; },
|
||||
set: function (newValue) {
|
||||
newTemplateSettings = this.templateSettings;
|
||||
newTemplateSettings.fakedns = newValue.length >0 ? newValue : null;
|
||||
this.templateSettings = newTemplateSettings;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -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:6} }" :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}}
|
||||
@@ -42,7 +42,7 @@
|
||||
outModal.visible = false;
|
||||
outModal.loading(false);
|
||||
},
|
||||
loading(loading=true) {
|
||||
loading(loading) {
|
||||
outModal.confirmLoading = loading;
|
||||
},
|
||||
check(){
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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 (
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package job
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -14,22 +12,21 @@ 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.GetIPLimitPrevLogPath(),
|
||||
xray.GetIPLimitBannedLogPath(),
|
||||
xray.GetIPLimitBannedPrevLogPath(),
|
||||
xray.GetIPLimitBannedPrevLogPath(),
|
||||
xray.GetAccessPersistentLogPath(),
|
||||
xray.GetAccessPersistentPrevLogPath(),
|
||||
xray.GetAccessPersistentPrevLogPath(),
|
||||
}
|
||||
|
||||
func NewCheckClientIpJob() *CheckClientIpJob {
|
||||
@@ -39,10 +36,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 +48,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 +91,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 +123,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 +141,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 +237,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 +255,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 +263,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
|
||||
|
||||
@@ -15,8 +15,8 @@ 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()}
|
||||
|
||||
logFilesPrev := []string{xray.GetIPLimitPrevLogPath(), xray.GetIPLimitBannedPrevLogPath(), xray.GetAccessPersistentPrevLogPath()}
|
||||
|
||||
// clear old previous logs
|
||||
for i := 0; i < len(logFilesPrev); i++ {
|
||||
if err := os.Truncate(logFilesPrev[i], 0); err != nil {
|
||||
@@ -26,26 +26,25 @@ func (j *ClearLogsJob) Run() {
|
||||
|
||||
// clear log files and copy to previous logs
|
||||
for i := 0; i < len(logFiles); i++ {
|
||||
if i > 0 {
|
||||
// copy to previous logs
|
||||
logFilePrev, err := os.OpenFile(logFilesPrev[i-1], os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
|
||||
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()
|
||||
|
||||
// copy to previous logs
|
||||
logFilePrev, err := os.OpenFile(logFilesPrev[i], os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
|
||||
if err != nil {
|
||||
logger.Warning("clear logs job err:", err)
|
||||
}
|
||||
|
||||
err := os.Truncate(logFiles[i], 0)
|
||||
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 {
|
||||
logger.Warning("clear logs job err:", err)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -1900,13 +1876,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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -57,9 +57,6 @@ var defaultValueMap = map[string]string{
|
||||
"subEncrypt": "true",
|
||||
"subShowInfo": "true",
|
||||
"subURI": "",
|
||||
"subJsonPath": "/json/",
|
||||
"subJsonURI": "",
|
||||
"subJsonFragment": "",
|
||||
"datepicker": "gregorian",
|
||||
"warp": "",
|
||||
}
|
||||
@@ -390,11 +387,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 +412,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) {
|
||||
@@ -429,14 +432,6 @@ 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")
|
||||
}
|
||||
@@ -489,7 +484,6 @@ 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() },
|
||||
}
|
||||
@@ -504,11 +498,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 +522,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
|
||||
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"slices"
|
||||
"time"
|
||||
"x-ui/config"
|
||||
"x-ui/database"
|
||||
@@ -115,19 +115,14 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
|
||||
}
|
||||
|
||||
func (t *Tgbot) NewBot(token string, proxyUrl string) (*telego.Bot, error) {
|
||||
if proxyUrl == "" {
|
||||
// No proxy URL provided, use default instance
|
||||
return telego.NewBot(token)
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(proxyUrl, "socks5://") {
|
||||
logger.Warning("Invalid socks5 URL, starting with default")
|
||||
if proxyUrl == "" || !strings.HasPrefix(proxyUrl, "socks5://") {
|
||||
logger.Warning("invalid socks5 url, start with default")
|
||||
return telego.NewBot(token)
|
||||
}
|
||||
|
||||
_, err := url.Parse(proxyUrl)
|
||||
if err != nil {
|
||||
logger.Warning("Can't parse proxy URL, using default instance for tgbot:", err)
|
||||
logger.Warning("cant parse proxy url, use default instance for tgbot:", err)
|
||||
return telego.NewBot(token)
|
||||
}
|
||||
|
||||
@@ -202,13 +197,9 @@ func (t *Tgbot) OnReceive() {
|
||||
}, th.AnyCallbackQueryWithMessage())
|
||||
|
||||
botHandler.HandleMessage(func(_ *telego.Bot, message telego.Message) {
|
||||
if message.UsersShared != nil {
|
||||
if message.UserShared != nil {
|
||||
if checkAdmin(message.From.ID) {
|
||||
userIDsStr := ""
|
||||
for _, userID := range message.UsersShared.UserIDs {
|
||||
userIDsStr += strconv.FormatInt(userID, 10) + " "
|
||||
}
|
||||
err := t.inboundService.SetClientTelegramUserID(message.UsersShared.RequestID, userIDsStr)
|
||||
err := t.inboundService.SetClientTelegramUserID(message.UserShared.RequestID, strconv.FormatInt(message.UserShared.UserID, 10))
|
||||
output := ""
|
||||
if err != nil {
|
||||
output += t.I18nBot("tgbot.messages.selectUserFailed")
|
||||
@@ -269,7 +260,7 @@ func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin boo
|
||||
msg += t.I18nBot("tgbot.commands.unknown")
|
||||
}
|
||||
|
||||
if msg != "" {
|
||||
if msg != ""{
|
||||
if onlyMessage {
|
||||
t.SendMsgToTgbot(chatId, msg)
|
||||
return
|
||||
@@ -281,7 +272,7 @@ func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin boo
|
||||
|
||||
func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool) {
|
||||
|
||||
chatId := callbackQuery.Message.GetChat().ID
|
||||
chatId := callbackQuery.Message.Chat.ID
|
||||
|
||||
if isAdmin {
|
||||
// get query from hash storage
|
||||
@@ -300,22 +291,22 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
t.searchClient(chatId, email)
|
||||
case "client_refresh":
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.clientRefreshSuccess", "Email=="+email))
|
||||
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
t.searchClient(chatId, email, callbackQuery.Message.MessageID)
|
||||
case "client_cancel":
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+email))
|
||||
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
t.searchClient(chatId, email, callbackQuery.Message.MessageID)
|
||||
case "ips_refresh":
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.IpRefreshSuccess", "Email=="+email))
|
||||
t.searchClientIps(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
t.searchClientIps(chatId, email, callbackQuery.Message.MessageID)
|
||||
case "ips_cancel":
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+email))
|
||||
t.searchClientIps(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
t.searchClientIps(chatId, email, callbackQuery.Message.MessageID)
|
||||
case "tgid_refresh":
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.TGIdRefreshSuccess", "Email=="+email))
|
||||
t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.MessageID)
|
||||
case "tgid_cancel":
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+email))
|
||||
t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.MessageID)
|
||||
case "reset_traffic":
|
||||
inlineKeyboard := tu.InlineKeyboard(
|
||||
tu.InlineKeyboardRow(
|
||||
@@ -325,13 +316,13 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmResetTraffic")).WithCallbackData(t.encodeQuery("reset_traffic_c "+email)),
|
||||
),
|
||||
)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
|
||||
case "reset_traffic_c":
|
||||
err := t.inboundService.ResetClientTrafficByEmail(email)
|
||||
if err == nil {
|
||||
t.xrayService.SetToNeedRestart()
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.resetTrafficSuccess", "Email=="+email))
|
||||
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
t.searchClient(chatId, email, callbackQuery.Message.MessageID)
|
||||
} else {
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
|
||||
}
|
||||
@@ -355,7 +346,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
tu.InlineKeyboardButton("40 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 40")),
|
||||
),
|
||||
tu.InlineKeyboardRow(
|
||||
tu.InlineKeyboardButton("50 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 50")),
|
||||
tu.InlineKeyboardButton("50 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 60")),
|
||||
tu.InlineKeyboardButton("60 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 60")),
|
||||
tu.InlineKeyboardButton("80 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 80")),
|
||||
),
|
||||
@@ -365,7 +356,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
tu.InlineKeyboardButton("200 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 200")),
|
||||
),
|
||||
)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
|
||||
case "limit_traffic_c":
|
||||
if len(dataArray) == 3 {
|
||||
limitTraffic, err := strconv.Atoi(dataArray[2])
|
||||
@@ -374,13 +365,13 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
if err == nil {
|
||||
t.xrayService.SetToNeedRestart()
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.setTrafficLimitSuccess", "Email=="+email))
|
||||
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
t.searchClient(chatId, email, callbackQuery.Message.MessageID)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
|
||||
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
t.searchClient(chatId, email, callbackQuery.Message.MessageID)
|
||||
case "limit_traffic_in":
|
||||
if len(dataArray) >= 3 {
|
||||
oldInputNumber, err := strconv.Atoi(dataArray[2])
|
||||
@@ -436,12 +427,12 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
tu.InlineKeyboardButton("⬅️").WithCallbackData(t.encodeQuery("limit_traffic_in "+email+" "+strconv.Itoa(inputNumber)+" -1")),
|
||||
),
|
||||
)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
|
||||
return
|
||||
}
|
||||
}
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
|
||||
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
t.searchClient(chatId, email, callbackQuery.Message.MessageID)
|
||||
case "reset_exp":
|
||||
inlineKeyboard := tu.InlineKeyboard(
|
||||
tu.InlineKeyboardRow(
|
||||
@@ -468,7 +459,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 12 "+t.I18nBot("tgbot.months")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 365")),
|
||||
),
|
||||
)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
|
||||
case "reset_exp_c":
|
||||
if len(dataArray) == 3 {
|
||||
days, err := strconv.Atoi(dataArray[2])
|
||||
@@ -503,13 +494,13 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
if err == nil {
|
||||
t.xrayService.SetToNeedRestart()
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.expireResetSuccess", "Email=="+email))
|
||||
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
t.searchClient(chatId, email, callbackQuery.Message.MessageID)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
|
||||
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
t.searchClient(chatId, email, callbackQuery.Message.MessageID)
|
||||
case "reset_exp_in":
|
||||
if len(dataArray) >= 3 {
|
||||
oldInputNumber, err := strconv.Atoi(dataArray[2])
|
||||
@@ -565,12 +556,12 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
tu.InlineKeyboardButton("⬅️").WithCallbackData(t.encodeQuery("reset_exp_in "+email+" "+strconv.Itoa(inputNumber)+" -1")),
|
||||
),
|
||||
)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
|
||||
return
|
||||
}
|
||||
}
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
|
||||
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
t.searchClient(chatId, email, callbackQuery.Message.MessageID)
|
||||
case "ip_limit":
|
||||
inlineKeyboard := tu.InlineKeyboard(
|
||||
tu.InlineKeyboardRow(
|
||||
@@ -599,7 +590,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
tu.InlineKeyboardButton("10").WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 10")),
|
||||
),
|
||||
)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
|
||||
case "ip_limit_c":
|
||||
if len(dataArray) == 3 {
|
||||
count, err := strconv.Atoi(dataArray[2])
|
||||
@@ -608,13 +599,13 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
if err == nil {
|
||||
t.xrayService.SetToNeedRestart()
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.resetIpSuccess", "Email=="+email, "Count=="+strconv.Itoa(count)))
|
||||
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
t.searchClient(chatId, email, callbackQuery.Message.MessageID)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
|
||||
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
t.searchClient(chatId, email, callbackQuery.Message.MessageID)
|
||||
case "ip_limit_in":
|
||||
if len(dataArray) >= 3 {
|
||||
oldInputNumber, err := strconv.Atoi(dataArray[2])
|
||||
@@ -670,12 +661,12 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
tu.InlineKeyboardButton("⬅️").WithCallbackData(t.encodeQuery("ip_limit_in "+email+" "+strconv.Itoa(inputNumber)+" -1")),
|
||||
),
|
||||
)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
|
||||
return
|
||||
}
|
||||
}
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
|
||||
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
t.searchClient(chatId, email, callbackQuery.Message.MessageID)
|
||||
case "clear_ips":
|
||||
inlineKeyboard := tu.InlineKeyboard(
|
||||
tu.InlineKeyboardRow(
|
||||
@@ -685,12 +676,12 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmClearIps")).WithCallbackData(t.encodeQuery("clear_ips_c "+email)),
|
||||
),
|
||||
)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
|
||||
case "clear_ips_c":
|
||||
err := t.inboundService.ClearClientIps(email)
|
||||
if err == nil {
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.clearIpSuccess", "Email=="+email))
|
||||
t.searchClientIps(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
t.searchClientIps(chatId, email, callbackQuery.Message.MessageID)
|
||||
} else {
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
|
||||
}
|
||||
@@ -709,7 +700,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmRemoveTGUser")).WithCallbackData(t.encodeQuery("tgid_remove_c "+email)),
|
||||
),
|
||||
)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
|
||||
case "tgid_remove_c":
|
||||
traffic, err := t.inboundService.GetClientTrafficByEmail(email)
|
||||
if err != nil || traffic == nil {
|
||||
@@ -719,7 +710,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
err = t.inboundService.SetClientTelegramUserID(traffic.Id, "")
|
||||
if err == nil {
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.removedTGUserSuccess", "Email=="+email))
|
||||
t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.MessageID)
|
||||
} else {
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
|
||||
}
|
||||
@@ -732,7 +723,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmToggle")).WithCallbackData(t.encodeQuery("toggle_enable_c "+email)),
|
||||
),
|
||||
)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
|
||||
case "toggle_enable_c":
|
||||
enabled, err := t.inboundService.ToggleClientEnableByEmail(email)
|
||||
if err == nil {
|
||||
@@ -742,7 +733,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
} else {
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.disableSuccess", "Email=="+email))
|
||||
}
|
||||
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
t.searchClient(chatId, email, callbackQuery.Message.MessageID)
|
||||
} else {
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
|
||||
}
|
||||
@@ -778,7 +769,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
t.onlineClients(chatId)
|
||||
case "onlines_refresh":
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation"))
|
||||
t.onlineClients(chatId, callbackQuery.Message.GetMessageID())
|
||||
t.onlineClients(chatId, callbackQuery.Message.MessageID)
|
||||
case "commands":
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.commands"))
|
||||
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.commands.helpAdminCommands"))
|
||||
@@ -1031,7 +1022,7 @@ func (t *Tgbot) getInboundUsages() string {
|
||||
}
|
||||
|
||||
func (t *Tgbot) clientInfoMsg(traffic *xray.ClientTraffic, printEnabled bool, printOnline bool, printActive bool,
|
||||
printDate bool, printTraffic bool, printRefreshed bool) string {
|
||||
printDate bool, printTraffic bool, printRefreshed bool) string {
|
||||
|
||||
now := time.Now().Unix()
|
||||
expiryTime := ""
|
||||
@@ -1219,13 +1210,13 @@ func (t *Tgbot) clientTelegramUserInfo(chatId int64, email string, messageID ...
|
||||
t.editMessageTgBot(chatId, messageID[0], output, inlineKeyboard)
|
||||
} else {
|
||||
t.SendMsgToTgbot(chatId, output, inlineKeyboard)
|
||||
requestUser := telego.KeyboardButtonRequestUsers{
|
||||
requestUser := telego.KeyboardButtonRequestUser{
|
||||
RequestID: int32(traffic.Id),
|
||||
UserIsBot: new(bool),
|
||||
}
|
||||
keyboard := tu.Keyboard(
|
||||
tu.KeyboardRow(
|
||||
tu.KeyboardButton(t.I18nBot("tgbot.buttons.selectTGUser")).WithRequestUsers(&requestUser),
|
||||
tu.KeyboardButton(t.I18nBot("tgbot.buttons.selectTGUser")).WithRequestUser(&requestUser),
|
||||
),
|
||||
tu.KeyboardRow(
|
||||
tu.KeyboardButton(t.I18nBot("tgbot.buttons.closeKeyboard")),
|
||||
@@ -1389,6 +1380,7 @@ func (t *Tgbot) getExhausted(chatId int64) {
|
||||
output += t.I18nBot("tgbot.messages.exhaustedCount", "Type=="+t.I18nBot("tgbot.clients"))
|
||||
output += t.I18nBot("tgbot.messages.disabled", "Disabled=="+strconv.Itoa(len(disabledClients)))
|
||||
output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+strconv.Itoa(exhaustedCC))
|
||||
|
||||
|
||||
if exhaustedCC > 0 {
|
||||
output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+t.I18nBot("tgbot.clients"))
|
||||
@@ -1498,6 +1490,7 @@ func (t *Tgbot) onlineClients(chatId int64, messageID ...int) {
|
||||
output := t.I18nBot("tgbot.messages.onlinesCount", "Count=="+fmt.Sprint(onlinesCount))
|
||||
keyboard := tu.InlineKeyboard(tu.InlineKeyboardRow(
|
||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.refresh")).WithCallbackData(t.encodeQuery("onlines_refresh"))))
|
||||
|
||||
|
||||
if onlinesCount > 0 {
|
||||
var buttons []telego.InlineKeyboardButton
|
||||
@@ -1572,44 +1565,30 @@ func (t *Tgbot) sendBanLogs(chatId int64, dt bool) {
|
||||
|
||||
file, err := os.Open(xray.GetIPLimitBannedPrevLogPath())
|
||||
if err == nil {
|
||||
// Check if the file is non-empty before attempting to upload
|
||||
fileInfo, _ := file.Stat()
|
||||
if fileInfo.Size() > 0 {
|
||||
document := tu.Document(
|
||||
tu.ID(chatId),
|
||||
tu.File(file),
|
||||
)
|
||||
_, err = bot.SendDocument(document)
|
||||
if err != nil {
|
||||
logger.Error("Error in uploading IPLimitBannedPrevLog: ", err)
|
||||
}
|
||||
} else {
|
||||
logger.Warning("IPLimitBannedPrevLog file is empty, not uploading.")
|
||||
document := tu.Document(
|
||||
tu.ID(chatId),
|
||||
tu.File(file),
|
||||
)
|
||||
_, err = bot.SendDocument(document)
|
||||
if err != nil {
|
||||
logger.Error("Error in uploading backup: ", err)
|
||||
}
|
||||
file.Close()
|
||||
} else {
|
||||
logger.Error("Error in opening IPLimitBannedPrevLog file for backup: ", err)
|
||||
logger.Error("Error in opening db file for backup: ", err)
|
||||
}
|
||||
|
||||
file, err = os.Open(xray.GetIPLimitBannedLogPath())
|
||||
if err == nil {
|
||||
// Check if the file is non-empty before attempting to upload
|
||||
fileInfo, _ := file.Stat()
|
||||
if fileInfo.Size() > 0 {
|
||||
document := tu.Document(
|
||||
tu.ID(chatId),
|
||||
tu.File(file),
|
||||
)
|
||||
_, err = bot.SendDocument(document)
|
||||
if err != nil {
|
||||
logger.Error("Error in uploading IPLimitBannedLog: ", err)
|
||||
}
|
||||
} else {
|
||||
logger.Warning("IPLimitBannedLog file is empty, not uploading.")
|
||||
document := tu.Document(
|
||||
tu.ID(chatId),
|
||||
tu.File(file),
|
||||
)
|
||||
_, err = bot.SendDocument(document)
|
||||
if err != nil {
|
||||
logger.Error("Error in uploading config.json: ", err)
|
||||
}
|
||||
file.Close()
|
||||
} else {
|
||||
logger.Error("Error in opening IPLimitBannedLog file for backup: ", err)
|
||||
logger.Error("Error in opening config.json file for backup: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ func (s *XraySettingService) RegWarp(secretKey string, publicKey string) (string
|
||||
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"
|
||||
url := fmt.Sprintf("https://api.cloudflareclient.com/v0a2158/reg")
|
||||
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte(data)))
|
||||
if err != nil {
|
||||
|
||||
@@ -52,16 +52,13 @@
|
||||
"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 configurations have been identified as susceptible to attacks, prompting immediate action to reinforce security protocols and safeguard against potential security breaches."
|
||||
|
||||
[menu]
|
||||
"dashboard" = "Overview"
|
||||
"inbounds" = "Inbounds"
|
||||
"settings" = "Panel Settings"
|
||||
"xray" = "Xray Configs"
|
||||
"logout" = "Log Out"
|
||||
"dashboard" = "OVERVIEW"
|
||||
"inbounds" = "INBOUNDS"
|
||||
"settings" = "PANEL SETTINGS"
|
||||
"xray" = "XRAY CONFIGS"
|
||||
"logout" = "LOG OUT"
|
||||
"link" = "Manage"
|
||||
|
||||
[pages.login]
|
||||
@@ -79,7 +76,7 @@
|
||||
"title" = "Overview"
|
||||
"memory" = "RAM"
|
||||
"hard" = "Disk"
|
||||
"xrayStatus" = "Xray"
|
||||
"xrayStatus" = "Status"
|
||||
"stopXray" = "Stop"
|
||||
"restartXray" = "Restart"
|
||||
"xraySwitch" = "Version"
|
||||
@@ -181,7 +178,7 @@
|
||||
"info" = "Info"
|
||||
"same" = "Same"
|
||||
"inboundData" = "Inbound's Data"
|
||||
"exportInbound" = "Export Inbound"
|
||||
"copyToClipboard" = "Copy to Clipboard"
|
||||
"import" = "Import"
|
||||
"importInbound" = "Import an Inbound"
|
||||
|
||||
@@ -251,7 +248,6 @@
|
||||
"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."
|
||||
"sampleRemark" = "Sample Remark"
|
||||
"oldUsername" = "Current Username"
|
||||
@@ -305,8 +301,6 @@
|
||||
"subShowInfoDesc" = "The remaining traffic and date will be displayed in the client apps."
|
||||
"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"
|
||||
|
||||
[pages.xray]
|
||||
"title" = "Xray Configs"
|
||||
@@ -314,10 +308,8 @@
|
||||
"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"
|
||||
"generalConfigs" = "General Strategy"
|
||||
"generalConfigsDesc" = "These options will determine general strategy adjustments."
|
||||
"blockConfigs" = "Protection Shield"
|
||||
"blockConfigsDesc" = "These options will block traffic based on specific requested protocols and websites."
|
||||
"blockCountryConfigs" = "Block Country"
|
||||
@@ -327,7 +319,7 @@
|
||||
"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."
|
||||
"warpConfigsDesc" = "These options will route traffic based on a specific destination via WARP. (follow the guide on the Panel’s GitHub)"
|
||||
"Template" = "Advanced Xray Configuration Template"
|
||||
"TemplateDesc" = "The final Xray config file will be generated based on this template."
|
||||
"FreedomStrategy" = "Freedom Protocol Strategy"
|
||||
@@ -388,12 +380,6 @@
|
||||
"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"
|
||||
@@ -401,17 +387,10 @@
|
||||
"Inbounds" = "Inbounds"
|
||||
"InboundsDesc" = "Accepting the specific clients."
|
||||
"Outbounds" = "Outbounds"
|
||||
"Balancers" = "Balancers"
|
||||
"OutboundsDesc" = "Set the outgoing traffic pathway."
|
||||
"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"
|
||||
@@ -422,7 +401,6 @@
|
||||
"dest" = "Destination"
|
||||
"inbound" = "Inbound"
|
||||
"outbound" = "Outbound"
|
||||
"balancer" = "Balancer"
|
||||
"info" = "Info"
|
||||
"add" = "Add Rule"
|
||||
"edit" = "Edit Rule"
|
||||
@@ -443,15 +421,6 @@
|
||||
"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"
|
||||
@@ -460,21 +429,6 @@
|
||||
"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"
|
||||
@@ -497,7 +451,7 @@
|
||||
"wentWrong" = "❌ Something went wrong!"
|
||||
"noIpRecord" = "❗ No IP Record!"
|
||||
"noInbounds" = "❗ No inbound found!"
|
||||
"unlimited" = "♾ Unlimited(Reset)"
|
||||
"unlimited" = "♾ Unlimited"
|
||||
"add" = "Add"
|
||||
"month" = "Month"
|
||||
"months" = "Months"
|
||||
@@ -522,6 +476,7 @@
|
||||
"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>"
|
||||
|
||||
|
||||
[tgbot.messages]
|
||||
"cpuThreshold" = "🔴 CPU Load {{ .Percent }}% exceeds the threshold of {{ .Threshold }}%"
|
||||
"selectUserFailed" = "❌ Error in user selection!"
|
||||
@@ -567,6 +522,7 @@
|
||||
"yes" = "✅ Yes"
|
||||
"no" = "❌ No"
|
||||
|
||||
|
||||
[tgbot.buttons]
|
||||
"closeKeyboard" = "❌ Close Keyboard"
|
||||
"cancel" = "❌ Cancel"
|
||||
@@ -600,6 +556,7 @@
|
||||
"limitTraffic" = "🚧 Traffic Limit"
|
||||
"getBanLogs" = "Get Ban Logs"
|
||||
|
||||
|
||||
[tgbot.answers]
|
||||
"successfulOperation" = "✅ Operation successful!"
|
||||
"errorOperation" = "❗ Error in operation."
|
||||
|
||||
@@ -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"
|
||||
@@ -52,20 +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" = "Se han identificado ciertas configuraciones como susceptibles a ataques, lo que genera acciones inmediatas para reforzar los protocolos de seguridad y proteger contra posibles violaciones de seguridad."
|
||||
|
||||
[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]
|
||||
"title" = "Bienvenido"
|
||||
"title" = "Grata"
|
||||
"loginAgain" = "El límite de tiempo de inicio de sesión ha expirado. Por favor, inicia sesión nuevamente."
|
||||
|
||||
[pages.login.toasts]
|
||||
@@ -79,7 +76,7 @@
|
||||
"title" = "Estado del Sistema"
|
||||
"memory" = "Memoria"
|
||||
"hard" = "Disco Duro"
|
||||
"xrayStatus" = "Xray"
|
||||
"xrayStatus" = "Estado de"
|
||||
"stopXray" = "Detener"
|
||||
"restartXray" = "Reiniciar"
|
||||
"xraySwitch" = "Versión"
|
||||
@@ -181,7 +178,7 @@
|
||||
"info" = "Info"
|
||||
"same" = "misma"
|
||||
"inboundData" = "Datos de entrada"
|
||||
"exportInbound" = "Exportación entrante"
|
||||
"copyToClipboard" = "Copiar al portapapeles"
|
||||
"import" = "Importar"
|
||||
"importInbound" = "Importar un entrante"
|
||||
|
||||
@@ -251,7 +248,6 @@
|
||||
"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"
|
||||
@@ -305,8 +301,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"
|
||||
@@ -316,8 +310,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"
|
||||
@@ -382,36 +374,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"
|
||||
@@ -422,7 +401,6 @@
|
||||
"dest" = "Destino"
|
||||
"inbound" = "Entrante"
|
||||
"outbound" = "saliente"
|
||||
"balancer" = "Balancín"
|
||||
"info" = "Información"
|
||||
"add" = "Agregar regla"
|
||||
"edit" = "Editar regla"
|
||||
@@ -443,15 +421,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"
|
||||
@@ -460,21 +429,6 @@
|
||||
"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"
|
||||
|
||||
@@ -52,9 +52,6 @@
|
||||
"secretToken" = "توکن امنیتی"
|
||||
"remained" = "باقیمانده"
|
||||
"security" = "امنیت"
|
||||
"secAlertTitle" = "هشدارامنیتی"
|
||||
"secAlertSsl" = "ایناتصالامن نیست. لطفا تازمانیکه تیالاس برای محافظت از دادهها فعال نشدهاست، از وارد کردن اطلاعات حساس خودداری کنید"
|
||||
"secAlertConf" = "پیکربندیهای خاصی مستعد حملات سایبری شناسایی شدهاند، اقدام فوری برای تقویت پروتکلهای امنیتی و محافظت در برابر نقضهای امنیتی لازم است"
|
||||
|
||||
[menu]
|
||||
"dashboard" = "نمای کلی"
|
||||
@@ -79,7 +76,7 @@
|
||||
"title" = "نمای کلی"
|
||||
"memory" = "RAM"
|
||||
"hard" = "Disk"
|
||||
"xrayStatus" = "ایکسری"
|
||||
"xrayStatus" = "وضعیتایکسری"
|
||||
"stopXray" = "توقف"
|
||||
"restartXray" = "شروعمجدد"
|
||||
"xraySwitch" = "نسخه"
|
||||
@@ -181,7 +178,7 @@
|
||||
"info" = "اطلاعات"
|
||||
"same" = "همسان"
|
||||
"inboundData" = "دادههای ورودی"
|
||||
"exportInbound" = "استخراج ورودی"
|
||||
"copyToClipboard" = "کپی در حافظه"
|
||||
"import" = "افزودن"
|
||||
"importInbound" = "افزودن یک ورودی"
|
||||
|
||||
@@ -251,7 +248,6 @@
|
||||
"pageSizeDesc" = "(اندازه صفحه برای جدول ورودیها.(0 = غیرفعال"
|
||||
"remarkModel" = "نامکانفیگ و جداکننده"
|
||||
"datepicker" = "نوع تقویم"
|
||||
"datepickerPlaceholder" = "انتخاب تاریخ"
|
||||
"datepickerDescription" = "وظایف برنامه ریزی شده بر اساس این تقویم اجرا میشود"
|
||||
"sampleRemark" = "نمونهنام"
|
||||
"oldUsername" = "نامکاربری فعلی"
|
||||
@@ -305,8 +301,6 @@
|
||||
"subShowInfoDesc" = "ترافیک و زمان باقیمانده را در برنامههای کاربری نمایش میدهد"
|
||||
"subURI" = "پروکسی معکوس URI مسیر"
|
||||
"subURIDesc" = "سابسکریپشن را برای استفاده در پشت پراکسیها تغییر میدهد URI مسیر"
|
||||
"fragment" = "تکهتکه شدن"
|
||||
"fragmentDesc" = "فعال کردن تکه تکه شدن برای بسته نخست تیالاس"
|
||||
|
||||
[pages.xray]
|
||||
"title" = "پیکربندی ایکسری"
|
||||
@@ -316,8 +310,6 @@
|
||||
"advancedTemplate" = "پیشرفته"
|
||||
"generalConfigs" = "استراتژی کلی"
|
||||
"generalConfigsDesc" = "این گزینهها استراتژی کلی ترافیک را تعیین میکنند"
|
||||
"logConfigs" = "گزارش"
|
||||
"logConfigsDesc" = "گزارشها ممکن است بر کارایی سرور شما تأثیر بگذارد. توصیه می شود فقط در صورت نیاز آن را عاقلانه فعال کنید"
|
||||
"blockConfigs" = "سپر محافظ"
|
||||
"blockConfigsDesc" = "این گزینهها ترافیک را بر اساس پروتکلهای درخواستی خاص، و وب سایتها مسدود میکند"
|
||||
"blockCountryConfigs" = "مسدودسازی کشور"
|
||||
@@ -388,12 +380,6 @@
|
||||
"OpenAIWARPDesc" = "ترافیک را از طریق وارپ به چت جیپیتی هدایت میکند"
|
||||
"NetflixWARP" = "نتفلیکس"
|
||||
"NetflixWARPDesc" = "ترافیک را از طریق وارپ به نتفلیکس هدایت میکند"
|
||||
"MetaWARP" = "متا"
|
||||
"MetaWARPDesc" = "ترافیک را از طریق وارپ به متا (اینستاگرام، فیس بوک، واتساپ، تردز و...) هدایت می کند."
|
||||
"AppleWARP" = "اپل"
|
||||
"AppleWARPDesc" = " ترافیک را از طریق وارپ به اپل هدایت میکند"
|
||||
"RedditWARP" = "ردیت"
|
||||
"RedditWARPDesc" = " ترافیک را از طریق وارپ به ردیت هدایت میکند"
|
||||
"SpotifyWARP" = "اسپاتیفای"
|
||||
"SpotifyWARPDesc" = " ترافیک را از طریق وارپ به اسپاتیفای هدایت میکند"
|
||||
"IRWARP" = "دامنههای ایران"
|
||||
@@ -401,17 +387,10 @@
|
||||
"Inbounds" = "ورودیها"
|
||||
"InboundsDesc" = "پذیرش کلاینت خاص"
|
||||
"Outbounds" = "خروجیها"
|
||||
"Balancers" = "بالانسرها"
|
||||
"OutboundsDesc" = "مسیر ترافیک خروجی را تنظیم کنید"
|
||||
"Routings" = "قوانین مسیریابی"
|
||||
"RoutingsDesc" = "اولویت هر قانون مهم است"
|
||||
"completeTemplate" = "کامل"
|
||||
"logLevel" = "سطح گزارش"
|
||||
"logLevelDesc" = "سطح گزارش برای گزارش های خطا، نشان دهنده اطلاعاتی است که باید ثبت شوند."
|
||||
"accessLog" = "مسیر گزارش"
|
||||
"accessLogDesc" = "مسیر فایل برای گزارش دسترسی. مقدار ویژه «هیچ» گزارشهای دسترسی را غیرفعال میکند."
|
||||
"errorLog" = "گزارش خطا"
|
||||
"errorLogDesc" = "مسیر فایل برای ورود به سیستم خطا. مقدار ویژه «هیچ» گزارش های خطا را غیرفعال میکند"
|
||||
|
||||
[pages.xray.rules]
|
||||
"first" = "اولین"
|
||||
@@ -422,7 +401,6 @@
|
||||
"dest" = "مقصد"
|
||||
"inbound" = "ورودی"
|
||||
"outbound" = "خروجی"
|
||||
"balancer" = "بالانسر"
|
||||
"info" = "اطلاعات"
|
||||
"add" = "افزودن قانون"
|
||||
"edit" = "ویرایش قانون"
|
||||
@@ -443,15 +421,6 @@
|
||||
"portal" = "پورتال"
|
||||
"intercon" = "اتصال میانی"
|
||||
|
||||
[pages.xray.balancer]
|
||||
"addBalancer" = "افزودن بالانسر"
|
||||
"editBalancer" = "ویرایش بالانسر"
|
||||
"balancerStrategy" = "استراتژی"
|
||||
"balancerSelectors" = "انتخابگرها"
|
||||
"tag" = "برچسب"
|
||||
"tagDesc" = "برچسب یگانه"
|
||||
"balancerDesc" = "امکان استفاده همزمان balancerTag و outboundTag باهم وجود ندارد. درصورت استفاده همزمان فقط outboundTag عمل خواهد کرد."
|
||||
|
||||
[pages.xray.wireguard]
|
||||
"secretKey" = "کلید شخصی"
|
||||
"publicKey" = "کلید عمومی"
|
||||
@@ -460,21 +429,6 @@
|
||||
"psk" = "کلید مشترک"
|
||||
"domainStrategy" = "استراتژی حل دامنه"
|
||||
|
||||
[pages.xray.dns]
|
||||
"enable" = "فعال کردن حل دامنه"
|
||||
"enableDesc" = "سرور حل دامنه داخلی را فعال کنید"
|
||||
"strategy" = "استراتژی پرسوجو"
|
||||
"strategyDesc" = "استراتژی کلی برای حل نام دامنه"
|
||||
"add" = "افزودن سرور"
|
||||
"edit" = "ویرایش سرور"
|
||||
"domains" = "دامنهها"
|
||||
|
||||
[pages.xray.fakedns]
|
||||
"add" = "افزودن دیاناس جعلی"
|
||||
"edit" = "ویرایش دیاناس جعلی"
|
||||
"ipPool" = "زیرشبکه استخر آیپی"
|
||||
"poolSize" = "اندازه استخر"
|
||||
|
||||
[pages.settings.security]
|
||||
"admin" = "مدیر"
|
||||
"secret" = "توکن مخفی"
|
||||
@@ -497,7 +451,7 @@
|
||||
"wentWrong" = "❌ مشکلی رخ داده است!"
|
||||
"noIpRecord" = "❗ رکورد IP یافت نشد!"
|
||||
"noInbounds" = "❗ هیچ ورودی یافت نشد!"
|
||||
"unlimited" = "♾ - نامحدود(ریست)"
|
||||
"unlimited" = "♾ نامحدود"
|
||||
"add" = "اضافه کردن"
|
||||
"month" = "ماه"
|
||||
"months" = "ماهها"
|
||||
@@ -547,8 +501,8 @@
|
||||
"time" = "⏰ زمان: {{ .Time }}\r\n"
|
||||
"inbound" = "📍 نامورودی: {{ .Remark }}\r\n"
|
||||
"port" = "🔌 پورت: {{ .Port }}\r\n"
|
||||
"expire" = "📅 تاریخانقضا: {{ .Time }}\r\n\r\n"
|
||||
"expireIn" = "📅 باقیماندهتاانقضا: {{ .Time }}\r\n\r\n"
|
||||
"expire" = "📅 تاریخانقضا: {{ .DateTime }}\r\n \r\n"
|
||||
"expireIn" = "📅 باقیماندهتاانقضا: {{ .Time }}\r\n \r\n"
|
||||
"active" = "💡 فعال: {{ .Enable }}\r\n"
|
||||
"enabled" = "🚨 وضعیت: {{ .Enable }}\r\n"
|
||||
"online" = "🌐 وضعیت اتصال: {{ .Status }}\r\n"
|
||||
@@ -561,7 +515,7 @@
|
||||
"exhaustedCount" = "🚨 تعداد {{ .Type }} بهاتمامرسیدهاست:\r\n"
|
||||
"onlinesCount" = "🌐 کاربرانآنلاین: {{ .Count }}\r\n"
|
||||
"disabled" = "🛑 غیرفعال: {{ .Disabled }}\r\n"
|
||||
"depleteSoon" = "🔜 بهزودیبهپایانخواهدرسید: {{ .Deplete }}\r\n\r\n"
|
||||
"depleteSoon" = "🔜 بهزودیبهپایانخواهدرسید: {{ .Deplete }}\r\n \r\n"
|
||||
"backupTime" = "🗄 زمانپشتیبانگیری: {{ .Time }}\r\n"
|
||||
"refreshedOn" = "\r\n📋🔄 تازهسازی شده در: {{ .Time }}\r\n\r\n"
|
||||
"yes" = "✅ بله"
|
||||
@@ -583,6 +537,7 @@
|
||||
"clientUsage" = "دریافت آمار کاربر"
|
||||
"onlines" = "کاربران آنلاین"
|
||||
"commands" = "دستورات"
|
||||
|
||||
"refresh" = "🔄 تازهسازی"
|
||||
"clearIPs" = "❌ پاکسازی آدرسها"
|
||||
"removeTGUser" = "❌ حذف کاربر تلگرام"
|
||||
|
||||
@@ -1,621 +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"
|
||||
"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" = "Konfigurasi tertentu telah diidentifikasi rentan terhadap serangan, sehingga mendorong tindakan segera untuk memperkuat protokol keamanan dan melindungi dari potensi pelanggaran keamanan."
|
||||
|
||||
[menu]
|
||||
"dashboard" = "Ikhtisar"
|
||||
"inbounds" = "Masuk"
|
||||
"settings" = "Pengaturan Panel"
|
||||
"xray" = "Konfigurasi Xray"
|
||||
"logout" = "Keluar"
|
||||
"link" = "Kelola"
|
||||
|
||||
[pages.login]
|
||||
"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"
|
||||
"publicKeyPath" = "Path Kunci Publik"
|
||||
"publicKeyContent" = "Konten Kunci Publik"
|
||||
"keyPath" = "Path Kunci Privat"
|
||||
"keyContent" = "Konten Kunci Privat"
|
||||
"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 saat Penggunaan 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>"
|
||||
@@ -52,9 +52,6 @@
|
||||
"secretToken" = "Секретный токен"
|
||||
"remained" = "остались"
|
||||
"security" = "Безопасность"
|
||||
"secAlertTitle" = "Предупреждение системы безопасности"
|
||||
"secAlertSsl" = "Это соединение не защищено. Пожалуйста, воздержитесь от ввода конфиденциальной информации до тех пор, пока не будет активирован TLS для защиты данных"
|
||||
"secAlertConf" = "Некоторые конфигурации были определены как уязвимые для атак, что требует немедленных действий по усилению протоколов безопасности и защите от потенциальных нарушений безопасности."
|
||||
|
||||
[menu]
|
||||
"dashboard" = "Статус системы"
|
||||
@@ -79,7 +76,7 @@
|
||||
"title" = "Статус системы"
|
||||
"memory" = "Память"
|
||||
"hard" = "Жесткий диск"
|
||||
"xrayStatus" = "Xray"
|
||||
"xrayStatus" = "Статус"
|
||||
"stopXray" = "Остановить"
|
||||
"restartXray" = "Перезапустить"
|
||||
"xraySwitch" = "Версия"
|
||||
@@ -181,7 +178,7 @@
|
||||
"info" = "Информация"
|
||||
"same" = "Тот же"
|
||||
"inboundData" = "Входящие данные"
|
||||
"exportInbound" = "Экспорт входящих"
|
||||
"copyToClipboard" = "Копировать в буфер обмена"
|
||||
"import" = "Импортировать"
|
||||
"importInbound" = "Импортировать входящее сообщение"
|
||||
|
||||
@@ -251,7 +248,6 @@
|
||||
"pageSizeDesc" = "Определить размер страницы для входящей таблицы. Установите 0, чтобы отключить"
|
||||
"remarkModel" = "Модель примечания и символ разделения"
|
||||
"datepicker" = "выбор даты"
|
||||
"datepickerPlaceholder" = "Выберите дату"
|
||||
"datepickerDescription" = "Тип календаря выбора указывает дату истечения срока действия."
|
||||
"sampleRemark" = "Пример замечания"
|
||||
"oldUsername" = "Текущее имя пользователя"
|
||||
@@ -305,8 +301,6 @@
|
||||
"subShowInfoDesc" = "Показывать восстановленный трафик и дату после имени конфигурации"
|
||||
"subURI" = "URI обратного прокси"
|
||||
"subURIDesc" = "Изменить базовый URI URL-адреса подписки для использования за прокси-серверами"
|
||||
"fragment" = "Фрагментация"
|
||||
"fragmentDesc" = "Включить фрагментацию для пакета приветствия TLS"
|
||||
|
||||
[pages.xray]
|
||||
"title" = "Настройки Xray"
|
||||
@@ -316,8 +310,6 @@
|
||||
"advancedTemplate" = "Расширенный шаблон"
|
||||
"generalConfigs" = "Основные настройки"
|
||||
"generalConfigsDesc" = "Эти параметры описывают общие настройки"
|
||||
"logConfigs" = "Журнал"
|
||||
"logConfigsDesc" = "Журналы могут повлиять на эффективность вашего сервера. Рекомендуется включать их с умом только в случае ваших нужд!"
|
||||
"blockConfigs" = "Блокировка конфигураций"
|
||||
"blockConfigsDesc" = "Эти параметры не позволят пользователям подключаться к определенным протоколам и веб-сайтам"
|
||||
"blockCountryConfigs" = "Конфигурации блокировки страны"
|
||||
@@ -382,36 +374,23 @@
|
||||
"GoogleIPv4Desc" = "Добавить маршрутизацию для Google для подключения к IPv4"
|
||||
"NetflixIPv4" = "Использовать IPv4 для Netflix"
|
||||
"NetflixIPv4Desc" = "Добавить маршрутизацию для Netflix для подключения к IPv4"
|
||||
"GoogleWARP" = "Google"
|
||||
"GoogleWARPDesc" = "Направляет трафик в Google через WARP."
|
||||
"OpenAIWARP" = "OpenAI (ChatGPT)"
|
||||
"OpenAIWARPDesc" = "Направляет трафик в OpenAI (ChatGPT) через WARP."
|
||||
"NetflixWARP" = "Netflix"
|
||||
"NetflixWARPDesc" = "Направляет трафик в Apple через WARP."
|
||||
"MetaWARP" = "Мета"
|
||||
"MetaWARPDesc" = "Направляет трафик в Meta (Instagram, Facebook, WhatsApp, Threads...) через WARP."
|
||||
"AppleWARP" = "Apple"
|
||||
"AppleWARPDesc" = "Направляет трафик в Apple через WARP."
|
||||
"RedditWARP" = "Reddit"
|
||||
"RedditWARPDesc" = "Направляет трафик в Reddit через WARP."
|
||||
"SpotifyWARP" = "Spotify"
|
||||
"SpotifyWARPDesc" = "Направляет трафик в Spotify через WARP."
|
||||
"GoogleWARP" = "Маршрутизация Google через WARP"
|
||||
"GoogleWARPDesc" = "Добавить маршрутизацию для Google через WARP"
|
||||
"OpenAIWARP" = "Маршрутизация OpenAI (ChatGPT) через WARP"
|
||||
"OpenAIWARPDesc" = "Добавить маршрутизацию для OpenAI (ChatGPT) через WARP"
|
||||
"NetflixWARP" = "Маршрутизация Netflix через WARP"
|
||||
"NetflixWARPDesc" = "Добавить маршрутизацию для Netflix через WARP"
|
||||
"SpotifyWARP" = "Маршрутизация Spotify через WARP"
|
||||
"SpotifyWARPDesc" = "Добавить маршрутизацию для Spotify через WARP"
|
||||
"IRWARP" = "Маршрутизация доменов Ирана через WARP"
|
||||
"IRWARPDesc" = "Добавить маршрутизацию для доменов Ирана через WARP"
|
||||
"Inbounds" = "Входящие"
|
||||
"InboundsDesc" = "Изменение шаблона конфигурации для подключения определенных пользователей"
|
||||
"Outbounds" = "Исходящие"
|
||||
"Balancers" = "Балансиры"
|
||||
"OutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить исходящие пути для этого сервера"
|
||||
"Routings" = "Правила маршрутизации"
|
||||
"RoutingsDesc" = "Важен приоритет каждого правила!"
|
||||
"completeTemplate" = "Все"
|
||||
"logLevel" = "Уровень журнала"
|
||||
"logLevelDesc" = "Уровень журнала для журналов ошибок, указывающий информацию, которую необходимо записать."
|
||||
"accessLog" = "Журнал доступа"
|
||||
"accessLogDesc" = "Путь к файлу журнала доступа. Специальное значение «none» отключило журналы доступа."
|
||||
"errorLog" = "Журнал ошибок"
|
||||
"errorLogDesc" = "Путь к файлу журнала ошибок. Специальное значение «none» отключает журналы ошибок."
|
||||
|
||||
[pages.xray.rules]
|
||||
"first" = "Первый"
|
||||
@@ -422,7 +401,6 @@
|
||||
"dest" = "Пункт назначения"
|
||||
"inbound" = "Входящий"
|
||||
"outbound" = "Исходящий"
|
||||
"balancer" = "балансир"
|
||||
"info" = "Информация"
|
||||
"add" = "Добавить правило"
|
||||
"edit" = "Редактировать правило"
|
||||
@@ -443,15 +421,6 @@
|
||||
"portal" = "Портал"
|
||||
"intercon" = "Соединение"
|
||||
|
||||
[pages.xray.balancer]
|
||||
"addBalancer" = "Добавить балансир"
|
||||
"editBalancer" = "Редактировать балансир"
|
||||
"balancerStrategy" = "Стратегия"
|
||||
"balancerSelectors" = "Селекторы"
|
||||
"tag" = "Тег"
|
||||
"tagDesc" = "уникальный тег"
|
||||
"balancerDesc" = "Невозможно одновременно использовать balancerTag и outboundTag. При одновременном использовании будет работать только outboundTag."
|
||||
|
||||
[pages.xray.wireguard]
|
||||
"secretKey" = "Секретный ключ"
|
||||
"publicKey" = "Открытый ключ"
|
||||
@@ -460,21 +429,6 @@
|
||||
"psk" = "Общий ключ"
|
||||
"domainStrategy" = "Стратегия домена"
|
||||
|
||||
[pages.xray.dns]
|
||||
"enable" = "Включить DNS"
|
||||
"enableDesc" = "Включить встроенный DNS-сервер"
|
||||
"strategy" = "Стратегия запроса"
|
||||
"strategyDesc" = "Общая стратегия разрешения доменных имен"
|
||||
"add" = "Добавить сервер"
|
||||
"edit" = "Редактировать сервер"
|
||||
"domains" = "Домены"
|
||||
|
||||
[pages.xray.fakedns]
|
||||
"add" = "Добавить поддельный DNS"
|
||||
"edit" = "Редактировать поддельный DNS"
|
||||
"ipPool" = "Подсеть пула IP"
|
||||
"poolSize" = "Размер пула"
|
||||
|
||||
[pages.settings.security]
|
||||
"admin" = "Админ"
|
||||
"secret" = "Секретный токен"
|
||||
@@ -549,7 +503,7 @@
|
||||
"port" = "🔌 Порт: {{ .Port }}\r\n"
|
||||
"expire" = "📅 Дата окончания: {{ .Time }}\r\n"
|
||||
"expireIn" = "📅 Окончание через: {{ .Time }}\r\n"
|
||||
"active" = "💡 Активен: {{ .Enable }}\r\n"
|
||||
"active" = "💡 Активен: ✅ Да\r\n"
|
||||
"enabled" = "🚨 Включен: {{ .Enable }}\r\n"
|
||||
"online" = "🌐 Статус соединения: {{ .Status }}\r\n"
|
||||
"email" = "📧 Email: {{ .Email }}\r\n"
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"check" = "Kiểm tra"
|
||||
"indefinite" = "Không xác định"
|
||||
"unlimited" = "Không giới hạn"
|
||||
"none" = "None"
|
||||
"none" = "Không có"
|
||||
"qrCode" = "Mã QR"
|
||||
"info" = "Thông tin thêm"
|
||||
"edit" = "Chỉnh sửa"
|
||||
@@ -32,7 +32,7 @@
|
||||
"transmission" = "Truyền tải"
|
||||
"host" = "Máy chủ"
|
||||
"path" = "Đường dẫn"
|
||||
"camouflage" = "Ngụy trang"
|
||||
"camouflage" = "camouflage"
|
||||
"status" = "Trạng thái"
|
||||
"enabled" = "Đã kích hoạt"
|
||||
"disabled" = "Đã tắt"
|
||||
@@ -52,9 +52,6 @@
|
||||
"secretToken" = "Mã bí mật"
|
||||
"remained" = "Còn lại"
|
||||
"security" = "Bảo vệ"
|
||||
"secAlertTitle" = "Cảnh báo an ninh-Tiếng Việt by Ohoang7"
|
||||
"secAlertSsl" = "Kết nối này không an toàn; Vui lòng không nhập thông tin nhạy cảm cho đến khi TLS được kích hoạt để bảo vệ dữ liệu của Bạn"
|
||||
"secAlertConf" = "Một số cấu hình nhất định đã được xác định là dễ bị tấn công, thúc đẩy hành động ngay lập tức để củng cố các giao thức bảo mật và bảo vệ chống lại các vi phạm bảo mật tiềm ẩn."
|
||||
|
||||
[menu]
|
||||
"dashboard" = "Trạng thái hệ thống"
|
||||
@@ -62,7 +59,7 @@
|
||||
"settings" = "Cài đặt bảng điều khiển"
|
||||
"logout" = "Đăng xuất"
|
||||
"xray" = "Cài đặt Xray"
|
||||
"link" = "Quản lý"
|
||||
"link" = "sự quản lý"
|
||||
|
||||
[pages.login]
|
||||
"title" = "Chào mừng"
|
||||
@@ -77,9 +74,9 @@
|
||||
|
||||
[pages.index]
|
||||
"title" = "Trạng thái hệ thống"
|
||||
"memory" = "Ram"
|
||||
"memory" = "Bộ nhớ"
|
||||
"hard" = "Dung lượng"
|
||||
"xrayStatus" = "Xray"
|
||||
"xrayStatus" = "Trạng thái"
|
||||
"stopXray" = "Dừng lại"
|
||||
"restartXray" = "Khởi động lại"
|
||||
"xraySwitch" = "Phiên bản"
|
||||
@@ -128,8 +125,8 @@
|
||||
"modifyInbound" = "Chỉnh sửa điểm vào (Inbound)"
|
||||
"deleteInbound" = "Xóa điểm vào (Inbound)"
|
||||
"deleteInboundContent" = "Xác nhận xóa điểm vào? (Inbound)"
|
||||
"deleteClient" = "Xóa người dùng"
|
||||
"deleteClientContent" = "Bạn có chắc chắn muốn xóa người dùng không?"
|
||||
"deleteClient" = "Xóa khách hàng"
|
||||
"deleteClientContent" = "Bạn có chắc chắn muốn xóa ứng dụng khách không?"
|
||||
"resetTrafficContent" = "Xác nhận đặt lại lưu lượng?"
|
||||
"copyLink" = "Sao chép liên kết"
|
||||
"address" = "Địa chỉ"
|
||||
@@ -148,7 +145,7 @@
|
||||
"keyPath" = "Đường dẫn khóa riêng tư"
|
||||
"keyContent" = "Nội dung khóa riêng tư"
|
||||
"clickOnQRcode" = "Nhấn vào Mã QR để sao chép"
|
||||
"client" = "Người dùng"
|
||||
"client" = "Máy Khách"
|
||||
"export" = "Xuất liên kết"
|
||||
"clone" = "Sao chép"
|
||||
"cloneInbound" = "Sao chép điểm vào (Inbound)"
|
||||
@@ -157,15 +154,15 @@
|
||||
"resetAllTraffic" = "Đặt lại lưu lượng cho tất cả điểm vào"
|
||||
"resetAllTrafficTitle" = "Đặt lại lưu lượng cho tất cả điểm vào"
|
||||
"resetAllTrafficContent" = "Bạn có chắc chắn muốn đặt lại lưu lượng cho tất cả điểm vào không?"
|
||||
"resetInboundClientTraffics" = "Đặt lại lưu lượng toàn bộ người dùng của điểm vào"
|
||||
"resetInboundClientTrafficTitle" = "Đặt lại lưu lượng cho toàn bộ người dùng của điểm vào"
|
||||
"resetInboundClientTrafficContent" = "Bạn có chắc chắn muốn đặt lại tất cả lưu lượng cho các người dùng của điểm vào này không?"
|
||||
"resetAllClientTraffics" = "Đặt lại lưu lượng cho toàn bộ người dùng"
|
||||
"resetAllClientTrafficTitle" = "Đặt lại lưu lượng cho toàn bộ người dùng"
|
||||
"resetAllClientTrafficContent" = "Bạn có chắc chắn muốn đặt lại tất cả lưu lượng cho toàn bộ người dùng không?"
|
||||
"delDepletedClients" = "Xóa các người dùng đã cạn kiệt"
|
||||
"delDepletedClientsTitle" = "Xóa các người dùng đã cạn kiệt"
|
||||
"delDepletedClientsContent" = "Bạn có chắc chắn muốn xóa toàn bộ người dùng đã cạn kiệt không?"
|
||||
"resetInboundClientTraffics" = "Đặt lại lưu lượng cho các client của điểm vào"
|
||||
"resetInboundClientTrafficTitle" = "Đặt lại lưu lượng cho tất cả lưu lượng của client"
|
||||
"resetInboundClientTrafficContent" = "Bạn có chắc chắn muốn đặt lại tất cả lưu lượng cho các client của điểm vào này không?"
|
||||
"resetAllClientTraffics" = "Đặt lại lưu lượng cho tất cả client"
|
||||
"resetAllClientTrafficTitle" = "Đặt lại lưu lượng cho tất cả client"
|
||||
"resetAllClientTrafficContent" = "Bạn có chắc chắn muốn đặt lại tất cả lưu lượng cho tất cả client không?"
|
||||
"delDepletedClients" = "Xóa các client đã cạn kiệt"
|
||||
"delDepletedClientsTitle" = "Xóa các client đã cạn kiệt"
|
||||
"delDepletedClientsContent" = "Bạn có chắc chắn muốn xóa tất cả các client đã cạn kiệt không?"
|
||||
"email" = "Email"
|
||||
"emailDesc" = "Vui lòng cung cấp một địa chỉ email duy nhất."
|
||||
"IPLimit" = "Giới hạn IP"
|
||||
@@ -177,20 +174,20 @@
|
||||
"xtlsDesc" = "Xray core cần phiên bản 1.7.5"
|
||||
"realityDesc" = "Xray core cần phiên bản 1.8.0 hoặc cao hơn."
|
||||
"telegramDesc" = "Chỉ sử dụng ID trò chuyện (bạn có thể nhận được nó ở đây @userinfobot hoặc sử dụng lệnh '/id' trong bot)"
|
||||
"subscriptionDesc" = "Bạn có thể tìm liên kết gói đăng ký của mình trong Chi tiết, cũng như bạn có thể sử dụng cùng tên cho nhiều cấu hình khác nhau"
|
||||
"subscriptionDesc" = "Bạn có thể tìm liên kết đăng ký của mình trong Chi tiết, cũng như bạn có thể sử dụng cùng tên cho nhiều cấu hình khác nhau"
|
||||
"info" = "Thông tin"
|
||||
"same" = "Giống nhau"
|
||||
"inboundData" = "Dữ liệu gửi đến"
|
||||
"exportInbound" = "Xuất nhập khẩu"
|
||||
"copyToClipboard" = "Sao chép vào bảng nhớ tạm"
|
||||
"import" = "Nhập"
|
||||
"importInbound" = "Nhập inbound"
|
||||
"importInbound" = "Nhập hàng gửi về"
|
||||
|
||||
[pages.client]
|
||||
"add" = "Thêm người dùng"
|
||||
"edit" = "Chỉnh sửa người dùng"
|
||||
"submitAdd" = "Thêm"
|
||||
"add" = "Thêm Client"
|
||||
"edit" = "Chỉnh sửa Client"
|
||||
"submitAdd" = "Thêm Client"
|
||||
"submitEdit" = "Lưu thay đổi"
|
||||
"clientCount" = "Số lượng người dùng"
|
||||
"clientCount" = "Số lượng Client"
|
||||
"bulk" = "Thêm hàng loạt"
|
||||
"method" = "Phương pháp"
|
||||
"first" = "Đầu tiên"
|
||||
@@ -215,11 +212,11 @@
|
||||
[pages.inbounds.stream.tcp]
|
||||
"version" = "Phiên bản"
|
||||
"method" = "Phương pháp"
|
||||
"path" = "Đường dẫn"
|
||||
"path" = "Con đường"
|
||||
"status" = "Trạng thái"
|
||||
"statusDescription" = "Tình trạng Mô tả"
|
||||
"requestHeader" = "Header yêu cầu"
|
||||
"responseHeader" = "Header phản hồi"
|
||||
"requestHeader" = "Tiêu đề yêu cầu"
|
||||
"responseHeader" = "Tiêu đề phản hồi"
|
||||
|
||||
[pages.inbounds.stream.quic]
|
||||
"encryption" = "Mã hóa"
|
||||
@@ -228,31 +225,30 @@
|
||||
"title" = "Cài đặt"
|
||||
"save" = "Lưu"
|
||||
"infoDesc" = "Mọi thay đổi được thực hiện ở đây cần phải được lưu. Vui lòng khởi động lại bảng điều khiển để áp dụng các thay đổi."
|
||||
"restartPanel" = "Khởi động lại bảng điều khiển"
|
||||
"restartPanel" = "Khởi động lại Bảng điều khiển"
|
||||
"restartPanelDesc" = "Bạn có chắc chắn muốn khởi động lại bảng điều khiển? Nhấn OK để khởi động lại sau 3 giây. Nếu bạn không thể truy cập bảng điều khiển sau khi khởi động lại, vui lòng xem thông tin nhật ký của bảng điều khiển trên máy chủ."
|
||||
"actions" = "Hành động"
|
||||
"resetDefaultConfig" = "Đặt lại cấu hình mặc định"
|
||||
"panelSettings" = "Bảng điều khiển"
|
||||
"securitySettings" = "Bảo mật"
|
||||
"TGBotSettings" = "Bot Telegram"
|
||||
"panelListeningIP" = "IP Nghe của bảng điều khiển"
|
||||
"resetDefaultConfig" = "Đặt lại Cấu hình Mặc định"
|
||||
"panelSettings" = "Cài đặt Bảng điều khiển"
|
||||
"securitySettings" = "Cài đặt Bảo mật"
|
||||
"TGBotSettings" = "Cài đặt Bot Telegram"
|
||||
"panelListeningIP" = "IP Nghe của Bảng điều khiển"
|
||||
"panelListeningIPDesc" = "Mặc định để trống để nghe tất cả các IP."
|
||||
"panelListeningDomain" = "Tên miền của nghe bảng điều khiển"
|
||||
"panelListeningDomain" = "Tên miền của nghe Bảng điều khiển"
|
||||
"panelListeningDomainDesc" = "Mặc định để trống để nghe tất cả các tên miền và IP"
|
||||
"panelPort" = "Cổng bảng điều khiển"
|
||||
"panelPortDesc" = "Cổng được sử dụng để kết nối với bảng điều khiển này"
|
||||
"publicKeyPath" = "Đường dẫn file chứng chỉ bảng điều khiển"
|
||||
"publicKeyPathDesc" = "Điền vào đường dẫn đầy đủ (bắt đầu từ '/')"
|
||||
"privateKeyPath" = "Đường dẫn file khóa của chứng chỉ bảng điều khiển"
|
||||
"privateKeyPathDesc" = "Điền vào đường dẫn đầy đủ (bắt đầu từ '/')"
|
||||
"panelUrlPath" = "Đường dẫn gốc URL bảng điều khiển"
|
||||
"panelUrlPathDesc" = "Phải bắt đầu và kết thúc bằng '/'"
|
||||
"panelPort" = "Cổng Bảng điều khiển"
|
||||
"panelPortDesc" = "Cổng được sử dụng để hiển thị bảng điều khiển này"
|
||||
"publicKeyPath" = "Đường dẫn tập tin khóa công khai Chứng chỉ Bảng điều khiển"
|
||||
"publicKeyPathDesc" = "Điền vào đường dẫn tuyệt đối bắt đầu với."
|
||||
"privateKeyPath" = "Đường dẫn tập tin khóa riêng tư Chứng chỉ Bảng điều khiển"
|
||||
"privateKeyPathDesc" = "Điền vào đường dẫn tuyệt đối bắt đầu với."
|
||||
"panelUrlPath" = "Đường dẫn gốc URL Bảng điều khiển"
|
||||
"panelUrlPathDesc" = "Phải bắt đầu bằng '/' và kết thúc bằng."
|
||||
"pageSize" = "Kích thước phân trang"
|
||||
"pageSizeDesc" = "Xác định kích thước trang cho bảng gửi đến. Đặt 0 để tắt"
|
||||
"remarkModel" = "Ghi chú mô hình và ký tự phân tách"
|
||||
"datepicker" = "Kiểu lịch"
|
||||
"datepickerPlaceholder" = "Chọn ngày"
|
||||
"datepickerDescription" = "Tác vụ chạy theo lịch trình sẽ chạy theo kiểu lịch này."
|
||||
"datepicker" = "bảng chọn ngày"
|
||||
"datepickerDescription" = "Loại lịch chọn chỉ định ngày hết hạn"
|
||||
"sampleRemark" = "Nhận xét mẫu"
|
||||
"oldUsername" = "Tên người dùng hiện tại"
|
||||
"currentPassword" = "Mật khẩu hiện tại"
|
||||
@@ -272,7 +268,7 @@
|
||||
"tgNotifyBackupDesc" = "Bao gồm tệp sao lưu cơ sở dữ liệu với thông báo báo cáo."
|
||||
"tgNotifyLogin" = "Thông báo Đăng nhập"
|
||||
"tgNotifyLoginDesc" = "Hiển thị tên người dùng, địa chỉ IP và thời gian khi ai đó cố gắng đăng nhập vào bảng điều khiển của bạn."
|
||||
"sessionMaxAge" = "Thời gian tối đa của phiên"
|
||||
"sessionMaxAge" = "Tuổi tối đa của phiên"
|
||||
"sessionMaxAgeDesc" = "Thời gian của phiên đăng nhập (đơn vị: phút)"
|
||||
"expireTimeDiff" = "Ngưỡng hết hạn cho thông báo"
|
||||
"expireTimeDiffDesc" = "Nhận thông báo về việc hết hạn tài khoản trước ngưỡng này (đơn vị: ngày)"
|
||||
@@ -282,31 +278,29 @@
|
||||
"tgNotifyCpuDesc" = "Nhận thông báo nếu tỷ lệ sử dụng CPU vượt quá ngưỡng này (đơn vị: %)"
|
||||
"timeZone" = "Múi giờ"
|
||||
"timeZoneDesc" = "Các tác vụ được lên lịch chạy theo thời gian trong múi giờ này."
|
||||
"subSettings" = "Gói đăng ký"
|
||||
"subSettings" = "Đăng ký"
|
||||
"subEnable" = "Bật dịch vụ"
|
||||
"subEnableDesc" = "Tính năng gói đăng ký với cấu hình riêng"
|
||||
"subEnableDesc" = "Tính năng đăng ký với cấu hình riêng"
|
||||
"subListen" = "Listening IP"
|
||||
"subListenDesc" = "Mặc định để trống để nghe tất cả các IP"
|
||||
"subPort" = "Cổng gói đăng ký"
|
||||
"subPort" = "Cổng Đăng ký"
|
||||
"subPortDesc" = "Số cổng dịch vụ đăng ký phải chưa được sử dụng trên máy chủ"
|
||||
"subCertPath" = "Đường dẫn file chứng chỉ gói đăng ký"
|
||||
"subCertPathDesc" = "Điền vào đường dẫn đầy đủ (bắt đầu với '/')"
|
||||
"subKeyPath" = "Đường dẫn file khóa của chứng chỉ gói đăng ký"
|
||||
"subKeyPathDesc" = "Điền vào đường dẫn đầy đủ (bắt đầu với '/')"
|
||||
"subPath" = "Đường dẫn gốc URL gói đăng ký"
|
||||
"subPathDesc" = "Phải bắt đầu và kết thúc bằng '/'"
|
||||
"subCertPath" = "Đường dẫn tập tin khóa công khai Chứng chỉ Đăng ký"
|
||||
"subCertPathDesc" = "Điền vào đường dẫn tuyệt đối bắt đầu với '/'"
|
||||
"subKeyPath" = "Đường dẫn tập tin khóa riêng tư Chứng chỉ Đăng ký"
|
||||
"subKeyPathDesc" = "Điền vào đường dẫn tuyệt đối bắt đầu với '/'"
|
||||
"subPath" = "Đường dẫn gốc URL Đăng ký"
|
||||
"subPathDesc" = "Phải bắt đầu bằng '/' và kết thúc bằng '/'"
|
||||
"subDomain" = "Tên miền con"
|
||||
"subDomainDesc" = "Mặc định để trống để nghe tất cả các tên miền và IP"
|
||||
"subUpdates" = "Khoảng thời gian cập nhật gói đăng ký"
|
||||
"subUpdates" = "Khoảng thời gian cập nhật đăng ký"
|
||||
"subUpdatesDesc" = "Số giờ giữa các cập nhật trong ứng dụng khách"
|
||||
"subEncrypt" = "Mã hóa cấu hình"
|
||||
"subEncryptDesc" = "Mã hóa các cấu hình được trả về trong gói đăng ký"
|
||||
"subEncryptDesc" = "Mã hóa các cấu hình được trả về trong đăng ký"
|
||||
"subShowInfo" = "Hiển thị thông tin sử dụng"
|
||||
"subShowInfoDesc" = "Hiển thị lưu lượng truy cập còn lại và ngày sau tên cấu hình"
|
||||
"subURI" = "URI proxy trung gian"
|
||||
"subURIDesc" = "Thay đổi URI cơ sở của URL gói đăng ký để sử dụng cho proxy trung gian"
|
||||
"fragment" = "Sự phân mảnh"
|
||||
"fragmentDesc" = "Kích hoạt phân mảnh cho gói TLS hello"
|
||||
"subURI" = "URI proxy ngược"
|
||||
"subURIDesc" = "Thay đổi URI cơ sở của URL đăng ký để sử dụng ở phía sau proxy"
|
||||
|
||||
[pages.xray]
|
||||
"title" = "Cài đặt Xray"
|
||||
@@ -316,8 +310,6 @@
|
||||
"advancedTemplate" = "Mẫu Nâng cao"
|
||||
"generalConfigs" = "Cấu hình Chung"
|
||||
"generalConfigsDesc" = "Những tùy chọn này sẽ cung cấp điều chỉnh tổng quát."
|
||||
"logConfigs" = "Nhật ký"
|
||||
"logConfigsDesc" = "Nhật ký có thể ảnh hưởng đến hiệu suất máy chủ của bạn. Bạn chỉ nên kích hoạt nó một cách khôn ngoan trong trường hợp bạn cần"
|
||||
"blockConfigs" = "Cấu hình Chặn"
|
||||
"blockConfigsDesc" = "Những tùy chọn này sẽ ngăn người dùng kết nối đến các giao thức và trang web cụ thể."
|
||||
"blockCountryConfigs" = "Cấu hình Chặn Quốc gia"
|
||||
@@ -382,36 +374,23 @@
|
||||
"GoogleIPv4Desc" = "Thêm định tuyến cho Google để kết nối qua IPv4."
|
||||
"NetflixIPv4" = "Sử dụng IPv4 cho Netflix"
|
||||
"NetflixIPv4Desc" = "Thêm định tuyến cho Netflix để kết nối qua IPv4."
|
||||
"GoogleWARP" = "Google"
|
||||
"GoogleWARPDesc" = "Định tuyến lưu lượng truy cập tới Google thông qua WARP."
|
||||
"OpenAIWARP" = "OpenAI (ChatGPT)"
|
||||
"OpenAIWARPDesc" = "Định tuyến lưu lượng truy cập tới OpenAI (ChatGPT) thông qua WARP."
|
||||
"NetflixWARP" = "Netflix"
|
||||
"NetflixWARPDesc" = "Định tuyến lưu lượng truy cập tới Netflix thông qua WARP."
|
||||
"MetaWARP" = "Meta"
|
||||
"MetaWARPDesc" = "Định tuyến lưu lượng truy cập tới Meta (Instagram, Facebook, WhatsApp, Threads,...) thông qua WARP."
|
||||
"AppleWARP" = "Apple"
|
||||
"AppleWARPDesc" = "Định tuyến lưu lượng truy cập tới Apple thông qua WARP."
|
||||
"RedditWARP" = "Reddit"
|
||||
"RedditWARPDesc" = "Định tuyến lưu lượng truy cập tới Reddit thông qua WARP."
|
||||
"SpotifyWARP" = "Spotify"
|
||||
"SpotifyWARPDesc" = "Định tuyến lưu lượng truy cập tới Spotify thông qua WARP."
|
||||
"GoogleWARP" = "Định tuyến Google qua WARP."
|
||||
"GoogleWARPDesc" = "Thêm định tuyến cho Google qua WARP."
|
||||
"OpenAIWARP" = "Định tuyến OpenAI (ChatGPT) qua WARP."
|
||||
"OpenAIWARPDesc" = "Thêm định tuyến cho OpenAI (ChatGPT) qua WARP."
|
||||
"NetflixWARP" = "Định tuyến Netflix qua WARP."
|
||||
"NetflixWARPDesc" = "Thêm định tuyến cho Netflix qua WARP."
|
||||
"SpotifyWARP" = "Định tuyến Spotify qua WARP."
|
||||
"SpotifyWARPDesc" = "Thêm định tuyến cho Spotify qua WARP."
|
||||
"IRWARP" = "Định tuyến tên miền của Iran qua WARP."
|
||||
"IRWARPDesc" = "Thêm định tuyến cho các tên miền của Iran qua WARP."
|
||||
"Inbounds" = "Đầu vào"
|
||||
"InboundsDesc" = "Thay đổi mẫu cấu hình để chấp nhận các máy khách cụ thể."
|
||||
"Outbounds" = "Đầu ra"
|
||||
"Balancers" = "Cân bằng"
|
||||
"OutboundsDesc" = "Thay đổi mẫu cấu hình để xác định các cách ra đi cho máy chủ này."
|
||||
"Routings" = "Quy tắc định tuyến"
|
||||
"RoutingsDesc" = "Mức độ ưu tiên của mỗi quy tắc đều quan trọng!"
|
||||
"completeTemplate" = "All"
|
||||
"logLevel" = "Mức đăng nhập"
|
||||
"logLevelDesc" = "Cấp độ nhật ký cho nhật ký lỗi, cho biết thông tin cần được ghi lại."
|
||||
"accessLog" = "Nhật ký truy cập"
|
||||
"accessLogDesc" = "Đường dẫn tệp cho nhật ký truy cập. Nhật ký truy cập bị vô hiệu hóa có giá trị đặc biệt 'không'"
|
||||
"errorLog" = "Nhật ký lỗi"
|
||||
"errorLogDesc" = "Đường dẫn tệp cho nhật ký lỗi. Nhật ký lỗi bị vô hiệu hóa có giá trị đặc biệt 'không'"
|
||||
|
||||
[pages.xray.rules]
|
||||
"first" = "Đầu tiên"
|
||||
@@ -422,7 +401,6 @@
|
||||
"dest" = "Đích"
|
||||
"inbound" = "Vào"
|
||||
"outbound" = "Ra"
|
||||
"balancer" = "Cân bằng"
|
||||
"info" = "Thông tin"
|
||||
"add" = "Thêm quy tắc"
|
||||
"edit" = "Chỉnh sửa quy tắc"
|
||||
@@ -443,45 +421,21 @@
|
||||
"portal" = "Cổng thông tin"
|
||||
"intercon" = "Kết nối"
|
||||
|
||||
[pages.xray.balancer]
|
||||
"addBalancer" = "Thêm cân bằng"
|
||||
"editBalancer" = "Chỉnh sửa cân bằng"
|
||||
"balancerStrategy" = "Chiến lược"
|
||||
"balancerSelectors" = "Bộ chọn"
|
||||
"tag" = "Thẻ"
|
||||
"tagDesc" = "thẻ duy nhất"
|
||||
"balancerDesc" = "Không thể sử dụng balancerTag và outboundTag cùng một lúc. Nếu sử dụng cùng lúc thì chỉ outboundTag mới hoạt động."
|
||||
|
||||
[pages.xray.wireguard]
|
||||
"secretKey" = "Khoá bí mật"
|
||||
"secretKey" = "Chìa khoá bí mật"
|
||||
"publicKey" = "Khóa công khai"
|
||||
"allowedIPs" = "IP được phép"
|
||||
"endpoint" = "Điểm cuối"
|
||||
"psk" = "Khóa chia sẻ"
|
||||
"domainStrategy" = "Chiến lược tên miền"
|
||||
|
||||
[pages.xray.dns]
|
||||
"enable" = "Kích hoạt DNS"
|
||||
"enableDesc" = "Kích hoạt máy chủ DNS tích hợp"
|
||||
"strategy" = "Chiến lược truy vấn"
|
||||
"strategyDesc" = "Chiến lược tổng thể để phân giải tên miền"
|
||||
"add" = "Thêm máy chủ"
|
||||
"edit" = "Chỉnh sửa máy chủ"
|
||||
"domains" = "Tên miền"
|
||||
|
||||
[pages.xray.fakedns]
|
||||
"add" = "Thêm DNS giả"
|
||||
"edit" = "Chỉnh sửa DNS giả"
|
||||
"ipPool" = "Mạng con nhóm IP"
|
||||
"poolSize" = "Kích thước bể bơi"
|
||||
|
||||
[pages.settings.security]
|
||||
"admin" = "Quản trị viên"
|
||||
"secret" = "Mã thông báo bí mật"
|
||||
"loginSecurity" = "Bảo mật đăng nhập"
|
||||
"loginSecurityDesc" = "Bật bước bảo mật đăng nhập bổ sung cho người dùng"
|
||||
"secretToken" = "Mã bí mật"
|
||||
"secretTokenDesc" = "Vui lòng sao chép và lưu trữ mã này một cách an toàn ở nơi an toàn. Mã này cần thiết để đăng nhập và không thể phục hồi từ công cụ lệnh x-ui."
|
||||
"secretToken" = "Mã thông báo bí mật"
|
||||
"secretTokenDesc" = "Vui lòng sao chép và lưu trữ mã thông báo này một cách an toàn ở nơi an toàn. Mã thông báo này cần thiết để đăng nhập và không thể phục hồi từ công cụ lệnh x-ui."
|
||||
|
||||
[pages.settings.toasts]
|
||||
"modifySettings" = "Chỉnh sửa cài đặt "
|
||||
@@ -506,7 +460,7 @@
|
||||
"hours" = "Giờ"
|
||||
"unknown" = "Không rõ"
|
||||
"inbounds" = "Vào"
|
||||
"clients" = "Các người dùng"
|
||||
"clients" = "Các khách hàng"
|
||||
"offline" = "🔴 Ngoại tuyến"
|
||||
"online" = "🟢 Trực tuyến"
|
||||
|
||||
|
||||
@@ -52,9 +52,6 @@
|
||||
"secretToken" = "安全密钥"
|
||||
"remained" = "剩余"
|
||||
"security" = "安全"
|
||||
"secAlertTitle" = "安全警报"
|
||||
"secAlertSsl" = "此连接不安全;在激活 TLS 进行数据保护之前,请勿输入敏感信息"
|
||||
"secAlertConf" = "某些配置已被确定为容易受到攻击,促使立即采取行动以加强安全协议并防范潜在的安全漏洞。"
|
||||
|
||||
[menu]
|
||||
"dashboard" = "系统状态"
|
||||
@@ -79,7 +76,7 @@
|
||||
"title" = "系统状态"
|
||||
"memory" = "内存"
|
||||
"hard" = "硬盘"
|
||||
"xrayStatus" = "Xray"
|
||||
"xrayStatus" = "状态"
|
||||
"stopXray" = "停止"
|
||||
"restartXray" = "重启"
|
||||
"xraySwitch" = "版本"
|
||||
@@ -181,7 +178,7 @@
|
||||
"info" = "信息"
|
||||
"same" = "相同"
|
||||
"inboundData" = "入站数据"
|
||||
"exportInbound" = "出口 入境"
|
||||
"copyToClipboard" = "复制到剪贴板"
|
||||
"import"="导入"
|
||||
"importInbound" = "导入入站"
|
||||
|
||||
@@ -251,7 +248,6 @@
|
||||
"pageSizeDesc" = "定义入站表的页面大小。设置 0 表示禁用"
|
||||
"remarkModel" = "备注模型和分隔符"
|
||||
"datepicker" = "日期选择器"
|
||||
"datepickerPlaceholder" = "选择日期"
|
||||
"datepickerDescription" = "选择器日历类型指定到期日期"
|
||||
"sampleRemark" = "备注示例"
|
||||
"oldUsername" = "原用户名"
|
||||
@@ -305,8 +301,6 @@
|
||||
"subShowInfoDesc" = "在配置名称后显示剩余流量和日期"
|
||||
"subURI" = "反向代理 URI"
|
||||
"subURIDesc" = "更改订阅 URL 的基本 URI 以在代理后面使用"
|
||||
"fragment" = "碎片"
|
||||
"fragmentDesc" = "启用 TLS hello 数据包分段"
|
||||
|
||||
[pages.xray]
|
||||
"title" = "Xray 设置"
|
||||
@@ -316,8 +310,6 @@
|
||||
"advancedTemplate" = "高级模板部件"
|
||||
"generalConfigs" = "通用配置"
|
||||
"generalConfigsDesc" = "这些选项将提供一般调整"
|
||||
"logConfigs"="日志"
|
||||
"logConfigsDesc" = "日志可能会影响您服务器的效率。建议仅在您需要时明智地启用它"
|
||||
"blockConfigs" = "阻塞配置"
|
||||
"blockConfigsDesc" = "这些选项将阻止用户连接到特定协议和网站"
|
||||
"blockCountryConfigs" = "阻止国家配置"
|
||||
@@ -382,36 +374,23 @@
|
||||
"GoogleIPv4Desc" = "添加谷歌连接IPv4的路由"
|
||||
"NetflixIPv4" = "为 Netflix 使用 IPv4"
|
||||
"NetflixIPv4Desc" = "添加Netflix连接IPv4的路由"
|
||||
"GoogleWARP" = "Google"
|
||||
"GoogleWARPDesc" = "通过 WARP 将流量路由到 Google。"
|
||||
"OpenAIWARP" = "OpenAI (ChatGPT)"
|
||||
"OpenAIWARPDesc" = "通过 WARP 将流量路由到 OpenAI (ChatGPT)。"
|
||||
"NetflixWARP" = "Netflix"
|
||||
"NetflixWARPDesc" = "通过 WARP 将流量路由到 Netflix。"
|
||||
"MetaWARP"="Meta"
|
||||
"MetaWARPDesc" = "通过 WARP 将流量路由到 Meta(Instagram、Facebook、WhatsApp、Threads...)"
|
||||
"AppleWARP" = "Apple"
|
||||
"AppleWARPDesc" = "通过 WARP 将流量路由到 Apple。"
|
||||
"RedditWARP" = "Reddit"
|
||||
"RedditWARPDesc" = "通过 WARP 将流量路由到 Reddit。"
|
||||
"SpotifyWARP" = "Spotify"
|
||||
"SpotifyWARPDesc" = "通过 WARP 将流量路由到 Spotify。"
|
||||
"GoogleWARP" = "将谷歌路由到 WARP"
|
||||
"GoogleWARPDesc" = "为谷歌添加路由到WARP"
|
||||
"OpenAIWARP" = "将 OpenAI (ChatGPT) 路由到 WARP"
|
||||
"OpenAIWARPDesc" = "将OpenAI(ChatGPT)路由添加到WARP"
|
||||
"NetflixWARP" = "将 Netflix 路由到 WARP"
|
||||
"NetflixWARPDesc" = "为Netflix添加路由到WARP"
|
||||
"SpotifyWARP" = "将 Spotify 路由到 WARP"
|
||||
"SpotifyWARPDesc" = "为Spotify添加路由到WARP"
|
||||
"IRWARP" = "将伊朗域名路由到 WARP"
|
||||
"IRWARPDesc" = "将伊朗域的路由添加到 WARP。 重启面板生效"
|
||||
"Inbounds" = "入站"
|
||||
"InboundsDesc" = "更改配置模板接受特殊客户端"
|
||||
"Outbounds" = "出站"
|
||||
"Balancers" = "平衡器"
|
||||
"OutboundsDesc" = "更改配置模板定义此服务器的传出方式"
|
||||
"Routings" = "路由规则"
|
||||
"RoutingsDesc" = "每条规则的优先级都很重要"
|
||||
"completeTemplate" = "全部"
|
||||
"logLevel" = "日志级别"
|
||||
"logLevelDesc" = "错误日志的日志级别,表示需要记录的信息。"
|
||||
"accessLog" = "访问日志"
|
||||
"accessLogDesc" = "访问日志的文件路径。 特殊值“none”禁用访问日志"
|
||||
"errorLog" = "错误日志"
|
||||
"errorLogDesc" = "错误日志的文件路径。 特殊值“none”禁用错误日志"
|
||||
|
||||
[pages.xray.rules]
|
||||
"first" = "第一个"
|
||||
@@ -422,7 +401,6 @@
|
||||
"dest" = "目的地"
|
||||
"inbound" = "入站"
|
||||
"outbound" = "出站"
|
||||
"balancer" = "平衡器"
|
||||
"info" = "信息"
|
||||
"add" = "添加规则"
|
||||
"edit" = "编辑规则"
|
||||
@@ -443,15 +421,6 @@
|
||||
"portal" = "门户"
|
||||
"intercon" = "互连"
|
||||
|
||||
[pages.xray.balancer]
|
||||
"addBalancer" = "添加平衡器"
|
||||
"editBalancer" = "编辑平衡器"
|
||||
"balancerStrategy" = "战略"
|
||||
"balancerSelectors" = "选择器"
|
||||
"tag" = "标签"
|
||||
"tagDesc" = "唯一标记"
|
||||
"balancerDesc" = "不能同时使用balancerTag和outboundTag。 如果同时使用,则只有outboundTag起作用。"
|
||||
|
||||
[pages.xray.wireguard]
|
||||
"secretKey" = "密钥"
|
||||
"publicKey" = "公钥"
|
||||
@@ -460,21 +429,6 @@
|
||||
"psk" = "共享密钥"
|
||||
"domainStrategy" = "域策略"
|
||||
|
||||
[pages.xray.dns]
|
||||
"enable" = "启用 DNS"
|
||||
"enableDesc" = "启用内置 DNS 服务器"
|
||||
"strategy" = "查询策略"
|
||||
"strategyDesc" = "解析域名的总体策略"
|
||||
"add" = "添加服务器"
|
||||
"edit" = "编辑服务器"
|
||||
"domains" = "域"
|
||||
|
||||
[pages.xray.fakedns]
|
||||
"add" = "添加假 DNS"
|
||||
"edit" = "编辑假 DNS"
|
||||
"ipPool" = "IP 池子网"
|
||||
"poolSize" = "池大小"
|
||||
|
||||
[pages.settings.security]
|
||||
"admin" = "管理员"
|
||||
"secret" = "密钥"
|
||||
@@ -549,7 +503,7 @@
|
||||
"port" = "🔌 端口:{{ .Port }}\r\n"
|
||||
"expire" = "📅 过期日期:{{ .Time }}\r\n"
|
||||
"expireIn" = "📅 剩余时间:{{ .Time }}\r\n"
|
||||
"active" = "💡 激活:{{ .Enable }}\r\n"
|
||||
"active" = "💡 激活:✅\r\n"
|
||||
"enabled" = "🚨 已启用:{{ .Enable }}\r\n"
|
||||
"online" = "🌐 连接状态:{{ .Status }}\r\n"
|
||||
"email" = "📧 邮箱:{{ .Email }}\r\n"
|
||||
|
||||
505
x-ui.sh
505
x-ui.sh
@@ -70,11 +70,13 @@ elif [[ "${release}" == "armbian" ]]; then
|
||||
echo "Your OS is Armbian"
|
||||
fi
|
||||
|
||||
|
||||
# Declare Variables
|
||||
log_folder="${XUI_LOG_FOLDER:=/var/log}"
|
||||
iplimit_log_path="${log_folder}/3xipl.log"
|
||||
iplimit_banned_log_path="${log_folder}/3xipl-banned.log"
|
||||
|
||||
|
||||
confirm() {
|
||||
if [[ $# > 1 ]]; then
|
||||
echo && read -p "$1 [Default $2]: " temp
|
||||
@@ -138,7 +140,7 @@ custom_version() {
|
||||
|
||||
if [ -z "$panel_version" ]; then
|
||||
echo "Panel version cannot be empty. Exiting."
|
||||
exit 1
|
||||
exit 1
|
||||
fi
|
||||
|
||||
download_link="https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh"
|
||||
@@ -150,12 +152,6 @@ custom_version() {
|
||||
eval $install_command
|
||||
}
|
||||
|
||||
# Function to handle the deletion of the script file
|
||||
delete_script() {
|
||||
rm "$0" # Remove the script file itself
|
||||
exit 1
|
||||
}
|
||||
|
||||
uninstall() {
|
||||
confirm "Are you sure you want to uninstall the panel? xray will also uninstalled!" "n"
|
||||
if [[ $? != 0 ]]; then
|
||||
@@ -173,13 +169,12 @@ uninstall() {
|
||||
rm /usr/local/x-ui/ -rf
|
||||
|
||||
echo ""
|
||||
echo -e "Uninstalled Successfully.\n"
|
||||
echo "If you need to install this panel again, you can use below command:"
|
||||
echo -e "${green}bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)${plain}"
|
||||
echo -e "Uninstalled Successfully, If you want to remove this script, then after exiting the script run ${green}rm /usr/bin/x-ui -f${plain} to delete it."
|
||||
echo ""
|
||||
# Trap the SIGTERM signal
|
||||
trap delete_script SIGTERM
|
||||
delete_script
|
||||
|
||||
if [[ $# == 0 ]]; then
|
||||
before_show_menu
|
||||
fi
|
||||
}
|
||||
|
||||
reset_user() {
|
||||
@@ -334,56 +329,15 @@ show_log() {
|
||||
}
|
||||
|
||||
show_banlog() {
|
||||
if test -f "${iplimit_banned_log_path}"; then
|
||||
if [[ -s "${iplimit_banned_log_path}" ]]; then
|
||||
cat ${iplimit_banned_log_path}
|
||||
else
|
||||
echo -e "${red}Log file is empty.${plain}\n"
|
||||
fi
|
||||
if test -f "${iplimit_banned_log_path}"; then
|
||||
if [[ -s "${iplimit_banned_log_path}" ]]; then
|
||||
cat ${iplimit_banned_log_path}
|
||||
else
|
||||
echo -e "${red}Log file not found. Please Install Fail2ban and IP Limit first.${plain}\n"
|
||||
fi
|
||||
}
|
||||
|
||||
bbr_menu() {
|
||||
echo -e "${green}\t1.${plain} Enable BBR"
|
||||
echo -e "${green}\t2.${plain} Disable BBR"
|
||||
echo -e "${green}\t0.${plain} Back to Main Menu"
|
||||
read -p "Choose an option: " choice
|
||||
case "$choice" in
|
||||
0)
|
||||
show_menu
|
||||
;;
|
||||
1)
|
||||
enable_bbr
|
||||
;;
|
||||
2)
|
||||
disable_bbr
|
||||
;;
|
||||
*) echo "Invalid choice" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
disable_bbr() {
|
||||
|
||||
if ! grep -q "net.core.default_qdisc=fq" /etc/sysctl.conf || ! grep -q "net.ipv4.tcp_congestion_control=bbr" /etc/sysctl.conf; then
|
||||
echo -e "${yellow}BBR is not currently enabled.${plain}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Replace BBR with CUBIC configurations
|
||||
sed -i 's/net.core.default_qdisc=fq/net.core.default_qdisc=pfifo_fast/' /etc/sysctl.conf
|
||||
sed -i 's/net.ipv4.tcp_congestion_control=bbr/net.ipv4.tcp_congestion_control=cubic/' /etc/sysctl.conf
|
||||
|
||||
# Apply changes
|
||||
sysctl -p
|
||||
|
||||
# Verify that BBR is replaced with CUBIC
|
||||
if [[ $(sysctl net.ipv4.tcp_congestion_control | awk '{print $3}') == "cubic" ]]; then
|
||||
echo -e "${green}BBR has been replaced with CUBIC successfully.${plain}"
|
||||
else
|
||||
echo -e "${red}Failed to replace BBR with CUBIC. Please check your system configuration.${plain}"
|
||||
echo -e "${red}Log file is empty.${plain}\n"
|
||||
fi
|
||||
else
|
||||
echo -e "${red}Log file not found. Please Install Fail2ban and IP Limit first.${plain}\n"
|
||||
fi
|
||||
}
|
||||
|
||||
enable_bbr() {
|
||||
@@ -394,19 +348,19 @@ enable_bbr() {
|
||||
|
||||
# Check the OS and install necessary packages
|
||||
case "${release}" in
|
||||
ubuntu | debian)
|
||||
apt-get update && apt-get install -yqq --no-install-recommends ca-certificates
|
||||
;;
|
||||
centos | almalinux | rocky)
|
||||
yum -y update && yum -y install ca-certificates
|
||||
;;
|
||||
fedora)
|
||||
dnf -y update && dnf -y install ca-certificates
|
||||
;;
|
||||
*)
|
||||
echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n"
|
||||
exit 1
|
||||
;;
|
||||
ubuntu|debian)
|
||||
apt-get update && apt-get install -yqq --no-install-recommends ca-certificates
|
||||
;;
|
||||
centos|almalinux|rocky)
|
||||
yum -y update && yum -y install ca-certificates
|
||||
;;
|
||||
fedora)
|
||||
dnf -y update && dnf -y install ca-certificates
|
||||
;;
|
||||
*)
|
||||
echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Enable BBR
|
||||
@@ -531,33 +485,6 @@ show_xray_status() {
|
||||
fi
|
||||
}
|
||||
|
||||
firewall_menu() {
|
||||
echo -e "${green}\t1.${plain} Install Firewall & open ports"
|
||||
echo -e "${green}\t2.${plain} Allowed List"
|
||||
echo -e "${green}\t3.${plain} Delete Ports from List"
|
||||
echo -e "${green}\t4.${plain} Disable Firewall"
|
||||
echo -e "${green}\t0.${plain} Back to Main Menu"
|
||||
read -p "Choose an option: " choice
|
||||
case "$choice" in
|
||||
0)
|
||||
show_menu
|
||||
;;
|
||||
1)
|
||||
open_ports
|
||||
;;
|
||||
2)
|
||||
sudo ufw status
|
||||
;;
|
||||
3)
|
||||
delete_ports
|
||||
;;
|
||||
4)
|
||||
sudo ufw disable
|
||||
;;
|
||||
*) echo "Invalid choice" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
open_ports() {
|
||||
if ! command -v ufw &>/dev/null; then
|
||||
echo "ufw firewall is not installed. Installing now..."
|
||||
@@ -610,37 +537,6 @@ open_ports() {
|
||||
ufw status | grep $ports
|
||||
}
|
||||
|
||||
delete_ports() {
|
||||
# Prompt the user to enter the ports they want to delete
|
||||
read -p "Enter the ports you want to delete (e.g. 80,443,2053 or range 400-500): " ports
|
||||
|
||||
# Check if the input is valid
|
||||
if ! [[ $ports =~ ^([0-9]+|[0-9]+-[0-9]+)(,([0-9]+|[0-9]+-[0-9]+))*$ ]]; then
|
||||
echo "Error: Invalid input. Please enter a comma-separated list of ports or a range of ports (e.g. 80,443,2053 or 400-500)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Delete the specified ports using ufw
|
||||
IFS=',' read -ra PORT_LIST <<<"$ports"
|
||||
for port in "${PORT_LIST[@]}"; do
|
||||
if [[ $port == *-* ]]; then
|
||||
# Split the range into start and end ports
|
||||
start_port=$(echo $port | cut -d'-' -f1)
|
||||
end_port=$(echo $port | cut -d'-' -f2)
|
||||
# Loop through the range and delete each port
|
||||
for ((i = start_port; i <= end_port; i++)); do
|
||||
ufw delete allow $i
|
||||
done
|
||||
else
|
||||
ufw delete allow "$port"
|
||||
fi
|
||||
done
|
||||
|
||||
# Confirm that the ports are deleted
|
||||
echo "Deleted the specified ports:"
|
||||
ufw status | grep $ports
|
||||
}
|
||||
|
||||
update_geo() {
|
||||
local defaultBinFolder="/usr/local/x-ui/bin"
|
||||
read -p "Please enter x-ui bin folder path. Leave blank for default. (Default: '${defaultBinFolder}')" binFolder
|
||||
@@ -685,24 +581,21 @@ ssl_cert_issue_main() {
|
||||
echo -e "${green}\t0.${plain} Back to Main Menu"
|
||||
read -p "Choose an option: " choice
|
||||
case "$choice" in
|
||||
0)
|
||||
show_menu
|
||||
;;
|
||||
1)
|
||||
ssl_cert_issue
|
||||
;;
|
||||
2)
|
||||
local domain=""
|
||||
read -p "Please enter your domain name to revoke the certificate: " domain
|
||||
~/.acme.sh/acme.sh --revoke -d ${domain}
|
||||
LOGI "Certificate revoked"
|
||||
;;
|
||||
3)
|
||||
local domain=""
|
||||
read -p "Please enter your domain name to forcefully renew an SSL certificate: " domain
|
||||
~/.acme.sh/acme.sh --renew -d ${domain} --force
|
||||
;;
|
||||
*) echo "Invalid choice" ;;
|
||||
0)
|
||||
show_menu ;;
|
||||
1)
|
||||
ssl_cert_issue ;;
|
||||
2)
|
||||
local domain=""
|
||||
read -p "Please enter your domain name to revoke the certificate: " domain
|
||||
~/.acme.sh/acme.sh --revoke -d ${domain}
|
||||
LOGI "Certificate revoked"
|
||||
;;
|
||||
3)
|
||||
local domain=""
|
||||
read -p "Please enter your domain name to forcefully renew an SSL certificate: " domain
|
||||
~/.acme.sh/acme.sh --renew -d ${domain} --force ;;
|
||||
*) echo "Invalid choice" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
@@ -718,19 +611,15 @@ ssl_cert_issue() {
|
||||
fi
|
||||
# install socat second
|
||||
case "${release}" in
|
||||
ubuntu | debian | armbian)
|
||||
apt update && apt install socat -y
|
||||
;;
|
||||
centos | almalinux | rocky)
|
||||
yum -y update && yum -y install socat
|
||||
;;
|
||||
fedora)
|
||||
dnf -y update && dnf -y install socat
|
||||
;;
|
||||
*)
|
||||
echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n"
|
||||
exit 1
|
||||
;;
|
||||
ubuntu|debian|armbian)
|
||||
apt update && apt install socat -y ;;
|
||||
centos|almalinux|rocky)
|
||||
yum -y update && yum -y install socat ;;
|
||||
fedora)
|
||||
dnf -y update && dnf -y install socat ;;
|
||||
*)
|
||||
echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n"
|
||||
exit 1 ;;
|
||||
esac
|
||||
if [ $? -ne 0 ]; then
|
||||
LOGE "install socat failed, please check logs"
|
||||
@@ -861,8 +750,8 @@ ssl_cert_issue_CF() {
|
||||
LOGI "Certificate issued Successfully, Installing..."
|
||||
fi
|
||||
~/.acme.sh/acme.sh --installcert -d ${CF_Domain} -d *.${CF_Domain} --ca-file /root/cert/ca.cer \
|
||||
--cert-file /root/cert/${CF_Domain}.cer --key-file /root/cert/${CF_Domain}.key \
|
||||
--fullchain-file /root/cert/fullchain.cer
|
||||
--cert-file /root/cert/${CF_Domain}.cer --key-file /root/cert/${CF_Domain}.key \
|
||||
--fullchain-file /root/cert/fullchain.cer
|
||||
if [ $? -ne 0 ]; then
|
||||
LOGE "Certificate installation failed, script exiting..."
|
||||
exit 1
|
||||
@@ -893,46 +782,72 @@ warp_cloudflare() {
|
||||
echo -e "${green}\t0.${plain} Back to Main Menu"
|
||||
read -p "Choose an option: " choice
|
||||
case "$choice" in
|
||||
0)
|
||||
show_menu
|
||||
;;
|
||||
1)
|
||||
bash <(curl -sSL https://raw.githubusercontent.com/hamid-gh98/x-ui-scripts/main/install_warp_proxy.sh)
|
||||
;;
|
||||
2)
|
||||
warp a
|
||||
;;
|
||||
3)
|
||||
warp y
|
||||
;;
|
||||
4)
|
||||
warp u
|
||||
;;
|
||||
*) echo "Invalid choice" ;;
|
||||
0)
|
||||
show_menu ;;
|
||||
1)
|
||||
bash <(curl -sSL https://raw.githubusercontent.com/hamid-gh98/x-ui-scripts/main/install_warp_proxy.sh)
|
||||
;;
|
||||
2)
|
||||
warp a
|
||||
;;
|
||||
3)
|
||||
warp y
|
||||
;;
|
||||
4)
|
||||
warp u
|
||||
;;
|
||||
*) echo "Invalid choice" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
multi_protocol() {
|
||||
echo "This script only supports Vless and Vmess. if you use another protocols, DON'T INSTALL or get backup first! "
|
||||
echo -e "${green}\t1.${plain} Install Multi Protocol Script"
|
||||
echo -e "${green}\t2.${plain} Uninstall"
|
||||
echo -e "${green}\t3.${plain} Start Service"
|
||||
echo -e "${green}\t4.${plain} Stop Service"
|
||||
echo -e "${green}\t0.${plain} Back to Main Menu"
|
||||
read -p "Choose an option: " choice
|
||||
case "$choice" in
|
||||
0)
|
||||
show_menu ;;
|
||||
1)
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/M4mmad/3xui-multi-protocol/master/install.sh --ipv4)
|
||||
;;
|
||||
2)
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/M4mmad/3xui-multi-protocol/master/unistall.sh --ipv4)
|
||||
;;
|
||||
3)
|
||||
systemctl start 3xui-multi-protocol
|
||||
;;
|
||||
4)
|
||||
systemctl stop 3xui-multi-protocol
|
||||
;;
|
||||
*) echo "Invalid choice" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
run_speedtest() {
|
||||
# Check if Speedtest is already installed
|
||||
if ! command -v speedtest &>/dev/null; then
|
||||
if ! command -v speedtest &> /dev/null; then
|
||||
# If not installed, install it
|
||||
local pkg_manager=""
|
||||
local speedtest_install_script=""
|
||||
|
||||
if command -v dnf &>/dev/null; then
|
||||
|
||||
if command -v dnf &> /dev/null; then
|
||||
pkg_manager="dnf"
|
||||
speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh"
|
||||
elif command -v yum &>/dev/null; then
|
||||
elif command -v yum &> /dev/null; then
|
||||
pkg_manager="yum"
|
||||
speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh"
|
||||
elif command -v apt-get &>/dev/null; then
|
||||
elif command -v apt-get &> /dev/null; then
|
||||
pkg_manager="apt-get"
|
||||
speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh"
|
||||
elif command -v apt &>/dev/null; then
|
||||
elif command -v apt &> /dev/null; then
|
||||
pkg_manager="apt"
|
||||
speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh"
|
||||
fi
|
||||
|
||||
|
||||
if [[ -z $pkg_manager ]]; then
|
||||
echo "Error: Package manager not found. You may need to install Speedtest manually."
|
||||
return 1
|
||||
@@ -947,11 +862,8 @@ run_speedtest() {
|
||||
}
|
||||
|
||||
create_iplimit_jails() {
|
||||
# Use default bantime if not passed => 30 minutes
|
||||
local bantime="${1:-30}"
|
||||
|
||||
# Uncomment 'allowipv6 = auto' in fail2ban.conf
|
||||
sed -i 's/#allowipv6 = auto/allowipv6 = auto/g' /etc/fail2ban/fail2ban.conf
|
||||
# Use default bantime if not passed => 5 minutes
|
||||
local bantime="${1:-5}"
|
||||
|
||||
cat << EOF > /etc/fail2ban/jail.d/3x-ipl.conf
|
||||
[3x-ipl]
|
||||
@@ -995,7 +907,7 @@ actionunban = <iptables> -D f2b-<name> -s <ip> -j <blocktype>
|
||||
[Init]
|
||||
EOF
|
||||
|
||||
echo -e "${green}Ip Limit jail files created with a bantime of ${bantime} minutes.${plain}"
|
||||
echo -e "${green}Created Ip Limit jail files with a bantime of ${bantime} minutes.${plain}"
|
||||
}
|
||||
|
||||
iplimit_remove_conflicts() {
|
||||
@@ -1023,80 +935,62 @@ iplimit_main() {
|
||||
echo -e "${green}\t0.${plain} Back to Main Menu"
|
||||
read -p "Choose an option: " choice
|
||||
case "$choice" in
|
||||
0)
|
||||
show_menu
|
||||
;;
|
||||
1)
|
||||
confirm "Proceed with installation of Fail2ban & IP Limit?" "y"
|
||||
if [[ $? == 0 ]]; then
|
||||
install_iplimit
|
||||
else
|
||||
iplimit_main
|
||||
fi
|
||||
;;
|
||||
2)
|
||||
read -rp "Please enter new Ban Duration in Minutes [default 30]: " NUM
|
||||
if [[ $NUM =~ ^[0-9]+$ ]]; then
|
||||
create_iplimit_jails ${NUM}
|
||||
systemctl restart fail2ban
|
||||
else
|
||||
echo -e "${red}${NUM} is not a number! Please, try again.${plain}"
|
||||
fi
|
||||
iplimit_main
|
||||
;;
|
||||
3)
|
||||
confirm "Proceed with Unbanning everyone from IP Limit jail?" "y"
|
||||
if [[ $? == 0 ]]; then
|
||||
fail2ban-client reload --restart --unban 3x-ipl
|
||||
truncate -s 0 "${iplimit_banned_log_path}"
|
||||
echo -e "${green}All users Unbanned successfully.${plain}"
|
||||
iplimit_main
|
||||
else
|
||||
echo -e "${yellow}Cancelled.${plain}"
|
||||
fi
|
||||
iplimit_main
|
||||
;;
|
||||
4)
|
||||
show_banlog
|
||||
;;
|
||||
5)
|
||||
service fail2ban status
|
||||
;;
|
||||
0)
|
||||
show_menu ;;
|
||||
1)
|
||||
confirm "Proceed with installation of Fail2ban & IP Limit?" "y"
|
||||
if [[ $? == 0 ]]; then
|
||||
install_iplimit
|
||||
else
|
||||
iplimit_main
|
||||
fi ;;
|
||||
2)
|
||||
read -rp "Please enter new Ban Duration in Minutes [default 5]: " NUM
|
||||
if [[ $NUM =~ ^[0-9]+$ ]]; then
|
||||
create_iplimit_jails ${NUM}
|
||||
systemctl restart fail2ban
|
||||
else
|
||||
echo -e "${red}${NUM} is not a number! Please, try again.${plain}"
|
||||
fi
|
||||
iplimit_main ;;
|
||||
3)
|
||||
confirm "Proceed with Unbanning everyone from IP Limit jail?" "y"
|
||||
if [[ $? == 0 ]]; then
|
||||
fail2ban-client reload --restart --unban 3x-ipl
|
||||
echo -e "${green}All users Unbanned successfully.${plain}"
|
||||
iplimit_main
|
||||
else
|
||||
echo -e "${yellow}Cancelled.${plain}"
|
||||
fi
|
||||
iplimit_main ;;
|
||||
4)
|
||||
show_banlog
|
||||
;;
|
||||
5)
|
||||
service fail2ban status
|
||||
;;
|
||||
|
||||
6)
|
||||
remove_iplimit
|
||||
;;
|
||||
*) echo "Invalid choice" ;;
|
||||
6)
|
||||
remove_iplimit ;;
|
||||
*) echo "Invalid choice" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
install_iplimit() {
|
||||
if ! command -v fail2ban-client &>/dev/null; then
|
||||
echo -e "${green}Fail2ban is not installed. Installing now...!${plain}\n"
|
||||
|
||||
# Check the OS and install necessary packages
|
||||
case "${release}" in
|
||||
ubuntu | debian)
|
||||
apt update && apt install fail2ban -y
|
||||
;;
|
||||
centos | almalinux | rocky)
|
||||
yum update -y && yum install epel-release -y
|
||||
yum -y install fail2ban
|
||||
;;
|
||||
fedora)
|
||||
dnf -y update && dnf -y install fail2ban
|
||||
;;
|
||||
*)
|
||||
echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n"
|
||||
exit 1
|
||||
;;
|
||||
ubuntu|debian)
|
||||
apt update && apt install fail2ban -y ;;
|
||||
centos|almalinux|rocky)
|
||||
yum -y update && yum -y install fail2ban ;;
|
||||
fedora)
|
||||
dnf -y update && dnf -y install fail2ban ;;
|
||||
*)
|
||||
echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n"
|
||||
exit 1 ;;
|
||||
esac
|
||||
|
||||
if ! command -v fail2ban-client &>/dev/null; then
|
||||
echo -e "${red}Fail2ban installation failed.${plain}\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${green}Fail2ban installed successfully!${plain}\n"
|
||||
else
|
||||
echo -e "${yellow}Fail2ban is already installed.${plain}\n"
|
||||
@@ -1124,7 +1018,6 @@ install_iplimit() {
|
||||
# Launching fail2ban
|
||||
if ! systemctl is-active --quiet fail2ban; then
|
||||
systemctl start fail2ban
|
||||
systemctl enable fail2ban
|
||||
else
|
||||
systemctl restart fail2ban
|
||||
fi
|
||||
@@ -1134,53 +1027,41 @@ install_iplimit() {
|
||||
before_show_menu
|
||||
}
|
||||
|
||||
remove_iplimit() {
|
||||
remove_iplimit(){
|
||||
echo -e "${green}\t1.${plain} Only remove IP Limit configurations"
|
||||
echo -e "${green}\t2.${plain} Uninstall Fail2ban and IP Limit"
|
||||
echo -e "${green}\t0.${plain} Abort"
|
||||
read -p "Choose an option: " num
|
||||
case "$num" in
|
||||
1)
|
||||
rm -f /etc/fail2ban/filter.d/3x-ipl.conf
|
||||
rm -f /etc/fail2ban/action.d/3x-ipl.conf
|
||||
rm -f /etc/fail2ban/jail.d/3x-ipl.conf
|
||||
systemctl restart fail2ban
|
||||
echo -e "${green}IP Limit removed successfully!${plain}\n"
|
||||
before_show_menu
|
||||
;;
|
||||
2)
|
||||
rm -rf /etc/fail2ban
|
||||
systemctl stop fail2ban
|
||||
case "${release}" in
|
||||
ubuntu | debian)
|
||||
apt-get remove -y fail2ban
|
||||
apt-get purge -y fail2ban -y
|
||||
apt-get autoremove -y
|
||||
;;
|
||||
centos | almalinux | rocky)
|
||||
yum remove fail2ban -y
|
||||
yum autoremove -y
|
||||
;;
|
||||
fedora)
|
||||
dnf remove fail2ban -y
|
||||
dnf autoremove -y
|
||||
;;
|
||||
*)
|
||||
echo -e "${red}Unsupported operating system. Please uninstall Fail2ban manually.${plain}\n"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
echo -e "${green}Fail2ban and IP Limit removed successfully!${plain}\n"
|
||||
before_show_menu
|
||||
;;
|
||||
0)
|
||||
echo -e "${yellow}Cancelled.${plain}\n"
|
||||
iplimit_main
|
||||
;;
|
||||
*)
|
||||
echo -e "${red}Invalid option. Please select a valid number.${plain}\n"
|
||||
remove_iplimit
|
||||
;;
|
||||
1)
|
||||
rm -f /etc/fail2ban/filter.d/3x-ipl.conf
|
||||
rm -f /etc/fail2ban/action.d/3x-ipl.conf
|
||||
rm -f /etc/fail2ban/jail.d/3x-ipl.conf
|
||||
systemctl restart fail2ban
|
||||
echo -e "${green}IP Limit removed successfully!${plain}\n"
|
||||
before_show_menu ;;
|
||||
2)
|
||||
rm -rf /etc/fail2ban
|
||||
systemctl stop fail2ban
|
||||
case "${release}" in
|
||||
ubuntu|debian)
|
||||
apt-get purge fail2ban -y;;
|
||||
centos|almalinux|rocky)
|
||||
yum remove fail2ban -y;;
|
||||
fedora)
|
||||
dnf remove fail2ban -y;;
|
||||
*)
|
||||
echo -e "${red}Unsupported operating system. Please uninstall Fail2ban manually.${plain}\n"
|
||||
exit 1 ;;
|
||||
esac
|
||||
echo -e "${green}Fail2ban and IP Limit removed successfully!${plain}\n"
|
||||
before_show_menu ;;
|
||||
0)
|
||||
echo -e "${yellow}Cancelled.${plain}\n"
|
||||
iplimit_main ;;
|
||||
*)
|
||||
echo -e "${red}Invalid option. Please select a valid number.${plain}\n"
|
||||
remove_iplimit ;;
|
||||
esac
|
||||
}
|
||||
|
||||
@@ -1223,21 +1104,22 @@ show_menu() {
|
||||
${green}12.${plain} Check Status
|
||||
${green}13.${plain} Check Logs
|
||||
————————————————
|
||||
${green}14.${plain} Enable Autostart
|
||||
${green}15.${plain} Disable Autostart
|
||||
${green}14.${plain} Enable x-ui On System Startup
|
||||
${green}15.${plain} Disable x-ui On System Startup
|
||||
————————————————
|
||||
${green}16.${plain} SSL Certificate Management
|
||||
${green}17.${plain} Cloudflare SSL Certificate
|
||||
${green}18.${plain} IP Limit Management
|
||||
${green}19.${plain} WARP Management
|
||||
${green}20.${plain} Firewall Management
|
||||
${green}20.${plain} Multi Protocol Management
|
||||
————————————————
|
||||
${green}21.${plain} Enable BBR
|
||||
${green}22.${plain} Update Geo Files
|
||||
${green}23.${plain} Speedtest by Ookla
|
||||
${green}23.${plain} Active Firewall and open ports
|
||||
${green}24.${plain} Speedtest by Ookla
|
||||
"
|
||||
show_status
|
||||
echo && read -p "Please enter your selection [0-23]: " num
|
||||
echo && read -p "Please enter your selection [0-24]: " num
|
||||
|
||||
case "${num}" in
|
||||
0)
|
||||
@@ -1301,19 +1183,22 @@ show_menu() {
|
||||
warp_cloudflare
|
||||
;;
|
||||
20)
|
||||
firewall_menu
|
||||
multi_protocol
|
||||
;;
|
||||
21)
|
||||
bbr_menu
|
||||
enable_bbr
|
||||
;;
|
||||
22)
|
||||
update_geo
|
||||
;;
|
||||
23)
|
||||
run_speedtest
|
||||
open_ports
|
||||
;;
|
||||
24)
|
||||
run_speedtest
|
||||
;;
|
||||
*)
|
||||
LOGE "Please enter the correct number [0-23]"
|
||||
LOGE "Please enter the correct number [0-24]"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
@@ -213,7 +213,6 @@ func (x *XrayAPI) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) {
|
||||
continue
|
||||
}
|
||||
isInbound := matchs[1] == "inbound"
|
||||
isOutbound := matchs[1] == "outbound"
|
||||
tag := matchs[2]
|
||||
isDown := matchs[3] == "downlink"
|
||||
if tag == "api" {
|
||||
@@ -222,9 +221,8 @@ func (x *XrayAPI) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) {
|
||||
traffic, ok := tagTrafficMap[tag]
|
||||
if !ok {
|
||||
traffic = &Traffic{
|
||||
IsInbound: isInbound,
|
||||
IsOutbound: isOutbound,
|
||||
Tag: tag,
|
||||
IsInbound: isInbound,
|
||||
Tag: tag,
|
||||
}
|
||||
tagTrafficMap[tag] = traffic
|
||||
traffics = append(traffics, traffic)
|
||||
|
||||
@@ -16,8 +16,7 @@ type Config struct {
|
||||
API json_util.RawMessage `json:"api"`
|
||||
Stats json_util.RawMessage `json:"stats"`
|
||||
Reverse json_util.RawMessage `json:"reverse"`
|
||||
FakeDNS json_util.RawMessage `json:"fakedns"`
|
||||
Observatory json_util.RawMessage `json:"observatory"`
|
||||
FakeDNS json_util.RawMessage `json:"fakeDns"`
|
||||
}
|
||||
|
||||
func (c *Config) Equals(other *Config) bool {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package xray
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"x-ui/logger"
|
||||
)
|
||||
@@ -15,29 +14,37 @@ type LogWriter struct {
|
||||
}
|
||||
|
||||
func (lw *LogWriter) Write(m []byte) (n int, err error) {
|
||||
regex := regexp.MustCompile(`^(\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}) \[([^\]]+)\] (.+)$`)
|
||||
// Convert the data to a string
|
||||
message := strings.TrimSpace(string(m))
|
||||
messages := strings.Split(message, "\n")
|
||||
lw.lastLine = messages[len(messages)-1]
|
||||
|
||||
for _, msg := range messages {
|
||||
matches := regex.FindStringSubmatch(msg)
|
||||
messageBody := msg
|
||||
|
||||
if len(matches) > 3 {
|
||||
level := matches[2]
|
||||
msgBody := matches[3]
|
||||
// Remove timestamp
|
||||
splittedMsg := strings.SplitN(msg, " ", 3)
|
||||
if len(splittedMsg) > 2 {
|
||||
messageBody = strings.TrimSpace(strings.SplitN(msg, " ", 3)[2])
|
||||
}
|
||||
|
||||
// Find level in []
|
||||
startIndex := strings.Index(messageBody, "[")
|
||||
endIndex := strings.Index(messageBody, "]")
|
||||
if startIndex != -1 && endIndex != -1 {
|
||||
level := strings.TrimSpace(messageBody[startIndex+1 : endIndex])
|
||||
msgBody := "XRAY: " + strings.TrimSpace(messageBody[endIndex+1:])
|
||||
|
||||
// Map the level to the appropriate logger function
|
||||
switch level {
|
||||
case "Debug":
|
||||
logger.Debug("XRAY: " + msgBody)
|
||||
logger.Debug(msgBody)
|
||||
case "Info":
|
||||
logger.Info("XRAY: " + msgBody)
|
||||
logger.Info(msgBody)
|
||||
case "Warning":
|
||||
logger.Warning("XRAY: " + msgBody)
|
||||
logger.Warning(msgBody)
|
||||
case "Error":
|
||||
logger.Error("XRAY: " + msgBody)
|
||||
logger.Error(msgBody)
|
||||
default:
|
||||
logger.Debug("XRAY: " + msg)
|
||||
}
|
||||
|
||||
@@ -41,6 +41,10 @@ func GetIPLimitLogPath() string {
|
||||
return config.GetLogFolder() + "/3xipl.log"
|
||||
}
|
||||
|
||||
func GetIPLimitPrevLogPath() string {
|
||||
return config.GetLogFolder() + "/3xipl.prev.log"
|
||||
}
|
||||
|
||||
func GetIPLimitBannedLogPath() string {
|
||||
return config.GetLogFolder() + "/3xipl-banned.log"
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package xray
|
||||
|
||||
type Traffic struct {
|
||||
IsInbound bool
|
||||
IsOutbound bool
|
||||
Tag string
|
||||
Up int64
|
||||
Down int64
|
||||
IsInbound bool
|
||||
Tag string
|
||||
Up int64
|
||||
Down int64
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user