Compare commits
203 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e6424a085 | ||
|
|
1d4b5975b3 | ||
|
|
9f3efff88c | ||
|
|
7c1ecef589 | ||
|
|
44970e79b4 | ||
|
|
b0aaece6eb | ||
|
|
c0ef791f01 | ||
|
|
b63612ab98 | ||
|
|
7e7d80f8e1 | ||
|
|
e08ddc82ba | ||
|
|
e87fbbfde4 | ||
|
|
547b341d13 | ||
|
|
1bacf26c0a | ||
|
|
c9a6e2be86 | ||
|
|
2a354819c7 | ||
|
|
e926500870 | ||
|
|
0b96622b4c | ||
|
|
02c3bb90e9 | ||
|
|
77470ad8f2 | ||
|
|
60e9f01be8 | ||
|
|
d1f8796bb6 | ||
|
|
634111e815 | ||
|
|
084e26e353 | ||
|
|
86cfa14408 | ||
|
|
0927f44350 | ||
|
|
6bd0a2c772 | ||
|
|
73e97e1f42 | ||
|
|
6816b8085e | ||
|
|
80878517f6 | ||
|
|
388556a965 | ||
|
|
57cc9954af | ||
|
|
463eac1433 | ||
|
|
73be36869e | ||
|
|
45976e8114 | ||
|
|
be4a68bd8b | ||
|
|
4e6484caec | ||
|
|
30559007dd | ||
|
|
4ab5bf04eb | ||
|
|
a808170efb | ||
|
|
a7470a71e4 | ||
|
|
6765babe7b | ||
|
|
4b4be5f837 | ||
|
|
25430b7818 | ||
|
|
6aeedac051 | ||
|
|
9c231a41d1 | ||
|
|
ba673778ea | ||
|
|
8698024b61 | ||
|
|
e8b576fa65 | ||
|
|
c49147c5db | ||
|
|
149bd0ec51 | ||
|
|
f3280b46fe | ||
|
|
0f91e4d5f6 | ||
|
|
18617afd43 | ||
|
|
731e83a7da | ||
|
|
1c1f53267a | ||
|
|
8489f5f528 | ||
|
|
872974910c | ||
|
|
147999dd88 | ||
|
|
fe22cbd0e5 | ||
|
|
1277285d08 | ||
|
|
75df8a05f1 | ||
|
|
38e1d0f94e | ||
|
|
71c1a05386 | ||
|
|
4a6bd23873 | ||
|
|
8a1d647547 | ||
|
|
fb5180a890 | ||
|
|
d2c75b94cf | ||
|
|
edfc2d8d93 | ||
|
|
dd177b19f7 | ||
|
|
ae7283fc73 | ||
|
|
51a7c56cae | ||
|
|
c812168d6e | ||
|
|
fa10ae14f7 | ||
|
|
ed53df5ef3 | ||
|
|
b541290ded | ||
|
|
792fe6d9ef | ||
|
|
eb0c1dabf1 | ||
|
|
e00c3f1823 | ||
|
|
05bc655e16 | ||
|
|
7cddada8b4 | ||
|
|
24eb36715a | ||
|
|
22cf278ce2 | ||
|
|
5ab5986bd0 | ||
|
|
f0775abc67 | ||
|
|
d424b4bc32 | ||
|
|
329889ec00 | ||
|
|
95268dbc61 | ||
|
|
9a3200c9b5 | ||
|
|
c213fb6216 | ||
|
|
dd0217b46b | ||
|
|
b805bf6222 | ||
|
|
07b9474212 | ||
|
|
bf971911d5 | ||
|
|
c46ced0c4c | ||
|
|
dd3bbbc4af | ||
|
|
a97f90d225 | ||
|
|
6066edd510 | ||
|
|
eaec9e54ad | ||
|
|
fafcb2e8e7 | ||
|
|
145ea1e6f1 | ||
|
|
4cfed17650 | ||
|
|
c2b1fb4855 | ||
|
|
09807b39aa | ||
|
|
a0ec2f3972 | ||
|
|
e4b1dc20c3 | ||
|
|
2f05d4960e | ||
|
|
e63d2644bd | ||
|
|
56e4d13179 | ||
|
|
0dd0ba717f | ||
|
|
8050330e2e | ||
|
|
65e35c1711 | ||
|
|
0311ae4d05 | ||
|
|
6f09fae28b | ||
|
|
c2e9ee3665 | ||
|
|
1f78842b70 | ||
|
|
81a057d638 | ||
|
|
64d17bee70 | ||
|
|
6beb19b945 | ||
|
|
6b84d39700 | ||
|
|
5f3b91f3f1 | ||
|
|
9e433ea4c4 | ||
|
|
39537f6f44 | ||
|
|
44b34c437e | ||
|
|
467c5f807e | ||
|
|
1028319386 | ||
|
|
f726474a5d | ||
|
|
cd7a790637 | ||
|
|
4f74f5154b | ||
|
|
6e22aa59e7 | ||
|
|
85df1301dc | ||
|
|
e92aba0179 | ||
|
|
85fb2fda5e | ||
|
|
f57e693023 | ||
|
|
83f6f13b50 | ||
|
|
b833ed7992 | ||
|
|
5188d516e3 | ||
|
|
97925eeebe | ||
|
|
1328bb5aba | ||
|
|
4cc755c883 | ||
|
|
4e89c71095 | ||
|
|
d40e61fc45 | ||
|
|
c0f1a926e5 | ||
|
|
970dd0915e | ||
|
|
526e2578b0 | ||
|
|
ad9134bc1a | ||
|
|
b5657ab87d | ||
|
|
2c7ec3dc8a | ||
|
|
0fe35fde49 | ||
|
|
c7658973c3 | ||
|
|
dea61b839c | ||
|
|
9063336778 | ||
|
|
7fc3d37851 | ||
|
|
81aa3ed10e | ||
|
|
ca10623e40 | ||
|
|
547e38079f | ||
|
|
29e40a0bce | ||
|
|
4c1fa59453 | ||
|
|
84b0471ca0 | ||
|
|
1c785b930f | ||
|
|
c8ea9e43ec | ||
|
|
594d682e20 | ||
|
|
70f250dfe1 | ||
|
|
1030bcf321 | ||
|
|
fdc1124ea4 | ||
|
|
33a598366b | ||
|
|
459c19eee2 | ||
|
|
d73a995257 | ||
|
|
e4e0deeed4 | ||
|
|
48dce38e14 | ||
|
|
3eebd7c3e5 | ||
|
|
2fa151d52e | ||
|
|
c182f48079 | ||
|
|
94fad02737 | ||
|
|
d694e6eafc | ||
|
|
2a9cb6d29e | ||
|
|
c565a429af | ||
|
|
572d912858 | ||
|
|
6ae80fc992 | ||
|
|
ef7b979d53 | ||
|
|
b203067dfd | ||
|
|
1c9fc9422e | ||
|
|
6c26e40aea | ||
|
|
fe963d91ef | ||
|
|
c9461f1647 | ||
|
|
ea7fe09c27 | ||
|
|
8170b65db4 | ||
|
|
a2d8c98b0d | ||
|
|
8442836512 | ||
|
|
331f2adb1c | ||
|
|
45a8d6c95e | ||
|
|
1f2eb2ca1a | ||
|
|
de571a2da4 | ||
|
|
2c233dffa5 | ||
|
|
31339d6bf8 | ||
|
|
8865443438 | ||
|
|
835deb77f4 | ||
|
|
6a7c3716ac | ||
|
|
15211f81b1 | ||
|
|
b3f7a6572e | ||
|
|
6f28a3a2fe | ||
|
|
896cc5386c | ||
|
|
76f70ce1e9 | ||
|
|
6aa3c8d4a2 |
12
.github/workflows/docker.yml
vendored
@@ -11,15 +11,15 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out the code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4.1.1
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2.1.0
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.5.0
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v2.1.0
|
||||
uses: docker/login-action@v3.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -27,12 +27,12 @@ jobs:
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4.4.0
|
||||
uses: docker/metadata-action@v5.0.0
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
uses: docker/build-push-action@v5.0.0
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
125
.github/workflows/release.yml
vendored
@@ -1,4 +1,5 @@
|
||||
name: Release X-ui
|
||||
name: Release 3X-ui
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
@@ -6,85 +7,71 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
linuxamd64build:
|
||||
name: build x-ui amd64 version
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
platform: [amd64, arm64]
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3.5.2
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4.0.1
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.1.1
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v4.1.0
|
||||
with:
|
||||
go-version: "stable"
|
||||
- name: build linux amd64 version
|
||||
run: |
|
||||
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o xui-release -v main.go
|
||||
mkdir x-ui
|
||||
cp xui-release x-ui/xui-release
|
||||
cp x-ui.service x-ui/x-ui.service
|
||||
cp x-ui.sh x-ui/x-ui.sh
|
||||
cd x-ui
|
||||
mv xui-release x-ui
|
||||
mkdir bin
|
||||
cd bin
|
||||
wget https://github.com/mhsanaei/Xray-core/releases/latest/download/Xray-linux-64.zip
|
||||
unzip Xray-linux-64.zip
|
||||
rm -f Xray-linux-64.zip geoip.dat geosite.dat iran.dat
|
||||
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
|
||||
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
|
||||
wget https://github.com/bootmortis/iran-hosted-domains/releases/latest/download/iran.dat
|
||||
mv xray xray-linux-amd64
|
||||
cd ..
|
||||
cd ..
|
||||
- name: package
|
||||
run: tar -zcvf x-ui-linux-amd64.tar.gz x-ui
|
||||
- name: upload
|
||||
uses: svenstaro/upload-release-action@2.5.0
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ github.ref }}
|
||||
file: x-ui-linux-amd64.tar.gz
|
||||
asset_name: x-ui-linux-amd64.tar.gz
|
||||
prerelease: true
|
||||
overwrite: true
|
||||
linuxarm64build:
|
||||
name: build x-ui arm64 version
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3.5.2
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4.0.1
|
||||
with:
|
||||
go-version: "stable"
|
||||
- name: build linux arm64 version
|
||||
go-version: '1.21'
|
||||
|
||||
- name: Install dependencies for arm64
|
||||
if: matrix.platform == 'arm64'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt install gcc-aarch64-linux-gnu
|
||||
CGO_ENABLED=1 GOOS=linux GOARCH=arm64 CC=aarch64-linux-gnu-gcc go build -o xui-release -v main.go
|
||||
|
||||
- name: Build x-ui
|
||||
run: |
|
||||
export CGO_ENABLED=1
|
||||
export GOOS=linux
|
||||
export GOARCH=${{ matrix.platform }}
|
||||
if [ "${{ matrix.platform }}" == "arm64" ]; then
|
||||
export CC=aarch64-linux-gnu-gcc
|
||||
fi
|
||||
go build -o xui-release -v main.go
|
||||
|
||||
mkdir x-ui
|
||||
cp xui-release x-ui/xui-release
|
||||
cp x-ui.service x-ui/x-ui.service
|
||||
cp x-ui.sh x-ui/x-ui.sh
|
||||
cd x-ui
|
||||
mv xui-release x-ui
|
||||
mkdir bin
|
||||
cd bin
|
||||
wget https://github.com/mhsanaei/xray-core/releases/latest/download/Xray-linux-arm64-v8a.zip
|
||||
unzip Xray-linux-arm64-v8a.zip
|
||||
rm -f Xray-linux-arm64-v8a.zip geoip.dat geosite.dat iran.dat
|
||||
cp xui-release x-ui/
|
||||
cp x-ui.service x-ui/
|
||||
cp x-ui.sh x-ui/
|
||||
mv x-ui/xui-release x-ui/x-ui
|
||||
mkdir x-ui/bin
|
||||
cd x-ui/bin
|
||||
|
||||
# Download dependencies
|
||||
if [ "${{ matrix.platform }}" == "amd64" ]; then
|
||||
wget https://github.com/XTLS/Xray-core/releases/download/v1.8.4/Xray-linux-64.zip
|
||||
unzip Xray-linux-64.zip
|
||||
rm -f Xray-linux-64.zip
|
||||
else
|
||||
wget https://github.com/XTLS/Xray-core/releases/download/v1.8.4/Xray-linux-arm64-v8a.zip
|
||||
unzip Xray-linux-arm64-v8a.zip
|
||||
rm -f Xray-linux-arm64-v8a.zip
|
||||
fi
|
||||
rm -f geoip.dat geosite.dat geoip_IR.dat geosite_IR.dat
|
||||
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
|
||||
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
|
||||
wget https://github.com/bootmortis/iran-hosted-domains/releases/latest/download/iran.dat
|
||||
mv xray xray-linux-arm64
|
||||
cd ..
|
||||
cd ..
|
||||
- name: package
|
||||
run: tar -zcvf x-ui-linux-arm64.tar.gz x-ui
|
||||
- name: upload
|
||||
uses: svenstaro/upload-release-action@2.5.0
|
||||
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
|
||||
mv xray xray-linux-${{ matrix.platform }}
|
||||
cd ../..
|
||||
|
||||
- name: Package
|
||||
run: tar -zcvf x-ui-linux-${{ matrix.platform }}.tar.gz x-ui
|
||||
|
||||
- name: Upload
|
||||
uses: svenstaro/upload-release-action@2.7.0
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ github.ref }}
|
||||
file: x-ui-linux-arm64.tar.gz
|
||||
asset_name: x-ui-linux-arm64.tar.gz
|
||||
file: x-ui-linux-${{ matrix.platform }}.tar.gz
|
||||
asset_name: x-ui-linux-${{ matrix.platform }}.tar.gz
|
||||
prerelease: true
|
||||
overwrite: true
|
||||
|
||||
7
DockerEntrypoint.sh
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Start fail2ban
|
||||
fail2ban-client -x start
|
||||
|
||||
# Run x-ui
|
||||
exec /app/x-ui
|
||||
@@ -1,22 +1,29 @@
|
||||
#!/bin/sh
|
||||
if [ $1 == "amd64" ]; then
|
||||
ARCH="64";
|
||||
FNAME="amd64";
|
||||
elif [ $1 == "arm64" ]; then
|
||||
ARCH="arm64-v8a"
|
||||
FNAME="arm64";
|
||||
else
|
||||
ARCH="64";
|
||||
FNAME="amd64";
|
||||
fi
|
||||
|
||||
case $1 in
|
||||
amd64)
|
||||
ARCH="64"
|
||||
FNAME="amd64"
|
||||
;;
|
||||
arm64)
|
||||
ARCH="arm64-v8a"
|
||||
FNAME="arm64"
|
||||
;;
|
||||
*)
|
||||
ARCH="64"
|
||||
FNAME="amd64"
|
||||
;;
|
||||
esac
|
||||
|
||||
mkdir -p build/bin
|
||||
cd build/bin
|
||||
wget "https://github.com/mhsanaei/xray-core/releases/latest/download/Xray-linux-${ARCH}.zip"
|
||||
unzip "Xray-linux-${ARCH}.zip"
|
||||
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat iran.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 "https://github.com/bootmortis/iran-hosted-domains/releases/latest/download/iran.dat"
|
||||
|
||||
cd ../../
|
||||
wget "https://github.com/XTLS/Xray-core/releases/download/v1.8.4/Xray-linux-${ARCH}.zip"
|
||||
unzip "Xray-linux-${ARCH}.zip"
|
||||
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat geoip_IR.dat geosite_IR.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
|
||||
|
||||
47
Dockerfile
@@ -1,20 +1,49 @@
|
||||
#Build latest x-ui from source
|
||||
FROM --platform=$BUILDPLATFORM golang:1.20.4-alpine AS builder
|
||||
# ========================================================
|
||||
# Stage: Builder
|
||||
# ========================================================
|
||||
FROM --platform=$BUILDPLATFORM golang:1.21-alpine AS builder
|
||||
WORKDIR /app
|
||||
ARG TARGETARCH
|
||||
RUN apk --no-cache --update add build-base gcc wget unzip
|
||||
ARG TARGETARCH
|
||||
ENV CGO_ENABLED=1
|
||||
|
||||
RUN apk --no-cache --update add \
|
||||
build-base \
|
||||
gcc \
|
||||
wget \
|
||||
unzip
|
||||
|
||||
COPY . .
|
||||
RUN env CGO_ENABLED=1 go build -o build/x-ui main.go
|
||||
|
||||
RUN go build -o build/x-ui main.go
|
||||
RUN ./DockerInit.sh "$TARGETARCH"
|
||||
|
||||
|
||||
#Build app image using latest x-ui
|
||||
# ========================================================
|
||||
# Stage: Final Image of 3x-ui
|
||||
# ========================================================
|
||||
FROM alpine
|
||||
ENV TZ=Asia/Tehran
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add ca-certificates tzdata
|
||||
RUN apk add --no-cache --update \
|
||||
ca-certificates \
|
||||
tzdata \
|
||||
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 \
|
||||
&& cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local \
|
||||
&& sed -i "s/^\[ssh\]$/&\nenabled = false/" /etc/fail2ban/jail.local \
|
||||
&& sed -i "s/^\[sshd\]$/&\nenabled = false/" /etc/fail2ban/jail.local \
|
||||
&& sed -i "s/#allowipv6 = auto/allowipv6 = auto/g" /etc/fail2ban/fail2ban.conf
|
||||
|
||||
RUN chmod +x \
|
||||
/app/DockerEntrypoint.sh \
|
||||
/app/x-ui \
|
||||
/usr/bin/x-ui
|
||||
|
||||
VOLUME [ "/etc/x-ui" ]
|
||||
ENTRYPOINT [ "/app/x-ui" ]
|
||||
ENTRYPOINT [ "/app/DockerEntrypoint.sh" ]
|
||||
|
||||
323
README.md
@@ -8,8 +8,7 @@
|
||||
[](#)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
3x-ui panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese,Russian)**
|
||||
|
||||
3x-ui panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese,Russian,Vietnamese,Spanish)**
|
||||
**If you think this project is helpful to you, you may wish to give a** :star2:
|
||||
|
||||
**Buy Me a Coffee :**
|
||||
@@ -22,12 +21,12 @@
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
|
||||
```
|
||||
|
||||
## Install custom version
|
||||
# Install custom version
|
||||
|
||||
To install your desired version you can add the version to the end of install command. Example for ver `v1.6.0`:
|
||||
To install your desired version you can add the version to the end of install command. Example for ver `v1.7.9`:
|
||||
|
||||
```
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v1.6.0
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v1.7.9
|
||||
```
|
||||
|
||||
# SSL
|
||||
@@ -38,97 +37,7 @@ certbot certonly --standalone --agree-tos --register-unsafely-without-email -d y
|
||||
certbot renew --dry-run
|
||||
```
|
||||
|
||||
or you can use x-ui menu then number '16' (Apply for an SSL Certificate)
|
||||
|
||||
# Default settings
|
||||
|
||||
- Port: 2053
|
||||
- username and password will be generated randomly if you skip to modify your own security(x-ui "7")
|
||||
- database path: /etc/x-ui/x-ui.db
|
||||
- xray config path: /usr/local/x-ui/bin/config.json
|
||||
|
||||
Before you set ssl on settings
|
||||
|
||||
- http://ip:2053/panel
|
||||
- http://domain:2053/panel
|
||||
|
||||
After you set ssl on settings
|
||||
|
||||
- https://yourdomain:2053/panel
|
||||
|
||||
# Environment Variables
|
||||
|
||||
| Variable | Type | Default |
|
||||
| -------------- | :--------------------------------------------: | :------------ |
|
||||
| XUI_LOG_LEVEL | `"debug"` \| `"info"` \| `"warn"` \| `"error"` | `"info"` |
|
||||
| XUI_DEBUG | `boolean` | `false` |
|
||||
| XUI_BIN_FOLDER | `string` | `"bin"` |
|
||||
| XUI_DB_FOLDER | `string` | `"/etc/x-ui"` |
|
||||
|
||||
Example:
|
||||
|
||||
```sh
|
||||
XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
|
||||
```
|
||||
|
||||
# Install with Docker
|
||||
|
||||
1. Install Docker:
|
||||
```sh
|
||||
bash <(curl -sSL https://get.docker.com)
|
||||
```
|
||||
2. Run 3x-ui:
|
||||
|
||||
```sh
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
OR
|
||||
|
||||
```sh
|
||||
docker run -itd \
|
||||
-e XRAY_VMESS_AEAD_FORCED=false \
|
||||
-v $PWD/db/:/etc/x-ui/ \
|
||||
-v $PWD/cert/:/root/cert/ \
|
||||
--network=host \
|
||||
--restart=unless-stopped \
|
||||
--name 3x-ui \
|
||||
ghcr.io/mhsanaei/3x-ui:latest
|
||||
```
|
||||
|
||||
# Xray Configurations:
|
||||
|
||||
**copy and paste to xray Configuration :** (you don't need to do this if you have a fresh install)
|
||||
|
||||
- [traffic](./media/configs/traffic.json)
|
||||
- [traffic + Block all Iran IP address](./media/configs/traffic+block-iran-ip.json)
|
||||
- [traffic + Block all Iran Domains](./media/configs/traffic+block-iran-domains.json)
|
||||
- [traffic + Block Ads + Use IPv4 for Google](./media/configs/traffic+block-ads+ipv4-google.json)
|
||||
- [traffic + Block Ads + Route Google + Netflix + Spotify + OpenAI (ChatGPT) to WARP](./media/configs/traffic+block-ads+warp.json)
|
||||
|
||||
# [WARP Configuration](https://github.com/fscarmen/warp) (Optional)
|
||||
|
||||
If you want to use routing to WARP follow steps as below:
|
||||
|
||||
1. If you already installed warp, you can uninstall using below command:
|
||||
|
||||
```sh
|
||||
warp u
|
||||
```
|
||||
|
||||
2. Install WARP on **socks proxy mode**:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://gist.githubusercontent.com/hamid-gh98/dc5dd9b0cc5b0412af927b1ccdb294c7/raw/install_warp_proxy.sh | bash
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
- Block Ads
|
||||
- Route Google + Netflix + Spotify + OpenAI (ChatGPT) to WARP
|
||||
- Fix Google 403 error
|
||||
You also can use `x-ui` menu then select `SSL Certificate Management`
|
||||
|
||||
# Features
|
||||
|
||||
@@ -147,7 +56,166 @@ If you want to use routing to WARP follow steps as below:
|
||||
- Support to change configs by different items provided in panel
|
||||
- Support export/import database from panel
|
||||
|
||||
# Tg robot use
|
||||
# Manual Install & Upgrade
|
||||
|
||||
<details>
|
||||
<summary>Click for Manual Install details</summary>
|
||||
|
||||
1. To download the latest version of the compressed package directly to your server, run the following command:
|
||||
|
||||
```sh
|
||||
ARCH=$(uname -m)
|
||||
[[ "${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
|
||||
```
|
||||
|
||||
2. Once the compressed package is downloaded, execute the following commands to install or upgrade x-ui:
|
||||
|
||||
```sh
|
||||
ARCH=$(uname -m)
|
||||
[[ "${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
|
||||
chmod +x x-ui/x-ui x-ui/bin/xray-linux-* x-ui/x-ui.sh
|
||||
cp x-ui/x-ui.sh /usr/bin/x-ui
|
||||
cp -f x-ui/x-ui.service /etc/systemd/system/
|
||||
mv x-ui/ /usr/local/
|
||||
systemctl daemon-reload
|
||||
systemctl enable x-ui
|
||||
systemctl restart x-ui
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
# Install with Docker
|
||||
|
||||
<details>
|
||||
<summary>Click for Docker details</summary>
|
||||
|
||||
1. Install Docker:
|
||||
|
||||
```sh
|
||||
bash <(curl -sSL https://get.docker.com)
|
||||
```
|
||||
|
||||
2. Clone the Project Repository:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/MHSanaei/3x-ui.git
|
||||
cd 3x-ui
|
||||
```
|
||||
|
||||
3. Start the Service
|
||||
|
||||
```sh
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
OR
|
||||
|
||||
```sh
|
||||
docker run -itd \
|
||||
-e XRAY_VMESS_AEAD_FORCED=false \
|
||||
-v $PWD/db/:/etc/x-ui/ \
|
||||
-v $PWD/cert/:/root/cert/ \
|
||||
--network=host \
|
||||
--restart=unless-stopped \
|
||||
--name 3x-ui \
|
||||
ghcr.io/mhsanaei/3x-ui:latest
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
# Default settings
|
||||
|
||||
<details>
|
||||
<summary>Click for Default settings details</summary>
|
||||
|
||||
- Port: 2053
|
||||
- username and password will be generated randomly if you skip to modify your own security(x-ui "7")
|
||||
- database path: /etc/x-ui/x-ui.db
|
||||
- xray config path: /usr/local/x-ui/bin/config.json
|
||||
|
||||
Before you set ssl on settings
|
||||
|
||||
- http://ip:2053/panel
|
||||
- http://domain:2053/panel
|
||||
|
||||
After you set ssl on settings
|
||||
|
||||
- https://yourdomain:2053/panel
|
||||
</details>
|
||||
|
||||
# Xray Configurations:
|
||||
|
||||
<details>
|
||||
<summary>Click for Xray Configurations details</summary>
|
||||
|
||||
**copy and paste to xray Configuration :** (you don't need to do this if you have a fresh install)
|
||||
|
||||
- [traffic](./media/configs/traffic.json)
|
||||
- [traffic + Block all Iran IP address](./media/configs/traffic+block-iran-ip.json)
|
||||
- [traffic + Block all Iran Domains](./media/configs/traffic+block-iran-domains.json)
|
||||
- [traffic + Block Ads + Use IPv4 for Google](./media/configs/traffic+block-ads+ipv4-google.json)
|
||||
- [traffic + Block Ads + Route Google + Netflix + Spotify + OpenAI (ChatGPT) to WARP](./media/configs/traffic+block-ads+warp.json)
|
||||
|
||||
</details>
|
||||
|
||||
# [WARP Configuration](https://github.com/fscarmen/warp) (Optional)
|
||||
|
||||
<details>
|
||||
<summary>Click for WARP Configuration details</summary>
|
||||
|
||||
If you want to use routing to WARP follow steps as below:
|
||||
|
||||
1. If you already installed warp, you can uninstall using below command:
|
||||
|
||||
```sh
|
||||
warp u
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
- Block Ads
|
||||
- Route Google + Netflix + Spotify + OpenAI (ChatGPT) to WARP
|
||||
- Fix Google 403 error
|
||||
|
||||
</details>
|
||||
|
||||
# IP Limit
|
||||
|
||||
<details>
|
||||
<summary>Click for IP Limit details</summary>
|
||||
|
||||
**Note: IP Limit won't work correctly when using IP Tunnel**
|
||||
|
||||
- For versions up to `v1.6.1`:
|
||||
|
||||
- IP limit is built-in into the panel.
|
||||
|
||||
- For versions `v1.7.0` and newer:
|
||||
|
||||
- To make IP Limit work properly, you need to install fail2ban and its required files by following these steps:
|
||||
|
||||
1. Use the `x-ui` command inside the shell.
|
||||
2. Select `IP Limit Management`.
|
||||
3. Choose the appropriate options based on your needs.
|
||||
|
||||
</details>
|
||||
|
||||
# Telegram Bot
|
||||
|
||||
<details>
|
||||
<summary>Click for Telegram Bot details</summary>
|
||||
|
||||
X-UI supports daily traffic notification, panel login reminder and other functions through the Tg robot. To use the Tg robot, you need to apply for the specific application tutorial. You can refer to the [blog](https://coderfan.net/how-to-use-telegram-bot-to-alarm-you-when-someone-login-into-your-vps.html)
|
||||
Set the robot-related parameters in the panel background, including:
|
||||
@@ -176,17 +244,43 @@ Reference syntax:
|
||||
- CPU threshold notification
|
||||
- Threshold for Expiration time and Traffic to report in advance
|
||||
- Support client report menu if client's telegram username added to the user's configurations
|
||||
- Support telegram traffic report searched with UID (VMESS/VLESS) or Password (TROJAN) - anonymously
|
||||
- Support telegram traffic report searched with UUID (VMESS/VLESS) or Password (TROJAN) - anonymously
|
||||
- Menu based bot
|
||||
- Search client by email ( only admin )
|
||||
- Check all inbounds
|
||||
- Check server status
|
||||
- Check depleted users
|
||||
- Receive backup by request and in periodic reports
|
||||
- Multi language bot
|
||||
</details>
|
||||
|
||||
## API routes
|
||||
# Setting up Telegram bot
|
||||
|
||||
- `/login` with `PUSH` user data: `{username: '', password: ''}` for login
|
||||
- Start [Botfather](https://t.me/BotFather) in your Telegram account:
|
||||

|
||||
|
||||
- Create a new Bot using /newbot command: It will ask you 2 questions, A name and a username for your bot. Note that the username has to end with the word "bot".
|
||||

|
||||
|
||||
- Start the bot you've just created. You can find the link to your bot here.
|
||||

|
||||
|
||||
- Enter your panel and config Telegram bot settings like below:
|
||||

|
||||
|
||||
Enter your bot token in input field number 3.
|
||||
Enter the user ID in input field number 4. The Telegram accounts with this id will be the bot admin. (You can enter more than one, Just separate them with ,)
|
||||
|
||||
- How to get Telegram user ID? Use this [bot](https://t.me/useridinfobot), Start the bot and it will give you the Telegram user ID.
|
||||

|
||||
|
||||
|
||||
# API routes
|
||||
|
||||
<details>
|
||||
<summary>Click for API routes details</summary>
|
||||
|
||||
- `/login` with `POST` user data: `{username: '', password: ''}` for login
|
||||
- `/panel/api/inbounds` base for following actions:
|
||||
|
||||
| Method | Path | Action |
|
||||
@@ -215,17 +309,46 @@ Reference syntax:
|
||||
- `client.email` for Shadowsocks
|
||||
|
||||
- [Postman Collection](https://gist.github.com/mehdikhody/9a862801a2e41f6b5fb6bbc7e1326044)
|
||||
</details>
|
||||
|
||||
# Environment Variables
|
||||
|
||||
<details>
|
||||
<summary>Click for Environment Variables details</summary>
|
||||
|
||||
| Variable | Type | Default |
|
||||
| -------------- | :--------------------------------------------: | :------------ |
|
||||
| XUI_LOG_LEVEL | `"debug"` \| `"info"` \| `"warn"` \| `"error"` | `"info"` |
|
||||
| XUI_DEBUG | `boolean` | `false` |
|
||||
| XUI_BIN_FOLDER | `string` | `"bin"` |
|
||||
| XUI_DB_FOLDER | `string` | `"/etc/x-ui"` |
|
||||
| XUI_LOG_FOLDER | `string` | `"/var/log"` |
|
||||
|
||||
Example:
|
||||
|
||||
```sh
|
||||
XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
# A Special Thanks To
|
||||
|
||||
- [alireza0](https://github.com/alireza0/)
|
||||
|
||||
# Acknowledgment
|
||||
|
||||
- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-ruless) (License: **GPL-3.0**): _Enhanced v2ray/xray and v2ray/xray-clients routing rules with built-in Iranian domains and a focus on security and adblocking._
|
||||
- [Iran Hosted Domains](https://github.com/bootmortis/iran-hosted-domains) (License: **MIT**): _A comprehensive list of Iranian domains and services that are hosted within the country._
|
||||
- [PersianBlocker](https://github.com/MasterKia/PersianBlocker) (License: **AGPLv3**): _An optimal and extensive list to block ads and trackers on Persian websites._
|
||||
|
||||
# Suggestion System
|
||||
|
||||
- Ubuntu 20.04+
|
||||
- Debian 10+
|
||||
- CentOS 8+
|
||||
- Fedora 36+
|
||||
- Arch Linux
|
||||
|
||||
# Pictures
|
||||
|
||||
|
||||
@@ -16,10 +16,11 @@ var name string
|
||||
type LogLevel string
|
||||
|
||||
const (
|
||||
Debug LogLevel = "debug"
|
||||
Info LogLevel = "info"
|
||||
Warn LogLevel = "warn"
|
||||
Error LogLevel = "error"
|
||||
Debug LogLevel = "debug"
|
||||
Info LogLevel = "info"
|
||||
Notice LogLevel = "notice"
|
||||
Warn LogLevel = "warn"
|
||||
Error LogLevel = "error"
|
||||
)
|
||||
|
||||
func GetVersion() string {
|
||||
@@ -64,3 +65,11 @@ func GetDBFolderPath() string {
|
||||
func GetDBPath() string {
|
||||
return fmt.Sprintf("%s/%s.db", GetDBFolderPath(), GetName())
|
||||
}
|
||||
|
||||
func GetLogFolder() string {
|
||||
logFolderPath := os.Getenv("XUI_LOG_FOLDER")
|
||||
if logFolderPath == "" {
|
||||
logFolderPath = "/var/log"
|
||||
}
|
||||
return logFolderPath
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.6.0
|
||||
1.7.9
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"x-ui/config"
|
||||
"x-ui/database/model"
|
||||
"x-ui/xray"
|
||||
@@ -26,7 +27,6 @@ var initializers = []func() error{
|
||||
}
|
||||
|
||||
func initUser() error {
|
||||
|
||||
err := db.AutoMigrate(&model.User{})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -54,9 +54,11 @@ func initInbound() error {
|
||||
func initSetting() error {
|
||||
return db.AutoMigrate(&model.Setting{})
|
||||
}
|
||||
|
||||
func initInboundClientIps() error {
|
||||
return db.AutoMigrate(&model.InboundClientIps{})
|
||||
}
|
||||
|
||||
func initClientTraffic() error {
|
||||
return db.AutoMigrate(&xray.ClientTraffic{})
|
||||
}
|
||||
|
||||
@@ -76,7 +76,6 @@ type Client struct {
|
||||
ID string `json:"id"`
|
||||
Password string `json:"password"`
|
||||
Flow string `json:"flow"`
|
||||
AlterIds uint16 `json:"alterId"`
|
||||
Email string `json:"email"`
|
||||
LimitIP int `json:"limitIp"`
|
||||
TotalGB int64 `json:"totalGB" form:"totalGB"`
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
version: "3.9"
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
3x-ui:
|
||||
|
||||
96
go.mod
@@ -1,70 +1,100 @@
|
||||
module x-ui
|
||||
|
||||
go 1.20
|
||||
go 1.21
|
||||
|
||||
toolchain go1.21.0
|
||||
|
||||
require (
|
||||
github.com/Workiva/go-datastructures v1.1.0
|
||||
github.com/gin-contrib/sessions v0.0.4
|
||||
github.com/gin-gonic/gin v1.9.0
|
||||
github.com/go-cmd/cmd v1.4.1
|
||||
github.com/Calidity/gin-sessions v1.3.1
|
||||
github.com/Workiva/go-datastructures v1.1.1
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/goccy/go-json v0.10.2
|
||||
github.com/mymmrac/telego v0.24.0
|
||||
github.com/mymmrac/telego v0.26.3
|
||||
github.com/nicksnyder/go-i18n/v2 v2.2.1
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||
github.com/pelletier/go-toml/v2 v2.0.7
|
||||
github.com/pelletier/go-toml/v2 v2.1.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/shirou/gopsutil/v3 v3.23.4
|
||||
github.com/xtls/xray-core v1.8.1
|
||||
github.com/shirou/gopsutil/v3 v3.23.9
|
||||
github.com/xtls/xray-core v1.8.4
|
||||
go.uber.org/atomic v1.11.0
|
||||
golang.org/x/text v0.9.0
|
||||
google.golang.org/grpc v1.55.0
|
||||
gorm.io/driver/sqlite v1.5.1
|
||||
gorm.io/gorm v1.25.1
|
||||
golang.org/x/text v0.13.0
|
||||
google.golang.org/grpc v1.59.0
|
||||
gorm.io/driver/sqlite v1.5.4
|
||||
gorm.io/gorm v1.25.5
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.2.1 // indirect
|
||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||
github.com/bytedance/sonic v1.8.9 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/fasthttp/router v1.4.19 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/BurntSushi/toml v1.3.2 // 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.0 // indirect
|
||||
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect
|
||||
github.com/fasthttp/router v1.4.20 // indirect
|
||||
github.com/francoispqt/gojay v1.2.13 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gaukas/godicttls v0.0.4 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.15.5 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/golang/mock v1.6.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/btree v1.1.2 // indirect
|
||||
github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f // indirect
|
||||
github.com/gorilla/context v1.1.1 // indirect
|
||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||
github.com/gorilla/sessions v1.2.1 // indirect
|
||||
github.com/gorilla/websocket v1.5.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.16.5 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||
github.com/klauspost/compress v1.17.1 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.16 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.17 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.12.0 // indirect
|
||||
github.com/pires/go-proxyproto v0.7.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect
|
||||
github.com/quic-go/qtls-go1-20 v0.3.3 // indirect
|
||||
github.com/quic-go/quic-go v0.38.1 // indirect
|
||||
github.com/refraction-networking/utls v1.4.3 // indirect
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
||||
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
||||
github.com/sagernet/sing v0.2.12 // indirect
|
||||
github.com/sagernet/sing-shadowsocks v0.2.4 // indirect
|
||||
github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c // indirect
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.11 // indirect
|
||||
github.com/tklauser/numcpus v0.6.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.47.0 // indirect
|
||||
github.com/valyala/fasthttp v1.50.0 // indirect
|
||||
github.com/xtls/reality v0.0.0-20231010003020-da6c695a34db // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.3 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.9.0 // indirect
|
||||
golang.org/x/net v0.10.0 // indirect
|
||||
golang.org/x/sys v0.8.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
go4.org/netipx v0.0.0-20230824141953-6213f710f925 // indirect
|
||||
golang.org/x/arch v0.4.0 // indirect
|
||||
golang.org/x/crypto v0.14.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 // indirect
|
||||
golang.org/x/mod v0.12.0 // indirect
|
||||
golang.org/x/net v0.17.0 // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.12.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20230822212503-5bf4e5f98744 // indirect
|
||||
lukechampine.com/blake3 v1.2.1 // indirect
|
||||
)
|
||||
|
||||
417
go.sum
@@ -1,272 +1,455 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
|
||||
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
|
||||
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
|
||||
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
|
||||
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
|
||||
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
|
||||
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/Workiva/go-datastructures v1.1.0 h1:hu20UpgZneBhQ3ZvwiOGlqJSKIosin2Rd5wAKUHEO/k=
|
||||
github.com/Workiva/go-datastructures v1.1.0/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A=
|
||||
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/antonlindstrom/pgstore v0.0.0-20200229204646-b08ebf1105e0/go.mod h1:2Ti6VUHVxpC0VSmTZzEvpzysnaGAfGBOoMIz5ykPyyw=
|
||||
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
|
||||
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
|
||||
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/Workiva/go-datastructures v1.1.1 h1:9G5u1UqKt6ABseAffHGNfbNQd7omRlWE5QaxNruzhE0=
|
||||
github.com/Workiva/go-datastructures v1.1.1/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A=
|
||||
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=
|
||||
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.8.9 h1:mXB6OoHaI9OrWugkvNxWiuHTy5RCrVfxg2Nn40sf0oc=
|
||||
github.com/bytedance/sonic v1.8.9/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
|
||||
github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE=
|
||||
github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
|
||||
github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo=
|
||||
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
|
||||
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 h1:y7y0Oa6UawqTFPCDw9JG6pdKt4F9pAhHv0B7FMGaGD0=
|
||||
github.com/fasthttp/router v1.4.19 h1:RLE539IU/S4kfb4MP56zgP0TIBU9kEg0ID9GpWO0vqk=
|
||||
github.com/fasthttp/router v1.4.19/go.mod h1:+Fh3YOd8x1+he6ZS+d2iUDBH9MGGZ1xQFUor0DE9rKE=
|
||||
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/fasthttp/router v1.4.20 h1:yPeNxz5WxZGojzolKqiP15DTXnxZce9Drv577GBrDgU=
|
||||
github.com/fasthttp/router v1.4.20/go.mod h1:um867yNQKtERxBm+C+yzgWxjspTiQoA8z86Ec3fK/tc=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/gaukas/godicttls v0.0.3 h1:YNDIf0d9adcxOijiLrEzpfZGAkNwLRzPaG6OjU7EITk=
|
||||
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
|
||||
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
|
||||
github.com/gin-contrib/sessions v0.0.4 h1:gq4fNa1Zmp564iHP5G6EBuktilEos8VKhe2sza1KMgo=
|
||||
github.com/gin-contrib/sessions v0.0.4/go.mod h1:pQ3sIyviBBGcxgyR8mkeJuXbeV3h3NYmhJADQTq5+Vo=
|
||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
|
||||
github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8=
|
||||
github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k=
|
||||
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
||||
github.com/go-cmd/cmd v1.4.1 h1:JUcEIE84v8DSy02XTZpUDeGKExk2oW3DA10hTjbQwmc=
|
||||
github.com/go-cmd/cmd v1.4.1/go.mod h1:tbBenttXtZU4c5djS1o7PWL5pd2xAr5sIqH1kGdNiRc=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
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.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-playground/validator/v10 v10.15.5 h1:LEBecTWb/1j5TNY1YYG2RcOUN3R7NLylN+x8TTueE24=
|
||||
github.com/go-playground/validator/v10 v10.15.5/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
|
||||
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20230406165453-00490a63f317 h1:hFhpt7CTmR3DX+b4R19ydQFtofxT0Sv3QsKNMVQYTMQ=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f h1:pDhu5sgp8yJlEF/g6osliIIpF9K4F5jvkULXa4daRDQ=
|
||||
github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
|
||||
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
||||
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
||||
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
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=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw=
|
||||
github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
|
||||
github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
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.1 h1:NE3C767s2ak2bweCZo3+rdP4U/HoyVXLv/X9f2gPS5g=
|
||||
github.com/klauspost/compress v1.17.1/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a h1:N9zuLhTvBSRt0gWSiJswwQ2HqDmtX/ZCDJURnKUt1Ik=
|
||||
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc=
|
||||
github.com/miekg/dns v1.1.53 h1:ZBkuHr5dxHtB1caEOlZTLPo7D3L3TWckgUUs/RHfDxw=
|
||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
||||
github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo=
|
||||
github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
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.24.0 h1:0fd+v2/dToL6/DtsnWr+2saK7ZxIgLY+LI9kqJQbPEo=
|
||||
github.com/mymmrac/telego v0.24.0/go.mod h1:y557P/iMHSaOVDi5Nmy1gNelqrw+jaBMvP9guPaNJsQ=
|
||||
github.com/mymmrac/telego v0.26.3 h1:qdlddiur25YBUu8MFnm5r/AVdgy77QLyMTOKSId60M8=
|
||||
github.com/mymmrac/telego v0.26.3/go.mod h1:X0MAdPClcdoAfwm7LPJIHosWaIRiGZkEd6n0u4fQgVA=
|
||||
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.2.1 h1:aOzRCdwsJuoExfZhoiXHy4bjruwCMdt5otbYojM/PaA=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.2.1/go.mod h1:fF2++lPHlo+/kPaj3nB0uxtPwzlPm+BlgwGX7MkeGj0=
|
||||
github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU=
|
||||
github.com/onsi/ginkgo/v2 v2.12.0 h1:UIVDowFPwpg6yMUpPjGkYvf06K3RAiJXUhCxEwQVHRI=
|
||||
github.com/onsi/ginkgo/v2 v2.12.0/go.mod h1:ZNEzXISYlqpb8S36iN71ifqLi3vVD1rVJGvWRCJOUpQ=
|
||||
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
|
||||
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
|
||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||
github.com/pelletier/go-toml/v2 v2.0.7 h1:muncTPStnKRos5dpVKULv2FVd4bMOhNePj9CjgDb8Us=
|
||||
github.com/pelletier/go-toml/v2 v2.0.7/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
|
||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
|
||||
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3gMACTjAbMZBjXAqTOzOwFaj2Ld6cjeQ7Rig=
|
||||
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
|
||||
github.com/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U=
|
||||
github.com/quic-go/qtls-go1-20 v0.2.2 h1:WLOPx6OY/hxtTxKV1Zrq20FtXtDEkeY00CGQm8GEa3E=
|
||||
github.com/quic-go/quic-go v0.33.0 h1:ItNoTDN/Fm/zBlq769lLJc8ECe9gYaW40veHCCco7y0=
|
||||
github.com/refraction-networking/utls v1.3.2 h1:o+AkWB57mkcoW36ET7uJ002CpBWHu0KPxi6vzxvPnv8=
|
||||
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/quic-go/qtls-go1-20 v0.3.3 h1:17/glZSLI9P9fDAeyCHBFSWSqJcwx1byhLwP5eUIDCM=
|
||||
github.com/quic-go/qtls-go1-20 v0.3.3/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
|
||||
github.com/quic-go/quic-go v0.38.1 h1:M36YWA5dEhEeT+slOu/SwMEucbYd0YFidxG3KlGPZaE=
|
||||
github.com/quic-go/quic-go v0.38.1/go.mod h1:ijnZM7JsFIkp4cRyjxJNIzdSfCLmUMg9wdyhGmg+SN4=
|
||||
github.com/refraction-networking/utls v1.4.3 h1:BdWS3BSzCwWCFfMIXP3mjLAyQkdmog7diaD/OqFbAzM=
|
||||
github.com/refraction-networking/utls v1.4.3/go.mod h1:4u9V/awOSBrRw6+federGmVJQfPtemEqLBXkML1b0bo=
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/sagernet/sing v0.2.3 h1:V50MvZ4c3Iij2lYFWPlzL1PyipwSzjGeN9x+Ox89vpk=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.1 h1:FvdLQOqpvxHBJUcUe4fvgiYP2XLLwH5i1DtXQviVEPw=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/sagernet/sing v0.2.12 h1:wwdLm3c4qvU4hW8tNtadh60V5z2FGlDZSYYGRzHhD74=
|
||||
github.com/sagernet/sing v0.2.12/go.mod h1:GQ673iPfUnkbK/dIPkfd1Xh1MjOGo36gkl/mkiHY7Jg=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.4 h1:s/CqXlvFAZhlIoHWUwPw5CoNnQ9Ibki9pckjuugtVfY=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.4/go.mod h1:80fNKP0wnqlu85GZXV1H1vDPC/2t+dQbFggOw4XuFUM=
|
||||
github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c h1:vK2wyt9aWYHHvNLWniwijBu/n4pySypiKRhN32u/JGo=
|
||||
github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c/go.mod h1:euOmN6O5kk9dQmgSS8Df4psAl3TCjxOz0NW60EWkSaI=
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U=
|
||||
github.com/shirou/gopsutil/v3 v3.23.4 h1:hZwmDxZs7Ewt75DV81r4pFMqbq+di2cbt9FsQBqLD2o=
|
||||
github.com/shirou/gopsutil/v3 v3.23.4/go.mod h1:ZcGxyfzAMRevhUR2+cfhXDH6gQdFYE/t8j1nsU4mPI8=
|
||||
github.com/shoenig/go-m1cpu v0.1.5/go.mod h1:Wwvst4LR89UxjeFtLRMrpgRiyY4xPsejnVZym39dbAQ=
|
||||
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.23.9 h1:ZI5bWVeu2ep4/DIxB4U9okeYJ7zp/QLTO4auRb/ty/E=
|
||||
github.com/shirou/gopsutil/v3 v3.23.9/go.mod h1:x/NWSb71eMcjFIO0vhyGW5nZ7oSIgVjrCnADckb85GA=
|
||||
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.3/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
|
||||
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
||||
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
|
||||
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
|
||||
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
|
||||
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
|
||||
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
||||
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
|
||||
github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
|
||||
github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
|
||||
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
|
||||
github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
|
||||
github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
|
||||
github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
|
||||
github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
|
||||
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
|
||||
github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
|
||||
github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
|
||||
github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
|
||||
github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
|
||||
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
|
||||
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
|
||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
|
||||
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
|
||||
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
|
||||
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
|
||||
github.com/tinylib/msgp v1.1.5/go.mod h1:eQsjooMTnV42mHu917E26IogZ2930nFyBQdofk10Udg=
|
||||
github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM=
|
||||
github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
|
||||
github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms=
|
||||
github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||
github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31/go.mod h1:onvgF043R+lC5RZ8IT9rBXDaEDnpnw/Cl+HFiw+v/7Q=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI=
|
||||
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.47.0 h1:y7moDoxYzMooFpT5aHgNgVOQDrS3qlkfiP9mDtGGK9c=
|
||||
github.com/valyala/fasthttp v1.47.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
|
||||
github.com/xtls/reality v0.0.0-20230331223127-176a94313eda h1:psRJD2RrZbnI0OWyHvXfgYCPqlRM5q5SPDcjDoDBWhE=
|
||||
github.com/xtls/xray-core v1.8.1 h1:iSTTqXj82ZdwC1ah+eV331X4JTcnrDz+WuKuB/EB3P4=
|
||||
github.com/xtls/xray-core v1.8.1/go.mod h1:AXxSso0MZwUE4NhRocCfHCg73BtJ+T2dSpQVo1Cg9VM=
|
||||
github.com/valyala/fasthttp v1.50.0 h1:H7fweIlBm0rXLs2q0XbalvJ6r0CUPFWK3/bB4N13e9M=
|
||||
github.com/valyala/fasthttp v1.50.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
|
||||
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/xtls/reality v0.0.0-20231010003020-da6c695a34db h1:RhSYAydV4Y52dMVSIj30aIWetFampMaXdIqUe1ow/9o=
|
||||
github.com/xtls/reality v0.0.0-20231010003020-da6c695a34db/go.mod h1:rkuAY1S9F8eI8gDiPDYvACE8e2uwkyg8qoOTuwWov7Y=
|
||||
github.com/xtls/xray-core v1.8.4 h1:YEoY3iLx/5zoNbt5HORG5LtPyzwICInFfoS+oPkYDIw=
|
||||
github.com/xtls/xray-core v1.8.4/go.mod h1:GGD9elFSHa4IqOArW8gzMsEksPIqK/jdNLo8RcSMfnI=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
|
||||
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 h1:Ss6D3hLXTM0KobyBYEAygXzFfGcjnmfEJOBgSbemCtg=
|
||||
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo=
|
||||
go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
||||
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
|
||||
go4.org/netipx v0.0.0-20230824141953-6213f710f925 h1:eeQDDVKFkx0g4Hyy8pHgmZaK0EqB4SD6rvKbUdN3ziQ=
|
||||
go4.org/netipx v0.0.0-20230824141953-6213f710f925/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc=
|
||||
golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
|
||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
|
||||
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY=
|
||||
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
|
||||
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
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=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||
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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y=
|
||||
golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss=
|
||||
golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
|
||||
google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag=
|
||||
google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
|
||||
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
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-20231016165738-49dd2c1f3d0b h1:ZlWIi1wSK56/8hn4QcBp/j9M7Gt3U/3hZw3mC7vDICo=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:swOH3j0KzcDDgGUWr+SNpyTen5YrXjS3eyPzFYKc6lc=
|
||||
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.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
|
||||
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
|
||||
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.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/sqlite v1.5.1 h1:hYyrLkAWE71bcarJDPdZNTLWtr8XrSjOWyjUYI6xdL4=
|
||||
gorm.io/driver/sqlite v1.5.1/go.mod h1:7MZZ2Z8bqyfSQA1gYEV6MagQWj3cpUkJj9Z+d1HEMEQ=
|
||||
gorm.io/gorm v1.25.1 h1:nsSALe5Pr+cM3V1qwwQ7rOkw+6UeLrX5O4v3llhHa64=
|
||||
gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||
gvisor.dev/gvisor v0.0.0-20220901235040-6ca97ef2ce1c h1:m5lcgWnL3OElQNVyp3qcncItJ2c0sQlSGjYK2+nJTA4=
|
||||
lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0=
|
||||
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-20230822212503-5bf4e5f98744 h1:tE44CyJgxEGzoPtHs9GI7ddKdgEGCREQBP54AmaVM+I=
|
||||
gvisor.dev/gvisor v0.0.0-20230822212503-5bf4e5f98744/go.mod h1:lYEMhXbxgudVhALYsMQrBaUAjM3NMinh8mKL1CJv7rc=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI=
|
||||
lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
|
||||
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
|
||||
|
||||
30
install.sh
@@ -8,7 +8,7 @@ plain='\033[0m'
|
||||
cur_dir=$(pwd)
|
||||
|
||||
# check root
|
||||
[[ $EUID -ne 0 ]] && echo -e "${red}Fatal error:${plain} Please run this script with root privilege \n " && exit 1
|
||||
[[ $EUID -ne 0 ]] && echo -e "${red}Fatal error: ${plain} Please run this script with root privilege \n " && exit 1
|
||||
|
||||
# Check OS and set release variable
|
||||
if [[ -f /etc/os-release ]]; then
|
||||
@@ -41,34 +41,41 @@ if [[ "${release}" == "centos" ]]; then
|
||||
fi
|
||||
elif [[ "${release}" == "ubuntu" ]]; then
|
||||
if [[ ${os_version} -lt 20 ]]; then
|
||||
echo -e "${red}please use Ubuntu 20 or higher version!${plain}\n" && exit 1
|
||||
echo -e "${red}please use Ubuntu 20 or higher version!${plain}\n" && exit 1
|
||||
fi
|
||||
|
||||
elif [[ "${release}" == "fedora" ]]; then
|
||||
if [[ ${os_version} -lt 36 ]]; then
|
||||
echo -e "${red}please use Fedora 36 or higher version!${plain}\n" && exit 1
|
||||
echo -e "${red}please use Fedora 36 or higher version!${plain}\n" && exit 1
|
||||
fi
|
||||
|
||||
elif [[ "${release}" == "debian" ]]; then
|
||||
if [[ ${os_version} -lt 10 ]]; then
|
||||
echo -e "${red} Please use Debian 10 or higher ${plain}\n" && exit 1
|
||||
fi
|
||||
elif [[ "${release}" == "arch" ]]; then
|
||||
echo "OS is ArchLinux"
|
||||
|
||||
else
|
||||
echo -e "${red}Failed to check the OS version, please contact the author!${plain}" && exit 1
|
||||
fi
|
||||
|
||||
install_base() {
|
||||
case "${release}" in
|
||||
centos | fedora)
|
||||
yum install -y -q wget curl tar
|
||||
;;
|
||||
*)
|
||||
apt install -y -q wget curl tar
|
||||
;;
|
||||
centos|fedora)
|
||||
yum install -y -q wget curl tar
|
||||
;;
|
||||
arch)
|
||||
pacman -Syu --noconfirm wget curl tar
|
||||
;;
|
||||
*)
|
||||
apt install -y -q wget curl tar
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
#This function will be called when user installed x-ui out of sercurity
|
||||
|
||||
# This function will be called when user installed x-ui out of sercurity
|
||||
config_after_install() {
|
||||
echo -e "${yellow}Install/update finished! For security it's recommended to modify panel settings ${plain}"
|
||||
read -p "Do you want to continue with the modification [y/n]? ": config_confirm
|
||||
@@ -104,7 +111,6 @@ config_after_install() {
|
||||
}
|
||||
|
||||
install_x-ui() {
|
||||
systemctl stop x-ui
|
||||
cd /usr/local/
|
||||
|
||||
if [ $# == 0 ]; then
|
||||
@@ -131,6 +137,7 @@ install_x-ui() {
|
||||
fi
|
||||
|
||||
if [[ -e /usr/local/x-ui/ ]]; then
|
||||
systemctl stop x-ui
|
||||
rm /usr/local/x-ui/ -rf
|
||||
fi
|
||||
|
||||
@@ -164,6 +171,7 @@ install_x-ui() {
|
||||
echo -e "x-ui enable - Enable x-ui on system startup"
|
||||
echo -e "x-ui disable - Disable x-ui on system startup"
|
||||
echo -e "x-ui log - Check x-ui logs"
|
||||
echo -e "x-ui banlog - Check Fail2ban ban logs"
|
||||
echo -e "x-ui update - Update x-ui"
|
||||
echo -e "x-ui install - Install x-ui"
|
||||
echo -e "x-ui uninstall - Uninstall x-ui"
|
||||
|
||||
112
logger/logger.go
@@ -1,86 +1,118 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/op/go-logging"
|
||||
)
|
||||
|
||||
var (
|
||||
logger *logging.Logger
|
||||
mu sync.Mutex
|
||||
)
|
||||
var logger *logging.Logger
|
||||
var logBuffer []struct {
|
||||
time string
|
||||
level logging.Level
|
||||
log string
|
||||
}
|
||||
|
||||
func init() {
|
||||
InitLogger(logging.INFO)
|
||||
}
|
||||
|
||||
func InitLogger(level logging.Level) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
newLogger := logging.MustGetLogger("x-ui")
|
||||
var err error
|
||||
var backend logging.Backend
|
||||
var format logging.Formatter
|
||||
ppid := os.Getppid()
|
||||
|
||||
if logger != nil {
|
||||
return
|
||||
if ppid == 1 {
|
||||
backend, err = logging.NewSyslogBackend("")
|
||||
format = logging.MustStringFormatter(
|
||||
`%{level} - %{message}`,
|
||||
)
|
||||
}
|
||||
if err != nil || ppid != 1 {
|
||||
backend = logging.NewLogBackend(os.Stderr, "", 0)
|
||||
format = logging.MustStringFormatter(
|
||||
`%{time:2006/01/02 15:04:05} %{level} - %{message}`,
|
||||
)
|
||||
}
|
||||
|
||||
format := logging.MustStringFormatter(
|
||||
`%{time:2006/01/02 15:04:05} %{level} - %{message}`,
|
||||
)
|
||||
newLogger := logging.MustGetLogger("x-ui")
|
||||
backend := logging.NewLogBackend(os.Stderr, "", 0)
|
||||
backendFormatter := logging.NewBackendFormatter(backend, format)
|
||||
backendLeveled := logging.AddModuleLevel(backendFormatter)
|
||||
backendLeveled.SetLevel(level, "")
|
||||
newLogger.SetBackend(logging.MultiLogger(backendLeveled))
|
||||
backendLeveled.SetLevel(level, "x-ui")
|
||||
newLogger.SetBackend(backendLeveled)
|
||||
|
||||
logger = newLogger
|
||||
}
|
||||
|
||||
func Debug(args ...interface{}) {
|
||||
if logger != nil {
|
||||
logger.Debug(args...)
|
||||
}
|
||||
logger.Debug(args...)
|
||||
addToBuffer("DEBUG", fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
func Debugf(format string, args ...interface{}) {
|
||||
if logger != nil {
|
||||
logger.Debugf(format, args...)
|
||||
}
|
||||
logger.Debugf(format, args...)
|
||||
addToBuffer("DEBUG", fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func Info(args ...interface{}) {
|
||||
if logger != nil {
|
||||
logger.Info(args...)
|
||||
}
|
||||
logger.Info(args...)
|
||||
addToBuffer("INFO", fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
func Infof(format string, args ...interface{}) {
|
||||
if logger != nil {
|
||||
logger.Infof(format, args...)
|
||||
}
|
||||
logger.Infof(format, args...)
|
||||
addToBuffer("INFO", fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func Warning(args ...interface{}) {
|
||||
if logger != nil {
|
||||
logger.Warning(args...)
|
||||
}
|
||||
logger.Warning(args...)
|
||||
addToBuffer("WARNING", fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
func Warningf(format string, args ...interface{}) {
|
||||
if logger != nil {
|
||||
logger.Warningf(format, args...)
|
||||
}
|
||||
logger.Warningf(format, args...)
|
||||
addToBuffer("WARNING", fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func Error(args ...interface{}) {
|
||||
if logger != nil {
|
||||
logger.Error(args...)
|
||||
}
|
||||
logger.Error(args...)
|
||||
addToBuffer("ERROR", fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
func Errorf(format string, args ...interface{}) {
|
||||
if logger != nil {
|
||||
logger.Errorf(format, args...)
|
||||
}
|
||||
logger.Errorf(format, args...)
|
||||
addToBuffer("ERROR", fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func addToBuffer(level string, newLog string) {
|
||||
t := time.Now()
|
||||
if len(logBuffer) >= 10240 {
|
||||
logBuffer = logBuffer[1:]
|
||||
}
|
||||
|
||||
logLevel, _ := logging.LogLevel(level)
|
||||
logBuffer = append(logBuffer, struct {
|
||||
time string
|
||||
level logging.Level
|
||||
log string
|
||||
}{
|
||||
time: t.Format("2006/01/02 15:04:05"),
|
||||
level: logLevel,
|
||||
log: newLog,
|
||||
})
|
||||
}
|
||||
|
||||
func GetLogs(c int, level string) []string {
|
||||
var output []string
|
||||
logLevel, _ := logging.LogLevel(level)
|
||||
|
||||
for i := len(logBuffer) - 1; i >= 0 && len(output) <= c; i-- {
|
||||
if logBuffer[i].level <= logLevel {
|
||||
output = append(output, fmt.Sprintf("%s %s - %s", logBuffer[i].time, logBuffer[i].level, logBuffer[i].log))
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
22
main.go
@@ -12,7 +12,6 @@ import (
|
||||
"x-ui/database"
|
||||
"x-ui/logger"
|
||||
"x-ui/sub"
|
||||
"x-ui/v2ui"
|
||||
"x-ui/web"
|
||||
"x-ui/web/global"
|
||||
"x-ui/web/service"
|
||||
@@ -28,6 +27,8 @@ func runWebServer() {
|
||||
logger.InitLogger(logging.DEBUG)
|
||||
case config.Info:
|
||||
logger.InitLogger(logging.INFO)
|
||||
case config.Notice:
|
||||
logger.InitLogger(logging.NOTICE)
|
||||
case config.Warn:
|
||||
logger.InitLogger(logging.WARNING)
|
||||
case config.Error:
|
||||
@@ -270,10 +271,6 @@ func main() {
|
||||
|
||||
runCmd := flag.NewFlagSet("run", flag.ExitOnError)
|
||||
|
||||
v2uiCmd := flag.NewFlagSet("v2-ui", flag.ExitOnError)
|
||||
var dbPath string
|
||||
v2uiCmd.StringVar(&dbPath, "db", fmt.Sprintf("%s/v2-ui.db", config.GetDBFolderPath()), "set v2-ui db file path")
|
||||
|
||||
settingCmd := flag.NewFlagSet("setting", flag.ExitOnError)
|
||||
var port int
|
||||
var username string
|
||||
@@ -301,7 +298,6 @@ func main() {
|
||||
fmt.Println()
|
||||
fmt.Println("Commands:")
|
||||
fmt.Println(" run run web panel")
|
||||
fmt.Println(" v2-ui migrate form v2-ui")
|
||||
fmt.Println(" migrate migrate form other/old x-ui")
|
||||
fmt.Println(" setting set settings")
|
||||
}
|
||||
@@ -322,16 +318,6 @@ func main() {
|
||||
runWebServer()
|
||||
case "migrate":
|
||||
migrateDb()
|
||||
case "v2-ui":
|
||||
err := v2uiCmd.Parse(os.Args[2:])
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
err = v2ui.MigrateFromV2UI(dbPath)
|
||||
if err != nil {
|
||||
fmt.Println("migrate from v2-ui failed:", err)
|
||||
}
|
||||
case "setting":
|
||||
err := settingCmd.Parse(os.Args[2:])
|
||||
if err != nil {
|
||||
@@ -356,12 +342,10 @@ func main() {
|
||||
updateTgbotEnableSts(enabletgbot)
|
||||
}
|
||||
default:
|
||||
fmt.Println("except 'run' or 'v2-ui' or 'setting' subcommands")
|
||||
fmt.Println("except 'run' or 'setting' subcommands")
|
||||
fmt.Println()
|
||||
runCmd.Usage()
|
||||
fmt.Println()
|
||||
v2uiCmd.Usage()
|
||||
fmt.Println()
|
||||
settingCmd.Usage()
|
||||
}
|
||||
}
|
||||
|
||||
BIN
media/1.png
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 54 KiB |
BIN
media/2.png
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 26 KiB |
BIN
media/3.png
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 70 KiB |
BIN
media/4.png
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 36 KiB |
BIN
media/5.png
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 54 KiB |
BIN
media/botfather.png
Normal file
|
After Width: | Height: | Size: 544 KiB |
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"log": {
|
||||
"loglevel": "warning",
|
||||
"access": "./access.log",
|
||||
"error": "./error.log"
|
||||
},
|
||||
"api": {
|
||||
@@ -81,8 +80,7 @@
|
||||
"type": "field",
|
||||
"outboundTag": "blocked",
|
||||
"domain": [
|
||||
"geosite:category-ads-all",
|
||||
"ext:iran.dat:ads"
|
||||
"ext:geosite_IR.dat:category-ads-all"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"log": {
|
||||
"loglevel": "warning",
|
||||
"access": "./access.log",
|
||||
"error": "./error.log"
|
||||
},
|
||||
"api": {
|
||||
@@ -86,8 +85,7 @@
|
||||
"type": "field",
|
||||
"outboundTag": "blocked",
|
||||
"domain": [
|
||||
"geosite:category-ads-all",
|
||||
"ext:iran.dat:ads"
|
||||
"ext:geosite_IR.dat:category-ads-all"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"log": {
|
||||
"loglevel": "warning",
|
||||
"access": "./access.log",
|
||||
"error": "./error.log"
|
||||
},
|
||||
"api": {
|
||||
@@ -75,9 +74,8 @@
|
||||
"outboundTag": "blocked",
|
||||
"domain": [
|
||||
"regexp:.*\\.ir$",
|
||||
"ext:iran.dat:ir",
|
||||
"ext:iran.dat:other",
|
||||
"geosite:category-ir"
|
||||
"regexp:.*\\.xn--mgba3a4f16a$",
|
||||
"ext:geosite_IR.dat:ir"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"log": {
|
||||
"loglevel": "warning",
|
||||
"access": "./access.log",
|
||||
"error": "./error.log"
|
||||
},
|
||||
"api": {
|
||||
@@ -61,7 +60,7 @@
|
||||
"outboundTag": "blocked",
|
||||
"ip": [
|
||||
"geoip:private",
|
||||
"geoip:ir"
|
||||
"ext:geoip_IR.dat:ir"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"log": {
|
||||
"loglevel": "warning",
|
||||
"access": "./access.log",
|
||||
"error": "./error.log"
|
||||
},
|
||||
"api": {
|
||||
|
||||
BIN
media/newbot.png
Normal file
|
After Width: | Height: | Size: 455 KiB |
BIN
media/panel-bot-config.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
media/token.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
media/user-id.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
19
sub/sub.go
@@ -7,10 +7,10 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"x-ui/config"
|
||||
"x-ui/logger"
|
||||
"x-ui/util/common"
|
||||
"x-ui/web/middleware"
|
||||
"x-ui/web/network"
|
||||
"x-ui/web/service"
|
||||
|
||||
@@ -58,18 +58,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
}
|
||||
|
||||
if subDomain != "" {
|
||||
validateDomain := func(c *gin.Context) {
|
||||
host := strings.Split(c.Request.Host, ":")[0]
|
||||
|
||||
if host != subDomain {
|
||||
c.AbortWithStatus(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
|
||||
engine.Use(validateDomain)
|
||||
engine.Use(middleware.DomainValidatorMiddleware(subDomain))
|
||||
}
|
||||
|
||||
g := engine.Group(subPath)
|
||||
@@ -116,11 +105,13 @@ func (s *Server) Start() (err error) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
listenAddr := net.JoinHostPort(listen, strconv.Itoa(port))
|
||||
listener, err := net.Listen("tcp", listenAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if certFile != "" || keyFile != "" {
|
||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
@@ -168,4 +159,4 @@ func (s *Server) Stop() error {
|
||||
|
||||
func (s *Server) GetCtx() context.Context {
|
||||
return s.ctx
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,14 @@ package sub
|
||||
import (
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
"x-ui/web/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type SUBController struct {
|
||||
subService SubService
|
||||
subService SubService
|
||||
settingService service.SettingService
|
||||
}
|
||||
|
||||
func NewSUBController(g *gin.RouterGroup) *SUBController {
|
||||
@@ -24,9 +26,11 @@ func (a *SUBController) initRouter(g *gin.RouterGroup) {
|
||||
}
|
||||
|
||||
func (a *SUBController) subs(c *gin.Context) {
|
||||
subEncrypt, _ := a.settingService.GetSubEncrypt()
|
||||
subShowInfo, _ := a.settingService.GetSubShowInfo()
|
||||
subId := c.Param("subid")
|
||||
host := strings.Split(c.Request.Host, ":")[0]
|
||||
subs, headers, 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 {
|
||||
@@ -40,6 +44,10 @@ func (a *SUBController) subs(c *gin.Context) {
|
||||
c.Writer.Header().Set("Profile-Update-Interval", headers[1])
|
||||
c.Writer.Header().Set("Profile-Title", headers[2])
|
||||
|
||||
c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
|
||||
if subEncrypt {
|
||||
c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
|
||||
} else {
|
||||
c.String(200, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,11 @@ import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
"x-ui/database"
|
||||
"x-ui/database/model"
|
||||
"x-ui/logger"
|
||||
"x-ui/util/common"
|
||||
"x-ui/web/service"
|
||||
"x-ui/xray"
|
||||
|
||||
@@ -16,12 +18,14 @@ import (
|
||||
|
||||
type SubService struct {
|
||||
address string
|
||||
showInfo bool
|
||||
inboundService service.InboundService
|
||||
settingServics service.SettingService
|
||||
}
|
||||
|
||||
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 headers []string
|
||||
var traffic xray.ClientTraffic
|
||||
@@ -139,10 +143,8 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
||||
if inbound.Protocol != model.VMess {
|
||||
return ""
|
||||
}
|
||||
remark := fmt.Sprintf("%s-%s", inbound.Remark, email)
|
||||
obj := map[string]interface{}{
|
||||
"v": "2",
|
||||
"ps": remark,
|
||||
"add": s.address,
|
||||
"port": inbound.Port,
|
||||
"type": "none",
|
||||
@@ -236,13 +238,12 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
||||
}
|
||||
}
|
||||
obj["id"] = clients[clientIndex].ID
|
||||
obj["aid"] = clients[clientIndex].AlterIds
|
||||
|
||||
if len(domains) > 0 {
|
||||
links := ""
|
||||
for index, d := range domains {
|
||||
domain := d.(map[string]interface{})
|
||||
obj["ps"] = remark + "-" + domain["remark"].(string)
|
||||
obj["ps"] = s.genRemark(inbound, email, domain["remark"].(string))
|
||||
obj["add"] = domain["domain"].(string)
|
||||
if index > 0 {
|
||||
links += "\n"
|
||||
@@ -253,6 +254,8 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
||||
return links
|
||||
}
|
||||
|
||||
obj["ps"] = s.genRemark(inbound, email, "")
|
||||
|
||||
jsonStr, _ := json.MarshalIndent(obj, "", " ")
|
||||
return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
|
||||
}
|
||||
@@ -435,6 +438,10 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||
}
|
||||
}
|
||||
|
||||
if security != "tls" && security != "reality" && security != "xtls" {
|
||||
params["security"] = "none"
|
||||
}
|
||||
|
||||
link := fmt.Sprintf("vless://%s@%s:%d", uuid, address, port)
|
||||
url, _ := url.Parse(link)
|
||||
q := url.Query()
|
||||
@@ -446,13 +453,11 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||
// Set the new query values on the URL
|
||||
url.RawQuery = q.Encode()
|
||||
|
||||
remark := fmt.Sprintf("%s-%s", inbound.Remark, email)
|
||||
|
||||
if len(domains) > 0 {
|
||||
links := ""
|
||||
for index, d := range domains {
|
||||
domain := d.(map[string]interface{})
|
||||
url.Fragment = remark + "-" + domain["remark"].(string)
|
||||
url.Fragment = s.genRemark(inbound, email, domain["remark"].(string))
|
||||
url.Host = fmt.Sprintf("%s:%d", domain["domain"].(string), port)
|
||||
if index > 0 {
|
||||
links += "\n"
|
||||
@@ -461,7 +466,8 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||
}
|
||||
return links
|
||||
}
|
||||
url.Fragment = remark
|
||||
|
||||
url.Fragment = s.genRemark(inbound, email, "")
|
||||
return url.String()
|
||||
}
|
||||
|
||||
@@ -576,7 +582,7 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
|
||||
if pbkValue, ok := searchKey(realitySettings, "publicKey"); ok {
|
||||
params["pbk"], _ = pbkValue.(string)
|
||||
}
|
||||
if sidValue, ok := searchKey(realitySettings, "shortIds"); ok {
|
||||
if sidValue, ok := searchKey(realitySetting, "shortIds"); ok {
|
||||
shortIds, _ := sidValue.([]interface{})
|
||||
params["sid"], _ = shortIds[0].(string)
|
||||
}
|
||||
@@ -639,6 +645,10 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
|
||||
}
|
||||
}
|
||||
|
||||
if security != "tls" && security != "reality" && security != "xtls" {
|
||||
params["security"] = "none"
|
||||
}
|
||||
|
||||
link := fmt.Sprintf("trojan://%s@%s:%d", password, address, port)
|
||||
|
||||
url, _ := url.Parse(link)
|
||||
@@ -651,13 +661,11 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
|
||||
// Set the new query values on the URL
|
||||
url.RawQuery = q.Encode()
|
||||
|
||||
remark := fmt.Sprintf("%s-%s", inbound.Remark, email)
|
||||
|
||||
if len(domains) > 0 {
|
||||
links := ""
|
||||
for index, d := range domains {
|
||||
domain := d.(map[string]interface{})
|
||||
url.Fragment = remark + "-" + domain["remark"].(string)
|
||||
url.Fragment = s.genRemark(inbound, email, domain["remark"].(string))
|
||||
url.Host = fmt.Sprintf("%s:%d", domain["domain"].(string), port)
|
||||
if index > 0 {
|
||||
links += "\n"
|
||||
@@ -667,7 +675,7 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
|
||||
return links
|
||||
}
|
||||
|
||||
url.Fragment = remark
|
||||
url.Fragment = s.genRemark(inbound, email, "")
|
||||
return url.String()
|
||||
}
|
||||
|
||||
@@ -676,6 +684,8 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
|
||||
if inbound.Protocol != model.Shadowsocks {
|
||||
return ""
|
||||
}
|
||||
var stream map[string]interface{}
|
||||
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
|
||||
clients, _ := s.inboundService.GetClients(inbound)
|
||||
|
||||
var settings map[string]interface{}
|
||||
@@ -689,9 +699,112 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
|
||||
break
|
||||
}
|
||||
}
|
||||
encPart := fmt.Sprintf("%s:%s:%s", method, inboundPassword, clients[clientIndex].Password)
|
||||
remark := fmt.Sprintf("%s-%s", inbound.Remark, clients[clientIndex].Email)
|
||||
return fmt.Sprintf("ss://%s@%s:%d#%s", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port, remark)
|
||||
streamNetwork := stream["network"].(string)
|
||||
params := make(map[string]string)
|
||||
params["type"] = streamNetwork
|
||||
|
||||
switch streamNetwork {
|
||||
case "tcp":
|
||||
tcp, _ := stream["tcpSettings"].(map[string]interface{})
|
||||
header, _ := tcp["header"].(map[string]interface{})
|
||||
typeStr, _ := header["type"].(string)
|
||||
if typeStr == "http" {
|
||||
request := header["request"].(map[string]interface{})
|
||||
requestPath, _ := request["path"].([]interface{})
|
||||
params["path"] = requestPath[0].(string)
|
||||
headers, _ := request["headers"].(map[string]interface{})
|
||||
params["host"] = searchHost(headers)
|
||||
params["headerType"] = "http"
|
||||
}
|
||||
case "kcp":
|
||||
kcp, _ := stream["kcpSettings"].(map[string]interface{})
|
||||
header, _ := kcp["header"].(map[string]interface{})
|
||||
params["headerType"] = header["type"].(string)
|
||||
params["seed"] = kcp["seed"].(string)
|
||||
case "ws":
|
||||
ws, _ := stream["wsSettings"].(map[string]interface{})
|
||||
params["path"] = ws["path"].(string)
|
||||
headers, _ := ws["headers"].(map[string]interface{})
|
||||
params["host"] = searchHost(headers)
|
||||
case "http":
|
||||
http, _ := stream["httpSettings"].(map[string]interface{})
|
||||
params["path"] = http["path"].(string)
|
||||
params["host"] = searchHost(http)
|
||||
case "quic":
|
||||
quic, _ := stream["quicSettings"].(map[string]interface{})
|
||||
params["quicSecurity"] = quic["security"].(string)
|
||||
params["key"] = quic["key"].(string)
|
||||
header := quic["header"].(map[string]interface{})
|
||||
params["headerType"] = header["type"].(string)
|
||||
case "grpc":
|
||||
grpc, _ := stream["grpcSettings"].(map[string]interface{})
|
||||
params["serviceName"] = grpc["serviceName"].(string)
|
||||
if grpc["multiMode"].(bool) {
|
||||
params["mode"] = "multi"
|
||||
}
|
||||
}
|
||||
|
||||
encPart := fmt.Sprintf("%s:%s", method, clients[clientIndex].Password)
|
||||
if method[0] == '2' {
|
||||
encPart = fmt.Sprintf("%s:%s:%s", method, inboundPassword, clients[clientIndex].Password)
|
||||
}
|
||||
link := fmt.Sprintf("ss://%s@%s:%d", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port)
|
||||
url, _ := url.Parse(link)
|
||||
q := url.Query()
|
||||
|
||||
for k, v := range params {
|
||||
q.Add(k, v)
|
||||
}
|
||||
|
||||
// Set the new query values on the URL
|
||||
url.RawQuery = q.Encode()
|
||||
url.Fragment = s.genRemark(inbound, email, "")
|
||||
return url.String()
|
||||
}
|
||||
|
||||
func (s *SubService) genRemark(inbound *model.Inbound, email string, extra string) string {
|
||||
var remark []string
|
||||
if len(email) > 0 {
|
||||
if len(inbound.Remark) > 0 {
|
||||
remark = append(remark, inbound.Remark)
|
||||
}
|
||||
remark = append(remark, email)
|
||||
if len(extra) > 0 {
|
||||
remark = append(remark, extra)
|
||||
}
|
||||
} else {
|
||||
return inbound.Remark
|
||||
}
|
||||
|
||||
if s.showInfo {
|
||||
statsExist := false
|
||||
var stats xray.ClientTraffic
|
||||
for _, clientStat := range inbound.ClientStats {
|
||||
if clientStat.Email == email {
|
||||
stats = clientStat
|
||||
statsExist = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Get remained days
|
||||
if statsExist {
|
||||
if !stats.Enable {
|
||||
return fmt.Sprintf("⛔️N/A-%s", strings.Join(remark, "-"))
|
||||
}
|
||||
if vol := stats.Total - (stats.Up + stats.Down); vol > 0 {
|
||||
remark = append(remark, fmt.Sprintf("%s%s", common.FormatTraffic(vol), "📊"))
|
||||
}
|
||||
now := time.Now().Unix()
|
||||
switch exp := stats.ExpiryTime / 1000; {
|
||||
case exp > 0:
|
||||
remark = append(remark, fmt.Sprintf("%d%s⏳", (exp-now)/86400, "Days"))
|
||||
case exp < 0:
|
||||
remark = append(remark, fmt.Sprintf("%d%s⏳", exp/-86400, "Days"))
|
||||
}
|
||||
}
|
||||
}
|
||||
return strings.Join(remark, " : ")
|
||||
}
|
||||
|
||||
func searchKey(data interface{}, key string) (interface{}, bool) {
|
||||
|
||||
28
v2ui/db.go
@@ -1,28 +0,0 @@
|
||||
package v2ui
|
||||
|
||||
import (
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
var v2db *gorm.DB
|
||||
|
||||
func initDB(dbPath string) error {
|
||||
c := &gorm.Config{
|
||||
Logger: logger.Discard,
|
||||
}
|
||||
var err error
|
||||
v2db, err = gorm.Open(sqlite.Open(dbPath), c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getV2Inbounds() ([]*V2Inbound, error) {
|
||||
inbounds := make([]*V2Inbound, 0)
|
||||
err := v2db.Model(V2Inbound{}).Find(&inbounds).Error
|
||||
return inbounds, err
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package v2ui
|
||||
|
||||
import "x-ui/database/model"
|
||||
|
||||
type V2Inbound struct {
|
||||
Id int `gorm:"primaryKey;autoIncrement"`
|
||||
Port int `gorm:"unique"`
|
||||
Listen string
|
||||
Protocol string
|
||||
Settings string
|
||||
StreamSettings string
|
||||
Tag string `gorm:"unique"`
|
||||
Sniffing string
|
||||
Remark string
|
||||
Up int64
|
||||
Down int64
|
||||
Enable bool
|
||||
}
|
||||
|
||||
func (i *V2Inbound) TableName() string {
|
||||
return "inbound"
|
||||
}
|
||||
|
||||
func (i *V2Inbound) ToInbound(userId int) *model.Inbound {
|
||||
return &model.Inbound{
|
||||
UserId: userId,
|
||||
Up: i.Up,
|
||||
Down: i.Down,
|
||||
Total: 0,
|
||||
Remark: i.Remark,
|
||||
Enable: i.Enable,
|
||||
ExpiryTime: 0,
|
||||
Listen: i.Listen,
|
||||
Port: i.Port,
|
||||
Protocol: model.Protocol(i.Protocol),
|
||||
Settings: i.Settings,
|
||||
StreamSettings: i.StreamSettings,
|
||||
Tag: i.Tag,
|
||||
Sniffing: i.Sniffing,
|
||||
}
|
||||
}
|
||||
51
v2ui/v2ui.go
@@ -1,51 +0,0 @@
|
||||
package v2ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"x-ui/config"
|
||||
"x-ui/database"
|
||||
"x-ui/database/model"
|
||||
"x-ui/util/common"
|
||||
"x-ui/web/service"
|
||||
)
|
||||
|
||||
func MigrateFromV2UI(dbPath string) error {
|
||||
err := initDB(dbPath)
|
||||
if err != nil {
|
||||
return common.NewError("init v2-ui database failed:", err)
|
||||
}
|
||||
err = database.InitDB(config.GetDBPath())
|
||||
if err != nil {
|
||||
return common.NewError("init x-ui database failed:", err)
|
||||
}
|
||||
|
||||
v2Inbounds, err := getV2Inbounds()
|
||||
if err != nil {
|
||||
return common.NewError("get v2-ui inbounds failed:", err)
|
||||
}
|
||||
if len(v2Inbounds) == 0 {
|
||||
fmt.Println("migrate v2-ui inbounds success: 0")
|
||||
return nil
|
||||
}
|
||||
|
||||
userService := service.UserService{}
|
||||
user, err := userService.GetFirstUser()
|
||||
if err != nil {
|
||||
return common.NewError("get x-ui user failed:", err)
|
||||
}
|
||||
|
||||
inbounds := make([]*model.Inbound, 0)
|
||||
for _, v2inbound := range v2Inbounds {
|
||||
inbounds = append(inbounds, v2inbound.ToInbound(user.Id))
|
||||
}
|
||||
|
||||
inboundService := service.InboundService{}
|
||||
err = inboundService.AddInbounds(inbounds)
|
||||
if err != nil {
|
||||
return common.NewError("add x-ui inbounds failed:", err)
|
||||
}
|
||||
|
||||
fmt.Println("migrate v2-ui inbounds success:", len(inbounds))
|
||||
|
||||
return nil
|
||||
}
|
||||
9
web/assets/ant-design-vue@1.7.2/antd.min.css
vendored
@@ -996,7 +996,8 @@ to{transform:scale(0) translate(50%,-50%);opacity:0}
|
||||
.ant-menu-horizontal .ant-menu-item,.ant-menu-horizontal .ant-menu-submenu{margin-top:-1px}
|
||||
.ant-menu-horizontal>.ant-menu-item-active,.ant-menu-horizontal>.ant-menu-item:hover,.ant-menu-horizontal>.ant-menu-submenu .ant-menu-submenu-title:hover{background-color:transparent}
|
||||
.ant-menu-item-selected,.ant-menu-item-selected>a,.ant-menu-item-selected>a:hover{color:#1890ff}
|
||||
.ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected{background: linear-gradient(90deg,#009670 0,#026247 100%);color: #fff;border-radius: 0.5rem}
|
||||
.ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected{background-color: #0a7557;background-image: linear-gradient( 270deg, rgba(123, 199, 77, 0) 30%, #00ab80, rgba(123, 199, 77, 0) 100% );background-repeat: no-repeat;animation: ma-bg-move linear 6.6s infinite;/*background: linear-gradient(90deg,#009670 0,#026247 100%);*/color: #fff;border-radius: 0.5rem}
|
||||
@-webkit-keyframes ma-bg-move {0% {background-position: -500px 0;}100% {background-position: 1000px 0;}}@keyframes ma-bg-move {0% {background-position: -500px 0;}50% {background-position: 1000px 0;}100% {background-position: 1000px 0;}}
|
||||
.ant-menu-vertical-right{border-left:1px solid #e8e8e8}
|
||||
.ant-menu-vertical-left.ant-menu-sub,.ant-menu-vertical-right.ant-menu-sub,.ant-menu-vertical.ant-menu-sub{min-width:160px;padding:0;border-right:0;transform-origin:0 0}
|
||||
.ant-menu-vertical-left.ant-menu-sub .ant-menu-item,.ant-menu-vertical-right.ant-menu-sub .ant-menu-item,.ant-menu-vertical.ant-menu-sub .ant-menu-item{left:0;margin-left:0;border-right:0}
|
||||
@@ -1081,8 +1082,8 @@ to{transform:scale(0) translate(50%,-50%);opacity:0}
|
||||
.ant-menu-dark .ant-menu-item-selected{color:#fff;border-right:0}
|
||||
.ant-menu-dark .ant-menu-item-selected:after{border-right:0}
|
||||
.ant-menu-dark .ant-menu-item-selected .anticon,.ant-menu-dark .ant-menu-item-selected .anticon+span,.ant-menu-dark .ant-menu-item-selected>a,.ant-menu-dark .ant-menu-item-selected>a:hover{color:#ffffff}
|
||||
.ant-menu-submenu-popup.ant-menu-dark .ant-menu-item-selected,.ant-menu.ant-menu-dark .ant-menu-item-selected{background-color:#15223a}
|
||||
.ant-menu-submenu-popup.ant-menu-dark .ant-menu-item-active,.ant-menu.ant-menu-dark .ant-menu-item-active{background-color:#38383800}
|
||||
.ant-menu-submenu-popup.ant-menu-dark .ant-menu-item-selected,.ant-menu.ant-menu-dark .ant-menu-item-selected{background-color:#0a7557}
|
||||
/*.ant-menu-submenu-popup.ant-menu-dark .ant-menu-item-active,.ant-menu.ant-menu-dark .ant-menu-item-active{background-color:#0a7557}*/
|
||||
.ant-menu-dark .ant-menu-item-disabled,.ant-menu-dark .ant-menu-item-disabled>a,.ant-menu-dark .ant-menu-submenu-disabled,.ant-menu-dark .ant-menu-submenu-disabled>a{color:hsla(0,0%,100%,.35)!important;opacity:.8}
|
||||
.ant-menu-dark .ant-menu-item-disabled>.ant-menu-submenu-title,.ant-menu-dark .ant-menu-submenu-disabled>.ant-menu-submenu-title{color:hsla(0,0%,100%,.35)!important}
|
||||
.ant-menu-dark .ant-menu-item-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-item-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:before,.ant-menu-dark .ant-menu-submenu-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-submenu-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:before{background:hsla(0,0%,100%,.35)!important}
|
||||
@@ -3231,7 +3232,7 @@ textarea.ant-input-number{max-width:100%;height:auto;min-height:32px;line-height
|
||||
.ant-input-number-handler-down-inner:before,.ant-input-number-handler-up-inner:before{display:none}
|
||||
.ant-input-number-handler-down-inner .ant-input-number-handler-down-inner-icon,.ant-input-number-handler-down-inner .ant-input-number-handler-up-inner-icon,.ant-input-number-handler-up-inner .ant-input-number-handler-down-inner-icon,.ant-input-number-handler-up-inner .ant-input-number-handler-up-inner-icon{display:block}
|
||||
.ant-input-number-focused,.ant-input-number:hover{border-color:rgb(0, 150, 112) !important;border-right-width:1px!important}
|
||||
.ant-input-number-focused{outline:0;box-shadow:0 0 0 2px rgba(24,144,255,.2)}
|
||||
.ant-input-number-focused{outline:0;box-shadow:rgba(0, 150, 112, 0.2) 0px 0px 0px 2px}
|
||||
.ant-input-number-disabled{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}
|
||||
.ant-input-number-disabled:hover{border-color:#d9d9d9;border-right-width:1px!important}
|
||||
.ant-input-number-disabled .ant-input-number-input{cursor:not-allowed}
|
||||
|
||||
@@ -202,9 +202,25 @@ body {
|
||||
}
|
||||
|
||||
.ant-card-dark:hover {
|
||||
border-color: #e8e8e8;
|
||||
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 70%);
|
||||
/*border-color: #e8e8e8;
|
||||
animation:light-shadow ease-in 3s infinite;*/
|
||||
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 80%);
|
||||
}
|
||||
/*
|
||||
@keyframes light-shadow {
|
||||
0% {
|
||||
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 60%);
|
||||
}
|
||||
20% {
|
||||
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 60%);
|
||||
}
|
||||
60% {
|
||||
box-shadow: 0 1px 11px 2px rgb(154 175 238 / 70%);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 60%);
|
||||
}
|
||||
}*/
|
||||
|
||||
.ant-setting-textarea {
|
||||
margin-top: 1.5rem;
|
||||
@@ -213,6 +229,7 @@ body {
|
||||
}
|
||||
|
||||
.ant-card-dark-box-nohover{
|
||||
margin-top: .5rem;
|
||||
padding: 0 20px 20px !important;
|
||||
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 0%) !important;
|
||||
}
|
||||
@@ -245,6 +262,7 @@ body {
|
||||
.ant-card-dark .ant-collapse-content,
|
||||
.ant-card-dark .ant-calendar,
|
||||
.ant-card-dark .ant-table-placeholder,
|
||||
.ant-card-dark .ant-select-selection__choice,
|
||||
.ant-card-dark .ant-input-group-addon {
|
||||
color: hsla(0,0%,100%,.65);
|
||||
background-color: #262f3d;
|
||||
|
||||
@@ -6,7 +6,7 @@ const supportLangs = [
|
||||
},
|
||||
{
|
||||
name: 'فارسی',
|
||||
value: 'fa_IR',
|
||||
value: 'fa-IR',
|
||||
icon: '🇮🇷',
|
||||
},
|
||||
{
|
||||
@@ -16,9 +16,19 @@ const supportLangs = [
|
||||
},
|
||||
{
|
||||
name: 'Русский',
|
||||
value: 'ru_RU',
|
||||
value: 'ru-RU',
|
||||
icon: '🇷🇺',
|
||||
},
|
||||
{
|
||||
name: 'Tiếng Việt',
|
||||
value: 'vi-VN',
|
||||
icon: '🇻🇳',
|
||||
},
|
||||
{
|
||||
name: 'Español',
|
||||
value: 'es-ES',
|
||||
icon: '🇪🇸',
|
||||
},
|
||||
];
|
||||
|
||||
function getLang() {
|
||||
|
||||
@@ -168,6 +168,7 @@ class AllSetting {
|
||||
|
||||
constructor(data) {
|
||||
this.webListen = "";
|
||||
this.webDomain = "";
|
||||
this.webPort = 2053;
|
||||
this.webCertFile = "";
|
||||
this.webKeyFile = "";
|
||||
@@ -180,6 +181,7 @@ class AllSetting {
|
||||
this.tgBotChatId = "";
|
||||
this.tgRunTime = "@daily";
|
||||
this.tgBotBackup = false;
|
||||
this.tgBotLoginNotify = true;
|
||||
this.tgCpu = "";
|
||||
this.tgLang = "en-US";
|
||||
this.xrayTemplateConfig = "";
|
||||
@@ -187,11 +189,13 @@ class AllSetting {
|
||||
this.subEnable = false;
|
||||
this.subListen = "";
|
||||
this.subPort = "2096";
|
||||
this.subPath = "sub/";
|
||||
this.subPath = "/sub/";
|
||||
this.subDomain = "";
|
||||
this.subCertFile = "";
|
||||
this.subKeyFile = "";
|
||||
this.subUpdates = 0;
|
||||
this.subEncrypt = true;
|
||||
this.subShowInfo = true;
|
||||
|
||||
this.timeLocation = "Asia/Tehran";
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ const Protocols = {
|
||||
TROJAN: 'trojan',
|
||||
SHADOWSOCKS: 'shadowsocks',
|
||||
DOKODEMO: 'dokodemo-door',
|
||||
MTPROTO: 'mtproto',
|
||||
SOCKS: 'socks',
|
||||
HTTP: 'http',
|
||||
};
|
||||
@@ -17,8 +16,15 @@ const VmessMethods = {
|
||||
};
|
||||
|
||||
const SSMethods = {
|
||||
BLAKE3_AES_128_GCM: '2022-blake3-aes-128-gcm',
|
||||
BLAKE3_AES_256_GCM: '2022-blake3-aes-256-gcm',
|
||||
AES_256_GCM: 'aes-256-gcm',
|
||||
AES_128_GCM: 'aes-128-gcm',
|
||||
CHACHA20_POLY1305: 'chacha20-poly1305',
|
||||
CHACHA20_IETF_POLY1305: 'chacha20-ietf-poly1305',
|
||||
XCHACHA20_POLY1305: 'xchacha20-poly1305',
|
||||
XCHACHA20_IETF_POLY1305: 'xchacha20-ietf-poly1305',
|
||||
BLAKE3_AES_128_GCM: '2022-blake3-aes-128-gcm',
|
||||
BLAKE3_AES_256_GCM: '2022-blake3-aes-256-gcm',
|
||||
BLAKE3_CHACHA20_POLY1305: '2022-blake3-chacha20-poly1305',
|
||||
};
|
||||
|
||||
const XTLS_FLOW_CONTROL = {
|
||||
@@ -72,15 +78,16 @@ const UTLS_FINGERPRINT = {
|
||||
};
|
||||
|
||||
const ALPN_OPTION = {
|
||||
HTTP1: "http/1.1",
|
||||
H2: "h2",
|
||||
H3: "h3",
|
||||
H2: "h2",
|
||||
HTTP1: "http/1.1",
|
||||
};
|
||||
|
||||
const SNIFFING_OPTION = {
|
||||
HTTP: "http",
|
||||
TLS: "tls",
|
||||
QUIC: "quic",
|
||||
FAKEDNS: "fakedns"
|
||||
};
|
||||
|
||||
Object.freeze(Protocols);
|
||||
@@ -379,7 +386,10 @@ class WsStreamSettings extends XrayCommonClass {
|
||||
}
|
||||
|
||||
class HttpStreamSettings extends XrayCommonClass {
|
||||
constructor(path='/', host=['']) {
|
||||
constructor(
|
||||
path='/',
|
||||
host=[''],
|
||||
) {
|
||||
super();
|
||||
this.path = path;
|
||||
this.host = host.length === 0 ? [''] : host;
|
||||
@@ -413,7 +423,7 @@ class HttpStreamSettings extends XrayCommonClass {
|
||||
|
||||
class QuicStreamSettings extends XrayCommonClass {
|
||||
constructor(security=VmessMethods.NONE,
|
||||
key='', type='none') {
|
||||
key=RandomUtil.randomSeq(10), type='none') {
|
||||
super();
|
||||
this.security = security;
|
||||
this.key = key;
|
||||
@@ -442,7 +452,7 @@ class QuicStreamSettings extends XrayCommonClass {
|
||||
class GrpcStreamSettings extends XrayCommonClass {
|
||||
constructor(
|
||||
serviceName="",
|
||||
multiMode=false
|
||||
multiMode=false,
|
||||
) {
|
||||
super();
|
||||
this.serviceName = serviceName;
|
||||
@@ -459,24 +469,27 @@ class GrpcStreamSettings extends XrayCommonClass {
|
||||
toJson() {
|
||||
return {
|
||||
serviceName: this.serviceName,
|
||||
multiMode: this.multiMode
|
||||
multiMode: this.multiMode,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TlsStreamSettings extends XrayCommonClass {
|
||||
constructor(serverName='',
|
||||
minVersion = TLS_VERSION_OPTION.TLS12,
|
||||
maxVersion = TLS_VERSION_OPTION.TLS13,
|
||||
cipherSuites = '',
|
||||
rejectUnknownSni = false,
|
||||
certificates=[new TlsStreamSettings.Cert()],
|
||||
alpn=[ALPN_OPTION.H2,ALPN_OPTION.HTTP1],
|
||||
alpn=[ALPN_OPTION.HTTP1,ALPN_OPTION.H2],
|
||||
settings=new TlsStreamSettings.Settings()) {
|
||||
super();
|
||||
this.server = serverName;
|
||||
this.minVersion = minVersion;
|
||||
this.maxVersion = maxVersion;
|
||||
this.cipherSuites = cipherSuites;
|
||||
this.rejectUnknownSni = rejectUnknownSni;
|
||||
this.certs = certificates;
|
||||
this.alpn = alpn;
|
||||
this.settings = settings;
|
||||
@@ -498,12 +511,14 @@ class TlsStreamSettings extends XrayCommonClass {
|
||||
}
|
||||
|
||||
if (!ObjectUtil.isEmpty(json.settings)) {
|
||||
settings = new TlsStreamSettings.Settings(json.settings.allowInsecure , json.settings.fingerprint, json.settings.serverName, json.settings.domains); }
|
||||
settings = new TlsStreamSettings.Settings(json.settings.allowInsecure , json.settings.fingerprint, json.settings.serverName, json.settings.domains);
|
||||
}
|
||||
return new TlsStreamSettings(
|
||||
json.serverName,
|
||||
json.minVersion,
|
||||
json.maxVersion,
|
||||
json.cipherSuites,
|
||||
json.rejectUnknownSni,
|
||||
certs,
|
||||
json.alpn,
|
||||
settings,
|
||||
@@ -516,6 +531,7 @@ class TlsStreamSettings extends XrayCommonClass {
|
||||
minVersion: this.minVersion,
|
||||
maxVersion: this.maxVersion,
|
||||
cipherSuites: this.cipherSuites,
|
||||
rejectUnknownSni: this.rejectUnknownSni,
|
||||
certificates: TlsStreamSettings.toJsonArray(this.certs),
|
||||
alpn: this.alpn,
|
||||
settings: this.settings,
|
||||
@@ -524,13 +540,14 @@ class TlsStreamSettings extends XrayCommonClass {
|
||||
}
|
||||
|
||||
TlsStreamSettings.Cert = class extends XrayCommonClass {
|
||||
constructor(useFile=true, certificateFile='', keyFile='', certificate='', key='') {
|
||||
constructor(useFile=true, certificateFile='', keyFile='', certificate='', key='', ocspStapling=3600) {
|
||||
super();
|
||||
this.useFile = useFile;
|
||||
this.certFile = certificateFile;
|
||||
this.keyFile = keyFile;
|
||||
this.cert = certificate instanceof Array ? certificate.join('\n') : certificate;
|
||||
this.key = key instanceof Array ? key.join('\n') : key;
|
||||
this.ocspStapling = ocspStapling;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
@@ -538,13 +555,15 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
|
||||
return new TlsStreamSettings.Cert(
|
||||
true,
|
||||
json.certificateFile,
|
||||
json.keyFile,
|
||||
json.keyFile, '', '',
|
||||
json.ocspStapling,
|
||||
);
|
||||
} else {
|
||||
return new TlsStreamSettings.Cert(
|
||||
false, '', '',
|
||||
json.certificate.join('\n'),
|
||||
json.key.join('\n'),
|
||||
json.ocspStapling,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -554,11 +573,13 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
|
||||
return {
|
||||
certificateFile: this.certFile,
|
||||
keyFile: this.keyFile,
|
||||
ocspStapling: this.ocspStapling,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
certificate: this.cert.split('\n'),
|
||||
key: this.key.split('\n'),
|
||||
ocspStapling: this.ocspStapling,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -785,6 +806,34 @@ RealityStreamSettings.Settings = class extends XrayCommonClass {
|
||||
}
|
||||
};
|
||||
|
||||
class SockoptStreamSettings extends XrayCommonClass {
|
||||
constructor(acceptProxyProtocol = false, tcpFastOpen = false, mark = 0, tproxy="off") {
|
||||
super();
|
||||
this.acceptProxyProtocol = acceptProxyProtocol;
|
||||
this.tcpFastOpen = tcpFastOpen;
|
||||
this.mark = mark;
|
||||
this.tproxy = tproxy;
|
||||
}
|
||||
static fromJson(json = {}) {
|
||||
if (Object.keys(json).length === 0) return undefined;
|
||||
return new SockoptStreamSettings(
|
||||
json.acceptProxyProtocol,
|
||||
json.tcpFastOpen,
|
||||
json.mark,
|
||||
json.tproxy,
|
||||
);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
acceptProxyProtocol: this.acceptProxyProtocol,
|
||||
tcpFastOpen: this.tcpFastOpen,
|
||||
mark: this.mark,
|
||||
tproxy: this.tproxy,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class StreamSettings extends XrayCommonClass {
|
||||
constructor(network='tcp',
|
||||
security='none',
|
||||
@@ -797,6 +846,7 @@ class StreamSettings extends XrayCommonClass {
|
||||
httpSettings=new HttpStreamSettings(),
|
||||
quicSettings=new QuicStreamSettings(),
|
||||
grpcSettings=new GrpcStreamSettings(),
|
||||
sockopt = undefined,
|
||||
) {
|
||||
super();
|
||||
this.network = network;
|
||||
@@ -810,6 +860,7 @@ class StreamSettings extends XrayCommonClass {
|
||||
this.http = httpSettings;
|
||||
this.quic = quicSettings;
|
||||
this.grpc = grpcSettings;
|
||||
this.sockopt = sockopt;
|
||||
}
|
||||
|
||||
get isTls() {
|
||||
@@ -849,6 +900,14 @@ class StreamSettings extends XrayCommonClass {
|
||||
}
|
||||
}
|
||||
|
||||
get sockoptSwitch() {
|
||||
return this.sockopt != undefined;
|
||||
}
|
||||
|
||||
set sockoptSwitch(value) {
|
||||
this.sockopt = value ? new SockoptStreamSettings() : undefined;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
|
||||
return new StreamSettings(
|
||||
@@ -863,6 +922,7 @@ class StreamSettings extends XrayCommonClass {
|
||||
HttpStreamSettings.fromJson(json.httpSettings),
|
||||
QuicStreamSettings.fromJson(json.quicSettings),
|
||||
GrpcStreamSettings.fromJson(json.grpcSettings),
|
||||
SockoptStreamSettings.fromJson(json.sockopt),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -880,12 +940,13 @@ class StreamSettings extends XrayCommonClass {
|
||||
httpSettings: network === 'http' ? this.http.toJson() : undefined,
|
||||
quicSettings: network === 'quic' ? this.quic.toJson() : undefined,
|
||||
grpcSettings: network === 'grpc' ? this.grpc.toJson() : undefined,
|
||||
sockopt: this.sockopt != undefined ? this.sockopt.toJson() : undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class Sniffing extends XrayCommonClass {
|
||||
constructor(enabled=true, destOverride=['http', 'tls', 'quic']) {
|
||||
constructor(enabled=true, destOverride=['http', 'tls', 'quic', 'fakedns']) {
|
||||
super();
|
||||
this.enabled = enabled;
|
||||
this.destOverride = destOverride;
|
||||
@@ -895,7 +956,7 @@ class Sniffing extends XrayCommonClass {
|
||||
let destOverride = ObjectUtil.clone(json.destOverride);
|
||||
if (!ObjectUtil.isEmpty(destOverride) && !ObjectUtil.isArrEmpty(destOverride)) {
|
||||
if (ObjectUtil.isEmpty(destOverride[0])) {
|
||||
destOverride = ['http', 'tls', 'quic'];
|
||||
destOverride = ['http', 'tls', 'quic', 'fakedns'];
|
||||
}
|
||||
}
|
||||
return new Sniffing(
|
||||
@@ -965,7 +1026,6 @@ class Inbound extends XrayCommonClass {
|
||||
}
|
||||
}
|
||||
|
||||
//for Reality
|
||||
get reality() {
|
||||
return this.stream.security === 'reality';
|
||||
}
|
||||
@@ -1019,6 +1079,12 @@ class Inbound extends XrayCommonClass {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
get isSSMultiUser() {
|
||||
return this.method != SSMethods.BLAKE3_CHACHA20_POLY1305;
|
||||
}
|
||||
get isSS2022(){
|
||||
return this.method.substring(0,4) === "2022";
|
||||
}
|
||||
|
||||
get serverName() {
|
||||
if (this.stream.isTls || this.stream.isXtls || this.stream.isReality) {
|
||||
@@ -1044,7 +1110,7 @@ class Inbound extends XrayCommonClass {
|
||||
} else if (this.isWs) {
|
||||
return this.stream.ws.path;
|
||||
} else if (this.isH2) {
|
||||
return this.stream.http.path[0];
|
||||
return this.stream.http.path;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -1088,7 +1154,7 @@ class Inbound extends XrayCommonClass {
|
||||
return this.settings.trojans[index].expiryTime < new Date().getTime();
|
||||
return false
|
||||
case Protocols.SHADOWSOCKS:
|
||||
if(this.settings.shadowsockses[index].expiryTime > 0)
|
||||
if(this.settings.shadowsockses.length > 0 && this.settings.shadowsockses[index].expiryTime > 0)
|
||||
return this.settings.shadowsockses[index].expiryTime < new Date().getTime();
|
||||
return false
|
||||
default:
|
||||
@@ -1169,6 +1235,7 @@ class Inbound extends XrayCommonClass {
|
||||
case Protocols.VMESS:
|
||||
case Protocols.VLESS:
|
||||
case Protocols.TROJAN:
|
||||
case Protocols.SHADOWSOCKS:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
@@ -1207,7 +1274,6 @@ class Inbound extends XrayCommonClass {
|
||||
add: address,
|
||||
port: this.port,
|
||||
id: this.settings.vmesses[clientIndex].id,
|
||||
aid: this.settings.vmesses[clientIndex].alterId,
|
||||
net: this.stream.network,
|
||||
type: 'none',
|
||||
tls: this.stream.security,
|
||||
@@ -1344,7 +1410,7 @@ class Inbound extends XrayCommonClass {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.xtls) {
|
||||
else if (this.xtls) {
|
||||
params.set("security", "xtls");
|
||||
params.set("alpn", this.stream.xtls.alpn);
|
||||
if(this.stream.xtls.settings.allowInsecure){
|
||||
@@ -1359,7 +1425,7 @@ class Inbound extends XrayCommonClass {
|
||||
params.set("flow", this.settings.vlesses[clientIndex].flow);
|
||||
}
|
||||
|
||||
if (this.reality) {
|
||||
else if (this.reality) {
|
||||
params.set("security", "reality");
|
||||
params.set("fp", this.stream.reality.settings.fingerprint);
|
||||
params.set("pbk", this.stream.reality.settings.publicKey);
|
||||
@@ -1380,6 +1446,10 @@ class Inbound extends XrayCommonClass {
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
params.set("security", "none");
|
||||
}
|
||||
|
||||
const link = `vless://${uuid}@${address}:${port}`;
|
||||
const url = new URL(link);
|
||||
for (const [key, value] of params) {
|
||||
@@ -1392,8 +1462,68 @@ class Inbound extends XrayCommonClass {
|
||||
genSSLink(address='', remark='', clientIndex = 0) {
|
||||
let settings = this.settings;
|
||||
const port = this.port;
|
||||
const type = this.stream.network;
|
||||
const params = new Map();
|
||||
params.set("type", this.stream.network);
|
||||
switch (type) {
|
||||
case "tcp":
|
||||
const tcp = this.stream.tcp;
|
||||
if (tcp.type === 'http') {
|
||||
const request = tcp.request;
|
||||
params.set("path", request.path.join(','));
|
||||
const index = request.headers.findIndex(header => header.name.toLowerCase() === 'host');
|
||||
if (index >= 0) {
|
||||
const host = request.headers[index].value;
|
||||
params.set("host", host);
|
||||
}
|
||||
params.set("headerType", 'http');
|
||||
}
|
||||
break;
|
||||
case "kcp":
|
||||
const kcp = this.stream.kcp;
|
||||
params.set("headerType", kcp.type);
|
||||
params.set("seed", kcp.seed);
|
||||
break;
|
||||
case "ws":
|
||||
const ws = this.stream.ws;
|
||||
params.set("path", ws.path);
|
||||
const index = ws.headers.findIndex(header => header.name.toLowerCase() === 'host');
|
||||
if (index >= 0) {
|
||||
const host = ws.headers[index].value;
|
||||
params.set("host", host);
|
||||
}
|
||||
break;
|
||||
case "http":
|
||||
const http = this.stream.http;
|
||||
params.set("path", http.path);
|
||||
params.set("host", http.host);
|
||||
break;
|
||||
case "quic":
|
||||
const quic = this.stream.quic;
|
||||
params.set("quicSecurity", quic.security);
|
||||
params.set("key", quic.key);
|
||||
params.set("headerType", quic.type);
|
||||
break;
|
||||
case "grpc":
|
||||
const grpc = this.stream.grpc;
|
||||
params.set("serviceName", grpc.serviceName);
|
||||
if(grpc.multiMode){
|
||||
params.set("mode", "multi");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return 'ss://' + safeBase64(settings.method + ':' + settings.password + ':' +settings.shadowsockses[clientIndex].password) + '@' + address + ':' + this.port + '#' + encodeURIComponent(remark);
|
||||
let password = new Array();
|
||||
if (this.isSS2022) password.push(settings.password);
|
||||
if (this.isSSMultiUser) password.push(settings.shadowsockses[clientIndex].password);
|
||||
|
||||
let link = `ss://${safeBase64(settings.method + ':' + password.join(':'))}@${address}:${this.port}`;
|
||||
const url = new URL(link);
|
||||
for (const [key, value] of params) {
|
||||
url.searchParams.set(key, value)
|
||||
}
|
||||
url.hash = encodeURIComponent(remark);
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
genTrojanLink(address = '', remark = '', clientIndex = 0) {
|
||||
@@ -1465,7 +1595,7 @@ class Inbound extends XrayCommonClass {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.reality) {
|
||||
else if (this.reality) {
|
||||
params.set("security", "reality");
|
||||
params.set("fp", this.stream.reality.settings.fingerprint);
|
||||
params.set("pbk", this.stream.reality.settings.publicKey);
|
||||
@@ -1483,7 +1613,7 @@ class Inbound extends XrayCommonClass {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.xtls) {
|
||||
else if (this.xtls) {
|
||||
params.set("security", "xtls");
|
||||
params.set("alpn", this.stream.xtls.alpn);
|
||||
if(this.stream.xtls.settings.allowInsecure){
|
||||
@@ -1498,6 +1628,10 @@ class Inbound extends XrayCommonClass {
|
||||
params.set("flow", this.settings.trojans[clientIndex].flow);
|
||||
}
|
||||
|
||||
else {
|
||||
params.set("security", "none");
|
||||
}
|
||||
|
||||
const link = `trojan://${settings.trojans[clientIndex].password}@${address}:${this.port}`;
|
||||
const url = new URL(link);
|
||||
for (const [key, value] of params) {
|
||||
@@ -1531,10 +1665,10 @@ class Inbound extends XrayCommonClass {
|
||||
JSON.parse(this.settings).clients.forEach((client,index) => {
|
||||
if(this.tls && !ObjectUtil.isArrEmpty(this.stream.tls.settings.domains)){
|
||||
this.stream.tls.settings.domains.forEach((domain) => {
|
||||
link += this.genLink(domain.domain, remark + '-' + client.email + '-' + domain.remark, index) + '\r\n';
|
||||
link += this.genLink(domain.domain, [remark, client.email, domain.remark].filter(x => x.length > 0).join('-'), index) + '\r\n';
|
||||
});
|
||||
} else {
|
||||
link += this.genLink(address, remark + '-' + client.email, index) + '\r\n';
|
||||
link += this.genLink(address, [remark, client.email].filter(x => x.length > 0).join('-'), index) + '\r\n';
|
||||
}
|
||||
});
|
||||
return link;
|
||||
@@ -1586,7 +1720,6 @@ Inbound.Settings = class extends XrayCommonClass {
|
||||
case Protocols.TROJAN: return new Inbound.TrojanSettings(protocol);
|
||||
case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings(protocol);
|
||||
case Protocols.DOKODEMO: return new Inbound.DokodemoSettings(protocol);
|
||||
case Protocols.MTPROTO: return new Inbound.MtprotoSettings(protocol);
|
||||
case Protocols.SOCKS: return new Inbound.SocksSettings(protocol);
|
||||
case Protocols.HTTP: return new Inbound.HttpSettings(protocol);
|
||||
default: return null;
|
||||
@@ -1600,7 +1733,6 @@ Inbound.Settings = class extends XrayCommonClass {
|
||||
case Protocols.TROJAN: return Inbound.TrojanSettings.fromJson(json);
|
||||
case Protocols.SHADOWSOCKS: return Inbound.ShadowsocksSettings.fromJson(json);
|
||||
case Protocols.DOKODEMO: return Inbound.DokodemoSettings.fromJson(json);
|
||||
case Protocols.MTPROTO: return Inbound.MtprotoSettings.fromJson(json);
|
||||
case Protocols.SOCKS: return Inbound.SocksSettings.fromJson(json);
|
||||
case Protocols.HTTP: return Inbound.HttpSettings.fromJson(json);
|
||||
default: return null;
|
||||
@@ -1614,11 +1746,9 @@ Inbound.Settings = class extends XrayCommonClass {
|
||||
|
||||
Inbound.VmessSettings = class extends Inbound.Settings {
|
||||
constructor(protocol,
|
||||
vmesses=[new Inbound.VmessSettings.Vmess()],
|
||||
disableInsecureEncryption=false) {
|
||||
vmesses=[new Inbound.VmessSettings.Vmess()]) {
|
||||
super(protocol);
|
||||
this.vmesses = vmesses;
|
||||
this.disableInsecure = disableInsecureEncryption;
|
||||
}
|
||||
|
||||
indexOfVmessById(id) {
|
||||
@@ -1643,22 +1773,19 @@ Inbound.VmessSettings = class extends Inbound.Settings {
|
||||
return new Inbound.VmessSettings(
|
||||
Protocols.VMESS,
|
||||
json.clients.map(client => Inbound.VmessSettings.Vmess.fromJson(client)),
|
||||
ObjectUtil.isEmpty(json.disableInsecureEncryption) ? false : json.disableInsecureEncryption,
|
||||
);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
clients: Inbound.VmessSettings.toJsonArray(this.vmesses),
|
||||
disableInsecureEncryption: this.disableInsecure,
|
||||
};
|
||||
}
|
||||
};
|
||||
Inbound.VmessSettings.Vmess = class extends XrayCommonClass {
|
||||
constructor(id=RandomUtil.randomUUID(), alterId=0, email=RandomUtil.randomText(),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId='') {
|
||||
constructor(id=RandomUtil.randomUUID(), email=RandomUtil.randomLowerAndNum(8),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16)) {
|
||||
super();
|
||||
this.id = id;
|
||||
this.alterId = alterId;
|
||||
this.email = email;
|
||||
this.limitIp = limitIp;
|
||||
this.totalGB = totalGB;
|
||||
@@ -1671,7 +1798,6 @@ Inbound.VmessSettings.Vmess = class extends XrayCommonClass {
|
||||
static fromJson(json={}) {
|
||||
return new Inbound.VmessSettings.Vmess(
|
||||
json.id,
|
||||
json.alterId,
|
||||
json.email,
|
||||
json.limitIp,
|
||||
json.totalGB,
|
||||
@@ -1747,7 +1873,7 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
|
||||
|
||||
};
|
||||
Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
|
||||
constructor(id=RandomUtil.randomUUID(), flow='', email=RandomUtil.randomText(),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId='') {
|
||||
constructor(id=RandomUtil.randomUUID(), flow='', email=RandomUtil.randomLowerAndNum(8),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16)) {
|
||||
super();
|
||||
this.id = id;
|
||||
this.flow = flow;
|
||||
@@ -1870,7 +1996,7 @@ Inbound.TrojanSettings = class extends Inbound.Settings {
|
||||
}
|
||||
};
|
||||
Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
|
||||
constructor(password=RandomUtil.randomSeq(10), flow='', email=RandomUtil.randomText(),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId='') {
|
||||
constructor(password=RandomUtil.randomSeq(10), flow='', email=RandomUtil.randomLowerAndNum(8),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16)) {
|
||||
super();
|
||||
this.password = password;
|
||||
this.flow = flow;
|
||||
@@ -2012,8 +2138,9 @@ Inbound.ShadowsocksSettings = class extends Inbound.Settings {
|
||||
};
|
||||
|
||||
Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
|
||||
constructor(password=RandomUtil.randomShadowsocksPassword(), email=RandomUtil.randomText(),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId='') {
|
||||
constructor(method='', password=RandomUtil.randomShadowsocksPassword(), email=RandomUtil.randomLowerAndNum(8),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16)) {
|
||||
super();
|
||||
this.method = method;
|
||||
this.password = password;
|
||||
this.email = email;
|
||||
this.limitIp = limitIp;
|
||||
@@ -2026,6 +2153,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
method: this.method,
|
||||
password: this.password,
|
||||
email: this.email,
|
||||
limitIp: this.limitIp,
|
||||
@@ -2039,6 +2167,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
|
||||
|
||||
static fromJson(json = {}) {
|
||||
return new Inbound.ShadowsocksSettings.Shadowsocks(
|
||||
json.method,
|
||||
json.password,
|
||||
json.email,
|
||||
json.limitIp,
|
||||
@@ -2106,36 +2235,6 @@ Inbound.DokodemoSettings = class extends Inbound.Settings {
|
||||
}
|
||||
};
|
||||
|
||||
Inbound.MtprotoSettings = class extends Inbound.Settings {
|
||||
constructor(protocol, users=[new Inbound.MtprotoSettings.MtUser()]) {
|
||||
super(protocol);
|
||||
this.users = users;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
return new Inbound.MtprotoSettings(
|
||||
Protocols.MTPROTO,
|
||||
json.users.map(user => Inbound.MtprotoSettings.MtUser.fromJson(user)),
|
||||
);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
users: XrayCommonClass.toJsonArray(this.users),
|
||||
};
|
||||
}
|
||||
};
|
||||
Inbound.MtprotoSettings.MtUser = class extends XrayCommonClass {
|
||||
constructor(secret=RandomUtil.randomMTSecret()) {
|
||||
super();
|
||||
this.secret = secret;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
return new Inbound.MtprotoSettings.MtUser(json.secret);
|
||||
}
|
||||
};
|
||||
|
||||
Inbound.SocksSettings = class extends Inbound.Settings {
|
||||
constructor(protocol, auth='password', accounts=[new Inbound.SocksSettings.SocksAccount()], udp=false, ip='127.0.0.1') {
|
||||
super(protocol);
|
||||
|
||||
@@ -5,7 +5,9 @@ const ONE_TB = ONE_GB * 1024;
|
||||
const ONE_PB = ONE_TB * 1024;
|
||||
|
||||
function sizeFormat(size) {
|
||||
if (size < ONE_KB) {
|
||||
if (size < 0) {
|
||||
return "0 B";
|
||||
} else if (size < ONE_KB) {
|
||||
return size.toFixed(0) + " B";
|
||||
} else if (size < ONE_MB) {
|
||||
return (size / ONE_KB).toFixed(2) + " KB";
|
||||
@@ -20,6 +22,23 @@ function sizeFormat(size) {
|
||||
}
|
||||
}
|
||||
|
||||
function cpuSpeedFormat(speed) {
|
||||
if (speed > 1000) {
|
||||
const GHz = speed / 1000;
|
||||
return GHz.toFixed(2) + " GHz";
|
||||
} else {
|
||||
return speed.toFixed(2) + " MHz";
|
||||
}
|
||||
}
|
||||
|
||||
function cpuCoreFormat(cores) {
|
||||
if (cores === 1) {
|
||||
return "1 Core";
|
||||
} else {
|
||||
return cores + " Cores";
|
||||
}
|
||||
}
|
||||
|
||||
function base64(str) {
|
||||
return Base64.encode(str);
|
||||
}
|
||||
@@ -116,3 +135,21 @@ function doAllItemsExist(array1, array2) {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function buildURL({ host, port, isTLS, base, path }) {
|
||||
if (!host || host.length === 0) host = window.location.hostname;
|
||||
if (!port || port.length === 0) port = window.location.port;
|
||||
|
||||
if (isTLS === undefined) isTLS = window.location.protocol === "https:";
|
||||
|
||||
const protocol = isTLS ? "https:" : "http:";
|
||||
|
||||
port = String(port);
|
||||
if (port === "" || (isTLS && port === "443") || (!isTLS && port === "80")) {
|
||||
port = "";
|
||||
} else {
|
||||
port = `:${port}`;
|
||||
}
|
||||
|
||||
return `${protocol}//${host}${port}${base}${path}`;
|
||||
}
|
||||
|
||||
@@ -75,9 +75,7 @@ class PromiseUtil {
|
||||
}
|
||||
}
|
||||
|
||||
const seq = 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
|
||||
|
||||
const shortIdSeq = 'abcdef0123456789'.split('');
|
||||
const seq = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
|
||||
|
||||
class RandomUtil {
|
||||
static randomIntRange(min, max) {
|
||||
@@ -96,57 +94,33 @@ class RandomUtil {
|
||||
return str;
|
||||
}
|
||||
|
||||
static randomShortIdSeq(count) {
|
||||
static randomShortId() {
|
||||
let str = '';
|
||||
for (let i = 0; i < count; ++i) {
|
||||
str += shortIdSeq[this.randomInt(16)];
|
||||
for (let i = 0; i < 8; ++i) {
|
||||
str += seq[this.randomInt(16)];
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
static randomShortId() {
|
||||
return this.randomShortIdSeq(8);
|
||||
}
|
||||
|
||||
static randomLowerAndNum(count) {
|
||||
static randomLowerAndNum(len) {
|
||||
let str = '';
|
||||
for (let i = 0; i < count; ++i) {
|
||||
for (let i = 0; i < len; ++i) {
|
||||
str += seq[this.randomInt(36)];
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
static randomMTSecret() {
|
||||
let str = '';
|
||||
for (let i = 0; i < 32; ++i) {
|
||||
let index = this.randomInt(16);
|
||||
if (index <= 9) {
|
||||
str += index;
|
||||
} else {
|
||||
str += seq[index - 10];
|
||||
}
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
static randomUUID() {
|
||||
let d = new Date().getTime();
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
let r = (d + Math.random() * 16) % 16 | 0;
|
||||
d = Math.floor(d / 16);
|
||||
return (c === 'x' ? r : (r & 0x7 | 0x8)).toString(16);
|
||||
const template = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx';
|
||||
return template.replace(/[xy]/g, function (c) {
|
||||
const randomValues = new Uint8Array(1);
|
||||
crypto.getRandomValues(randomValues);
|
||||
let randomValue = randomValues[0] % 16;
|
||||
let calculatedValue = (c === 'x') ? randomValue : (randomValue & 0x3 | 0x8);
|
||||
return calculatedValue.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
static randomText() {
|
||||
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
|
||||
var string = '';
|
||||
for (var ii = 0; ii < 8; ii++) {
|
||||
string += chars[Math.floor(Math.random() * chars.length)];
|
||||
}
|
||||
return string;
|
||||
}
|
||||
|
||||
static randomShadowsocksPassword() {
|
||||
let array = new Uint8Array(32);
|
||||
window.crypto.getRandomValues(array);
|
||||
|
||||
@@ -4,8 +4,6 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"x-ui/database/model"
|
||||
"x-ui/logger"
|
||||
"x-ui/web/global"
|
||||
"x-ui/web/service"
|
||||
"x-ui/web/session"
|
||||
|
||||
@@ -20,7 +18,6 @@ type InboundController struct {
|
||||
func NewInboundController(g *gin.RouterGroup) *InboundController {
|
||||
a := &InboundController{}
|
||||
a.initRouter(g)
|
||||
a.startTask()
|
||||
return a
|
||||
}
|
||||
|
||||
@@ -40,20 +37,6 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
|
||||
g.POST("/resetAllTraffics", a.resetAllTraffics)
|
||||
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
|
||||
g.POST("/delDepletedClients/:id", a.delDepletedClients)
|
||||
|
||||
}
|
||||
|
||||
func (a *InboundController) startTask() {
|
||||
webServer := global.GetWebServer()
|
||||
c := webServer.GetCron()
|
||||
c.AddFunc("@every 10s", func() {
|
||||
if a.xrayService.IsNeedRestartAndSetFalse() {
|
||||
err := a.xrayService.RestartXray(false)
|
||||
if err != nil {
|
||||
logger.Error("restart xray failed:", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (a *InboundController) getInbounds(c *gin.Context) {
|
||||
@@ -65,6 +48,7 @@ func (a *InboundController) getInbounds(c *gin.Context) {
|
||||
}
|
||||
jsonObj(c, inbounds, nil)
|
||||
}
|
||||
|
||||
func (a *InboundController) getInbound(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
@@ -98,11 +82,12 @@ func (a *InboundController) addInbound(c *gin.Context) {
|
||||
}
|
||||
user := session.GetLoginUser(c)
|
||||
inbound.UserId = user.Id
|
||||
inbound.Enable = true
|
||||
inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
|
||||
inbound, err = a.inboundService.AddInbound(inbound)
|
||||
|
||||
needRestart := false
|
||||
inbound, needRestart, err = a.inboundService.AddInbound(inbound)
|
||||
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.create"), inbound, err)
|
||||
if err == nil {
|
||||
if err == nil && needRestart {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
@@ -113,9 +98,10 @@ func (a *InboundController) delInbound(c *gin.Context) {
|
||||
jsonMsg(c, I18nWeb(c, "delete"), err)
|
||||
return
|
||||
}
|
||||
err = a.inboundService.DelInbound(id)
|
||||
needRestart := true
|
||||
needRestart, err = a.inboundService.DelInbound(id)
|
||||
jsonMsgObj(c, I18nWeb(c, "delete"), id, err)
|
||||
if err == nil {
|
||||
if err == nil && needRestart {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
@@ -134,9 +120,10 @@ func (a *InboundController) updateInbound(c *gin.Context) {
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
|
||||
return
|
||||
}
|
||||
inbound, err = a.inboundService.UpdateInbound(inbound)
|
||||
needRestart := true
|
||||
inbound, needRestart, err = a.inboundService.UpdateInbound(inbound)
|
||||
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.update"), inbound, err)
|
||||
if err == nil {
|
||||
if err == nil && needRestart {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
@@ -145,12 +132,7 @@ func (a *InboundController) getClientIps(c *gin.Context) {
|
||||
email := c.Param("email")
|
||||
|
||||
ips, err := a.inboundService.GetInboundClientIps(email)
|
||||
if err != nil {
|
||||
jsonObj(c, "Failed to get client IPs", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if ips == "" {
|
||||
if err != nil || ips == "" {
|
||||
jsonObj(c, "No IP Record", nil)
|
||||
return
|
||||
}
|
||||
@@ -168,6 +150,7 @@ func (a *InboundController) clearClientIps(c *gin.Context) {
|
||||
}
|
||||
jsonMsg(c, "Log Cleared", nil)
|
||||
}
|
||||
|
||||
func (a *InboundController) addInboundClient(c *gin.Context) {
|
||||
data := &model.Inbound{}
|
||||
err := c.ShouldBind(data)
|
||||
@@ -176,13 +159,15 @@ func (a *InboundController) addInboundClient(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
err = a.inboundService.AddInboundClient(data)
|
||||
needRestart := true
|
||||
|
||||
needRestart, err = a.inboundService.AddInboundClient(data)
|
||||
if err != nil {
|
||||
jsonMsg(c, "Something went wrong!", err)
|
||||
return
|
||||
}
|
||||
jsonMsg(c, "Client(s) added", nil)
|
||||
if err == nil {
|
||||
if err == nil && needRestart {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
@@ -195,13 +180,15 @@ func (a *InboundController) delInboundClient(c *gin.Context) {
|
||||
}
|
||||
clientId := c.Param("clientId")
|
||||
|
||||
err = a.inboundService.DelInboundClient(id, clientId)
|
||||
needRestart := true
|
||||
|
||||
needRestart, err = a.inboundService.DelInboundClient(id, clientId)
|
||||
if err != nil {
|
||||
jsonMsg(c, "Something went wrong!", err)
|
||||
return
|
||||
}
|
||||
jsonMsg(c, "Client deleted", nil)
|
||||
if err == nil {
|
||||
if err == nil && needRestart {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
@@ -216,13 +203,15 @@ func (a *InboundController) updateInboundClient(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
err = a.inboundService.UpdateInboundClient(inbound, clientId)
|
||||
needRestart := true
|
||||
|
||||
needRestart, err = a.inboundService.UpdateInboundClient(inbound, clientId)
|
||||
if err != nil {
|
||||
jsonMsg(c, "Something went wrong!", err)
|
||||
return
|
||||
}
|
||||
jsonMsg(c, "Client updated", nil)
|
||||
if err == nil {
|
||||
if err == nil && needRestart {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
@@ -235,13 +224,15 @@ func (a *InboundController) resetClientTraffic(c *gin.Context) {
|
||||
}
|
||||
email := c.Param("email")
|
||||
|
||||
err = a.inboundService.ResetClientTraffic(id, email)
|
||||
needRestart := true
|
||||
|
||||
needRestart, err = a.inboundService.ResetClientTraffic(id, email)
|
||||
if err != nil {
|
||||
jsonMsg(c, "Something went wrong!", err)
|
||||
return
|
||||
}
|
||||
jsonMsg(c, "traffic reseted", nil)
|
||||
if err == nil {
|
||||
if err == nil && needRestart {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
@@ -251,6 +242,8 @@ func (a *InboundController) resetAllTraffics(c *gin.Context) {
|
||||
if err != nil {
|
||||
jsonMsg(c, "Something went wrong!", err)
|
||||
return
|
||||
} else {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
jsonMsg(c, "All traffics reseted", nil)
|
||||
}
|
||||
@@ -266,6 +259,8 @@ func (a *InboundController) resetAllClientTraffics(c *gin.Context) {
|
||||
if err != nil {
|
||||
jsonMsg(c, "Something went wrong!", err)
|
||||
return
|
||||
} else {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
jsonMsg(c, "All traffics of client reseted", nil)
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ func (a *IndexController) login(c *gin.Context) {
|
||||
user := a.userService.CheckUser(form.Username, form.Password, form.LoginSecret)
|
||||
timeStr := time.Now().Format("2006-01-02 15:04:05")
|
||||
if user == nil {
|
||||
logger.Infof("wrong username or password: \"%s\" \"%s\"", form.Username, form.Password)
|
||||
logger.Warningf("wrong username or password: \"%s\" \"%s\"", form.Username, form.Password)
|
||||
a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0)
|
||||
pureJsonMsg(c, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword"))
|
||||
return
|
||||
@@ -75,13 +75,13 @@ func (a *IndexController) login(c *gin.Context) {
|
||||
|
||||
sessionMaxAge, err := a.settingService.GetSessionMaxAge()
|
||||
if err != nil {
|
||||
logger.Infof("Unable to get session's max age from DB")
|
||||
logger.Warningf("Unable to get session's max age from DB")
|
||||
}
|
||||
|
||||
if sessionMaxAge > 0 {
|
||||
err = session.SetMaxAge(c, sessionMaxAge*60)
|
||||
if err != nil {
|
||||
logger.Infof("Unable to set session's max age")
|
||||
logger.Warningf("Unable to set session's max age")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -118,11 +118,9 @@ func (a *ServerController) restartXrayService(c *gin.Context) {
|
||||
|
||||
func (a *ServerController) getLogs(c *gin.Context) {
|
||||
count := c.Param("count")
|
||||
logs, err := a.serverService.GetLogs(count)
|
||||
if err != nil {
|
||||
jsonMsg(c, "getLogs", err)
|
||||
return
|
||||
}
|
||||
level := c.PostForm("level")
|
||||
syslog := c.PostForm("syslog")
|
||||
logs := a.serverService.GetLogs(count, level, syslog)
|
||||
jsonObj(c, logs, nil)
|
||||
}
|
||||
|
||||
|
||||
@@ -65,77 +65,44 @@ func (a *SettingController) getDefaultJsonConfig(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (a *SettingController) getDefaultSettings(c *gin.Context) {
|
||||
expireDiff, err := a.settingService.GetExpireDiff()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
type settingFunc func() (interface{}, error)
|
||||
|
||||
settings := map[string]settingFunc{
|
||||
"expireDiff": func() (interface{}, error) { return a.settingService.GetExpireDiff() },
|
||||
"trafficDiff": func() (interface{}, error) { return a.settingService.GetTrafficDiff() },
|
||||
"defaultCert": func() (interface{}, error) { return a.settingService.GetCertFile() },
|
||||
"defaultKey": func() (interface{}, error) { return a.settingService.GetKeyFile() },
|
||||
"tgBotEnable": func() (interface{}, error) { return a.settingService.GetTgbotenabled() },
|
||||
"subEnable": func() (interface{}, error) { return a.settingService.GetSubEnable() },
|
||||
"subPort": func() (interface{}, error) { return a.settingService.GetSubPort() },
|
||||
"subPath": func() (interface{}, error) { return a.settingService.GetSubPath() },
|
||||
"subDomain": func() (interface{}, error) { return a.settingService.GetSubDomain() },
|
||||
"subKeyFile": func() (interface{}, error) { return a.settingService.GetSubKeyFile() },
|
||||
"subCertFile": func() (interface{}, error) { return a.settingService.GetSubCertFile() },
|
||||
"subEncrypt": func() (interface{}, error) { return a.settingService.GetSubEncrypt() },
|
||||
"subShowInfo": func() (interface{}, error) { return a.settingService.GetSubShowInfo() },
|
||||
}
|
||||
trafficDiff, err := a.settingService.GetTrafficDiff()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
defaultCert, err := a.settingService.GetCertFile()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
defaultKey, err := a.settingService.GetKeyFile()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
tgBotEnable, err := a.settingService.GetTgbotenabled()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
subEnable, err := a.settingService.GetSubEnable()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
subPort, err := a.settingService.GetSubPort()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
subPath, err := a.settingService.GetSubPath()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
subDomain, err := a.settingService.GetSubDomain()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
subKeyFile, err := a.settingService.GetSubKeyFile()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
subCertFile, err := a.settingService.GetSubCertFile()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
|
||||
result := make(map[string]interface{})
|
||||
|
||||
for key, fn := range settings {
|
||||
value, err := fn()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
result[key] = value
|
||||
}
|
||||
|
||||
subTLS := false
|
||||
if subKeyFile != "" || subCertFile != "" {
|
||||
if result["subKeyFile"] != "" || result["subCertFile"] != "" {
|
||||
subTLS = true
|
||||
}
|
||||
result := map[string]interface{}{
|
||||
"expireDiff": expireDiff,
|
||||
"trafficDiff": trafficDiff,
|
||||
"defaultCert": defaultCert,
|
||||
"defaultKey": defaultKey,
|
||||
"tgBotEnable": tgBotEnable,
|
||||
"subEnable": subEnable,
|
||||
"subPort": subPort,
|
||||
"subPath": subPath,
|
||||
"subDomain": subDomain,
|
||||
"subTLS": subTLS,
|
||||
}
|
||||
result["subTLS"] = subTLS
|
||||
|
||||
delete(result, "subKeyFile")
|
||||
delete(result, "subCertFile")
|
||||
|
||||
jsonObj(c, result, nil)
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ type Pager struct {
|
||||
|
||||
type AllSetting struct {
|
||||
WebListen string `json:"webListen" form:"webListen"`
|
||||
WebDomain string `json:"webDomain" form:"webDomain"`
|
||||
WebPort int `json:"webPort" form:"webPort"`
|
||||
WebCertFile string `json:"webCertFile" form:"webCertFile"`
|
||||
WebKeyFile string `json:"webKeyFile" form:"webKeyFile"`
|
||||
@@ -40,6 +41,7 @@ type AllSetting struct {
|
||||
TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"`
|
||||
TgRunTime string `json:"tgRunTime" form:"tgRunTime"`
|
||||
TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"`
|
||||
TgBotLoginNotify bool `json:"tgBotLoginNotify" form:"tgBotLoginNotify"`
|
||||
TgCpu int `json:"tgCpu" form:"tgCpu"`
|
||||
TgLang string `json:"tgLang" form:"tgLang"`
|
||||
XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"`
|
||||
@@ -53,6 +55,8 @@ type AllSetting struct {
|
||||
SubCertFile string `json:"subCertFile" form:"subCertFile"`
|
||||
SubKeyFile string `json:"subKeyFile" form:"subKeyFile"`
|
||||
SubUpdates int `json:"subUpdates" form:"subUpdates"`
|
||||
SubEncrypt bool `json:"subEncrypt" form:"subEncrypt"`
|
||||
SubShowInfo bool `json:"subShowInfo" form:"subShowInfo"`
|
||||
}
|
||||
|
||||
func (s *AllSetting) CheckValid() error {
|
||||
|
||||
@@ -18,7 +18,6 @@ type HashStorage struct {
|
||||
sync.RWMutex
|
||||
Data map[string]HashEntry
|
||||
Expiration time.Duration
|
||||
|
||||
}
|
||||
|
||||
func NewHashStorage(expiration time.Duration) *HashStorage {
|
||||
@@ -46,7 +45,6 @@ func (h *HashStorage) SaveHash(query string) string {
|
||||
return md5HashString
|
||||
}
|
||||
|
||||
|
||||
func (h *HashStorage) GetValue(hash string) (string, bool) {
|
||||
h.RLock()
|
||||
defer h.RUnlock()
|
||||
|
||||
@@ -37,15 +37,16 @@
|
||||
this.inbound = dbInbound.toInbound();
|
||||
settings = JSON.parse(this.inbound.settings);
|
||||
this.client = settings.clients[clientIndex];
|
||||
remark = this.dbInbound.remark + "-" + this.client.email;
|
||||
remark = [this.dbInbound.remark, ( this.client ? this.client.email : '')].filter(Boolean).join('-');
|
||||
address = this.dbInbound.address;
|
||||
this.subId = '';
|
||||
this.qrcodes = [];
|
||||
if (this.inbound.tls && !ObjectUtil.isArrEmpty(this.inbound.stream.tls.settings.domains)) {
|
||||
this.inbound.stream.tls.settings.domains.forEach((domain) => {
|
||||
remarkText = [remark, domain.remark].filter(Boolean).join('-');
|
||||
this.qrcodes.push({
|
||||
remark: remark + "-" + domain.remark,
|
||||
link: this.inbound.genLink(domain.domain, remark + "-" + domain.remark, clientIndex)
|
||||
remark: remarkText,
|
||||
link: this.inbound.genLink(domain.domain, remarkText, clientIndex)
|
||||
});
|
||||
});
|
||||
} else {
|
||||
@@ -68,8 +69,8 @@
|
||||
qrModal: qrModal,
|
||||
},
|
||||
methods: {
|
||||
copyToClipboard(elmentId,content) {
|
||||
this.qrModal.clipboard = new ClipboardJS('#'+elmentId, {
|
||||
copyToClipboard(elmentId, content) {
|
||||
this.qrModal.clipboard = new ClipboardJS('#' + elmentId, {
|
||||
text: () => content,
|
||||
});
|
||||
this.qrModal.clipboard.on('success', () => {
|
||||
@@ -77,29 +78,25 @@
|
||||
this.qrModal.clipboard.destroy();
|
||||
});
|
||||
},
|
||||
setQrCode(elmentId,content) {
|
||||
setQrCode(elmentId, content) {
|
||||
new QRious({
|
||||
element: document.querySelector('#'+elmentId),
|
||||
size: 260,
|
||||
value: content,
|
||||
});
|
||||
element: document.querySelector('#' + elmentId),
|
||||
size: 260,
|
||||
value: content,
|
||||
});
|
||||
},
|
||||
genSubLink(subID) {
|
||||
protocol = app.subSettings.tls ? "https://" : "http://";
|
||||
hostName = app.subSettings.domain === "" ? window.location.hostname : app.subSettings.domain;
|
||||
subPort = app.subSettings.port;
|
||||
port = (subPort === 443 && app.subSettings.tls) || (subPort === 80 && !app.subSettings.tls) ? "" : ":" + String(subPort);
|
||||
subPath = app.subSettings.path;
|
||||
return protocol + hostName + port + subPath + subID;
|
||||
const { domain: host, port, tls: isTLS, path: base } = app.subSettings;
|
||||
return buildURL({ host, port, isTLS, base, path: subID+'?name='+remark });
|
||||
}
|
||||
},
|
||||
updated() {
|
||||
if (qrModal.client.subId){
|
||||
if (qrModal.client && qrModal.client.subId) {
|
||||
qrModal.subId = qrModal.client.subId;
|
||||
this.setQrCode("qrCode-sub",this.genSubLink(qrModal.subId));
|
||||
this.setQrCode("qrCode-sub", this.genSubLink(qrModal.subId));
|
||||
}
|
||||
qrModal.qrcodes.forEach((element,index) => {
|
||||
this.setQrCode("qrCode-"+index, element.link);
|
||||
qrModal.qrcodes.forEach((element, index) => {
|
||||
this.setQrCode("qrCode-" + index, element.link);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -86,8 +86,8 @@
|
||||
<a-row justify="center" class="centered">
|
||||
<a-col :span="12">
|
||||
<a-select ref="selectLang" v-model="lang" @change="setLang(lang)" :dropdown-class-name="themeSwitcher.darkCardClass">
|
||||
<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>
|
||||
<a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs">
|
||||
<span role="img" :aria-label="l.name" v-text="l.icon"></span>
|
||||
<span v-text="l.name"></span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
</span>
|
||||
<a-input v-model.trim="clientsBulkModal.tgId"></a-input>
|
||||
</a-form-item>
|
||||
<br>
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
<span>{{ i18n "pages.inbounds.IPLimit" }}</span>
|
||||
@@ -199,21 +200,12 @@
|
||||
this.inbound = dbInbound.toInbound();
|
||||
this.delayedStart = false;
|
||||
},
|
||||
getClients(protocol, clientSettings) {
|
||||
switch (protocol) {
|
||||
case Protocols.VMESS: return clientSettings.vmesses;
|
||||
case Protocols.VLESS: return clientSettings.vlesses;
|
||||
case Protocols.TROJAN: return clientSettings.trojans;
|
||||
case Protocols.SHADOWSOCKS: return clientSettings.shadowsockses;
|
||||
default: return null;
|
||||
}
|
||||
},
|
||||
newClient(protocol) {
|
||||
switch (protocol) {
|
||||
case Protocols.VMESS: return new Inbound.VmessSettings.Vmess();
|
||||
case Protocols.VLESS: return new Inbound.VLESSSettings.VLESS();
|
||||
case Protocols.TROJAN: return new Inbound.TrojanSettings.Trojan();
|
||||
case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings.Shadowsocks();
|
||||
case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings.Shadowsocks(clientsBulkModal.inbound.settings.shadowsockses[0].method);
|
||||
default: return null;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
case Protocols.VMESS: return clients.push(new Inbound.VmessSettings.Vmess());
|
||||
case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS());
|
||||
case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan());
|
||||
case Protocols.SHADOWSOCKS: return clients.push(new Inbound.ShadowsocksSettings.Shadowsocks());
|
||||
case Protocols.SHADOWSOCKS: return clients.push(new Inbound.ShadowsocksSettings.Shadowsocks(clients[0].method));
|
||||
default: return null;
|
||||
}
|
||||
},
|
||||
@@ -120,26 +120,32 @@
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async getDBClientIps(email, event) {
|
||||
const msg = await HttpUtil.post('/panel/inbound/clientIps/' + email);
|
||||
async getDBClientIps(email) {
|
||||
const msg = await HttpUtil.post(`/panel/inbound/clientIps/${email}`);
|
||||
if (!msg.success) {
|
||||
document.getElementById("clientIPs").value = msg.obj;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
ips = JSON.parse(msg.obj)
|
||||
ips = ips.join(",")
|
||||
event.target.value = ips
|
||||
} catch (error) {
|
||||
// text
|
||||
event.target.value = msg.obj
|
||||
let ips = msg.obj;
|
||||
if (typeof ips === 'string' && ips.startsWith('[') && ips.endsWith(']')) {
|
||||
try {
|
||||
ips = JSON.parse(ips);
|
||||
ips = Array.isArray(ips) ? ips.join("\n") : ips;
|
||||
} catch (e) {
|
||||
console.error('Error parsing JSON:', e);
|
||||
}
|
||||
}
|
||||
document.getElementById("clientIPs").value = ips;
|
||||
},
|
||||
async clearDBClientIps(email) {
|
||||
const msg = await HttpUtil.post('/panel/inbound/clearClientIps/' + email);
|
||||
if (!msg.success) {
|
||||
return;
|
||||
try {
|
||||
const msg = await HttpUtil.post(`/panel/inbound/clearClientIps/${email}`);
|
||||
if (!msg.success) {
|
||||
return;
|
||||
}
|
||||
document.getElementById("clientIPs").value = "";
|
||||
} catch (error) {
|
||||
}
|
||||
document.getElementById("clientIPs").value = ""
|
||||
},
|
||||
resetClientTraffic(email, dbInboundId, iconElement) {
|
||||
this.$confirm({
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
</template>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-icon @click="client.email = RandomUtil.randomText()" type="sync"> </a-icon>
|
||||
<a-icon @click="client.email = RandomUtil.randomLowerAndNum(8)" type="sync"> </a-icon>
|
||||
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Password" v-if="inbound.protocol === Protocols.TROJAN || inbound.protocol === Protocols.SHADOWSOCKS">
|
||||
@@ -27,9 +27,6 @@
|
||||
<a-input v-model.trim="client.password" style="width: 300px;"></a-input>
|
||||
</a-form-item>
|
||||
<br>
|
||||
<a-form-item label='{{ i18n "additional" }} ID' v-if="inbound.protocol === Protocols.VMESS">
|
||||
<a-input-number v-model="client.alterId"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="ID" v-if="inbound.protocol === Protocols.VMESS || inbound.protocol === Protocols.VLESS">
|
||||
<a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"> </a-icon>
|
||||
<a-input v-model.trim="client.id" style="width: 300px;"></a-input>
|
||||
@@ -44,7 +41,7 @@
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-icon @click="client.subId = RandomUtil.randomText()" type="sync"> </a-icon>
|
||||
<a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"> </a-icon>
|
||||
<a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="client.email && app.tgBotEnable" >
|
||||
@@ -72,31 +69,32 @@
|
||||
<a-input-number v-model="client.limitIp" min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="client.email && client.limitIp > 0 && isEdit">
|
||||
<span slot="label">
|
||||
<span>{{ i18n "pages.inbounds.IPLimitlog" }}</span>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.IPLimitlogDesc" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.IPLimitlogclear" }}</span>
|
||||
</template>
|
||||
<span style="color: #FF4D4F">
|
||||
<a-icon type="delete" @click="clearDBClientIps(client.email)"></a-icon>
|
||||
</span>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-form layout="block">
|
||||
<a-textarea id="clientIPs" readonly
|
||||
@click="getDBClientIps(client.email,$event)"
|
||||
placeholder="Click To Get IPs"
|
||||
:auto-size="{ minRows: 2, maxRows: 10 }">
|
||||
</a-textarea>
|
||||
</a-form>
|
||||
</a-form-item>
|
||||
<span slot="label">
|
||||
<span>{{ i18n "pages.inbounds.IPLimitlog" }}</span>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.IPLimitlogDesc" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.IPLimitlogclear" }}</span>
|
||||
</template>
|
||||
<span style="color: #FF4D4F">
|
||||
<a-icon type="delete" @click="clearDBClientIps(client.email)"></a-icon>
|
||||
</span>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-form layout="block">
|
||||
<a-textarea id="clientIPs" readonly
|
||||
@click="getDBClientIps(client.email)"
|
||||
placeholder="Click To Get IPs"
|
||||
:auto-size="{ minRows: 5, maxRows: 10 }"
|
||||
>
|
||||
</a-textarea>
|
||||
</a-form>
|
||||
</a-form-item>
|
||||
<br>
|
||||
<a-form-item v-if="inbound.xtls" label="Flow">
|
||||
<a-select v-model="client.flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass">
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
{{define "form/inbound"}}
|
||||
<!-- base -->
|
||||
<a-form layout="inline">
|
||||
<a-form-item label='{{ i18n "remark" }}'>
|
||||
<a-input v-model.trim="dbInbound.remark"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "enable" }}'>
|
||||
<a-switch v-model="dbInbound.enable"></a-switch>
|
||||
</a-form-item>
|
||||
<br>
|
||||
<a-form-item label='{{ i18n "remark" }}'>
|
||||
<a-input v-model.trim="dbInbound.remark"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "protocol" }}'>
|
||||
<a-select v-model="inbound.protocol" style="width: 160px;" :disabled="isEdit" :dropdown-class-name="themeSwitcher.darkCardClass">
|
||||
<a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p ]]</a-select-option>
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
{{define "form/http"}}
|
||||
<a-form layout="inline">
|
||||
<a-form-item label='{{ i18n "username"}}'>
|
||||
<a-input v-model.trim="inbound.settings.accounts[0].user"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
<a-input v-model.trim="inbound.settings.accounts[0].pass"></a-input>
|
||||
<a-form-item>
|
||||
<a-row>
|
||||
<a-button type="primary" size="small" @click="inbound.settings.addAccount(new Inbound.SocksSettings.SocksAccount())">+</a-button>
|
||||
</a-row>
|
||||
<a-input-group v-for="(account, index) in inbound.settings.accounts">
|
||||
<a-input style="width: 45%" v-model.trim="account.user"
|
||||
addon-before='{{ i18n "username" }}'></a-input>
|
||||
<a-input style="width: 55%" v-model.trim="account.pass"
|
||||
addon-before='{{ i18n "password" }}'>
|
||||
<template slot="addonAfter">
|
||||
<a-button type="primary" size="small" @click="inbound.settings.delAccount(index)">-</a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
@@ -1,5 +1,6 @@
|
||||
{{define "form/shadowsocks"}}
|
||||
<a-form layout="inline" style="padding: 10px 0px;">
|
||||
<template v-if="inbound.isSSMultiUser">
|
||||
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.shadowsockses.slice(0,1)" v-if="!isEdit">
|
||||
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
|
||||
<a-form-item>
|
||||
@@ -11,7 +12,7 @@
|
||||
</template>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-icon @click="client.email = RandomUtil.randomText()" type="sync"> </a-icon>
|
||||
<a-icon @click="client.email = RandomUtil.randomLowerAndNum(8)" type="sync"> </a-icon>
|
||||
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Password">
|
||||
@@ -28,7 +29,7 @@
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-icon @click="client.subId = RandomUtil.randomText()" type="sync"> </a-icon>
|
||||
<a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"> </a-icon>
|
||||
<a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="client.email && app.tgBotEnable">
|
||||
@@ -96,22 +97,26 @@
|
||||
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.shadowsockses.length">
|
||||
<table width="100%">
|
||||
<tr class="client-table-header">
|
||||
<th v-for="col in Object.keys(inbound.settings.shadowsockses[0]).slice(0, 3)">[[ col ]]</th>
|
||||
<th>{{ i18n "pages.inbounds.email" }}</th>
|
||||
<th>Password</th>
|
||||
</tr>
|
||||
<tr v-for="(client, index) in inbound.settings.shadowsockses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
|
||||
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
|
||||
<td>[[ client.email ]]</td>
|
||||
<td>[[ client.password ]]</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</template>
|
||||
</a-form>
|
||||
<a-form layout="inline">
|
||||
<a-form-item label='{{ i18n "encryption" }}'>
|
||||
<a-select v-model="inbound.settings.method" style="width: 250px;" :dropdown-class-name="themeSwitcher.darkCardClass">
|
||||
<a-select v-model="inbound.settings.method" style="width: 250px;" :dropdown-class-name="themeSwitcher.darkCardClass" @change="SSMethodChange">
|
||||
<a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
<a-form-item v-if="inbound.isSS2022" label='{{ i18n "password" }}'>
|
||||
<a-icon @click="inbound.settings.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
|
||||
<a-input v-model.trim="inbound.settings.password" style="width: 250px;"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.network" }}'>
|
||||
|
||||
@@ -6,11 +6,20 @@
|
||||
</a-form-item>
|
||||
<br>
|
||||
<template v-if="inbound.settings.auth === 'password'">
|
||||
<a-form-item label='{{ i18n "username" }}'>
|
||||
<a-input v-model.trim="inbound.settings.accounts[0].user"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
<a-input v-model.trim="inbound.settings.accounts[0].pass"></a-input>
|
||||
<a-form-item>
|
||||
<a-row>
|
||||
<a-button type="primary" size="small" @click="inbound.settings.addAccount(new Inbound.SocksSettings.SocksAccount())">+</a-button>
|
||||
</a-row>
|
||||
<a-input-group v-for="(account, index) in inbound.settings.accounts">
|
||||
<a-input style="width: 45%" v-model.trim="account.user"
|
||||
addon-before='{{ i18n "username" }}'></a-input>
|
||||
<a-input style="width: 55%" v-model.trim="account.pass"
|
||||
addon-before='{{ i18n "password" }}'>
|
||||
<template slot="addonAfter">
|
||||
<a-button type="primary" size="small" @click="inbound.settings.delAccount(index)">-</a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<br>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</template>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-icon @click="client.email = RandomUtil.randomText()" type="sync"> </a-icon>
|
||||
<a-icon @click="client.email = RandomUtil.randomLowerAndNum(8)" type="sync"> </a-icon>
|
||||
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Password">
|
||||
@@ -28,7 +28,7 @@
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-icon @click="client.subId = RandomUtil.randomText()" type="sync"> </a-icon>
|
||||
<a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"> </a-icon>
|
||||
<a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="client.email && app.tgBotEnable">
|
||||
@@ -102,10 +102,12 @@
|
||||
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.trojans.length">
|
||||
<table width="100%">
|
||||
<tr class="client-table-header">
|
||||
<th v-for="col in Object.keys(inbound.settings.trojans[0]).slice(0, 3)">[[ col ]]</th>
|
||||
<th>{{ i18n "pages.inbounds.email" }}</th>
|
||||
<th>Password</th>
|
||||
</tr>
|
||||
<tr v-for="(client, index) in inbound.settings.trojans" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
|
||||
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
|
||||
<td>[[ client.email ]]</td>
|
||||
<td>[[ client.password ]]</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-collapse-panel>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</template>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-icon @click="client.email = RandomUtil.randomText()" type="sync"> </a-icon>
|
||||
<a-icon @click="client.email = RandomUtil.randomLowerAndNum(8)" type="sync"> </a-icon>
|
||||
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="ID">
|
||||
@@ -28,7 +28,7 @@
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-icon @click="client.subId = RandomUtil.randomText()" type="sync"> </a-icon>
|
||||
<a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"> </a-icon>
|
||||
<a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="client.email && app.tgBotEnable">
|
||||
@@ -108,10 +108,14 @@
|
||||
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.vlesses.length">
|
||||
<table width="100%">
|
||||
<tr class="client-table-header">
|
||||
<th v-for="col in Object.keys(inbound.settings.vlesses[0]).slice(0, 3)">[[ col ]]</th>
|
||||
<th>{{ i18n "pages.inbounds.email" }}</th>
|
||||
<th>Flow</th>
|
||||
<th>ID</th>
|
||||
</tr>
|
||||
<tr v-for="(client, index) in inbound.settings.vlesses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
|
||||
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
|
||||
<td>[[ client.email ]]</td>
|
||||
<td>[[ client.flow ]]</td>
|
||||
<td>[[ client.id ]]</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-collapse-panel>
|
||||
|
||||
@@ -11,14 +11,10 @@
|
||||
</template>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-icon @click="client.email = RandomUtil.randomText()" type="sync"> </a-icon>
|
||||
<a-icon @click="client.email = RandomUtil.randomLowerAndNum(8)" type="sync"> </a-icon>
|
||||
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
<br>
|
||||
<a-form-item label='{{ i18n "additional" }} ID'>
|
||||
<a-input-number v-model="client.alterId"></a-input-number>
|
||||
</a-form-item>
|
||||
<br>
|
||||
<a-form-item label="ID">
|
||||
<a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"> </a-icon>
|
||||
<a-input v-model.trim="client.id" style="width: 300px;"></a-input>
|
||||
@@ -33,7 +29,7 @@
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-icon @click="client.subId = RandomUtil.randomText()" type="sync"> </a-icon>
|
||||
<a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"> </a-icon>
|
||||
<a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="client.email && app.tgBotEnable">
|
||||
@@ -101,18 +97,15 @@
|
||||
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount" }}: ' + inbound.settings.vmesses.length">
|
||||
<table width="100%">
|
||||
<tr class="client-table-header">
|
||||
<th v-for="col in Object.keys(inbound.settings.vmesses[0]).slice(0, 3)">[[ col ]]</th>
|
||||
<th>{{ i18n "pages.inbounds.email" }}</th>
|
||||
<th>ID</th>
|
||||
</tr>
|
||||
<tr v-for="(client, index) in inbound.settings.vmesses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
|
||||
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
|
||||
<td>[[ client.email ]]</td>
|
||||
<td>[[ client.id ]]</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</a-form>
|
||||
<a-form layout="inline">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.disableInsecureEncryption" }}'>
|
||||
<a-switch v-model.number="inbound.settings.disableInsecure"></a-switch>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
Sniffing
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span >{{ i18n "pages.inbounds.noRecommendKeepDefault" }}</span>
|
||||
<span>{{ i18n "pages.inbounds.noRecommendKeepDefault" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
</a-form-item>
|
||||
<br>
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
<a-input v-model="inbound.stream.kcp.seed"></a-input>
|
||||
<a-icon @click="inbound.stream.kcp.seed = RandomUtil.randomSeq(10)" type="sync"> </a-icon>
|
||||
<a-input v-model="inbound.stream.kcp.seed" style="width: 150px;" ></a-input>
|
||||
</a-form-item>
|
||||
<br>
|
||||
<a-form-item label="MTU">
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
<a-input v-model.trim="inbound.stream.quic.key"></a-input>
|
||||
<a-icon @click="inbound.stream.quic.key = RandomUtil.randomSeq(10)" type="sync"> </a-icon>
|
||||
<a-input v-model.trim="inbound.stream.quic.key" style="width: 150px;"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "camouflage" }}'>
|
||||
<a-select v-model="inbound.stream.quic.type" style="width: 280px;" :dropdown-class-name="themeSwitcher.darkCardClass">
|
||||
|
||||
@@ -42,4 +42,9 @@
|
||||
<template v-if="inbound.stream.network === 'grpc'">
|
||||
{{template "form/streamGRPC"}}
|
||||
</template>
|
||||
|
||||
<!-- sockopt -->
|
||||
<template>
|
||||
{{template "form/streamSockopt"}}
|
||||
</template>
|
||||
{{end}}
|
||||
46
web/html/xui/form/stream/stream_sockopt.html
Normal file
@@ -0,0 +1,46 @@
|
||||
{{define "form/streamSockopt"}}
|
||||
<a-form layout="inline">
|
||||
<a-form-item label="Transparent Proxy">
|
||||
<a-switch v-model="inbound.stream.sockoptSwitch"></a-switch>
|
||||
</a-form-item>
|
||||
<table width="100%" class="ant-table-tbody" v-if="inbound.stream.sockoptSwitch">
|
||||
<tr>
|
||||
<td>Accept Proxy Protocol</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-switch v-model="inbound.stream.sockopt.acceptProxyProtocol"></a-switch>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>TCP FastOpen</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-switch v-model.trim="inbound.stream.sockopt.tcpFastOpen"></a-switch>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Route Mark</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model="inbound.stream.sockopt.mark" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>T-Proxy</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select v-model="inbound.stream.sockopt.tproxy" style="width: 250px;"
|
||||
:dropdown-class-name="themeSwitcher.darkCardClass">
|
||||
<a-select-option value="off">OFF</a-select-option>
|
||||
<a-select-option value="redirect">Redirect</a-select-option>
|
||||
<a-select-option value="tproxy">T-Proxy</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-form>
|
||||
{{end}}
|
||||
@@ -3,8 +3,7 @@
|
||||
<a-form-item label="AcceptProxyProtocol">
|
||||
<a-switch v-model="inbound.stream.ws.acceptProxyProtocol"></a-switch>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-form layout="inline">
|
||||
<br>
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
<a-input v-model.trim="inbound.stream.ws.path"></a-input>
|
||||
</a-form-item>
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
<a-form v-if="inbound.tls" layout="inline">
|
||||
<a-form-item label='Multi Domain'>
|
||||
<a-switch v-model="multiDomain"></a-switch>
|
||||
|
||||
</a-form-item>
|
||||
<a-form-item v-if="multiDomain">
|
||||
<a-row>
|
||||
@@ -81,13 +80,22 @@
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Alpn">
|
||||
<a-checkbox-group v-model="inbound.stream.tls.alpn" style="width:200px">
|
||||
<a-checkbox v-for="key,value in ALPN_OPTION" :value="key">[[ value ]]</a-checkbox>
|
||||
</a-checkbox-group>
|
||||
<a-select
|
||||
mode="multiple"
|
||||
style="width: 250px"
|
||||
:dropdown-class-name="themeSwitcher.darkCardClass"
|
||||
v-model="inbound.stream.tls.alpn">
|
||||
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<br>
|
||||
<a-form-item label="Allow insecure">
|
||||
<a-switch v-model="inbound.stream.tls.settings.allowInsecure"></a-switch>
|
||||
</a-form-item>
|
||||
<br>
|
||||
<a-form-item label="Reject Unknown SNI">
|
||||
<a-switch v-model="inbound.stream.tls.rejectUnknownSni"></a-switch>
|
||||
</a-form-item>
|
||||
<template v-for="cert,index in inbound.stream.tls.certs">
|
||||
<a-form-item label='{{ i18n "certificate" }}'>
|
||||
<a-radio-group v-model="cert.useFile" button-style="solid">
|
||||
@@ -114,6 +122,10 @@
|
||||
<a-input type="textarea" :rows="3" style="width:300px;" v-model="cert.key"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<br>
|
||||
<a-form-item label="ocspStapling">
|
||||
<a-input-number v-model.number="cert.ocspStapling" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</a-form>
|
||||
|
||||
|
||||
@@ -29,11 +29,27 @@
|
||||
<a-tag v-if="!isClientEnabled(record, client.email)" color="red">{{ i18n "depleted" }}</a-tag>
|
||||
</template>
|
||||
<template slot="traffic" slot-scope="text, client">
|
||||
<a-tag :color="statsColor(record, client.email)">[[ sizeFormat(getUpStats(record, client.email)) ]] / [[ sizeFormat(getDownStats(record, client.email)) ]]</a-tag>
|
||||
<template v-if="client._totalGB > 0">
|
||||
<a-tag :color="statsColor(record, client.email)">[[client._totalGB]]GB</a-tag>
|
||||
</template>
|
||||
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
|
||||
<a-popover :overlay-class-name="themeSwitcher.darkClass">
|
||||
<template slot="content" v-if="client.email">
|
||||
<table cellpadding="2" width="100%">
|
||||
<tr>
|
||||
<td>↑[[ sizeFormat(getUpStats(record, client.email)) ]]</td>
|
||||
<td>↓[[ sizeFormat(getDownStats(record, client.email)) ]]</td>
|
||||
</tr>
|
||||
<tr v-if="client.totalGB > 0">
|
||||
<td>{{ i18n "remained" }}</td>
|
||||
<td>[[ sizeFormat(client.totalGB - getUpStats(record, client.email) - getDownStats(record, client.email)) ]]</td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
<a-tag :color="statsColor(record, client.email)">
|
||||
[[ sizeFormat(getUpStats(record, client.email) + getDownStats(record, client.email)) ]] /
|
||||
<template v-if="client.totalGB > 0">[[client._totalGB]]GB</template>
|
||||
<template v-else>
|
||||
<svg style="fill: currentColor; height: 10px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z"/></svg>
|
||||
</template>
|
||||
</a-tag>
|
||||
</a-popover>
|
||||
</template>
|
||||
<template slot="expiryTime" slot-scope="text, client, index">
|
||||
<template v-if="client.expiryTime > 0">
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
width="600px"
|
||||
>
|
||||
<table style="margin-bottom: 10px; width: 100%;">
|
||||
<tr>
|
||||
<td>
|
||||
<tr><td>
|
||||
<table>
|
||||
<tr><td>{{ i18n "protocol" }}</td><td><a-tag color="green">[[ dbInbound.protocol ]]</a-tag></td></tr>
|
||||
<tr><td>{{ i18n "pages.inbounds.address" }}</td><td><a-tag color="blue">[[ dbInbound.address ]]</a-tag></td></tr>
|
||||
@@ -21,7 +20,6 @@
|
||||
<tr>
|
||||
<td>{{ i18n "transmission" }}</td><td><a-tag color="green">[[ inbound.network ]]</a-tag></td>
|
||||
</tr>
|
||||
|
||||
<template v-if="inbound.isTcp || inbound.isWs || inbound.isH2">
|
||||
<tr v-if="inbound.host"><td>{{ i18n "host" }}</td><td><a-tag color="green">[[ inbound.host ]]</a-tag></td></tr>
|
||||
<tr v-else><td>{{ i18n "host" }}</td><td><a-tag color="orange">{{ i18n "none" }}</a-tag></td></tr>
|
||||
@@ -46,8 +44,7 @@
|
||||
<tr><td>grpc multiMode</td><td><a-tag color="green">[[ inbound.stream.grpc.multiMode ]]</a-tag></td></tr>
|
||||
</template>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</td></tr>
|
||||
<tr colspan="2" v-if="dbInbound.hasLink()">
|
||||
<td v-if="inbound.tls">
|
||||
tls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
|
||||
@@ -61,41 +58,72 @@
|
||||
reality: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
|
||||
reality Destination: <a-tag :color="inbound.stream.reality.dest ? 'green' : 'orange'">[[ inbound.stream.reality.dest ]]</a-tag>
|
||||
</td>
|
||||
<td v-else>tls: <a-tag color="red">{{ i18n "disabled" }}</a-tag>
|
||||
</td>
|
||||
<td v-else>tls: <a-tag color="red">{{ i18n "disabled" }}</a-tag></td>
|
||||
</tr>
|
||||
</table>
|
||||
<table v-if="dbInbound.isSS" style="margin-bottom: 10px; width: 100%;">
|
||||
<tr>
|
||||
<td>{{ i18n "encryption" }}</td>
|
||||
<td><a-tag color="green">[[ inbound.settings.method ]]</a-tag></td>
|
||||
</tr><tr v-if="inbound.isSS2022">
|
||||
<td>{{ i18n "password" }}</td>
|
||||
<td><a-tag color="blue">[[ inbound.settings.password ]]</a-tag></td>
|
||||
</tr><tr>
|
||||
<td>{{ i18n "pages.inbounds.network" }}</td>
|
||||
<td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td>
|
||||
</tr>
|
||||
</table>
|
||||
<template v-if="infoModal.clientSettings">
|
||||
<a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
|
||||
<table style="margin-bottom: 10px;">
|
||||
<tr v-for="col,index in Object.keys(infoModal.clientSettings).slice(0, 3)">
|
||||
<td>[[ col ]]</td>
|
||||
<td><a-tag color="green">[[ infoModal.clientSettings[col] ]]</a-tag></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "status" }}</td>
|
||||
<td>
|
||||
<a-tag v-if="isEnable" color="blue">{{ i18n "enabled" }}</a-tag>
|
||||
<a-tag v-else color="red">{{ i18n "disabled" }}</a-tag>
|
||||
<a-tag v-if="!isActive" color="red">{{ i18n "depleted" }}</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table style="margin-bottom: 10px; width: 100%;">
|
||||
<a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
|
||||
<table style="margin-bottom: 10px;">
|
||||
<tr>
|
||||
<th>{{ i18n "usage" }}</th>
|
||||
<td>{{ i18n "pages.inbounds.email" }}</td>
|
||||
<td><a-tag color="green">[[ infoModal.clientSettings.email ]]</a-tag></td>
|
||||
</tr>
|
||||
<tr v-if="infoModal.clientSettings.id">
|
||||
<td>ID</td>
|
||||
<td><a-tag color="green">[[ infoModal.clientSettings.id ]]</a-tag></td>
|
||||
</tr>
|
||||
<tr v-if="infoModal.inbound.canEnableTlsFlow()">
|
||||
<td>Flow</td>
|
||||
<td><a-tag color="green">[[ infoModal.clientSettings.flow ]]</a-tag></td>
|
||||
</tr>
|
||||
<tr v-if="infoModal.clientSettings.password">
|
||||
<td>Password</td>
|
||||
<td><a-tag color="green">[[ infoModal.clientSettings.password ]]</a-tag></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "status" }}</td>
|
||||
<td>
|
||||
<a-tag v-if="isEnable" color="blue">{{ i18n "enabled" }}</a-tag>
|
||||
<a-tag v-else color="red">{{ i18n "disabled" }}</a-tag>
|
||||
<a-tag v-if="!isActive" color="red">{{ i18n "depleted" }}</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="infoModal.clientStats">
|
||||
<td>{{ i18n "usage" }}</td>
|
||||
<td>
|
||||
<a-tag color="green">[[ sizeFormat(infoModal.clientStats.up + infoModal.clientStats.down) ]]</a-tag>
|
||||
<a-tag color="blue">↑ [[ sizeFormat(infoModal.clientStats.up) ]] / [[ sizeFormat(infoModal.clientStats.down) ]] ↓</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table style="margin-bottom: 10px; width: 100%;">
|
||||
<tr>
|
||||
<th>{{ i18n "remained" }}</th>
|
||||
<th>{{ i18n "pages.inbounds.totalFlow" }}</th>
|
||||
<th>{{ i18n "pages.inbounds.expireDate" }}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a-tag v-if="infoModal.clientStats" color="green">
|
||||
[[ sizeFormat(infoModal.clientStats['up']) ]] /
|
||||
[[ sizeFormat(infoModal.clientStats['down']) ]]
|
||||
([[ sizeFormat(infoModal.clientStats['up'] + infoModal.clientStats['down']) ]])
|
||||
<a-tag v-if="infoModal.clientStats && infoModal.clientSettings.totalGB > 0" :color="statsColor(infoModal.clientStats)">
|
||||
[[ sizeFormat(infoModal.clientSettings.totalGB - infoModal.clientStats.up - infoModal.clientStats.down) ]]
|
||||
</a-tag>
|
||||
</td>
|
||||
<td>
|
||||
<a-tag v-if="infoModal.clientSettings.totalGB > 0" :color="statsColor(infoModal.clientStats)">[[ sizeFormat(infoModal.clientSettings.totalGB) ]]</a-tag>
|
||||
<a-tag v-if="infoModal.clientSettings.totalGB > 0" :color="statsColor(infoModal.clientStats)">
|
||||
[[ sizeFormat(infoModal.clientSettings.totalGB) ]]
|
||||
</a-tag>
|
||||
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
|
||||
</td>
|
||||
<td>
|
||||
@@ -111,45 +139,72 @@
|
||||
</table>
|
||||
<template v-if="app.subSettings.enable && infoModal.clientSettings.subId">
|
||||
<a-divider>Subscription link</a-divider>
|
||||
<a :href="[[ infoModal.subLink ]]" target="_blank">[[ infoModal.subLink ]]</a>
|
||||
<a-icon id="copy-sub-link" type="snippets" @click="copyToClipboard('copy-sub-link', infoModal.subLink)"></a-icon>
|
||||
<a-row>
|
||||
<a-col :span="22"><a :href="[[ infoModal.subLink ]]" target="_blank">[[ infoModal.subLink ]]</a></a-col>
|
||||
<a-col :span="2">
|
||||
<a-tooltip title='{{ i18n "copy" }}'>
|
||||
<button class="ant-btn ant-btn-primary" id="copy-sub-link" @click="copyToClipboard('copy-sub-link', infoModal.subLink)">
|
||||
<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 Username</a-divider>
|
||||
<a :href="[[ infoModal.tgLink ]]" target="_blank">@[[ infoModal.clientSettings.tgId ]]</a>
|
||||
<a-icon id="copy-tg-link" type="snippets" @click="copyToClipboard('copy-tg-link', '@' + infoModal.clientSettings.tgId)"></a-icon>
|
||||
<a-row>
|
||||
<a-col :span="22"><a :href="[[ infoModal.tgLink ]]" target="_blank">@[[ infoModal.clientSettings.tgId ]]</a></a-col>
|
||||
<a-col :span="2">
|
||||
<a-tooltip title='{{ i18n "copy" }}'>
|
||||
<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>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
<template v-if="dbInbound.hasLink()">
|
||||
<a-divider>URL</a-divider>
|
||||
<a-row v-for="(link,index) in infoModal.links">
|
||||
<a-col :span="22"><a-tag color="cyan">[[ link.remark ]]</a-tag><br />[[ link.link ]]</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, link.link)">
|
||||
<a-icon type="snippets"></a-icon>
|
||||
</button>
|
||||
</a-tooltip>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-divider></a-divider>
|
||||
<table v-if="inbound.protocol == Protocols.SHADOWSOCKS" style="margin-bottom: 10px; width: 100%;">
|
||||
<tr>
|
||||
<th>{{ i18n "encryption" }}</th>
|
||||
<th>{{ i18n "password" }}</th>
|
||||
<th>{{ i18n "pages.inbounds.network" }}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a-tag color="green">[[ inbound.settings.method ]]</a-tag></td>
|
||||
<td><a-tag color="blue">[[ inbound.settings.password ]]</a-tag></td>
|
||||
<td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td>
|
||||
</tr>
|
||||
</table>
|
||||
<template v-if="dbInbound.isSS && !inbound.isSSMultiUser">
|
||||
<a-divider>URL</a-divider>
|
||||
<a-row v-for="(link,index) in infoModal.links">
|
||||
<a-col :span="22"><a-tag color="cyan">[[ link.remark ]]</a-tag><br />[[ link.link ]]</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, link.link)">
|
||||
<a-icon type="snippets"></a-icon>
|
||||
</button>
|
||||
</a-tooltip>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
<table v-if="inbound.protocol == Protocols.DOKODEMO" style="margin-bottom: 10px; width: 100%;">
|
||||
<tr>
|
||||
<th>{{ i18n "pages.inbounds.targetAddress" }}</th>
|
||||
<th>{{ i18n "pages.inbounds.destinationPort" }}</th>
|
||||
<th>{{ i18n "pages.inbounds.network" }}</th>
|
||||
<th>FollowRedirect</th>
|
||||
</tr>
|
||||
<tr>
|
||||
</tr><tr>
|
||||
<td><a-tag color="green">[[ inbound.settings.address ]]</a-tag></td>
|
||||
<td><a-tag color="blue">[[ inbound.settings.port ]]</a-tag></td>
|
||||
<td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td>
|
||||
<td><a-tag color="blue">[[ inbound.settings.followRedirect ]]</a-tag></td>
|
||||
</tr>
|
||||
</table>
|
||||
</table>
|
||||
<table v-if="inbound.protocol == Protocols.SOCKS" style="margin-bottom: 10px; width: 100%;">
|
||||
<table v-if="dbInbound.isSocks" style="margin-bottom: 10px; width: 100%;">
|
||||
<tr>
|
||||
<th>{{ i18n "password" }} Auth</th>
|
||||
<th>{{ i18n "pages.inbounds.enable" }} udp</th>
|
||||
@@ -160,46 +215,32 @@
|
||||
<td><a-tag color="blue">[[ inbound.settings.udp]]</a-tag></td>
|
||||
<td><a-tag color="green">[[ inbound.settings.ip ]]</a-tag></td>
|
||||
</tr>
|
||||
<tr v-if="inbound.settings.auth == 'password'">
|
||||
<template v-if="inbound.settings.auth == 'password'">
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td>{{ i18n "username" }}</td>
|
||||
<td>{{ i18n "password" }}</td>
|
||||
</tr>
|
||||
<tr v-for="account,index in inbound.settings.accounts">
|
||||
</tr><tr v-for="account,index in inbound.settings.accounts">
|
||||
<td><a-tag color="green">[[ index ]]</a-tag></td>
|
||||
<td><a-tag color="blue">[[ account.user ]]</a-tag></td>
|
||||
<td><a-tag color="green">[[ account.pass ]]</a-tag></td>
|
||||
</tr>
|
||||
</table>
|
||||
</table>
|
||||
<table v-if="inbound.protocol == Protocols.HTTP" style="margin-bottom: 10px; width: 100%;">
|
||||
</template>
|
||||
</table>
|
||||
<table v-if="dbInbound.isHTTP" style="margin-bottom: 10px; width: 100%;">
|
||||
<tr>
|
||||
<th> </th>
|
||||
<th>{{ i18n "username" }}</th>
|
||||
<th>{{ i18n "password" }}</th>
|
||||
</tr>
|
||||
<tr v-for="account,index in inbound.settings.accounts">
|
||||
</tr><tr v-for="account,index in inbound.settings.accounts">
|
||||
<td><a-tag color="green">[[ index ]]</a-tag></td>
|
||||
<td><a-tag color="blue">[[ account.user ]]</a-tag></td>
|
||||
<td><a-tag color="green">[[ account.pass ]]</a-tag></td>
|
||||
</tr>
|
||||
</table>
|
||||
</table>
|
||||
</template>
|
||||
<div v-if="dbInbound.hasLink()">
|
||||
<a-divider>URL</a-divider>
|
||||
<a-row v-for="(link,index) in infoModal.links">
|
||||
<a-col :span="21"><a-tag color="cyan">[[ link.remark ]]</a-tag><br />[[ link.link ]]</a-col>
|
||||
<a-col :span="3" style="text-align: right;">
|
||||
<button class="ant-btn ant-btn-primary" :id="'copy-url-link-'+index" @click="copyToClipboard('copy-url-link-'+index, link.link)">
|
||||
<a-icon type="snippets"></a-icon>{{ i18n "copy" }}
|
||||
</button>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</a-modal>
|
||||
<script>
|
||||
|
||||
const infoModal = {
|
||||
visible: false,
|
||||
inbound: new Inbound(),
|
||||
@@ -223,14 +264,15 @@
|
||||
this.clientSettings = this.settings.clients ? Object.values(this.settings.clients)[index] : null;
|
||||
this.isExpired = this.inbound.isExpiry(index);
|
||||
this.clientStats = this.settings.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : [];
|
||||
remark = this.dbInbound.remark + "-" + this.clientSettings.email;
|
||||
remark = [this.dbInbound.remark, ( this.clientSettings ? this.clientSettings.email : '')].filter(Boolean).join('-');
|
||||
address = this.dbInbound.address;
|
||||
this.links = [];
|
||||
if (this.inbound.tls && !ObjectUtil.isArrEmpty(this.inbound.stream.tls.settings.domains)) {
|
||||
this.inbound.stream.tls.settings.domains.forEach((domain) => {
|
||||
remarkText = [remark, domain.remark].filter(Boolean).join('-');
|
||||
this.links.push({
|
||||
remark: remark + "-" + domain.remark,
|
||||
link: this.inbound.genLink(domain.domain, remark + "-" + domain.remark, index)
|
||||
remark: remarkText,
|
||||
link: this.inbound.genLink(domain.domain, remarkText, index)
|
||||
});
|
||||
});
|
||||
} else {
|
||||
@@ -253,12 +295,8 @@
|
||||
infoModal.visible = false;
|
||||
},
|
||||
genSubLink(subID) {
|
||||
protocol = app.subSettings.tls ? "https://" : "http://";
|
||||
hostName = app.subSettings.domain === "" ? window.location.hostname : app.subSettings.domain;
|
||||
subPort = app.subSettings.port;
|
||||
port = (subPort === 443 && app.subSettings.tls) || (subPort === 80 && !app.subSettings.tls) ? "" : ":" + String(subPort);
|
||||
subPath = app.subSettings.path;
|
||||
return protocol + hostName + port + subPath + subID;
|
||||
const { domain: host, port, tls: isTLS, path: base } = app.subSettings;
|
||||
return buildURL({ host, port, isTLS, base, path: subID+'?name='+remark });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -54,23 +54,11 @@
|
||||
},
|
||||
};
|
||||
|
||||
const protocols = {
|
||||
VMESS: Protocols.VMESS,
|
||||
VLESS: Protocols.VLESS,
|
||||
TROJAN: Protocols.TROJAN,
|
||||
SHADOWSOCKS: Protocols.SHADOWSOCKS,
|
||||
DOKODEMO: Protocols.DOKODEMO,
|
||||
SOCKS: Protocols.SOCKS,
|
||||
HTTP: Protocols.HTTP,
|
||||
};
|
||||
|
||||
new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#inbound-modal',
|
||||
data: {
|
||||
inModal: inModal,
|
||||
Protocols: protocols,
|
||||
SSMethods: SSMethods,
|
||||
delayedStart: false,
|
||||
get inbound() {
|
||||
return inModal.inbound;
|
||||
@@ -96,7 +84,7 @@
|
||||
set multiDomain(value) {
|
||||
if (value) {
|
||||
inModal.inbound.stream.tls.server = "";
|
||||
inModal.inbound.stream.tls.settings.domains = [{remark: "", domain: window.location.host.split(":")[0]}];
|
||||
inModal.inbound.stream.tls.settings.domains = [{ remark: "", domain: window.location.hostname }];
|
||||
} else {
|
||||
inModal.inbound.stream.tls.server = "";
|
||||
inModal.inbound.stream.tls.settings.domains = [];
|
||||
@@ -111,6 +99,31 @@
|
||||
if (!inModal.inbound.canEnableReality()) {
|
||||
this.inModal.inbound.reality = false;
|
||||
}
|
||||
if (this.inModal.inbound.protocol == Protocols.VLESS && !inModal.inbound.canEnableTlsFlow()) {
|
||||
this.inModal.inbound.settings.vlesses.forEach(client => {
|
||||
client.flow = "";
|
||||
});
|
||||
}
|
||||
},
|
||||
SSMethodChange() {
|
||||
if (this.inModal.inbound.isSSMultiUser) {
|
||||
if (this.inModal.inbound.settings.shadowsockses.length ==0){
|
||||
this.inModal.inbound.settings.shadowsockses = [new Inbound.ShadowsocksSettings.Shadowsocks()];
|
||||
}
|
||||
if (!this.inModal.inbound.isSS2022) {
|
||||
this.inModal.inbound.settings.shadowsockses.forEach(client => {
|
||||
client.method = this.inModal.inbound.settings.method;
|
||||
})
|
||||
} else {
|
||||
this.inModal.inbound.settings.shadowsockses.forEach(client => {
|
||||
client.method = "";
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if (this.inModal.inbound.settings.shadowsockses.length > 0){
|
||||
this.inModal.inbound.settings.shadowsockses = [];
|
||||
}
|
||||
}
|
||||
},
|
||||
setDefaultCertData(index) {
|
||||
inModal.inbound.stream.tls.certs[index].certFile = app.defaultCert;
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
.ant-col-sm-24 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
tr.hideExpandIcon .ant-table-row-expand-icon {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<body>
|
||||
@@ -118,8 +121,12 @@
|
||||
</a-radio-group>
|
||||
<a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
|
||||
:data-source="searchedInbounds"
|
||||
:loading="spinning" :scroll="{ x: 1300 }"
|
||||
:loading="spinning" :scroll="{ x: 1200 }"
|
||||
:pagination="false"
|
||||
:expand-icon-as-cell="false"
|
||||
:expand-row-by-click="false"
|
||||
:expand-icon-column-index="0"
|
||||
:row-class-name="dbInbound => (dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess || (dbInbound.isSS && dbInbound.toInbound().isSSMultiUser) ? '' : 'hideExpandIcon')"
|
||||
style="margin-top: 20px"
|
||||
@change="() => getDBInbounds()">
|
||||
<template slot="action" slot-scope="text, dbInbound">
|
||||
@@ -131,7 +138,11 @@
|
||||
<a-icon type="edit"></a-icon>
|
||||
{{ i18n "edit" }}
|
||||
</a-menu-item>
|
||||
<template v-if="dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess || dbInbound.isSS">
|
||||
<a-menu-item key="qrcode" v-if="dbInbound.isSS && !dbInbound.toInbound().isSSMultiUser">
|
||||
<a-icon type="qrcode"></a-icon>
|
||||
{{ i18n "qrCode" }}
|
||||
</a-menu-item>
|
||||
<template v-if="dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess || (dbInbound.isSS && dbInbound.toInbound().isSSMultiUser)">
|
||||
<a-menu-item key="addClient">
|
||||
<a-icon type="user-add"></a-icon>
|
||||
{{ i18n "pages.client.add"}}
|
||||
@@ -206,12 +217,29 @@
|
||||
</template>
|
||||
</template>
|
||||
<template slot="traffic" slot-scope="text, dbInbound">
|
||||
<a-tag color="blue">[[ sizeFormat(dbInbound.up) ]] / [[ sizeFormat(dbInbound.down) ]]</a-tag>
|
||||
<template v-if="dbInbound.total > 0">
|
||||
<a-tag v-if="dbInbound.up + dbInbound.down < dbInbound.total" color="cyan">[[ sizeFormat(dbInbound.total) ]]</a-tag>
|
||||
<a-tag v-else color="red">[[ sizeFormat(dbInbound.total) ]]</a-tag>
|
||||
</template>
|
||||
<a-tag v-else color="green">{{ i18n "unlimited" }}</a-tag>
|
||||
<a-popover :overlay-class-name="themeSwitcher.darkClass">
|
||||
<template slot="content">
|
||||
<table cellpadding="2" width="100%">
|
||||
<tr>
|
||||
<td>↑[[ sizeFormat(dbInbound.up) ]]</td>
|
||||
<td>↓[[ sizeFormat(dbInbound.down) ]]</td>
|
||||
</tr>
|
||||
<tr v-if="dbInbound.total > 0 && dbInbound.up + dbInbound.down < dbInbound.total">
|
||||
<td>{{ i18n "remained" }}</td>
|
||||
<td>[[ sizeFormat(dbInbound.total - dbInbound.up - dbInbound.down) ]]</td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
<a-tag :color="dbInbound.total == 0 ? 'green' : dbInbound.up + dbInbound.down < dbInbound.total ? 'cyan' : 'red'">
|
||||
[[ sizeFormat(dbInbound.up + dbInbound.down) ]] /
|
||||
<template v-if="dbInbound.total > 0">
|
||||
[[ sizeFormat(dbInbound.total) ]]
|
||||
</template>
|
||||
<template v-else>
|
||||
<svg style="fill: currentColor; height: 10px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z"/></svg>
|
||||
</template>
|
||||
</a-tag>
|
||||
</a-popover>
|
||||
</template>
|
||||
<template slot="enable" slot-scope="text, dbInbound">
|
||||
<a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound.id)"></a-switch>
|
||||
@@ -234,15 +262,17 @@
|
||||
:columns="innerColumns"
|
||||
:data-source="getInboundClients(record)"
|
||||
:pagination="false"
|
||||
style="margin-left: 20px;"
|
||||
>
|
||||
{{template "client_table"}}
|
||||
</a-table>
|
||||
<a-table
|
||||
v-else-if="record.protocol === Protocols.TROJAN || record.protocol === Protocols.SHADOWSOCKS"
|
||||
v-else-if="record.protocol === Protocols.TROJAN || record.toInbound().isSSMultiUser"
|
||||
:row-key="client => client.id"
|
||||
:columns="innerTrojanColumns"
|
||||
:data-source="getInboundClients(record)"
|
||||
:pagination="false"
|
||||
style="margin-left: 20px;"
|
||||
>
|
||||
{{template "client_table"}}
|
||||
</a-table>
|
||||
@@ -257,22 +287,21 @@
|
||||
{{template "js" .}}
|
||||
{{template "component/themeSwitcher" .}}
|
||||
<script>
|
||||
|
||||
const columns = [{
|
||||
title: "ID",
|
||||
align: 'right',
|
||||
dataIndex: "id",
|
||||
width: 30,
|
||||
}, {
|
||||
title: '{{ i18n "pages.inbounds.operate" }}',
|
||||
align: 'center',
|
||||
width: 60,
|
||||
width: 40,
|
||||
scopedSlots: { customRender: 'action' },
|
||||
}, {
|
||||
title: '{{ i18n "pages.inbounds.enable" }}',
|
||||
align: 'center',
|
||||
width: 40,
|
||||
scopedSlots: { customRender: 'enable' },
|
||||
}, {
|
||||
title: "ID",
|
||||
align: 'center',
|
||||
dataIndex: "id",
|
||||
width: 40,
|
||||
}, {
|
||||
title: '{{ i18n "pages.inbounds.remark" }}',
|
||||
align: 'center',
|
||||
@@ -291,12 +320,12 @@
|
||||
}, {
|
||||
title: '{{ i18n "clients" }}',
|
||||
align: 'left',
|
||||
width: 50,
|
||||
width: 40,
|
||||
scopedSlots: { customRender: 'clients' },
|
||||
}, {
|
||||
title: '{{ i18n "pages.inbounds.traffic" }}↑|↓',
|
||||
title: '{{ i18n "pages.inbounds.traffic" }}',
|
||||
align: 'center',
|
||||
width: 120,
|
||||
width: 60,
|
||||
scopedSlots: { customRender: 'traffic' },
|
||||
}, {
|
||||
title: '{{ i18n "pages.inbounds.expireDate" }}',
|
||||
@@ -309,18 +338,18 @@
|
||||
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } },
|
||||
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 40, scopedSlots: { customRender: 'enable' } },
|
||||
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
|
||||
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 120, scopedSlots: { customRender: 'traffic' } },
|
||||
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
|
||||
{ title: 'UID', width: 120, dataIndex: "id" },
|
||||
{ title: '{{ i18n "pages.inbounds.traffic" }}', width: 50, scopedSlots: { customRender: 'traffic' } },
|
||||
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 50, scopedSlots: { customRender: 'expiryTime' } },
|
||||
{ title: 'UUID', width: 120, dataIndex: "id" },
|
||||
];
|
||||
|
||||
const innerTrojanColumns = [
|
||||
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } },
|
||||
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 40, scopedSlots: { customRender: 'enable' } },
|
||||
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
|
||||
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 120, scopedSlots: { customRender: 'traffic' } },
|
||||
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
|
||||
{ title: 'Password', width: 170, dataIndex: "password" },
|
||||
{ title: '{{ i18n "pages.inbounds.traffic" }}', width: 50, scopedSlots: { customRender: 'traffic' } },
|
||||
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 50, scopedSlots: { customRender: 'expiryTime' } },
|
||||
{ title: '{{ i18n "password" }}', width: 170, dataIndex: "password" },
|
||||
];
|
||||
|
||||
const app = new Vue({
|
||||
@@ -340,7 +369,7 @@
|
||||
trafficDiff: 0,
|
||||
defaultCert: '',
|
||||
defaultKey: '',
|
||||
clientCount: {},
|
||||
clientCount: [],
|
||||
isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false,
|
||||
refreshing: false,
|
||||
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
|
||||
@@ -392,12 +421,16 @@
|
||||
setInbounds(dbInbounds) {
|
||||
this.inbounds.splice(0);
|
||||
this.dbInbounds.splice(0);
|
||||
this.clientCount.splice(0);
|
||||
for (const inbound of dbInbounds) {
|
||||
const dbInbound = new DBInbound(inbound);
|
||||
to_inbound = dbInbound.toInbound()
|
||||
this.inbounds.push(to_inbound);
|
||||
this.dbInbounds.push(dbInbound);
|
||||
if ([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(inbound.protocol)) {
|
||||
if (inbound.protocol === Protocols.SHADOWSOCKS && (!to_inbound.isSSMultiUser)) {
|
||||
continue;
|
||||
}
|
||||
this.clientCount[inbound.id] = this.getClientCounts(inbound, to_inbound);
|
||||
}
|
||||
}
|
||||
@@ -678,7 +711,7 @@
|
||||
openEditClient(dbInboundId, client) {
|
||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
clients = this.getInboundClients(dbInbound);
|
||||
index = this.findIndexOfClient(clients, client);
|
||||
index = this.findIndexOfClient(dbInbound.protocol, clients, client);
|
||||
clientModal.show({
|
||||
title: '{{ i18n "pages.client.edit"}}',
|
||||
okText: '{{ i18n "pages.client.submitEdit"}}',
|
||||
@@ -692,9 +725,13 @@
|
||||
isEdit: true
|
||||
});
|
||||
},
|
||||
findIndexOfClient(clients, client) {
|
||||
firstKey = Object.keys(client)[0];
|
||||
return clients.findIndex(c => c[firstKey] === client[firstKey]);
|
||||
findIndexOfClient(protocol, clients, client) {
|
||||
switch (protocol) {
|
||||
case Protocols.TROJAN:
|
||||
case Protocols.SHADOWSOCKS:
|
||||
return clients.findIndex(item => item.password === client.password && item.email === client.email);
|
||||
default: return clients.findIndex(item => item.id === client.id && item.email === client.email);
|
||||
}
|
||||
},
|
||||
async addClient(clients, dbInboundId) {
|
||||
const data = {
|
||||
@@ -800,7 +837,7 @@
|
||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
inbound = dbInbound.toInbound();
|
||||
clients = this.getClients(dbInbound.protocol, inbound.settings);
|
||||
index = this.findIndexOfClient(clients, client);
|
||||
index = this.findIndexOfClient(dbInbound.protocol, clients, client);
|
||||
clients[index].enable = !clients[index].enable;
|
||||
clientId = this.getClientId(dbInbound.protocol, clients[index]);
|
||||
await this.updateClient(clients[index], dbInboundId, clientId);
|
||||
|
||||
@@ -34,7 +34,8 @@
|
||||
:stroke-color="status.cpu.color"
|
||||
:class="themeSwitcher.darkCardClass"
|
||||
:percent="status.cpu.percent"></a-progress>
|
||||
<div>CPU</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"
|
||||
@@ -77,21 +78,17 @@
|
||||
<a-row>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-card hoverable :class="themeSwitcher.darkCardClass">
|
||||
3x-ui: <a href="https://github.com/MHSanaei/3x-ui/releases" target="_blank"><a-tag color="green">v{{ .cur_ver }}</a-tag></a>
|
||||
3X: <a href="https://github.com/MHSanaei/3x-ui/releases" target="_blank"><a-tag color="green">v{{ .cur_ver }}</a-tag></a>
|
||||
Xray: <a-tag color="green" style="cursor: pointer;" @click="openSelectV2rayVersion">v[[ status.xray.version ]]</a-tag>
|
||||
Telegram: <a href="https://t.me/panel3xui" target="_blank"><a-tag color="green">@panel3xui</a-tag></a>
|
||||
<a href="https://t.me/panel3xui" target="_blank"><a-tag color="green">@panel3xui</a-tag></a>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-card hoverable :class="themeSwitcher.darkCardClass">
|
||||
{{ i18n "pages.index.operationHours" }}:
|
||||
<a-tag color="green">[[ formatSecond(status.uptime) ]]</a-tag>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.operationHoursDesc" }}
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
{{ i18n "menu.link" }}:
|
||||
<a-tag color="blue" style="cursor: pointer;" @click="openLogs()">{{ i18n "pages.index.logs" }}</a-tag>
|
||||
<a-tag color="blue" style="cursor: pointer;" @click="openConfig">{{ i18n "pages.index.config" }}</a-tag>
|
||||
<a-tag color="blue" style="cursor: pointer;" @click="openBackup">{{ i18n "pages.index.backup" }}</a-tag>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
@@ -111,34 +108,86 @@
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-card hoverable :class="themeSwitcher.darkCardClass">
|
||||
{{ i18n "menu.link" }}:
|
||||
<a-tag color="blue" style="cursor: pointer;" @click="openLogs(20)">{{ i18n "pages.index.logs" }}</a-tag>
|
||||
<a-tag color="blue" style="cursor: pointer;" @click="openConfig">{{ i18n "pages.index.config" }}</a-tag>
|
||||
<a-tag color="blue" 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" :md="12">
|
||||
<a-card hoverable :class="themeSwitcher.darkCardClass">
|
||||
{{ i18n "pages.index.systemLoad" }}: [[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-card hoverable :class="themeSwitcher.darkCardClass">
|
||||
TCP / UDP {{ i18n "pages.index.connectionCount" }}: [[ status.tcpCount ]] / [[ status.udpCount ]]
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.connectionCountDesc" }}
|
||||
{{ i18n "pages.index.systemLoadDesc" }}
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-card hoverable :class="themeSwitcher.darkCardClass">
|
||||
{{ i18n "usage"}}:
|
||||
Memory [[ sizeFormat(status.appStats.mem) ]] -
|
||||
Threads [[ status.appStats.threads ]]
|
||||
</a-tooltip>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-card hoverable :class="themeSwitcher.darkCardClass">
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
IPv4:
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
[[ status.publicIP.ipv4 ]]
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
IPv6:
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
[[ status.publicIP.ipv6 ]]
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-card hoverable :class="themeSwitcher.darkCardClass">
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
TCP: [[ status.tcpCount ]]
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.connectionTcpCountDesc" }}
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
UDP: [[ status.udpCount ]]
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.connectionUdpCountDesc" }}
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-card hoverable :class="themeSwitcher.darkCardClass">
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
<a-icon type="arrow-up"></a-icon>
|
||||
[[ sizeFormat(status.netIO.up) ]] / S
|
||||
[[ sizeFormat(status.netIO.up) ]]/S
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.upSpeed" }}
|
||||
@@ -148,7 +197,7 @@
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-icon type="arrow-down"></a-icon>
|
||||
[[ sizeFormat(status.netIO.down) ]] / S
|
||||
[[ sizeFormat(status.netIO.down) ]]/S
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.downSpeed" }}
|
||||
@@ -213,7 +262,7 @@
|
||||
<a-form-item label="Count">
|
||||
<a-select v-model="logModal.rows"
|
||||
style="width: 80px"
|
||||
@change="openLogs(logModal.rows)"
|
||||
@change="openLogs()"
|
||||
:dropdown-class-name="themeSwitcher.darkCardClass">
|
||||
<a-select-option value="10">10</a-select-option>
|
||||
<a-select-option value="20">20</a-select-option>
|
||||
@@ -221,8 +270,23 @@
|
||||
<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.darkCardClass">
|
||||
<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>
|
||||
<button class="ant-btn ant-btn-primary" @click="openLogs(logModal.rows)"><a-icon type="sync"></a-icon> Reload</button>
|
||||
<button class="ant-btn ant-btn-primary" @click="openLogs()"><a-icon type="sync"></a-icon> Reload</button>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" style="margin-bottom: 10px;"
|
||||
@@ -294,30 +358,40 @@
|
||||
class Status {
|
||||
constructor(data) {
|
||||
this.cpu = new CurTotal(0, 0);
|
||||
this.cpuCores = 0;
|
||||
this.cpuSpeedMhz = 0;
|
||||
this.disk = new CurTotal(0, 0);
|
||||
this.loads = [0, 0, 0];
|
||||
this.mem = new CurTotal(0, 0);
|
||||
this.netIO = { up: 0, down: 0 };
|
||||
this.netTraffic = { sent: 0, recv: 0 };
|
||||
this.publicIP = { ipv4: 0, ipv6: 0 };
|
||||
this.swap = new CurTotal(0, 0);
|
||||
this.tcpCount = 0;
|
||||
this.udpCount = 0;
|
||||
this.uptime = 0;
|
||||
this.appUptime = 0;
|
||||
this.appStats = {threads: 0, mem: 0, uptime: 0};
|
||||
this.xray = { state: State.Stop, errorMsg: "", version: "", color: "" };
|
||||
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
this.cpu = new CurTotal(data.cpu, 100);
|
||||
this.cpuCores = data.cpuCores;
|
||||
this.cpuSpeedMhz = data.cpuSpeedMhz;
|
||||
this.disk = new CurTotal(data.disk.current, data.disk.total);
|
||||
this.loads = data.loads.map(load => toFixed(load, 2));
|
||||
this.mem = new CurTotal(data.mem.current, data.mem.total);
|
||||
this.netIO = data.netIO;
|
||||
this.netTraffic = data.netTraffic;
|
||||
this.publicIP = data.publicIP;
|
||||
this.swap = new CurTotal(data.swap.current, data.swap.total);
|
||||
this.tcpCount = data.tcpCount;
|
||||
this.udpCount = data.udpCount;
|
||||
this.uptime = data.uptime;
|
||||
this.appUptime = data.appUptime;
|
||||
this.appStats = data.appStats;
|
||||
this.xray = data.xray;
|
||||
switch (this.xray.state) {
|
||||
case State.Running:
|
||||
@@ -351,10 +425,11 @@
|
||||
visible: false,
|
||||
logs: '',
|
||||
rows: 20,
|
||||
show(logs, rows) {
|
||||
level: 'info',
|
||||
syslog: false,
|
||||
show(logs) {
|
||||
this.visible = true;
|
||||
this.rows = rows;
|
||||
this.logs = logs.join("\n");
|
||||
this.logs = logs? logs.join("\n"): "No Record...";
|
||||
},
|
||||
hide() {
|
||||
this.visible = false;
|
||||
@@ -403,9 +478,13 @@
|
||||
this.loadingTip = tip;
|
||||
},
|
||||
async getStatus() {
|
||||
const msg = await HttpUtil.post('/server/status');
|
||||
if (msg.success) {
|
||||
this.setStatus(msg.obj);
|
||||
try {
|
||||
const msg = await HttpUtil.post('/server/status');
|
||||
if (msg.success) {
|
||||
this.setStatus(msg.obj);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to get status:", e);
|
||||
}
|
||||
},
|
||||
setStatus(data) {
|
||||
@@ -451,14 +530,14 @@
|
||||
return;
|
||||
}
|
||||
},
|
||||
async openLogs(rows) {
|
||||
async openLogs(){
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.post('server/logs/' + rows);
|
||||
const msg = await HttpUtil.post('server/logs/'+logModal.rows,{level: logModal.level, syslog: logModal.syslog});
|
||||
this.loading(false);
|
||||
if (!msg.success) {
|
||||
return;
|
||||
}
|
||||
logModal.show(msg.obj, rows);
|
||||
logModal.show(msg.obj);
|
||||
},
|
||||
async openConfig() {
|
||||
this.loading(true);
|
||||
@@ -514,11 +593,14 @@
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -26,14 +26,32 @@
|
||||
|
||||
.alert-msg {
|
||||
color: rgb(194, 117, 18);
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
margin-top: 5px;
|
||||
padding: 16px 6px;
|
||||
font-weight: normal;
|
||||
font-size: 16px;
|
||||
padding: .5rem 1rem;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid;
|
||||
background: rgb(255 145 0 / 15%);
|
||||
margin: 1.5rem 2.5rem 0rem 2.5rem;
|
||||
border-radius: .5rem;
|
||||
transition: all 0.5s;
|
||||
animation: signal 3s cubic-bezier(0.18, 0.89, 0.32, 1.28) infinite;
|
||||
}
|
||||
.alert-msg:hover {
|
||||
cursor: default;
|
||||
transition-duration: .3s;
|
||||
animation: signal 0.9s ease infinite;
|
||||
}
|
||||
@keyframes signal{
|
||||
0%{
|
||||
box-shadow: 0 0 0 0 rgba(194, 118, 18, 0.5);
|
||||
}
|
||||
50%{
|
||||
box-shadow: 0 0 0 6px rgba(0 ,0,0,0);
|
||||
}
|
||||
100%{
|
||||
box-shadow: 0 0 0 6px rgba(0 ,0,0,0);
|
||||
}
|
||||
}
|
||||
|
||||
.alert-msg > i {
|
||||
color: inherit;
|
||||
font-size: 24px;
|
||||
@@ -73,6 +91,7 @@
|
||||
</a-row>
|
||||
<a-list item-layout="horizontal" :style="themeSwitcher.textStyle">
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.panelListeningIP"}}' desc='{{ i18n "pages.settings.panelListeningIPDesc"}}' v-model="allSetting.webListen"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.panelListeningDomain"}}' desc='{{ i18n "pages.settings.panelListeningDomainDesc"}}' v-model="allSetting.webDomain"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.settings.panelPort"}}' desc='{{ i18n "pages.settings.panelPortDesc"}}' v-model="allSetting.webPort" :min="0"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.publicKeyPath"}}' desc='{{ i18n "pages.settings.publicKeyPathDesc"}}' v-model="allSetting.webCertFile"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.privateKeyPath"}}' desc='{{ i18n "pages.settings.privateKeyPathDesc"}}' v-model="allSetting.webKeyFile"></setting-list-item>
|
||||
@@ -97,7 +116,7 @@
|
||||
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 role="img" :aria-label="l.name" v-text="l.icon"></span>
|
||||
<span v-text="l.name"></span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
@@ -288,23 +307,37 @@
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigNetflixWARP"}}' desc='{{ i18n "pages.settings.templates.xrayConfigNetflixWARPDesc"}}' v-model="NetflixWARPSettings"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigSpotifyWARP"}}' desc='{{ i18n "pages.settings.templates.xrayConfigSpotifyWARPDesc"}}' v-model="SpotifyWARPSettings"></setting-list-item>
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel header='{{ i18n "pages.settings.templates.manualLists"}}'>
|
||||
<a-row :xs="24" :sm="24" :lg="12">
|
||||
<h2 class="collapse-title">
|
||||
<a-icon type="warning"></a-icon>
|
||||
{{ i18n "pages.settings.templates.manualListsDesc" }}
|
||||
</h2>
|
||||
</a-row>
|
||||
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualBlockedIPs"}}' v-model="manualBlockedIPs"></setting-list-item>
|
||||
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualBlockedDomains"}}' v-model="manualBlockedDomains"></setting-list-item>
|
||||
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualDirectIPs"}}' v-model="manualDirectIPs"></setting-list-item>
|
||||
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualDirectDomains"}}' v-model="manualDirectDomains"></setting-list-item>
|
||||
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualIPv4Domains"}}' v-model="manualIPv4Domains"></setting-list-item>
|
||||
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualWARPDomains"}}' v-model="manualWARPDomains"></setting-list-item>
|
||||
</a-collapse>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="tpl-2" tab='{{ i18n "pages.settings.templates.manualLists"}}' style="padding-top: 20px;">
|
||||
<a-row :xs="24" :sm="24" :lg="12">
|
||||
<h2 class="collapse-title">
|
||||
<a-icon type="warning"></a-icon>
|
||||
{{ i18n "pages.settings.templates.manualListsDesc" }}
|
||||
</h2>
|
||||
</a-row>
|
||||
<a-collapse>
|
||||
<a-collapse-panel header='{{ i18n "pages.settings.templates.manualBlockedIPs"}}'>
|
||||
<setting-list-item type="textarea" v-model="manualBlockedIPs"></setting-list-item>
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel header='{{ i18n "pages.settings.templates.manualBlockedDomains"}}'>
|
||||
<setting-list-item type="textarea" v-model="manualBlockedDomains"></setting-list-item>
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel header='{{ i18n "pages.settings.templates.manualDirectIPs"}}'>
|
||||
<setting-list-item type="textarea" v-model="manualDirectIPs"></setting-list-item>
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel header='{{ i18n "pages.settings.templates.manualDirectDomains"}}'>
|
||||
<setting-list-item type="textarea" v-model="manualDirectDomains"></setting-list-item>
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel header='{{ i18n "pages.settings.templates.manualIPv4Domains"}}'>
|
||||
<setting-list-item type="textarea" v-model="manualIPv4Domains"></setting-list-item>
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel header='{{ i18n "pages.settings.templates.manualWARPDomains"}}'>
|
||||
<setting-list-item type="textarea" v-model="manualWARPDomains"></setting-list-item>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="tpl-2" tab='{{ i18n "pages.settings.templates.advancedTemplate"}}' style="padding-top: 20px;">
|
||||
<a-tab-pane key="tpl-3" tab='{{ i18n "pages.settings.templates.advancedTemplate"}}' style="padding-top: 20px;">
|
||||
<a-collapse>
|
||||
<a-collapse-panel header='{{ i18n "pages.settings.templates.xrayConfigInbounds"}}'>
|
||||
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigInbounds"}}' desc='{{ i18n "pages.settings.templates.xrayConfigInboundsDesc"}}' v-model="inboundSettings"></setting-list-item>
|
||||
@@ -317,7 +350,7 @@
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="tpl-3" tab='{{ i18n "pages.settings.templates.completeTemplate"}}' style="padding-top: 20px;">
|
||||
<a-tab-pane key="tpl-4" tab='{{ i18n "pages.settings.templates.completeTemplate"}}' style="padding-top: 20px;">
|
||||
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigTemplate"}}' desc='{{ i18n "pages.settings.templates.xrayConfigTemplateDesc"}}' v-model="allSetting.xrayTemplateConfig"></setting-list-item>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
@@ -337,6 +370,7 @@
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.telegramChatId"}}' desc='{{ i18n "pages.settings.telegramChatIdDesc"}}' v-model="allSetting.tgBotChatId"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.telegramNotifyTime"}}' desc='{{ i18n "pages.settings.telegramNotifyTimeDesc"}}' v-model="allSetting.tgRunTime"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.tgNotifyBackup" }}' desc='{{ i18n "pages.settings.tgNotifyBackupDesc" }}' v-model="allSetting.tgBotBackup"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.tgNotifyLogin" }}' desc='{{ i18n "pages.settings.tgNotifyLoginDesc" }}' v-model="allSetting.tgBotLoginNotify"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.settings.tgNotifyCpu" }}' desc='{{ i18n "pages.settings.tgNotifyCpuDesc" }}' v-model="allSetting.tgCpu" :min="0" :max="100"></setting-list-item>
|
||||
<a-list-item>
|
||||
<a-row style="padding: 20px">
|
||||
@@ -353,7 +387,7 @@
|
||||
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 role="img" :aria-label="l.name" v-text="l.icon"></span>
|
||||
<span v-text="l.name"></span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
@@ -372,10 +406,12 @@
|
||||
</a-row>
|
||||
<a-list item-layout="horizontal" :style="themeSwitcher.textStyle">
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.subEnable"}}' desc='{{ i18n "pages.settings.subEnableDesc"}}' v-model="allSetting.subEnable"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.subEncrypt"}}' desc='{{ i18n "pages.settings.subEncryptDesc"}}' v-model="allSetting.subEncrypt"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.subShowInfo"}}' desc='{{ i18n "pages.settings.subShowInfoDesc"}}' v-model="allSetting.subShowInfo"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subListen"}}' desc='{{ i18n "pages.settings.subListenDesc"}}' v-model="allSetting.subListen"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subDomain"}}' desc='{{ i18n "pages.settings.subDomainDesc"}}' v-model="allSetting.subDomain"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.settings.subPort"}}' desc='{{ i18n "pages.settings.subPortDesc"}}' v-model.number="allSetting.subPort"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subPath"}}' desc='{{ i18n "pages.settings.subPathDesc"}}' v-model="allSetting.subPath"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subDomain"}}' desc='{{ i18n "pages.settings.subDomainDesc"}}' v-model="allSetting.subDomain"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subCertPath"}}' desc='{{ i18n "pages.settings.subCertPathDesc"}}' v-model="allSetting.subCertFile"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subKeyPath"}}' desc='{{ i18n "pages.settings.subKeyPathDesc"}}' v-model="allSetting.subKeyFile"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.settings.subUpdates"}}' desc='{{ i18n "pages.settings.subUpdatesDesc"}}' v-model="allSetting.subUpdates"></setting-list-item>
|
||||
@@ -436,13 +472,12 @@
|
||||
ips: {
|
||||
local: ["geoip:private"],
|
||||
cn: ["geoip:cn"],
|
||||
ir: ["geoip:ir"],
|
||||
ir: ["ext:geoip_IR.dat:ir","ext:geoip_IR.dat:arvancloud","ext:geoip_IR.dat:derakcloud","ext:geoip_IR.dat:iranserver"],
|
||||
ru: ["geoip:ru"],
|
||||
},
|
||||
domains: {
|
||||
ads: [
|
||||
"geosite:category-ads-all",
|
||||
"ext:iran.dat:ads"
|
||||
"ext:geosite_IR.dat:category-ads-all"
|
||||
],
|
||||
speedtest: ["geosite:speedtest"],
|
||||
openai: ["geosite:openai"],
|
||||
@@ -459,9 +494,8 @@
|
||||
],
|
||||
ir: [
|
||||
"regexp:.*\\.ir$",
|
||||
"ext:iran.dat:ir",
|
||||
"ext:iran.dat:other",
|
||||
"geosite:category-ir"
|
||||
"regexp:.*\\.xn--mgba3a4f16a$", // .ایران
|
||||
"ext:geosite_IR.dat:ir" // have rules to bypass all .ir domains.
|
||||
]
|
||||
},
|
||||
familyProtectDNS: {
|
||||
@@ -504,7 +538,7 @@
|
||||
this.loading(false);
|
||||
if (msg.success) {
|
||||
this.user = {};
|
||||
window.location.replace(basePath + "logout")
|
||||
window.location.replace(basePath + "logout");
|
||||
}
|
||||
},
|
||||
async restartPanel() {
|
||||
@@ -523,12 +557,10 @@
|
||||
if (msg.success) {
|
||||
this.loading(true);
|
||||
await PromiseUtil.sleep(5000);
|
||||
let protocol = "http://";
|
||||
if (this.allSetting.webCertFile !== "") {
|
||||
protocol = "https://";
|
||||
}
|
||||
const { host } = window.location;
|
||||
window.location.replace(protocol + host + this.allSetting.webBasePath + "panel/settings");
|
||||
const { webCertFile, webKeyFile, webDomain: host, webPort: port, webBasePath: base } = this.allSetting;
|
||||
const isTLS = webCertFile !== "" || webKeyFile !== "";
|
||||
const url = buildURL({ host, port, isTLS, base, path: "panel/settings" });
|
||||
window.location.replace(url);
|
||||
}
|
||||
},
|
||||
async fetchUserSecret() {
|
||||
|
||||
@@ -2,28 +2,29 @@ package job
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"x-ui/database"
|
||||
"x-ui/database/model"
|
||||
"x-ui/logger"
|
||||
"x-ui/web/service"
|
||||
"x-ui/xray"
|
||||
|
||||
"net"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-cmd/cmd"
|
||||
"x-ui/database"
|
||||
"x-ui/database/model"
|
||||
"x-ui/logger"
|
||||
"x-ui/xray"
|
||||
)
|
||||
|
||||
type CheckClientIpJob struct {
|
||||
xrayService service.XrayService
|
||||
}
|
||||
type CheckClientIpJob struct{}
|
||||
|
||||
var job *CheckClientIpJob
|
||||
var disAllowedIps []string
|
||||
var ipFiles = []string{
|
||||
xray.GetIPLimitLogPath(),
|
||||
xray.GetIPLimitBannedLogPath(),
|
||||
xray.GetAccessPersistentLogPath(),
|
||||
}
|
||||
|
||||
func NewCheckClientIpJob() *CheckClientIpJob {
|
||||
job = new(CheckClientIpJob)
|
||||
@@ -31,24 +32,62 @@ func NewCheckClientIpJob() *CheckClientIpJob {
|
||||
}
|
||||
|
||||
func (j *CheckClientIpJob) Run() {
|
||||
logger.Debug("Check Client IP Job...")
|
||||
processLogFile()
|
||||
|
||||
// disAllowedIps = []string{"192.168.1.183","192.168.1.197"}
|
||||
blockedIps := []byte(strings.Join(disAllowedIps, ","))
|
||||
|
||||
// check if file exists, if not create one
|
||||
_, err := os.Stat(xray.GetBlockedIPsPath())
|
||||
if os.IsNotExist(err) {
|
||||
_, err = os.OpenFile(xray.GetBlockedIPsPath(), os.O_RDWR|os.O_CREATE, 0755)
|
||||
checkError(err)
|
||||
// create files required for iplimit if not exists
|
||||
for i := 0; i < len(ipFiles); i++ {
|
||||
file, err := os.OpenFile(ipFiles[i], os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
|
||||
j.checkError(err)
|
||||
defer file.Close()
|
||||
}
|
||||
|
||||
// check for limit ip
|
||||
if j.hasLimitIp() {
|
||||
j.checkFail2BanInstalled()
|
||||
j.processLogFile()
|
||||
}
|
||||
err = os.WriteFile(xray.GetBlockedIPsPath(), blockedIps, 0755)
|
||||
checkError(err)
|
||||
}
|
||||
|
||||
func processLogFile() {
|
||||
accessLogPath := GetAccessLogPath()
|
||||
func (j *CheckClientIpJob) hasLimitIp() bool {
|
||||
db := database.GetDB()
|
||||
var inbounds []*model.Inbound
|
||||
|
||||
err := db.Model(model.Inbound{}).Find(&inbounds).Error
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, inbound := range inbounds {
|
||||
if inbound.Settings == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
settings := map[string][]model.Client{}
|
||||
json.Unmarshal([]byte(inbound.Settings), &settings)
|
||||
clients := settings["clients"]
|
||||
|
||||
for _, client := range clients {
|
||||
limitIp := client.LimitIP
|
||||
if limitIp > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (j *CheckClientIpJob) checkFail2BanInstalled() {
|
||||
cmd := "fail2ban-client"
|
||||
args := []string{"-h"}
|
||||
|
||||
err := exec.Command(cmd, args...).Run()
|
||||
if err != nil {
|
||||
logger.Warning("fail2ban is not installed. IP limiting may not work properly.")
|
||||
}
|
||||
}
|
||||
|
||||
func (j *CheckClientIpJob) processLogFile() {
|
||||
accessLogPath := xray.GetAccessLogPath()
|
||||
if accessLogPath == "" {
|
||||
logger.Warning("access.log doesn't exist in your config.json")
|
||||
return
|
||||
@@ -56,12 +95,7 @@ func processLogFile() {
|
||||
|
||||
data, err := os.ReadFile(accessLogPath)
|
||||
InboundClientIps := make(map[string][]string)
|
||||
checkError(err)
|
||||
|
||||
// clean log
|
||||
if err := os.Truncate(GetAccessLogPath(), 0); err != nil {
|
||||
checkError(err)
|
||||
}
|
||||
j.checkError(err)
|
||||
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for _, line := range lines {
|
||||
@@ -82,7 +116,7 @@ func processLogFile() {
|
||||
matchesEmail = strings.TrimSpace(strings.Split(matchesEmail, "email: ")[1])
|
||||
|
||||
if InboundClientIps[matchesEmail] != nil {
|
||||
if contains(InboundClientIps[matchesEmail], ip) {
|
||||
if j.contains(InboundClientIps[matchesEmail], ip) {
|
||||
continue
|
||||
}
|
||||
InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail], ip)
|
||||
@@ -91,56 +125,50 @@ func processLogFile() {
|
||||
InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail], ip)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
disAllowedIps = []string{}
|
||||
shouldCleanLog := false
|
||||
|
||||
for clientEmail, ips := range InboundClientIps {
|
||||
inboundClientIps, err := GetInboundClientIps(clientEmail)
|
||||
inboundClientIps, err := j.getInboundClientIps(clientEmail)
|
||||
sort.Strings(ips)
|
||||
if err != nil {
|
||||
addInboundClientIps(clientEmail, ips)
|
||||
|
||||
j.addInboundClientIps(clientEmail, ips)
|
||||
} else {
|
||||
updateInboundClientIps(inboundClientIps, clientEmail, ips)
|
||||
shouldCleanLog = j.updateInboundClientIps(inboundClientIps, clientEmail, ips)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// check if inbound connection is more than limited ip and drop connection
|
||||
LimitDevice := func() { LimitDevice() }
|
||||
// added delay before cleaning logs to reduce chance of logging IP that already has been banned
|
||||
time.Sleep(time.Second * 2)
|
||||
|
||||
stop := schedule(LimitDevice, 1000*time.Millisecond)
|
||||
time.Sleep(10 * time.Second)
|
||||
stop <- true
|
||||
if shouldCleanLog {
|
||||
// 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()
|
||||
|
||||
}
|
||||
func GetAccessLogPath() string {
|
||||
|
||||
config, err := os.ReadFile(xray.GetConfigPath())
|
||||
checkError(err)
|
||||
|
||||
jsonConfig := map[string]interface{}{}
|
||||
err = json.Unmarshal([]byte(config), &jsonConfig)
|
||||
checkError(err)
|
||||
if jsonConfig["log"] != nil {
|
||||
jsonLog := jsonConfig["log"].(map[string]interface{})
|
||||
if jsonLog["access"] != nil {
|
||||
|
||||
accessLogPath := jsonLog["access"].(string)
|
||||
|
||||
return accessLogPath
|
||||
// clean access log
|
||||
if err := os.Truncate(xray.GetAccessLogPath(), 0); err != nil {
|
||||
j.checkError(err)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
|
||||
}
|
||||
func checkError(e error) {
|
||||
|
||||
func (j *CheckClientIpJob) checkError(e error) {
|
||||
if e != nil {
|
||||
logger.Warning("client ip job err:", e)
|
||||
}
|
||||
}
|
||||
func contains(s []string, str string) bool {
|
||||
|
||||
func (j *CheckClientIpJob) contains(s []string, str string) bool {
|
||||
for _, v := range s {
|
||||
if v == str {
|
||||
return true
|
||||
@@ -149,7 +177,8 @@ func contains(s []string, str string) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
func GetInboundClientIps(clientEmail string) (*model.InboundClientIps, error) {
|
||||
|
||||
func (j *CheckClientIpJob) getInboundClientIps(clientEmail string) (*model.InboundClientIps, error) {
|
||||
db := database.GetDB()
|
||||
InboundClientIps := &model.InboundClientIps{}
|
||||
err := db.Model(model.InboundClientIps{}).Where("client_email = ?", clientEmail).First(InboundClientIps).Error
|
||||
@@ -158,10 +187,11 @@ func GetInboundClientIps(clientEmail string) (*model.InboundClientIps, error) {
|
||||
}
|
||||
return InboundClientIps, nil
|
||||
}
|
||||
func addInboundClientIps(clientEmail string, ips []string) error {
|
||||
|
||||
func (j *CheckClientIpJob) addInboundClientIps(clientEmail string, ips []string) error {
|
||||
inboundClientIps := &model.InboundClientIps{}
|
||||
jsonIps, err := json.Marshal(ips)
|
||||
checkError(err)
|
||||
j.checkError(err)
|
||||
|
||||
inboundClientIps.ClientEmail = clientEmail
|
||||
inboundClientIps.Ips = string(jsonIps)
|
||||
@@ -183,35 +213,50 @@ func addInboundClientIps(clientEmail string, ips []string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmail string, ips []string) error {
|
||||
|
||||
func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmail string, ips []string) bool {
|
||||
jsonIps, err := json.Marshal(ips)
|
||||
checkError(err)
|
||||
j.checkError(err)
|
||||
|
||||
inboundClientIps.ClientEmail = clientEmail
|
||||
inboundClientIps.Ips = string(jsonIps)
|
||||
|
||||
// check inbound limitation
|
||||
inbound, err := GetInboundByEmail(clientEmail)
|
||||
checkError(err)
|
||||
inbound, err := j.getInboundByEmail(clientEmail)
|
||||
j.checkError(err)
|
||||
|
||||
if inbound.Settings == "" {
|
||||
logger.Debug("wrong data ", inbound)
|
||||
return nil
|
||||
return false
|
||||
}
|
||||
|
||||
settings := map[string][]model.Client{}
|
||||
json.Unmarshal([]byte(inbound.Settings), &settings)
|
||||
clients := settings["clients"]
|
||||
shouldCleanLog := false
|
||||
|
||||
// create iplimit log file channel
|
||||
logIpFile, err := os.OpenFile(xray.GetIPLimitLogPath(), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
|
||||
if err != nil {
|
||||
logger.Errorf("failed to create or open ip limit log file: %s", err)
|
||||
}
|
||||
defer logIpFile.Close()
|
||||
log.SetOutput(logIpFile)
|
||||
log.SetFlags(log.LstdFlags)
|
||||
|
||||
for _, client := range clients {
|
||||
if client.Email == clientEmail {
|
||||
|
||||
limitIp := client.LimitIP
|
||||
|
||||
if limitIp < len(ips) && limitIp != 0 && inbound.Enable {
|
||||
if limitIp != 0 {
|
||||
shouldCleanLog = true
|
||||
|
||||
disAllowedIps = append(disAllowedIps, ips[limitIp:]...)
|
||||
if limitIp < len(ips) && inbound.Enable {
|
||||
disAllowedIps = append(disAllowedIps, ips[limitIp:]...)
|
||||
for i := limitIp; i < len(ips); i++ {
|
||||
log.Printf("[LIMIT_IP] Email = %s || SRC = %s", clientEmail, ips[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -221,131 +266,19 @@ func updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmai
|
||||
db := database.GetDB()
|
||||
err = db.Save(inboundClientIps).Error
|
||||
if err != nil {
|
||||
return err
|
||||
return shouldCleanLog
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func DisableInbound(id int) error {
|
||||
db := database.GetDB()
|
||||
result := db.Model(model.Inbound{}).
|
||||
Where("id = ? and enable = ?", id, true).
|
||||
Update("enable", false)
|
||||
err := result.Error
|
||||
logger.Warning("disable inbound with id:", id)
|
||||
|
||||
if err == nil {
|
||||
job.xrayService.SetToNeedRestart()
|
||||
}
|
||||
|
||||
return err
|
||||
return shouldCleanLog
|
||||
}
|
||||
|
||||
func GetInboundByEmail(clientEmail string) (*model.Inbound, error) {
|
||||
func (j *CheckClientIpJob) getInboundByEmail(clientEmail string) (*model.Inbound, error) {
|
||||
db := database.GetDB()
|
||||
var inbounds *model.Inbound
|
||||
|
||||
err := db.Model(model.Inbound{}).Where("settings LIKE ?", "%"+clientEmail+"%").Find(&inbounds).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return inbounds, nil
|
||||
}
|
||||
|
||||
func LimitDevice() {
|
||||
|
||||
localIp, err := LocalIP()
|
||||
checkError(err)
|
||||
|
||||
c := cmd.NewCmd("bash", "-c", "ss --tcp | grep -E '"+IPsToRegex(localIp)+"'| awk '{if($1==\"ESTAB\") print $4,$5;}'", "| sort | uniq -c | sort -nr | head")
|
||||
|
||||
<-c.Start()
|
||||
if len(c.Status().Stdout) > 0 {
|
||||
ipRegx, _ := regexp.Compile(`[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+`)
|
||||
portRegx, _ := regexp.Compile(`(?:(:))([0-9]..[^.][0-9]+)`)
|
||||
|
||||
for _, row := range c.Status().Stdout {
|
||||
|
||||
data := strings.Split(row, " ")
|
||||
|
||||
destIp, destPort, srcIp, srcPort := "", "", "", ""
|
||||
|
||||
destIp = string(ipRegx.FindString(data[0]))
|
||||
|
||||
destPort = portRegx.FindString(data[0])
|
||||
destPort = strings.Replace(destPort, ":", "", -1)
|
||||
|
||||
srcIp = string(ipRegx.FindString(data[1]))
|
||||
|
||||
srcPort = portRegx.FindString(data[1])
|
||||
srcPort = strings.Replace(srcPort, ":", "", -1)
|
||||
|
||||
if contains(disAllowedIps, srcIp) {
|
||||
dropCmd := cmd.NewCmd("bash", "-c", "ss -K dport = "+srcPort)
|
||||
dropCmd.Start()
|
||||
|
||||
logger.Debug("request droped : ", srcIp, srcPort, "to", destIp, destPort)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func LocalIP() ([]string, error) {
|
||||
// get machine ips
|
||||
|
||||
ifaces, err := net.Interfaces()
|
||||
ips := []string{}
|
||||
if err != nil {
|
||||
return ips, err
|
||||
}
|
||||
for _, i := range ifaces {
|
||||
addrs, err := i.Addrs()
|
||||
if err != nil {
|
||||
return ips, err
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
var ip net.IP
|
||||
switch v := addr.(type) {
|
||||
case *net.IPNet:
|
||||
ip = v.IP
|
||||
case *net.IPAddr:
|
||||
ip = v.IP
|
||||
}
|
||||
|
||||
ips = append(ips, ip.String())
|
||||
|
||||
}
|
||||
}
|
||||
logger.Debug("System IPs : ", ips)
|
||||
|
||||
return ips, nil
|
||||
}
|
||||
|
||||
func IPsToRegex(ips []string) string {
|
||||
|
||||
regx := ""
|
||||
for _, ip := range ips {
|
||||
regx += "(" + strings.Replace(ip, ".", "\\.", -1) + ")"
|
||||
|
||||
}
|
||||
regx = "(" + strings.Replace(regx, ")(", ")|(.", -1) + ")"
|
||||
|
||||
return regx
|
||||
}
|
||||
|
||||
func schedule(LimitDevice func(), delay time.Duration) chan bool {
|
||||
stop := make(chan bool)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
LimitDevice()
|
||||
select {
|
||||
case <-time.After(delay):
|
||||
case <-stop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return stop
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
package job
|
||||
|
||||
import (
|
||||
"x-ui/logger"
|
||||
"x-ui/web/service"
|
||||
)
|
||||
|
||||
type CheckInboundJob struct {
|
||||
xrayService service.XrayService
|
||||
inboundService service.InboundService
|
||||
}
|
||||
|
||||
func NewCheckInboundJob() *CheckInboundJob {
|
||||
return new(CheckInboundJob)
|
||||
}
|
||||
|
||||
func (j *CheckInboundJob) Run() {
|
||||
count, err := j.inboundService.DisableInvalidClients()
|
||||
if err != nil {
|
||||
logger.Warning("disable invalid Client err:", err)
|
||||
} else if count > 0 {
|
||||
logger.Debugf("disabled %v Client", count)
|
||||
j.xrayService.SetToNeedRestart()
|
||||
}
|
||||
|
||||
count, err = j.inboundService.DisableInvalidInbounds()
|
||||
if err != nil {
|
||||
logger.Warning("disable invalid inbounds err:", err)
|
||||
} else if count > 0 {
|
||||
logger.Debugf("disabled %v inbounds", count)
|
||||
j.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
25
web/job/clear_logs_job.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package job
|
||||
|
||||
import (
|
||||
"os"
|
||||
"x-ui/logger"
|
||||
"x-ui/xray"
|
||||
)
|
||||
|
||||
type ClearLogsJob struct{}
|
||||
|
||||
func NewClearLogsJob() *ClearLogsJob {
|
||||
return new(ClearLogsJob)
|
||||
}
|
||||
|
||||
// Here Run is an interface method of the Job interface
|
||||
func (j *ClearLogsJob) Run() {
|
||||
logFiles := []string{xray.GetIPLimitLogPath(), xray.GetIPLimitBannedLogPath(), xray.GetAccessPersistentLogPath()}
|
||||
|
||||
// clear log files
|
||||
for i := 0; i < len(logFiles); i++ {
|
||||
if err := os.Truncate(logFiles[i], 0); err != nil {
|
||||
logger.Warning("clear logs job err:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,14 +24,12 @@ func (j *XrayTrafficJob) Run() {
|
||||
logger.Warning("get xray traffic failed:", err)
|
||||
return
|
||||
}
|
||||
err = j.inboundService.AddTraffic(traffics)
|
||||
err, needRestart := j.inboundService.AddTraffic(traffics, clientTraffics)
|
||||
if err != nil {
|
||||
logger.Warning("add traffic failed:", err)
|
||||
}
|
||||
|
||||
err = j.inboundService.AddClientTraffic(clientTraffics)
|
||||
if err != nil {
|
||||
logger.Warning("add client traffic failed:", err)
|
||||
if needRestart {
|
||||
j.xrayService.SetToNeedRestart()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ type SettingService interface {
|
||||
|
||||
func InitLocalizer(i18nFS embed.FS, settingService SettingService) error {
|
||||
// set default bundle to english
|
||||
i18nBundle = i18n.NewBundle(language.English)
|
||||
i18nBundle = i18n.NewBundle(language.MustParse("en-US"))
|
||||
i18nBundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
|
||||
|
||||
// parse files
|
||||
|
||||
21
web/middleware/domainValidator.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func DomainValidatorMiddleware(domain string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
host := strings.Split(c.Request.Host, ":")[0]
|
||||
|
||||
if host != domain {
|
||||
c.AbortWithStatus(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
34
web/middleware/redirect.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func RedirectMiddleware(basePath string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Redirect from old '/xui' path to '/panel'
|
||||
redirects := map[string]string{
|
||||
"panel/API": "panel/api",
|
||||
"xui/API": "panel/api",
|
||||
"xui": "panel",
|
||||
}
|
||||
|
||||
path := c.Request.URL.Path
|
||||
for from, to := range redirects {
|
||||
from, to = basePath+from, basePath+to
|
||||
|
||||
if strings.HasPrefix(path, from) {
|
||||
newPath := to + path[len(from):]
|
||||
|
||||
c.Redirect(http.StatusMovedPermanently, newPath)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"log": {
|
||||
"loglevel": "warning",
|
||||
"access": "./access.log",
|
||||
"error": "./error.log"
|
||||
},
|
||||
"api": {
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
)
|
||||
|
||||
type InboundService struct {
|
||||
xrayApi xray.XrayAPI
|
||||
}
|
||||
|
||||
func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) {
|
||||
@@ -133,53 +134,30 @@ func (s *InboundService) checkEmailExistForInbound(inbound *model.Inbound) (stri
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, error) {
|
||||
func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, bool, error) {
|
||||
exist, err := s.checkPortExist(inbound.Port, 0)
|
||||
if err != nil {
|
||||
return inbound, err
|
||||
return inbound, false, err
|
||||
}
|
||||
if exist {
|
||||
return inbound, common.NewError("Port already exists:", inbound.Port)
|
||||
return inbound, false, common.NewError("Port already exists:", inbound.Port)
|
||||
}
|
||||
|
||||
existEmail, err := s.checkEmailExistForInbound(inbound)
|
||||
if err != nil {
|
||||
return inbound, err
|
||||
return inbound, false, err
|
||||
}
|
||||
if existEmail != "" {
|
||||
return inbound, common.NewError("Duplicate email:", existEmail)
|
||||
return inbound, false, common.NewError("Duplicate email:", existEmail)
|
||||
}
|
||||
|
||||
clients, err := s.GetClients(inbound)
|
||||
if err != nil {
|
||||
return inbound, err
|
||||
}
|
||||
|
||||
db := database.GetDB()
|
||||
|
||||
err = db.Save(inbound).Error
|
||||
if err == nil {
|
||||
for _, client := range clients {
|
||||
s.AddClientStat(inbound.Id, &client)
|
||||
}
|
||||
}
|
||||
return inbound, err
|
||||
}
|
||||
|
||||
func (s *InboundService) AddInbounds(inbounds []*model.Inbound) error {
|
||||
for _, inbound := range inbounds {
|
||||
exist, err := s.checkPortExist(inbound.Port, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exist {
|
||||
return common.NewError("Port already exists:", inbound.Port)
|
||||
}
|
||||
return inbound, false, err
|
||||
}
|
||||
|
||||
db := database.GetDB()
|
||||
tx := db.Begin()
|
||||
var err error
|
||||
defer func() {
|
||||
if err == nil {
|
||||
tx.Commit()
|
||||
@@ -188,37 +166,75 @@ func (s *InboundService) AddInbounds(inbounds []*model.Inbound) error {
|
||||
}
|
||||
}()
|
||||
|
||||
for _, inbound := range inbounds {
|
||||
err = tx.Save(inbound).Error
|
||||
if err != nil {
|
||||
return err
|
||||
err = tx.Save(inbound).Error
|
||||
if err == nil {
|
||||
for _, client := range clients {
|
||||
s.AddClientStat(tx, inbound.Id, &client)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
needRestart := false
|
||||
if inbound.Enable {
|
||||
s.xrayApi.Init(p.GetAPIPort())
|
||||
inboundJson, err1 := json.MarshalIndent(inbound.GenXrayInboundConfig(), "", " ")
|
||||
if err1 != nil {
|
||||
logger.Debug("Unable to marshal inbound config:", err1)
|
||||
}
|
||||
|
||||
err1 = s.xrayApi.AddInbound(inboundJson)
|
||||
if err1 == nil {
|
||||
logger.Debug("New inbound added by api:", inbound.Tag)
|
||||
} else {
|
||||
logger.Debug("Unable to add inbound by api:", err1)
|
||||
needRestart = true
|
||||
}
|
||||
s.xrayApi.Close()
|
||||
}
|
||||
|
||||
return inbound, needRestart, err
|
||||
}
|
||||
|
||||
func (s *InboundService) DelInbound(id int) error {
|
||||
func (s *InboundService) DelInbound(id int) (bool, error) {
|
||||
db := database.GetDB()
|
||||
|
||||
var tag string
|
||||
needRestart := false
|
||||
result := db.Model(model.Inbound{}).Select("tag").Where("id = ? and enable = ?", id, true).First(&tag)
|
||||
if result.Error == nil {
|
||||
s.xrayApi.Init(p.GetAPIPort())
|
||||
err1 := s.xrayApi.DelInbound(tag)
|
||||
if err1 == nil {
|
||||
logger.Debug("Inbound deleted by api:", tag)
|
||||
} else {
|
||||
logger.Debug("Unable to delete inbound by api:", err1)
|
||||
needRestart = true
|
||||
}
|
||||
s.xrayApi.Close()
|
||||
} else {
|
||||
logger.Debug("No enabled inbound founded to removing by api", tag)
|
||||
}
|
||||
|
||||
// Delete client traffics of inbounds
|
||||
err := db.Where("inbound_id = ?", id).Delete(xray.ClientTraffic{}).Error
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
inbound, err := s.GetInbound(id)
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
clients, err := s.GetClients(inbound)
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
for _, client := range clients {
|
||||
err := s.DelClientIPs(db, client.Email)
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
return db.Delete(model.Inbound{}, id).Error
|
||||
|
||||
return needRestart, db.Delete(model.Inbound{}, id).Error
|
||||
}
|
||||
|
||||
func (s *InboundService) GetInbound(id int) (*model.Inbound, error) {
|
||||
@@ -231,19 +247,27 @@ func (s *InboundService) GetInbound(id int) (*model.Inbound, error) {
|
||||
return inbound, nil
|
||||
}
|
||||
|
||||
func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound, error) {
|
||||
func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound, bool, error) {
|
||||
exist, err := s.checkPortExist(inbound.Port, inbound.Id)
|
||||
if err != nil {
|
||||
return inbound, err
|
||||
return inbound, false, err
|
||||
}
|
||||
if exist {
|
||||
return inbound, common.NewError("Port already exists:", inbound.Port)
|
||||
return inbound, false, common.NewError("Port already exists:", inbound.Port)
|
||||
}
|
||||
|
||||
oldInbound, err := s.GetInbound(inbound.Id)
|
||||
if err != nil {
|
||||
return inbound, err
|
||||
return inbound, false, err
|
||||
}
|
||||
|
||||
tag := oldInbound.Tag
|
||||
|
||||
err = s.updateClientTraffics(oldInbound, inbound)
|
||||
if err != nil {
|
||||
return inbound, false, err
|
||||
}
|
||||
|
||||
oldInbound.Up = inbound.Up
|
||||
oldInbound.Down = inbound.Down
|
||||
oldInbound.Total = inbound.Total
|
||||
@@ -258,40 +282,118 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
|
||||
oldInbound.Sniffing = inbound.Sniffing
|
||||
oldInbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
|
||||
|
||||
needRestart := false
|
||||
s.xrayApi.Init(p.GetAPIPort())
|
||||
if s.xrayApi.DelInbound(tag) == nil {
|
||||
logger.Debug("Old inbound deleted by api:", tag)
|
||||
}
|
||||
if inbound.Enable {
|
||||
inboundJson, err2 := json.MarshalIndent(oldInbound.GenXrayInboundConfig(), "", " ")
|
||||
if err2 != nil {
|
||||
logger.Debug("Unable to marshal updated inbound config:", err2)
|
||||
needRestart = true
|
||||
} else {
|
||||
err2 = s.xrayApi.AddInbound(inboundJson)
|
||||
if err2 == nil {
|
||||
logger.Debug("Updated inbound added by api:", oldInbound.Tag)
|
||||
} else {
|
||||
logger.Debug("Unable to update inbound by api:", err2)
|
||||
needRestart = true
|
||||
}
|
||||
}
|
||||
}
|
||||
s.xrayApi.Close()
|
||||
|
||||
db := database.GetDB()
|
||||
return inbound, db.Save(oldInbound).Error
|
||||
return inbound, needRestart, db.Save(oldInbound).Error
|
||||
}
|
||||
|
||||
func (s *InboundService) AddInboundClient(data *model.Inbound) error {
|
||||
clients, err := s.GetClients(data)
|
||||
func (s *InboundService) updateClientTraffics(oldInbound *model.Inbound, newInbound *model.Inbound) error {
|
||||
oldClients, err := s.GetClients(oldInbound)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newClients, err := s.GetClients(newInbound)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
db := database.GetDB()
|
||||
tx := db.Begin()
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
} else {
|
||||
tx.Commit()
|
||||
}
|
||||
}()
|
||||
|
||||
var emailExists bool
|
||||
|
||||
for _, oldClient := range oldClients {
|
||||
emailExists = false
|
||||
for _, newClient := range newClients {
|
||||
if oldClient.Email == newClient.Email {
|
||||
emailExists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !emailExists {
|
||||
err = s.DelClientStat(tx, oldClient.Email)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, newClient := range newClients {
|
||||
emailExists = false
|
||||
for _, oldClient := range oldClients {
|
||||
if newClient.Email == oldClient.Email {
|
||||
emailExists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !emailExists {
|
||||
err = s.AddClientStat(tx, oldInbound.Id, &newClient)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {
|
||||
clients, err := s.GetClients(data)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
var settings map[string]interface{}
|
||||
err = json.Unmarshal([]byte(data.Settings), &settings)
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
|
||||
interfaceClients := settings["clients"].([]interface{})
|
||||
existEmail, err := s.checkEmailsExistForClients(clients)
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
if existEmail != "" {
|
||||
return common.NewError("Duplicate email:", existEmail)
|
||||
return false, common.NewError("Duplicate email:", existEmail)
|
||||
}
|
||||
|
||||
oldInbound, err := s.GetInbound(data.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
|
||||
var oldSettings map[string]interface{}
|
||||
err = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings)
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
|
||||
oldClients := oldSettings["clients"].([]interface{})
|
||||
@@ -301,30 +403,65 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) error {
|
||||
|
||||
newSettings, err := json.MarshalIndent(oldSettings, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
|
||||
oldInbound.Settings = string(newSettings)
|
||||
|
||||
db := database.GetDB()
|
||||
tx := db.Begin()
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
} else {
|
||||
tx.Commit()
|
||||
}
|
||||
}()
|
||||
|
||||
needRestart := false
|
||||
s.xrayApi.Init(p.GetAPIPort())
|
||||
for _, client := range clients {
|
||||
if len(client.Email) > 0 {
|
||||
s.AddClientStat(data.Id, &client)
|
||||
s.AddClientStat(tx, data.Id, &client)
|
||||
if client.Enable {
|
||||
cipher := ""
|
||||
if oldInbound.Protocol == "shadowsocks" {
|
||||
cipher = oldSettings["method"].(string)
|
||||
}
|
||||
err1 := s.xrayApi.AddUser(string(oldInbound.Protocol), oldInbound.Tag, map[string]interface{}{
|
||||
"email": client.Email,
|
||||
"id": client.ID,
|
||||
"flow": client.Flow,
|
||||
"password": client.Password,
|
||||
"cipher": cipher,
|
||||
})
|
||||
if err1 == nil {
|
||||
logger.Debug("Client added by api:", client.Email)
|
||||
} else {
|
||||
logger.Debug("Error in adding client by api:", err1)
|
||||
needRestart = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
needRestart = true
|
||||
}
|
||||
}
|
||||
db := database.GetDB()
|
||||
return db.Save(oldInbound).Error
|
||||
s.xrayApi.Close()
|
||||
|
||||
return needRestart, tx.Save(oldInbound).Error
|
||||
}
|
||||
|
||||
func (s *InboundService) DelInboundClient(inboundId int, clientId string) error {
|
||||
func (s *InboundService) DelInboundClient(inboundId int, clientId string) (bool, error) {
|
||||
oldInbound, err := s.GetInbound(inboundId)
|
||||
if err != nil {
|
||||
logger.Error("Load Old Data Error")
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
var settings map[string]interface{}
|
||||
err = json.Unmarshal([]byte(oldInbound.Settings), &settings)
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
|
||||
email := ""
|
||||
@@ -351,7 +488,7 @@ func (s *InboundService) DelInboundClient(inboundId int, clientId string) error
|
||||
settings["clients"] = newClients
|
||||
newSettings, err := json.MarshalIndent(settings, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
|
||||
oldInbound.Settings = string(newSettings)
|
||||
@@ -360,39 +497,52 @@ func (s *InboundService) DelInboundClient(inboundId int, clientId string) error
|
||||
err = s.DelClientStat(db, email)
|
||||
if err != nil {
|
||||
logger.Error("Delete stats Data Error")
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
|
||||
err = s.DelClientIPs(db, email)
|
||||
if err != nil {
|
||||
logger.Error("Error in delete client IPs")
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
return db.Save(oldInbound).Error
|
||||
needRestart := false
|
||||
if len(email) > 0 {
|
||||
s.xrayApi.Init(p.GetAPIPort())
|
||||
err1 := s.xrayApi.RemoveUser(oldInbound.Tag, email)
|
||||
if err1 == nil {
|
||||
logger.Debug("Client deleted by api:", email)
|
||||
needRestart = false
|
||||
} else {
|
||||
logger.Debug("Unable to del client by api:", err1)
|
||||
needRestart = true
|
||||
}
|
||||
s.xrayApi.Close()
|
||||
}
|
||||
return needRestart, db.Save(oldInbound).Error
|
||||
}
|
||||
|
||||
func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId string) error {
|
||||
func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId string) (bool, error) {
|
||||
clients, err := s.GetClients(data)
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
|
||||
var settings map[string]interface{}
|
||||
err = json.Unmarshal([]byte(data.Settings), &settings)
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
|
||||
inerfaceClients := settings["clients"].([]interface{})
|
||||
|
||||
oldInbound, err := s.GetInbound(data.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
|
||||
oldClients, err := s.GetClients(oldInbound)
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
|
||||
oldEmail := ""
|
||||
@@ -416,17 +566,17 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
|
||||
if len(clients[0].Email) > 0 && clients[0].Email != oldEmail {
|
||||
existEmail, err := s.checkEmailsExistForClients(clients)
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
if existEmail != "" {
|
||||
return common.NewError("Duplicate email:", existEmail)
|
||||
return false, common.NewError("Duplicate email:", existEmail)
|
||||
}
|
||||
}
|
||||
|
||||
var oldSettings map[string]interface{}
|
||||
err = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings)
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
settingsClients := oldSettings["clients"].([]interface{})
|
||||
settingsClients[clientIndex] = inerfaceClients[0]
|
||||
@@ -434,66 +584,10 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
|
||||
|
||||
newSettings, err := json.MarshalIndent(oldSettings, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
|
||||
oldInbound.Settings = string(newSettings)
|
||||
db := database.GetDB()
|
||||
|
||||
if len(clients[0].Email) > 0 {
|
||||
if len(oldEmail) > 0 {
|
||||
err = s.UpdateClientStat(oldEmail, &clients[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.UpdateClientIPs(db, oldEmail, clients[0].Email)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
s.AddClientStat(data.Id, &clients[0])
|
||||
}
|
||||
} else {
|
||||
err = s.DelClientStat(db, oldEmail)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.DelClientIPs(db, oldEmail)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return db.Save(oldInbound).Error
|
||||
}
|
||||
|
||||
func (s *InboundService) AddTraffic(traffics []*xray.Traffic) error {
|
||||
if len(traffics) == 0 {
|
||||
return nil
|
||||
}
|
||||
// Update traffics in a single transaction
|
||||
err := database.GetDB().Transaction(func(tx *gorm.DB) error {
|
||||
for _, traffic := range traffics {
|
||||
if traffic.IsInbound {
|
||||
update := tx.Model(&model.Inbound{}).Where("tag = ?", traffic.Tag).
|
||||
Updates(map[string]interface{}{
|
||||
"up": gorm.Expr("up + ?", traffic.Up),
|
||||
"down": gorm.Expr("down + ?", traffic.Down),
|
||||
})
|
||||
if update.Error != nil {
|
||||
return update.Error
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err error) {
|
||||
if len(traffics) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
db := database.GetDB()
|
||||
tx := db.Begin()
|
||||
|
||||
@@ -505,16 +599,141 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
|
||||
}
|
||||
}()
|
||||
|
||||
if len(clients[0].Email) > 0 {
|
||||
if len(oldEmail) > 0 {
|
||||
err = s.UpdateClientStat(oldEmail, &clients[0])
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
err = s.UpdateClientIPs(tx, oldEmail, clients[0].Email)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
} else {
|
||||
s.AddClientStat(tx, data.Id, &clients[0])
|
||||
}
|
||||
} else {
|
||||
err = s.DelClientStat(tx, oldEmail)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
err = s.DelClientIPs(tx, oldEmail)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
needRestart := false
|
||||
if len(oldEmail) > 0 {
|
||||
s.xrayApi.Init(p.GetAPIPort())
|
||||
if s.xrayApi.RemoveUser(oldInbound.Tag, oldEmail) == nil {
|
||||
logger.Debug("Old client deleted by api:", clients[0].Email)
|
||||
}
|
||||
if clients[0].Enable {
|
||||
cipher := ""
|
||||
if oldInbound.Protocol == "shadowsocks" {
|
||||
cipher = oldSettings["method"].(string)
|
||||
}
|
||||
err1 := s.xrayApi.AddUser(string(oldInbound.Protocol), oldInbound.Tag, map[string]interface{}{
|
||||
"email": clients[0].Email,
|
||||
"id": clients[0].ID,
|
||||
"flow": clients[0].Flow,
|
||||
"password": clients[0].Password,
|
||||
"cipher": cipher,
|
||||
})
|
||||
if err1 == nil {
|
||||
logger.Debug("Client edited by api:", clients[0].Email)
|
||||
} else {
|
||||
logger.Debug("Error in adding client by api:", err1)
|
||||
needRestart = true
|
||||
}
|
||||
}
|
||||
s.xrayApi.Close()
|
||||
} else {
|
||||
logger.Debug("Client old email not found")
|
||||
needRestart = true
|
||||
}
|
||||
return needRestart, tx.Save(oldInbound).Error
|
||||
}
|
||||
|
||||
func (s *InboundService) AddTraffic(inboundTraffics []*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.addInboundTraffic(tx, inboundTraffics)
|
||||
if err != nil {
|
||||
return err, false
|
||||
}
|
||||
err = s.addClientTraffic(tx, clientTraffics)
|
||||
if err != nil {
|
||||
return err, false
|
||||
}
|
||||
|
||||
needRestart1, count, err := s.disableInvalidClients(tx)
|
||||
if err != nil {
|
||||
logger.Warning("Error in disabling invalid clients:", err)
|
||||
} else if count > 0 {
|
||||
logger.Debugf("%v clients disabled", count)
|
||||
}
|
||||
|
||||
needRestart2, count, err := s.disableInvalidInbounds(tx)
|
||||
if err != nil {
|
||||
logger.Warning("Error in disabling invalid inbounds:", err)
|
||||
} else if count > 0 {
|
||||
logger.Debugf("%v inbounds disabled", count)
|
||||
}
|
||||
return nil, (needRestart1 || needRestart2)
|
||||
}
|
||||
|
||||
func (s *InboundService) addInboundTraffic(tx *gorm.DB, traffics []*xray.Traffic) error {
|
||||
if len(traffics) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
for _, traffic := range traffics {
|
||||
if traffic.IsInbound {
|
||||
err = tx.Model(&model.Inbound{}).Where("tag = ?", traffic.Tag).
|
||||
Updates(map[string]interface{}{
|
||||
"up": gorm.Expr("up + ?", traffic.Up),
|
||||
"down": gorm.Expr("down + ?", traffic.Down),
|
||||
}).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *InboundService) addClientTraffic(tx *gorm.DB, traffics []*xray.ClientTraffic) (err error) {
|
||||
if len(traffics) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
emails := make([]string, 0, len(traffics))
|
||||
for _, traffic := range traffics {
|
||||
emails = append(emails, traffic.Email)
|
||||
}
|
||||
dbClientTraffics := make([]*xray.ClientTraffic, 0, len(traffics))
|
||||
err = db.Model(xray.ClientTraffic{}).Where("email IN (?)", emails).Find(&dbClientTraffics).Error
|
||||
err = tx.Model(xray.ClientTraffic{}).Where("email IN (?)", emails).Find(&dbClientTraffics).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Avoid empty slice error
|
||||
if len(dbClientTraffics) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
dbClientTraffics, err = s.adjustTraffics(tx, dbClientTraffics)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -590,26 +809,76 @@ func (s *InboundService) adjustTraffics(tx *gorm.DB, dbClientTraffics []*xray.Cl
|
||||
return dbClientTraffics, nil
|
||||
}
|
||||
|
||||
func (s *InboundService) DisableInvalidInbounds() (int64, error) {
|
||||
db := database.GetDB()
|
||||
func (s *InboundService) disableInvalidInbounds(tx *gorm.DB) (bool, int64, error) {
|
||||
now := time.Now().Unix() * 1000
|
||||
result := db.Model(model.Inbound{}).
|
||||
needRestart := false
|
||||
|
||||
if p != nil {
|
||||
var tags []string
|
||||
err := tx.Table("inbounds").
|
||||
Select("inbounds.tag").
|
||||
Where("((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ?", now, true).
|
||||
Scan(&tags).Error
|
||||
if err != nil {
|
||||
return false, 0, err
|
||||
}
|
||||
s.xrayApi.Init(p.GetAPIPort())
|
||||
for _, tag := range tags {
|
||||
err1 := s.xrayApi.DelInbound(tag)
|
||||
if err == nil {
|
||||
logger.Debug("Inbound disabled by api:", tag)
|
||||
} else {
|
||||
logger.Debug("Error in disabling inbound by api:", err1)
|
||||
needRestart = true
|
||||
}
|
||||
}
|
||||
s.xrayApi.Close()
|
||||
}
|
||||
|
||||
result := tx.Model(model.Inbound{}).
|
||||
Where("((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ?", now, true).
|
||||
Update("enable", false)
|
||||
err := result.Error
|
||||
count := result.RowsAffected
|
||||
return count, err
|
||||
return needRestart, count, err
|
||||
}
|
||||
|
||||
func (s *InboundService) DisableInvalidClients() (int64, error) {
|
||||
db := database.GetDB()
|
||||
func (s *InboundService) disableInvalidClients(tx *gorm.DB) (bool, int64, error) {
|
||||
now := time.Now().Unix() * 1000
|
||||
result := db.Model(xray.ClientTraffic{}).
|
||||
needRestart := false
|
||||
|
||||
if p != nil {
|
||||
var results []struct {
|
||||
Tag string
|
||||
Email string
|
||||
}
|
||||
|
||||
err := tx.Table("inbounds").
|
||||
Select("inbounds.tag, client_traffics.email").
|
||||
Joins("JOIN client_traffics ON inbounds.id = client_traffics.inbound_id").
|
||||
Where("((client_traffics.total > 0 AND client_traffics.up + client_traffics.down >= client_traffics.total) OR (client_traffics.expiry_time > 0 AND client_traffics.expiry_time <= ?)) AND client_traffics.enable = ?", now, true).
|
||||
Scan(&results).Error
|
||||
if err != nil {
|
||||
return false, 0, err
|
||||
}
|
||||
s.xrayApi.Init(p.GetAPIPort())
|
||||
for _, result := range results {
|
||||
err1 := s.xrayApi.RemoveUser(result.Tag, result.Email)
|
||||
if err1 == nil {
|
||||
logger.Debug("Client disabled by api:", result.Email)
|
||||
} else {
|
||||
logger.Debug("Error in disabling client by api:", err1)
|
||||
needRestart = true
|
||||
}
|
||||
}
|
||||
s.xrayApi.Close()
|
||||
}
|
||||
result := tx.Model(xray.ClientTraffic{}).
|
||||
Where("((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ?", now, true).
|
||||
Update("enable", false)
|
||||
err := result.Error
|
||||
count := result.RowsAffected
|
||||
return count, err
|
||||
return needRestart, count, err
|
||||
}
|
||||
|
||||
func (s *InboundService) MigrationRemoveOrphanedTraffics() {
|
||||
@@ -624,9 +893,7 @@ func (s *InboundService) MigrationRemoveOrphanedTraffics() {
|
||||
`)
|
||||
}
|
||||
|
||||
func (s *InboundService) AddClientStat(inboundId int, client *model.Client) error {
|
||||
db := database.GetDB()
|
||||
|
||||
func (s *InboundService) AddClientStat(tx *gorm.DB, inboundId int, client *model.Client) error {
|
||||
clientTraffic := xray.ClientTraffic{}
|
||||
clientTraffic.InboundId = inboundId
|
||||
clientTraffic.Email = client.Email
|
||||
@@ -635,7 +902,7 @@ func (s *InboundService) AddClientStat(inboundId int, client *model.Client) erro
|
||||
clientTraffic.Enable = true
|
||||
clientTraffic.Up = 0
|
||||
clientTraffic.Down = 0
|
||||
result := db.Create(&clientTraffic)
|
||||
result := tx.Create(&clientTraffic)
|
||||
err := result.Error
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -779,7 +1046,11 @@ func (s *InboundService) SetClientTelegramUserID(trafficId int, tgId string) err
|
||||
return err
|
||||
}
|
||||
inbound.Settings = string(modifiedSettings)
|
||||
return s.UpdateInboundClient(inbound, clientId)
|
||||
_, err = s.UpdateInboundClient(inbound, clientId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, error) {
|
||||
@@ -835,7 +1106,13 @@ func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, er
|
||||
return false, err
|
||||
}
|
||||
inbound.Settings = string(modifiedSettings)
|
||||
return !clientOldEnabled, s.UpdateInboundClient(inbound, clientId)
|
||||
|
||||
_, err = s.UpdateInboundClient(inbound, clientId)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return !clientOldEnabled, nil
|
||||
}
|
||||
|
||||
func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int) error {
|
||||
@@ -889,7 +1166,11 @@ func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int
|
||||
return err
|
||||
}
|
||||
inbound.Settings = string(modifiedSettings)
|
||||
return s.UpdateInboundClient(inbound, clientId)
|
||||
_, err = s.UpdateInboundClient(inbound, clientId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *InboundService) ResetClientExpiryTimeByEmail(clientEmail string, expiry_time int64) error {
|
||||
@@ -943,7 +1224,11 @@ func (s *InboundService) ResetClientExpiryTimeByEmail(clientEmail string, expiry
|
||||
return err
|
||||
}
|
||||
inbound.Settings = string(modifiedSettings)
|
||||
return s.UpdateInboundClient(inbound, clientId)
|
||||
_, err = s.UpdateInboundClient(inbound, clientId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *InboundService) ResetClientTrafficByEmail(clientEmail string) error {
|
||||
@@ -954,26 +1239,71 @@ func (s *InboundService) ResetClientTrafficByEmail(clientEmail string) error {
|
||||
Updates(map[string]interface{}{"enable": true, "up": 0, "down": 0})
|
||||
|
||||
err := result.Error
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *InboundService) ResetClientTraffic(id int, clientEmail string) error {
|
||||
db := database.GetDB()
|
||||
|
||||
result := db.Model(xray.ClientTraffic{}).
|
||||
Where("inbound_id = ? and email = ?", id, clientEmail).
|
||||
Updates(map[string]interface{}{"enable": true, "up": 0, "down": 0})
|
||||
|
||||
err := result.Error
|
||||
func (s *InboundService) ResetClientTraffic(id int, clientEmail string) (bool, error) {
|
||||
needRestart := false
|
||||
|
||||
traffic, err := s.GetClientTrafficByEmail(clientEmail)
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
return nil
|
||||
|
||||
if !traffic.Enable {
|
||||
inbound, err := s.GetInbound(id)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
clients, err := s.GetClients(inbound)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, client := range clients {
|
||||
if client.Email == clientEmail {
|
||||
s.xrayApi.Init(p.GetAPIPort())
|
||||
cipher := ""
|
||||
if string(inbound.Protocol) == "shadowsocks" {
|
||||
var oldSettings map[string]interface{}
|
||||
err = json.Unmarshal([]byte(inbound.Settings), &oldSettings)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
cipher = oldSettings["method"].(string)
|
||||
}
|
||||
err1 := s.xrayApi.AddUser(string(inbound.Protocol), inbound.Tag, map[string]interface{}{
|
||||
"email": client.Email,
|
||||
"id": client.ID,
|
||||
"flow": client.Flow,
|
||||
"password": client.Password,
|
||||
"cipher": cipher,
|
||||
})
|
||||
if err1 == nil {
|
||||
logger.Debug("Client enabled due to reset traffic:", clientEmail)
|
||||
} else {
|
||||
logger.Debug("Error in enabling client by api:", err1)
|
||||
needRestart = true
|
||||
}
|
||||
s.xrayApi.Close()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
traffic.Up = 0
|
||||
traffic.Down = 0
|
||||
traffic.Enable = true
|
||||
|
||||
db := database.GetDB()
|
||||
err = db.Save(traffic).Error
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return needRestart, nil
|
||||
}
|
||||
|
||||
func (s *InboundService) ResetAllClientTraffics(id int) error {
|
||||
@@ -1185,6 +1515,7 @@ func (s *InboundService) GetInboundClientIps(clientEmail string) (string, error)
|
||||
}
|
||||
return InboundClientIps.Ips, nil
|
||||
}
|
||||
|
||||
func (s *InboundService) ClearClientIps(clientEmail string) error {
|
||||
db := database.GetDB()
|
||||
|
||||
@@ -1192,7 +1523,6 @@ func (s *InboundService) ClearClientIps(clientEmail string) error {
|
||||
Where("client_email = ?", clientEmail).
|
||||
Update("ips", "")
|
||||
err := result.Error
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1211,10 +1541,19 @@ func (s *InboundService) SearchInbounds(query string) ([]*model.Inbound, error)
|
||||
|
||||
func (s *InboundService) MigrationRequirements() {
|
||||
db := database.GetDB()
|
||||
tx := db.Begin()
|
||||
var err error
|
||||
defer func() {
|
||||
if err == nil {
|
||||
tx.Commit()
|
||||
} else {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
// Fix inbounds based problems
|
||||
var inbounds []*model.Inbound
|
||||
err := db.Model(model.Inbound{}).Where("protocol IN (?)", []string{"vmess", "vless", "trojan"}).Find(&inbounds).Error
|
||||
err = tx.Model(model.Inbound{}).Where("protocol IN (?)", []string{"vmess", "vless", "trojan"}).Find(&inbounds).Error
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
return
|
||||
}
|
||||
@@ -1249,6 +1588,7 @@ func (s *InboundService) MigrationRequirements() {
|
||||
|
||||
inbounds[inbound_index].Settings = string(modifiedSettings)
|
||||
}
|
||||
|
||||
// Add client traffic row for all clients which has email
|
||||
modelClients, err := s.GetClients(inbounds[inbound_index])
|
||||
if err != nil {
|
||||
@@ -1257,17 +1597,17 @@ func (s *InboundService) MigrationRequirements() {
|
||||
for _, modelClient := range modelClients {
|
||||
if len(modelClient.Email) > 0 {
|
||||
var count int64
|
||||
db.Model(xray.ClientTraffic{}).Where("email = ?", modelClient.Email).Count(&count)
|
||||
tx.Model(xray.ClientTraffic{}).Where("email = ?", modelClient.Email).Count(&count)
|
||||
if count == 0 {
|
||||
s.AddClientStat(inbounds[inbound_index].Id, &modelClient)
|
||||
s.AddClientStat(tx, inbounds[inbound_index].Id, &modelClient)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
db.Save(inbounds)
|
||||
tx.Save(inbounds)
|
||||
|
||||
// Remove orphaned traffics
|
||||
db.Where("inbound_id = 0").Delete(xray.ClientTraffic{})
|
||||
tx.Where("inbound_id = 0").Delete(xray.ClientTraffic{})
|
||||
}
|
||||
|
||||
func (s *InboundService) MigrateDB() {
|
||||
|
||||
@@ -12,8 +12,10 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"x-ui/config"
|
||||
"x-ui/database"
|
||||
"x-ui/logger"
|
||||
@@ -38,9 +40,11 @@ const (
|
||||
)
|
||||
|
||||
type Status struct {
|
||||
T time.Time `json:"-"`
|
||||
Cpu float64 `json:"cpu"`
|
||||
Mem struct {
|
||||
T time.Time `json:"-"`
|
||||
Cpu float64 `json:"cpu"`
|
||||
CpuCores int `json:"cpuCores"`
|
||||
CpuSpeedMhz float64 `json:"cpuSpeedMhz"`
|
||||
Mem struct {
|
||||
Current uint64 `json:"current"`
|
||||
Total uint64 `json:"total"`
|
||||
} `json:"mem"`
|
||||
@@ -69,6 +73,15 @@ type Status struct {
|
||||
Sent uint64 `json:"sent"`
|
||||
Recv uint64 `json:"recv"`
|
||||
} `json:"netTraffic"`
|
||||
PublicIP struct {
|
||||
IPv4 string `json:"ipv4"`
|
||||
IPv6 string `json:"ipv6"`
|
||||
} `json:"publicIP"`
|
||||
AppStats struct {
|
||||
Threads uint32 `json:"threads"`
|
||||
Mem uint64 `json:"mem"`
|
||||
Uptime uint64 `json:"uptime"`
|
||||
} `json:"appStats"`
|
||||
}
|
||||
|
||||
type Release struct {
|
||||
@@ -80,6 +93,26 @@ type ServerService struct {
|
||||
inboundService InboundService
|
||||
}
|
||||
|
||||
func getPublicIP(url string) string {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return "N/A"
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
ip, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "N/A"
|
||||
}
|
||||
|
||||
ipString := string(ip)
|
||||
if ipString == "" {
|
||||
return "N/A"
|
||||
}
|
||||
|
||||
return ipString
|
||||
}
|
||||
|
||||
func (s *ServerService) GetStatus(lastStatus *Status) *Status {
|
||||
now := time.Now()
|
||||
status := &Status{
|
||||
@@ -93,6 +126,21 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
|
||||
status.Cpu = percents[0]
|
||||
}
|
||||
|
||||
status.CpuCores, err = cpu.Counts(false)
|
||||
if err != nil {
|
||||
logger.Warning("get cpu cores count failed:", err)
|
||||
}
|
||||
|
||||
cpuInfos, err := cpu.Info()
|
||||
if err != nil {
|
||||
logger.Warning("get cpu info failed:", err)
|
||||
} else if len(cpuInfos) > 0 {
|
||||
cpuInfo := cpuInfos[0]
|
||||
status.CpuSpeedMhz = cpuInfo.Mhz // setting CPU speed in MHz
|
||||
} else {
|
||||
logger.Warning("could not find cpu info")
|
||||
}
|
||||
|
||||
upTime, err := host.Uptime()
|
||||
if err != nil {
|
||||
logger.Warning("get uptime failed:", err)
|
||||
@@ -161,6 +209,9 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
|
||||
logger.Warning("get udp connections failed:", err)
|
||||
}
|
||||
|
||||
status.PublicIP.IPv4 = getPublicIP("https://api.ipify.org")
|
||||
status.PublicIP.IPv6 = getPublicIP("https://api6.ipify.org")
|
||||
|
||||
if s.xrayService.IsXrayRunning() {
|
||||
status.Xray.State = Running
|
||||
status.Xray.ErrorMsg = ""
|
||||
@@ -174,12 +225,22 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
|
||||
status.Xray.ErrorMsg = s.xrayService.GetXrayResult()
|
||||
}
|
||||
status.Xray.Version = s.xrayService.GetXrayVersion()
|
||||
var rtm runtime.MemStats
|
||||
runtime.ReadMemStats(&rtm)
|
||||
|
||||
status.AppStats.Mem = rtm.Sys
|
||||
status.AppStats.Threads = uint32(runtime.NumGoroutine())
|
||||
if p.IsRunning() {
|
||||
status.AppStats.Uptime = p.GetUptime()
|
||||
} else {
|
||||
status.AppStats.Uptime = 0
|
||||
}
|
||||
|
||||
return status
|
||||
}
|
||||
|
||||
func (s *ServerService) GetXrayVersions() ([]string, error) {
|
||||
url := "https://api.github.com/repos/MHSanaei/Xray-core/releases"
|
||||
url := "https://api.github.com/repos/XTLS/Xray-core/releases"
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -198,15 +259,16 @@ func (s *ServerService) GetXrayVersions() ([]string, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
versions := make([]string, 0, len(releases))
|
||||
var versions []string
|
||||
for _, release := range releases {
|
||||
versions = append(versions, release.TagName)
|
||||
if release.TagName >= "v1.7.5" {
|
||||
versions = append(versions, release.TagName)
|
||||
}
|
||||
}
|
||||
return versions, nil
|
||||
}
|
||||
|
||||
func (s *ServerService) StopXrayService() (string error) {
|
||||
|
||||
err := s.xrayService.StopXray()
|
||||
if err != nil {
|
||||
logger.Error("stop xray failed:", err)
|
||||
@@ -217,7 +279,6 @@ func (s *ServerService) StopXrayService() (string error) {
|
||||
}
|
||||
|
||||
func (s *ServerService) RestartXrayService() (string error) {
|
||||
|
||||
s.xrayService.StopXray()
|
||||
defer func() {
|
||||
err := s.xrayService.RestartXray(true)
|
||||
@@ -246,7 +307,7 @@ func (s *ServerService) downloadXRay(version string) (string, error) {
|
||||
}
|
||||
|
||||
fileName := fmt.Sprintf("Xray-%s-%s.zip", osName, arch)
|
||||
url := fmt.Sprintf("https://github.com/MHSanaei/Xray-core/releases/download/%s/%s", version, fileName)
|
||||
url := fmt.Sprintf("https://github.com/XTLS/Xray-core/releases/download/%s/%s", version, fileName)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -327,54 +388,45 @@ func (s *ServerService) UpdateXray(version string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = copyZipFile("iran.dat", xray.GetIranPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (s *ServerService) GetLogs(count string) ([]string, error) {
|
||||
// Define the journalctl command and its arguments
|
||||
var cmdArgs []string
|
||||
if runtime.GOOS == "linux" {
|
||||
cmdArgs = []string{"journalctl", "-u", "x-ui", "--no-pager", "-n", count}
|
||||
func (s *ServerService) GetLogs(count string, level string, syslog string) []string {
|
||||
c, _ := strconv.Atoi(count)
|
||||
var lines []string
|
||||
|
||||
if syslog == "true" {
|
||||
cmdArgs := []string{"journalctl", "-u", "x-ui", "--no-pager", "-n", count, "-p", level}
|
||||
// Run the command
|
||||
cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return []string{"Failed to run journalctl command!"}
|
||||
}
|
||||
lines = strings.Split(out.String(), "\n")
|
||||
} else {
|
||||
return []string{"Unsupported operating system"}, nil
|
||||
lines = logger.GetLogs(c, level)
|
||||
}
|
||||
|
||||
// Run the command
|
||||
cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lines := strings.Split(out.String(), "\n")
|
||||
|
||||
return lines, nil
|
||||
return lines
|
||||
}
|
||||
|
||||
func (s *ServerService) GetConfigJson() (interface{}, error) {
|
||||
// Open the file for reading
|
||||
file, err := os.Open(xray.GetConfigPath())
|
||||
config, err := s.xrayService.GetXrayConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Read the file contents
|
||||
fileContents, err := io.ReadAll(file)
|
||||
contents, err := json.MarshalIndent(config, "", " ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var jsonData interface{}
|
||||
err = json.Unmarshal(fileContents, &jsonData)
|
||||
err = json.Unmarshal(contents, &jsonData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ var xrayTemplateConfig string
|
||||
var defaultValueMap = map[string]string{
|
||||
"xrayTemplateConfig": xrayTemplateConfig,
|
||||
"webListen": "",
|
||||
"webDomain": "",
|
||||
"webPort": "2053",
|
||||
"webCertFile": "",
|
||||
"webKeyFile": "",
|
||||
@@ -38,17 +39,20 @@ var defaultValueMap = map[string]string{
|
||||
"tgBotChatId": "",
|
||||
"tgRunTime": "@daily",
|
||||
"tgBotBackup": "false",
|
||||
"tgBotLoginNotify": "true",
|
||||
"tgCpu": "0",
|
||||
"tgLang": "en-US",
|
||||
"secretEnable": "false",
|
||||
"subEnable": "false",
|
||||
"subListen": "",
|
||||
"subPort": "2096",
|
||||
"subPath": "sub/",
|
||||
"subPath": "/sub/",
|
||||
"subDomain": "",
|
||||
"subCertFile": "",
|
||||
"subKeyFile": "",
|
||||
"subUpdates": "12",
|
||||
"subEncrypt": "true",
|
||||
"subShowInfo": "true",
|
||||
}
|
||||
|
||||
type SettingService struct {
|
||||
@@ -225,6 +229,10 @@ func (s *SettingService) GetListen() (string, error) {
|
||||
return s.getString("webListen")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetWebDomain() (string, error) {
|
||||
return s.getString("webDomain")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetTgBotToken() (string, error) {
|
||||
return s.getString("tgBotToken")
|
||||
}
|
||||
@@ -261,6 +269,10 @@ func (s *SettingService) GetTgBotBackup() (bool, error) {
|
||||
return s.getBool("tgBotBackup")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetTgBotLoginNotify() (bool, error) {
|
||||
return s.getBool("tgBotLoginNotify")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetTgCpu() (int, error) {
|
||||
return s.getInt("tgCpu")
|
||||
}
|
||||
@@ -386,6 +398,14 @@ func (s *SettingService) GetSubUpdates() (int, error) {
|
||||
return s.getInt("subUpdates")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubEncrypt() (bool, error) {
|
||||
return s.getBool("subEncrypt")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubShowInfo() (bool, error) {
|
||||
return s.getBool("subShowInfo")
|
||||
}
|
||||
|
||||
func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error {
|
||||
if err := allSetting.CheckValid(); err != nil {
|
||||
return err
|
||||
|
||||
@@ -77,13 +77,15 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, adminId := range strings.Split(tgBotid, ",") {
|
||||
id, err := strconv.Atoi(adminId)
|
||||
if err != nil {
|
||||
logger.Warning("Failed to get IDs from GetTgBotChatId:", err)
|
||||
return err
|
||||
if tgBotid != "" {
|
||||
for _, adminId := range strings.Split(tgBotid, ",") {
|
||||
id, err := strconv.Atoi(adminId)
|
||||
if err != nil {
|
||||
logger.Warning("Failed to get IDs from GetTgBotChatId:", err)
|
||||
return err
|
||||
}
|
||||
adminIds = append(adminIds, int64(id))
|
||||
}
|
||||
adminIds = append(adminIds, int64(id))
|
||||
}
|
||||
|
||||
bot, err = telego.NewBot(tgBottoken)
|
||||
@@ -188,7 +190,7 @@ func (t *Tgbot) OnReceive() {
|
||||
}
|
||||
|
||||
func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin bool) {
|
||||
msg := ""
|
||||
msg, onlyMessage := "", false
|
||||
|
||||
command, commandArgs := tu.ParseCommand(message.Text)
|
||||
|
||||
@@ -204,8 +206,13 @@ func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin boo
|
||||
}
|
||||
msg += "\n\n" + t.I18nBot("tgbot.commands.pleaseChoose")
|
||||
case "status":
|
||||
onlyMessage = true
|
||||
msg += t.I18nBot("tgbot.commands.status")
|
||||
case "id":
|
||||
onlyMessage = true
|
||||
msg += t.I18nBot("tgbot.commands.getID", "ID=="+strconv.FormatInt(message.From.ID, 10))
|
||||
case "usage":
|
||||
onlyMessage = true
|
||||
if len(commandArgs) > 0 {
|
||||
if isAdmin {
|
||||
t.searchClient(chatId, commandArgs[0])
|
||||
@@ -216,6 +223,7 @@ func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin boo
|
||||
msg += t.I18nBot("tgbot.commands.usage")
|
||||
}
|
||||
case "inbound":
|
||||
onlyMessage = true
|
||||
if isAdmin && len(commandArgs) > 0 {
|
||||
t.searchInbound(chatId, commandArgs[0])
|
||||
} else {
|
||||
@@ -224,6 +232,11 @@ func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin boo
|
||||
default:
|
||||
msg += t.I18nBot("tgbot.commands.unknown")
|
||||
}
|
||||
|
||||
if onlyMessage {
|
||||
t.SendMsgToTgbot(chatId, msg)
|
||||
return
|
||||
}
|
||||
t.SendAnswer(chatId, msg, isAdmin)
|
||||
}
|
||||
|
||||
@@ -498,6 +511,7 @@ func (t *Tgbot) SendMsgToTgbot(chatId int64, msg string, replyMarkup ...telego.R
|
||||
if !isRunning {
|
||||
return
|
||||
}
|
||||
|
||||
if msg == "" {
|
||||
logger.Info("[tgbot] message is empty!")
|
||||
return
|
||||
@@ -629,6 +643,11 @@ func (t *Tgbot) UserLoginNotify(username string, ip string, time string, status
|
||||
return
|
||||
}
|
||||
|
||||
loginNotifyEnabled, err := t.settingService.GetTgBotLoginNotify()
|
||||
if err != nil || !loginNotifyEnabled {
|
||||
return
|
||||
}
|
||||
|
||||
msg := ""
|
||||
if status == LoginSuccess {
|
||||
msg += t.I18nBot("tgbot.messages.loginSuccess")
|
||||
@@ -659,9 +678,9 @@ func (t *Tgbot) getInboundUsages() string {
|
||||
info += t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic((inbound.Up+inbound.Down)), "Upload=="+common.FormatTraffic(inbound.Up), "Download=="+common.FormatTraffic(inbound.Down))
|
||||
|
||||
if inbound.ExpiryTime == 0 {
|
||||
info += t.I18nBot("tgbot.messages.expire", "DateTime=="+t.I18nBot("tgbot.unlimited"))
|
||||
info += t.I18nBot("tgbot.messages.expire", "Time=="+t.I18nBot("tgbot.unlimited"))
|
||||
} else {
|
||||
info += t.I18nBot("tgbot.messages.expire", "DateTime=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
|
||||
info += t.I18nBot("tgbot.messages.expire", "Time=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -697,14 +716,21 @@ func (t *Tgbot) getClientUsage(chatId int64, tgUserName string, tgUserID string)
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
for _, traffic := range traffics {
|
||||
expiryTime := ""
|
||||
flag := false
|
||||
diff := traffic.ExpiryTime/1000 - now
|
||||
if traffic.ExpiryTime == 0 {
|
||||
expiryTime = t.I18nBot("tgbot.unlimited")
|
||||
} else if diff > 172800 || !traffic.Enable {
|
||||
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
|
||||
} else if traffic.ExpiryTime < 0 {
|
||||
expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
|
||||
flag = true
|
||||
} else {
|
||||
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
|
||||
expiryTime = fmt.Sprintf("%d %s", diff/3600, t.I18nBot("tgbot.hours"))
|
||||
flag = true
|
||||
}
|
||||
|
||||
total := ""
|
||||
@@ -715,13 +741,22 @@ func (t *Tgbot) getClientUsage(chatId int64, tgUserName string, tgUserID string)
|
||||
}
|
||||
|
||||
output := ""
|
||||
output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
|
||||
output += t.I18nBot("tgbot.messages.active", "Enable=="+strconv.FormatBool(traffic.Enable))
|
||||
output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
|
||||
if traffic.Enable {
|
||||
output += t.I18nBot("tgbot.messages.active")
|
||||
if flag {
|
||||
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
|
||||
} else {
|
||||
output += t.I18nBot("tgbot.messages.expire", "Time=="+expiryTime)
|
||||
}
|
||||
} else {
|
||||
output += t.I18nBot("tgbot.messages.inactive")
|
||||
output += t.I18nBot("tgbot.messages.expire", "Time=="+expiryTime)
|
||||
}
|
||||
output += t.I18nBot("tgbot.messages.upload", "Upload=="+common.FormatTraffic(traffic.Up))
|
||||
output += t.I18nBot("tgbot.messages.download", "Download=="+common.FormatTraffic(traffic.Down))
|
||||
output += t.I18nBot("tgbot.messages.total", "UpDown=="+common.FormatTraffic((traffic.Up+traffic.Down)), "Total=="+total)
|
||||
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
|
||||
output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
|
||||
|
||||
t.SendMsgToTgbot(chatId, output)
|
||||
}
|
||||
@@ -775,6 +810,7 @@ func (t *Tgbot) clientTelegramUserInfo(chatId int64, email string, messageID ...
|
||||
output := ""
|
||||
output += t.I18nBot("tgbot.messages.email", "Email=="+email)
|
||||
output += t.I18nBot("tgbot.messages.TGUser", "TelegramID=="+tgId)
|
||||
output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
|
||||
|
||||
inlineKeyboard := tu.InlineKeyboard(
|
||||
tu.InlineKeyboardRow(
|
||||
@@ -791,7 +827,7 @@ func (t *Tgbot) clientTelegramUserInfo(chatId int64, email string, messageID ...
|
||||
t.SendMsgToTgbot(chatId, output, inlineKeyboard)
|
||||
requestUser := telego.KeyboardButtonRequestUser{
|
||||
RequestID: int32(traffic.Id),
|
||||
UserIsBot: false,
|
||||
UserIsBot: new(bool),
|
||||
}
|
||||
keyboard := tu.Keyboard(
|
||||
tu.KeyboardRow(
|
||||
@@ -819,13 +855,20 @@ func (t *Tgbot) searchClient(chatId int64, email string, messageID ...int) {
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
expiryTime := ""
|
||||
flag := false
|
||||
diff := traffic.ExpiryTime/1000 - now
|
||||
if traffic.ExpiryTime == 0 {
|
||||
expiryTime = t.I18nBot("tgbot.unlimited")
|
||||
} else if diff > 172800 || !traffic.Enable {
|
||||
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
|
||||
} else if traffic.ExpiryTime < 0 {
|
||||
expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
|
||||
flag = true
|
||||
} else {
|
||||
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
|
||||
expiryTime = fmt.Sprintf("%d %s", diff/3600, t.I18nBot("tgbot.hours"))
|
||||
flag = true
|
||||
}
|
||||
|
||||
total := ""
|
||||
@@ -836,13 +879,22 @@ func (t *Tgbot) searchClient(chatId int64, email string, messageID ...int) {
|
||||
}
|
||||
|
||||
output := ""
|
||||
output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
|
||||
output += t.I18nBot("tgbot.messages.active", "Enable=="+strconv.FormatBool(traffic.Enable))
|
||||
output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
|
||||
if traffic.Enable {
|
||||
output += t.I18nBot("tgbot.messages.active")
|
||||
if flag {
|
||||
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
|
||||
} else {
|
||||
output += t.I18nBot("tgbot.messages.expire", "Time=="+expiryTime)
|
||||
}
|
||||
} else {
|
||||
output += t.I18nBot("tgbot.messages.inactive")
|
||||
output += t.I18nBot("tgbot.messages.expire", "Time=="+expiryTime)
|
||||
}
|
||||
output += t.I18nBot("tgbot.messages.upload", "Upload=="+common.FormatTraffic(traffic.Up))
|
||||
output += t.I18nBot("tgbot.messages.download", "Download=="+common.FormatTraffic(traffic.Down))
|
||||
output += t.I18nBot("tgbot.messages.total", "UpDown=="+common.FormatTraffic((traffic.Up+traffic.Down)), "Total=="+total)
|
||||
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
|
||||
output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
|
||||
|
||||
inlineKeyboard := tu.InlineKeyboard(
|
||||
tu.InlineKeyboardRow(
|
||||
@@ -887,6 +939,7 @@ func (t *Tgbot) searchInbound(chatId int64, remark string) {
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
for _, inbound := range inbouds {
|
||||
info := ""
|
||||
info += t.I18nBot("tgbot.messages.inbound", "Remark=="+inbound.Remark)
|
||||
@@ -894,20 +947,26 @@ func (t *Tgbot) searchInbound(chatId int64, remark string) {
|
||||
info += t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic((inbound.Up+inbound.Down)), "Upload=="+common.FormatTraffic(inbound.Up), "Download=="+common.FormatTraffic(inbound.Down))
|
||||
|
||||
if inbound.ExpiryTime == 0 {
|
||||
info += t.I18nBot("tgbot.messages.expire", "DateTime=="+t.I18nBot("tgbot.unlimited"))
|
||||
info += t.I18nBot("tgbot.messages.expire", "Time=="+t.I18nBot("tgbot.unlimited"))
|
||||
} else {
|
||||
info += t.I18nBot("tgbot.messages.expire", "DateTime=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
|
||||
info += t.I18nBot("tgbot.messages.expire", "Time=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
t.SendMsgToTgbot(chatId, info)
|
||||
|
||||
for _, traffic := range inbound.ClientStats {
|
||||
expiryTime := ""
|
||||
flag := false
|
||||
diff := traffic.ExpiryTime/1000 - now
|
||||
if traffic.ExpiryTime == 0 {
|
||||
expiryTime = t.I18nBot("tgbot.unlimited")
|
||||
} else if diff > 172800 || !traffic.Enable {
|
||||
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
|
||||
} else if traffic.ExpiryTime < 0 {
|
||||
expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
|
||||
flag = true
|
||||
} else {
|
||||
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
|
||||
expiryTime = fmt.Sprintf("%d %s", diff/3600, t.I18nBot("tgbot.hours"))
|
||||
flag = true
|
||||
}
|
||||
|
||||
total := ""
|
||||
@@ -918,13 +977,22 @@ func (t *Tgbot) searchInbound(chatId int64, remark string) {
|
||||
}
|
||||
|
||||
output := ""
|
||||
output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
|
||||
output += t.I18nBot("tgbot.messages.active", "Enable=="+strconv.FormatBool(traffic.Enable))
|
||||
output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
|
||||
if traffic.Enable {
|
||||
output += t.I18nBot("tgbot.messages.active")
|
||||
if flag {
|
||||
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
|
||||
} else {
|
||||
output += t.I18nBot("tgbot.messages.expire", "Time=="+expiryTime)
|
||||
}
|
||||
} else {
|
||||
output += t.I18nBot("tgbot.messages.inactive")
|
||||
output += t.I18nBot("tgbot.messages.expire", "Time=="+expiryTime)
|
||||
}
|
||||
output += t.I18nBot("tgbot.messages.upload", "Upload=="+common.FormatTraffic(traffic.Up))
|
||||
output += t.I18nBot("tgbot.messages.download", "Download=="+common.FormatTraffic(traffic.Down))
|
||||
output += t.I18nBot("tgbot.messages.total", "UpDown=="+common.FormatTraffic((traffic.Up+traffic.Down)), "Total=="+total)
|
||||
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
|
||||
output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
|
||||
|
||||
t.SendMsgToTgbot(chatId, output)
|
||||
}
|
||||
@@ -945,13 +1013,20 @@ func (t *Tgbot) searchForClient(chatId int64, query string) {
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
expiryTime := ""
|
||||
flag := false
|
||||
diff := traffic.ExpiryTime/1000 - now
|
||||
if traffic.ExpiryTime == 0 {
|
||||
expiryTime = t.I18nBot("tgbot.unlimited")
|
||||
} else if diff > 172800 || !traffic.Enable {
|
||||
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
|
||||
} else if traffic.ExpiryTime < 0 {
|
||||
expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
|
||||
flag = true
|
||||
} else {
|
||||
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
|
||||
expiryTime = fmt.Sprintf("%d %s", diff/3600, t.I18nBot("tgbot.hours"))
|
||||
flag = true
|
||||
}
|
||||
|
||||
total := ""
|
||||
@@ -962,13 +1037,22 @@ func (t *Tgbot) searchForClient(chatId int64, query string) {
|
||||
}
|
||||
|
||||
output := ""
|
||||
output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
|
||||
output += t.I18nBot("tgbot.messages.active", "Enable=="+strconv.FormatBool(traffic.Enable))
|
||||
output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
|
||||
if traffic.Enable {
|
||||
output += t.I18nBot("tgbot.messages.active")
|
||||
if flag {
|
||||
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
|
||||
} else {
|
||||
output += t.I18nBot("tgbot.messages.expire", "Time=="+expiryTime)
|
||||
}
|
||||
} else {
|
||||
output += t.I18nBot("tgbot.messages.inactive")
|
||||
output += t.I18nBot("tgbot.messages.expire", "Time=="+expiryTime)
|
||||
}
|
||||
output += t.I18nBot("tgbot.messages.upload", "Upload=="+common.FormatTraffic(traffic.Up))
|
||||
output += t.I18nBot("tgbot.messages.download", "Download=="+common.FormatTraffic(traffic.Down))
|
||||
output += t.I18nBot("tgbot.messages.total", "UpDown=="+common.FormatTraffic((traffic.Up+traffic.Down)), "Total=="+total)
|
||||
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
|
||||
output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
|
||||
|
||||
t.SendMsgToTgbot(chatId, output)
|
||||
}
|
||||
@@ -1033,9 +1117,9 @@ func (t *Tgbot) getExhausted() string {
|
||||
output += t.I18nBot("tgbot.messages.port", "Port=="+strconv.Itoa(inbound.Port))
|
||||
output += t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic((inbound.Up+inbound.Down)), "Upload=="+common.FormatTraffic(inbound.Up), "Download=="+common.FormatTraffic(inbound.Down))
|
||||
if inbound.ExpiryTime == 0 {
|
||||
output += t.I18nBot("tgbot.messages.expire", "DateTime=="+t.I18nBot("tgbot.unlimited"))
|
||||
output += t.I18nBot("tgbot.messages.expire", "Time=="+t.I18nBot("tgbot.unlimited"))
|
||||
} else {
|
||||
output += t.I18nBot("tgbot.messages.expire", "DateTime=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
|
||||
output += t.I18nBot("tgbot.messages.expire", "Time=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
output += "\r\n \r\n"
|
||||
}
|
||||
@@ -1052,12 +1136,18 @@ func (t *Tgbot) getExhausted() string {
|
||||
|
||||
for _, traffic := range exhaustedClients {
|
||||
expiryTime := ""
|
||||
flag := false
|
||||
diff := (traffic.ExpiryTime - now) / 1000
|
||||
if traffic.ExpiryTime == 0 {
|
||||
expiryTime = t.I18nBot("tgbot.unlimited")
|
||||
} else if traffic.ExpiryTime < 0 {
|
||||
expiryTime += fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
|
||||
} else {
|
||||
} else if diff > 172800 || !traffic.Enable {
|
||||
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
|
||||
} else if traffic.ExpiryTime < 0 {
|
||||
expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
|
||||
flag = true
|
||||
} else {
|
||||
expiryTime = fmt.Sprintf("%d %s", diff/3600, t.I18nBot("tgbot.hours"))
|
||||
flag = true
|
||||
}
|
||||
|
||||
total := ""
|
||||
@@ -1067,13 +1157,22 @@ func (t *Tgbot) getExhausted() string {
|
||||
total = common.FormatTraffic((traffic.Total))
|
||||
}
|
||||
|
||||
output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
|
||||
output += t.I18nBot("tgbot.messages.active", "Enable=="+strconv.FormatBool(traffic.Enable))
|
||||
output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
|
||||
if traffic.Enable {
|
||||
output += t.I18nBot("tgbot.messages.active")
|
||||
if flag {
|
||||
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
|
||||
} else {
|
||||
output += t.I18nBot("tgbot.messages.expire", "Time=="+expiryTime)
|
||||
}
|
||||
} else {
|
||||
output += t.I18nBot("tgbot.messages.inactive")
|
||||
output += t.I18nBot("tgbot.messages.expire", "Time=="+expiryTime)
|
||||
}
|
||||
output += t.I18nBot("tgbot.messages.upload", "Upload=="+common.FormatTraffic(traffic.Up))
|
||||
output += t.I18nBot("tgbot.messages.download", "Download=="+common.FormatTraffic(traffic.Down))
|
||||
output += t.I18nBot("tgbot.messages.total", "UpDown=="+common.FormatTraffic((traffic.Up+traffic.Down)), "Total=="+total)
|
||||
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
|
||||
output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
|
||||
output += "\r\n \r\n"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ var result string
|
||||
type XrayService struct {
|
||||
inboundService InboundService
|
||||
settingService SettingService
|
||||
xrayAPI xray.XrayAPI
|
||||
}
|
||||
|
||||
func (s *XrayService) IsXrayRunning() bool {
|
||||
@@ -68,7 +69,7 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.inboundService.DisableInvalidClients()
|
||||
s.inboundService.AddTraffic(nil, nil)
|
||||
|
||||
inbounds, err := s.inboundService.GetAllInbounds()
|
||||
if err != nil {
|
||||
@@ -115,7 +116,7 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
||||
}
|
||||
}
|
||||
for key := range c {
|
||||
if key != "email" && key != "id" && key != "password" && key != "flow" && key != "alterId" {
|
||||
if key != "email" && key != "id" && key != "password" && key != "flow" && key != "method" {
|
||||
delete(c, key)
|
||||
}
|
||||
if c["flow"] == "xtls-rprx-vision-udp443" {
|
||||
@@ -143,7 +144,9 @@ func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, []*xray.ClientTraffic,
|
||||
if !s.IsXrayRunning() {
|
||||
return nil, nil, errors.New("xray is not running")
|
||||
}
|
||||
return p.GetTraffic(true)
|
||||
s.xrayAPI.Init(p.GetAPIPort())
|
||||
defer s.xrayAPI.Close()
|
||||
return s.xrayAPI.GetTraffic(true)
|
||||
}
|
||||
|
||||
func (s *XrayService) RestartXray(isForce bool) error {
|
||||
@@ -158,7 +161,7 @@ func (s *XrayService) RestartXray(isForce bool) error {
|
||||
|
||||
if p != nil && p.IsRunning() {
|
||||
if !isForce && p.GetConfig().Equals(xrayConfig) {
|
||||
logger.Debug("not need to restart xray")
|
||||
logger.Debug("It does not need to restart xray")
|
||||
return nil
|
||||
}
|
||||
p.Stop()
|
||||
@@ -166,7 +169,11 @@ func (s *XrayService) RestartXray(isForce bool) error {
|
||||
|
||||
p = xray.NewProcess(xrayConfig)
|
||||
result = ""
|
||||
return p.Start()
|
||||
err = p.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *XrayService) StopXray() error {
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"encoding/gob"
|
||||
"x-ui/database/model"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
sessions "github.com/Calidity/gin-sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
|
||||
@@ -39,7 +39,6 @@
|
||||
"depleted" = "Depleted"
|
||||
"depletingSoon" = "Depleting soon"
|
||||
"domainName" = "Domain name"
|
||||
"additional" = "Alter"
|
||||
"monitor" = "Listening IP"
|
||||
"certificate" = "Certificate"
|
||||
"fail" = "Fail"
|
||||
@@ -49,6 +48,7 @@
|
||||
"clients" = "Clients"
|
||||
"usage" = "Usage"
|
||||
"secretToken" = "Secret Token"
|
||||
"remained" = "Remained"
|
||||
|
||||
[menu]
|
||||
"dashboard" = "System Status"
|
||||
@@ -72,17 +72,18 @@
|
||||
"title" = "System Status"
|
||||
"memory" = "Memory"
|
||||
"hard" = "Hard Disk"
|
||||
"xrayStatus" = "Xray Status"
|
||||
"xrayStatus" = "Status"
|
||||
"stopXray" = "Stop"
|
||||
"restartXray" = "Restart"
|
||||
"xraySwitch" = "Switch Version"
|
||||
"xraySwitch" = "SwitchV"
|
||||
"xraySwitchClick" = "Choose the version you want to switch to."
|
||||
"xraySwitchClickDesk" = "Choose wisely, as older versions may not be compatible with current configurations."
|
||||
"operationHours" = "Operation Hours"
|
||||
"operationHoursDesc" = "System uptime: time since startup."
|
||||
"operationHours" = "Uptime"
|
||||
"systemLoad" = "System Load"
|
||||
"systemLoadDesc" = "system load average for the past 1, 5, and 15 minutes"
|
||||
"connectionTcpCountDesc" = "Total TCP connections across all network cards."
|
||||
"connectionUdpCountDesc" = "Total UDP connections across all network cards."
|
||||
"connectionCount" = "Number of Connections"
|
||||
"connectionCountDesc" = "Total connections across all network cards."
|
||||
"upSpeed" = "Total upload speed for all network cards."
|
||||
"downSpeed" = "Total download speed for all network cards."
|
||||
"totalSent" = "Total upload traffic of all network cards since system startup."
|
||||
@@ -126,7 +127,6 @@
|
||||
"network" = "Network"
|
||||
"destinationPort" = "Destination Port"
|
||||
"targetAddress" = "Target Address"
|
||||
"disableInsecureEncryption" = "Disable Insecure Encryption"
|
||||
"monitorDesc" = "Leave blank by default"
|
||||
"meansNoLimit" = "Means No Limit"
|
||||
"totalFlow" = "Total Flow"
|
||||
@@ -167,7 +167,7 @@
|
||||
"setDefaultCert" = "Set cert from panel"
|
||||
"xtlsDesc" = "Xray core needs to be 1.7.5"
|
||||
"realityDesc" = "Xray core needs to be 1.8.0 or higher."
|
||||
"telegramDesc" = "use Telegram ID without @ or chat IDs ( you can get it here @userinfobot )"
|
||||
"telegramDesc" = "use Telegram ID without @ or chat IDs ( you can get it here @userinfobot or use '/id' command in bot )"
|
||||
"subscriptionDesc" = "you can find your sub link on Details, also you can use the same name for several configurations"
|
||||
|
||||
[pages.client]
|
||||
@@ -220,6 +220,8 @@
|
||||
"TGBotSettings" = "Telegram Bot Settings"
|
||||
"panelListeningIP" = "Panel Listening IP"
|
||||
"panelListeningIPDesc" = "Leave blank by default to monitor all IPs."
|
||||
"panelListeningDomain" = "Panel Listening Domain"
|
||||
"panelListeningDomainDesc" = "Leave blank by default to monitor all domains and IPs"
|
||||
"panelPort" = "Panel Port"
|
||||
"panelPortDesc" = "The port used to display this panel"
|
||||
"publicKeyPath" = "Panel Certificate Public Key File Path"
|
||||
@@ -237,11 +239,13 @@
|
||||
"telegramToken" = "Telegram Token"
|
||||
"telegramTokenDesc" = "You must get the token from the manager of Telegram bots @botfather"
|
||||
"telegramChatId" = "Telegram Admin Chat IDs"
|
||||
"telegramChatIdDesc" = "Multiple Chat IDs separated by comma. use @userinfobot to get your Chat IDs."
|
||||
"telegramChatIdDesc" = "Multiple Chat IDs separated by comma. use @userinfobot or use '/id' command in bot to get your Chat IDs."
|
||||
"telegramNotifyTime" = "Telegram bot notification time"
|
||||
"telegramNotifyTimeDesc" = "Use Crontab timing format."
|
||||
"tgNotifyBackup" = "Database Backup"
|
||||
"tgNotifyBackupDesc" = "Include database backup file with report notification."
|
||||
"tgNotifyLogin" = "Login Notification"
|
||||
"tgNotifyLoginDesc" = "Displays the username, IP address, and time when someone tries to log into your panel."
|
||||
"sessionMaxAge" = "Session maximum age"
|
||||
"sessionMaxAgeDesc" = "The duration of a login session (unit: minute)"
|
||||
"expireTimeDiff" = "Expiration threshold for notification"
|
||||
@@ -269,6 +273,10 @@
|
||||
"subDomainDesc" = "Leave blank by default to monitor all domains and IPs"
|
||||
"subUpdates" = "Subscription update intervals"
|
||||
"subUpdatesDesc" = "Interval hours between updates in client application"
|
||||
"subEncrypt" = "Encrypt configs"
|
||||
"subEncryptDesc" = "Encrypt the returned configs in subscription"
|
||||
"subShowInfo" = "Show usage info"
|
||||
"subShowInfoDesc" = "Show remianed traffic and date after config name"
|
||||
|
||||
[pages.settings.templates]
|
||||
"title" = "Templates"
|
||||
@@ -383,6 +391,7 @@
|
||||
"months" = "Months"
|
||||
"day" = "Day"
|
||||
"days" = "Days"
|
||||
"hours" = "Hours"
|
||||
"unknown" = "Unknown"
|
||||
"inbounds" = "Inbounds"
|
||||
"clients" = "Clients"
|
||||
@@ -395,6 +404,7 @@
|
||||
"welcome" = "🤖 Welcome to <b>{{ .Hostname }}</b> management bot.\r\n"
|
||||
"status" = "✅ Bot is ok!"
|
||||
"usage" = "❗ Please provide a text to search!"
|
||||
"getID" = "🆔 Your ID: <code>{{ .ID }}</code>"
|
||||
"helpAdminCommands" = "Search for a client email:\r\n<code>/usage [Email]</code>\r\n \r\nSearch for inbounds (with client stats):\r\n<code>/inbound [Remark]</code>"
|
||||
"helpClientCommands" = "To search for statistics, just use folowing command:\r\n \r\n<code>/usage [UUID|Password]</code>\r\n \r\nUse UUID for vmess/vless and Password for Trojan."
|
||||
|
||||
@@ -423,20 +433,21 @@
|
||||
"time" = "⏰ Time: {{ .Time }}\r\n"
|
||||
"inbound" = "📍 Inbound: {{ .Remark }}\r\n"
|
||||
"port" = "🔌 Port: {{ .Port }}\r\n"
|
||||
"expire" = "📅 Expire Date: {{ .DateTime }}\r\n \r\n"
|
||||
"expireIn" = "📅 Expire In: {{ .Time }}\r\n \r\n"
|
||||
"active" = "💡 Active: {{ .Enable }}\r\n"
|
||||
"expire" = "📅 Expire Date: {{ .Time }}\r\n"
|
||||
"expireIn" = "📅 Expire In: {{ .Time }}\r\n"
|
||||
"active" = "💡 Active: ✅ Yes\r\n"
|
||||
"inactive" = "💡 Active: ❌ No\r\n"
|
||||
"email" = "📧 Email: {{ .Email }}\r\n"
|
||||
"upload" = "🔼 Upload↑: {{ .Upload }}\r\n"
|
||||
"download" = "🔽 Download↓: {{ .Download }}\r\n"
|
||||
"total" = "🔄 Total: {{ .UpDown }} / {{ .Total }}\r\n"
|
||||
"upload" = "🔼 Upload: ↑{{ .Upload }}\r\n"
|
||||
"download" = "🔽 Download: ↓{{ .Download }}\r\n"
|
||||
"total" = "📊 Total: ↑↓{{ .UpDown }} / {{ .Total }}\r\n"
|
||||
"TGUser" = "👤 Telegram User: {{ .TelegramID }}\r\n"
|
||||
"exhaustedMsg" = "🚨 Exhausted {{ .Type }}:\r\n"
|
||||
"exhaustedCount" = "🚨 Exhausted {{ .Type }} count:\r\n"
|
||||
"disabled" = "🛑 Disabled: {{ .Disabled }}\r\n"
|
||||
"depleteSoon" = "🔜 Deplete soon: {{ .Deplete }}\r\n \r\n"
|
||||
"backupTime" = "🗄 Backup Time: {{ .Time }}\r\n"
|
||||
"refreshedOn" = "🔄🕒 Refreshed On: {{ .Time }}\r\n"
|
||||
"refreshedOn" = "\r\n📋🔄 Refreshed On: {{ .Time }}\r\n \r\n"
|
||||
|
||||
[tgbot.buttons]
|
||||
"closeKeyboard" = "❌ Close Keyboard"
|
||||
|
||||
495
web/translation/translate.es_ES.toml
Normal file
@@ -0,0 +1,495 @@
|
||||
"username" = "Nombre de Usuario"
|
||||
"password" = "Contraseña"
|
||||
"login" = "Acceso"
|
||||
"confirm" = "Confirmar"
|
||||
"cancel" = "Cancelar"
|
||||
"close" = "Cerrar"
|
||||
"copy" = "Copiar"
|
||||
"copied" = "Copiado"
|
||||
"download" = "Descargar"
|
||||
"remark" = "Nota"
|
||||
"enable" = "Habilitar"
|
||||
"protocol" = "Protocolo"
|
||||
"search" = "Buscar"
|
||||
"filter" = "Filtrar"
|
||||
"loading" = "Cargando"
|
||||
"second" = "Segundo"
|
||||
"minute" = "Minuto"
|
||||
"hour" = "Hora"
|
||||
"day" = "Día"
|
||||
"check" = "Verificar"
|
||||
"indefinite" = "Indefinido"
|
||||
"unlimited" = "Ilimitado"
|
||||
"none" = "Ninguno"
|
||||
"qrCode" = "Código QR"
|
||||
"info" = "Más Información"
|
||||
"edit" = "Editar"
|
||||
"delete" = "Eliminar"
|
||||
"reset" = "Restablecer"
|
||||
"copySuccess" = "Copiado exitosamente"
|
||||
"sure" = "Seguro"
|
||||
"encryption" = "Encriptación"
|
||||
"transmission" = "Transmisión"
|
||||
"host" = "Anfitrión"
|
||||
"path" = "Ruta"
|
||||
"camouflage" = "Camuflaje"
|
||||
"status" = "Estado"
|
||||
"enabled" = "Habilitado"
|
||||
"disabled" = "Deshabilitado"
|
||||
"depleted" = "Agotado"
|
||||
"depletingSoon" = "Agotándose pronto"
|
||||
"domainName" = "Nombre de dominio"
|
||||
"monitor" = "Listening IP"
|
||||
"certificate" = "Certificado"
|
||||
"fail" = "Falló"
|
||||
"success" = "Éxito"
|
||||
"getVersion" = "Obtener versión"
|
||||
"install" = "Instalar"
|
||||
"clients" = "Clientes"
|
||||
"usage" = "Uso"
|
||||
"secretToken" = "Token Secreto"
|
||||
"remained" = "Restante"
|
||||
|
||||
[menu]
|
||||
"dashboard" = "Estado del Sistema"
|
||||
"inbounds" = "Entradas"
|
||||
"settings" = "Configuraciones del Panel"
|
||||
"logout" = "Cerrar Sesión"
|
||||
"link" = "Otro"
|
||||
|
||||
[pages.login]
|
||||
"title" = "Iniciar Sesión"
|
||||
"loginAgain" = "El límite de tiempo de inicio de sesión ha expirado. Por favor, inicia sesión nuevamente."
|
||||
|
||||
[pages.login.toasts]
|
||||
"invalidFormData" = "El formato de los datos de entrada es inválido."
|
||||
"emptyUsername" = "Por favor ingresa el nombre de usuario."
|
||||
"emptyPassword" = "Por favor ingresa la contraseña."
|
||||
"wrongUsernameOrPassword" = "Nombre de usuario o contraseña inválidos."
|
||||
"successLogin" = "Inicio de Sesión Exitoso"
|
||||
|
||||
[pages.index]
|
||||
"title" = "Estado del Sistema"
|
||||
"memory" = "Memoria"
|
||||
"hard" = "Disco Duro"
|
||||
"xrayStatus" = "Estado de Xray"
|
||||
"stopXray" = "Detener Xray"
|
||||
"restartXray" = "Reiniciar"
|
||||
"xraySwitch" = "Cambiar Versión"
|
||||
"xraySwitchClick" = "Elige la versión a la que deseas cambiar."
|
||||
"xraySwitchClickDesk" = "Elige sabiamente, ya que las versiones anteriores pueden no ser compatibles con las configuraciones actuales."
|
||||
"operationHours" = "Tiempo de Funcionamiento"
|
||||
"systemLoad" = "Carga del Sistema"
|
||||
"systemLoadDesc" = "promedio de carga del sistema en los últimos 1, 5 y 15 minutos"
|
||||
"connectionTcpCountDesc" = "Conexiones TCP totales en todas las tarjetas de red."
|
||||
"connectionUdpCountDesc" = "Conexiones UDP totales en todas las tarjetas de red."
|
||||
"connectionCount" = "Número de Conexiones"
|
||||
"upSpeed" = "Velocidad de Subida Total para Todas las Tarjetas de Red."
|
||||
"downSpeed" = "Velocidad de Bajada Total para Todas las Tarjetas de Red."
|
||||
"totalSent" = "Tráfico Total de Subida de Todas las Tarjetas de Red desde el inicio del sistema."
|
||||
"totalReceive" = "Datos Descargados Totales en Todas las Tarjetas de Red desde el inicio del sistema."
|
||||
"xraySwitchVersionDialog" = "Cambiar Versión de Xray"
|
||||
"xraySwitchVersionDialogDesc" = "¿Estás seguro de que deseas cambiar la versión de Xray a"
|
||||
"dontRefresh" = "La instalación está en progreso, por favor no actualices esta página."
|
||||
"logs" = "Registros"
|
||||
"config" = "Configuración"
|
||||
"backup" = "Copia de Seguridad y Restauración"
|
||||
"backupTitle" = "Copia de Seguridad y Restauración de la Base de Datos"
|
||||
"backupDescription" = "Recuerda hacer una copia de seguridad antes de importar una nueva base de datos."
|
||||
"exportDatabase" = "Descargar Base de Datos"
|
||||
"importDatabase" = "Cargar Base de Datos"
|
||||
|
||||
[pages.inbounds]
|
||||
"title" = "Entradas"
|
||||
"totalDownUp" = "Subidas/Descargas Totales"
|
||||
"totalUsage" = "Uso Total"
|
||||
"inboundCount" = "Número de Entradas"
|
||||
"operate" = "Menú"
|
||||
"enable" = "Habilitar"
|
||||
"remark" = "Notas"
|
||||
"protocol" = "Protocolo"
|
||||
"port" = "Puerto"
|
||||
"traffic" = "Tráfico"
|
||||
"details" = "Detalles"
|
||||
"transportConfig" = "Transporte"
|
||||
"expireDate" = "Fecha de Expiración"
|
||||
"resetTraffic" = "Restablecer Tráfico"
|
||||
"addInbound" = "Agregar Entrada"
|
||||
"generalActions" = "Acciones Generales"
|
||||
"create" = "Crear"
|
||||
"update" = "Actualizar"
|
||||
"modifyInbound" = "Modificar Entrada"
|
||||
"deleteInbound" = "Eliminar Entrada"
|
||||
"deleteInboundContent" = "¿Confirmar eliminación de entrada?"
|
||||
"resetTrafficContent" = "¿Confirmar restablecimiento de tráfico?"
|
||||
"copyLink" = "Copiar Enlace"
|
||||
"address" = "Dirección"
|
||||
"network" = "Red"
|
||||
"destinationPort" = "Puerto de Destino"
|
||||
"targetAddress" = "Dirección de Destino"
|
||||
"monitorDesc" = "Dejar en blanco por defecto"
|
||||
"meansNoLimit" = "Significa Sin Límite"
|
||||
"totalFlow" = "Flujo Total"
|
||||
"leaveBlankToNeverExpire" = "Dejar en Blanco para Nunca Expirar"
|
||||
"noRecommendKeepDefault" = "No hay requisitos especiales para mantener la configuración predeterminada"
|
||||
"certificatePath" = "Ruta del Archivo de Certificado"
|
||||
"certificateContent" = "Contenido del Archivo de Certificado"
|
||||
"publicKeyPath" = "Ruta de la Clave Pública"
|
||||
"publicKeyContent" = "Contenido de la Clave Pública"
|
||||
"keyPath" = "Ruta de la Clave Privada"
|
||||
"keyContent" = "Contenido de la Clave Privada"
|
||||
"clickOnQRcode" = "Haz clic en el Código QR para Copiar"
|
||||
"client" = "Client"
|
||||
"export" = "Exportar Enlaces"
|
||||
"clone" = "Clonar"
|
||||
"cloneInbound" = "Clonar Entradas"
|
||||
"cloneInboundContent" = "Se aplicarán todas las configuraciones de esta entrada, excepto el Puerto, la IP de Escucha y los Clientes, al clon."
|
||||
"cloneInboundOk" = "Clonar"
|
||||
"resetAllTraffic" = "Restablecer Tráfico de Todas las Entradas"
|
||||
"resetAllTrafficTitle" = "Restablecer tráfico de todas las entradas"
|
||||
"resetAllTrafficContent" = "¿Estás seguro de que deseas restablecer el tráfico de todas las entradas?"
|
||||
"resetInboundClientTraffics" = "Restablecer Tráfico de Clientes"
|
||||
"resetInboundClientTrafficTitle" = "Restablecer todo el tráfico de clientes"
|
||||
"resetInboundClientTrafficContent" = "¿Estás seguro de que deseas restablecer todo el tráfico para los clientes de esta entrada?"
|
||||
"resetAllClientTraffics" = "Restablecer Tráfico de Todos los Clientes"
|
||||
"resetAllClientTrafficTitle" = "Restablecer todo el tráfico de clientes"
|
||||
"resetAllClientTrafficContent" = "¿Estás seguro de que deseas restablecer todo el tráfico para todos los clientes?"
|
||||
"delDepletedClients" = "Eliminar Clientes Agotados"
|
||||
"delDepletedClientsTitle" = "Eliminar clientes agotados"
|
||||
"delDepletedClientsContent" = "¿Estás seguro de que deseas eliminar todos los clientes agotados?"
|
||||
"email" = "Email"
|
||||
"emailDesc" = "Por favor proporciona una dirección de correo electrónico única."
|
||||
"IPLimit" = "Límite de IP"
|
||||
"IPLimitDesc" = "Desactiva la entrada si la cantidad supera el valor ingresado (ingresa 0 para desactivar el límite de IP)."
|
||||
"IPLimitlog" = "Registro de IP"
|
||||
"IPLimitlogDesc" = "Registro de historial de IPs (antes de habilitar la entrada después de que haya sido desactivada por el límite de IP, debes borrar el registro)."
|
||||
"IPLimitlogclear" = "Limpiar el Registro"
|
||||
"setDefaultCert" = "Establecer certificado desde el panel"
|
||||
"xtlsDesc" = "La versión del núcleo de Xray debe ser 1.7.5"
|
||||
"realityDesc" = "La versión del núcleo de Xray debe ser 1.8.0 o superior."
|
||||
"telegramDesc" = "Utiliza el ID de Telegram sin @ o los IDs de chat (puedes obtenerlo aquí @userinfobot o usando el comando '/id' en el bot)."
|
||||
"subscriptionDesc" = "Puedes encontrar tu enlace de suscripción en Detalles, también puedes usar el mismo nombre para varias configuraciones."
|
||||
|
||||
[pages.client]
|
||||
"add" = "Agregar Cliente"
|
||||
"edit" = "Editar Cliente"
|
||||
"submitAdd" = "Agregar Cliente"
|
||||
"submitEdit" = "Guardar Cambios"
|
||||
"clientCount" = "Número de Clientes"
|
||||
"bulk" = "Agregar en Lote"
|
||||
"method" = "Método"
|
||||
"first" = "Primero"
|
||||
"last" = "Último"
|
||||
"prefix" = "Prefijo"
|
||||
"postfix" = "Sufijo"
|
||||
"delayedStart" = "Iniciar después del primer uso"
|
||||
"expireDays" = "Días de Expiración"
|
||||
"days" = "día(s)"
|
||||
|
||||
[pages.inbounds.toasts]
|
||||
"obtain" = "Recibir"
|
||||
|
||||
[pages.inbounds.stream.general]
|
||||
"requestHeader" = "Encabezado de la Petición"
|
||||
"name" = "Nombre"
|
||||
"value" = "Valor"
|
||||
|
||||
[pages.inbounds.stream.tcp]
|
||||
"requestVersion" = "Versión de la Petición"
|
||||
"requestMethod" = "Método de la Petición"
|
||||
"requestPath" = "Ruta de la Petición"
|
||||
"responseVersion" = "Versión de la Respuesta"
|
||||
"responseStatus" = "Estado de la Respuesta"
|
||||
"responseStatusDescription" = "Descripción del Estado de la Respuesta"
|
||||
"responseHeader" = "Encabezado de la Respuesta"
|
||||
|
||||
[pages.inbounds.stream.quic]
|
||||
"encryption" = "Cifrado"
|
||||
|
||||
[pages.settings]
|
||||
"title" = "Configuraciones"
|
||||
"save" = "Guardar"
|
||||
"infoDesc" = "Cada cambio realizado aquí debe ser guardado. Por favor, reinicie el panel para aplicar los cambios."
|
||||
"restartPanel" = "Reiniciar Panel"
|
||||
"restartPanelDesc" = "¿Está seguro de que desea reiniciar el panel? Haga clic en Aceptar para reiniciar después de 3 segundos. Si no puede acceder al panel después de reiniciar, por favor, consulte la información de registro del panel en el servidor."
|
||||
"actions" = "Acciones"
|
||||
"resetDefaultConfig" = "Restablecer a Configuración Predeterminada"
|
||||
"panelSettings" = "Configuraciones del Panel"
|
||||
"securitySettings" = "Configuraciones de Seguridad"
|
||||
"xrayConfiguration" = "Configuración de Xray"
|
||||
"TGBotSettings" = "Configuraciones de Bot de Telegram"
|
||||
"panelListeningIP" = "IP de Escucha del Panel"
|
||||
"panelListeningIPDesc" = "Dejar en blanco por defecto para monitorear todas las IPs."
|
||||
"panelListeningDomain" = "Dominio de Escucha del Panel"
|
||||
"panelListeningDomainDesc" = "Dejar en blanco por defecto para monitorear todos los dominios e IPs."
|
||||
"panelPort" = "Puerto del Panel"
|
||||
"panelPortDesc" = "El puerto utilizado para mostrar este panel."
|
||||
"publicKeyPath" = "Ruta del Archivo de Clave Pública del Certificado del Panel"
|
||||
"publicKeyPathDesc" = "Complete con una ruta absoluta que comience con."
|
||||
"privateKeyPath" = "Ruta del Archivo de Clave Privada del Certificado del Panel"
|
||||
"privateKeyPathDesc" = "Complete con una ruta absoluta que comience con."
|
||||
"panelUrlPath" = "Ruta Raíz de la URL del Panel"
|
||||
"panelUrlPathDesc" = "Debe empezar con '/' y terminar con."
|
||||
"oldUsername" = "Nombre de Usuario Actual"
|
||||
"currentPassword" = "Contraseña Actual"
|
||||
"newUsername" = "Nuevo Nombre de Usuario"
|
||||
"newPassword" = "Nueva Contraseña"
|
||||
"telegramBotEnable" = "Habilitar bot de Telegram"
|
||||
"telegramBotEnableDesc" = "Conéctese a las funciones de este panel a través del bot de Telegram."
|
||||
"telegramToken" = "Token de Telegram"
|
||||
"telegramTokenDesc" = "Debe obtener el token del administrador de bots de Telegram @botfather."
|
||||
"telegramChatId" = "IDs de Chat de Telegram para Administradores"
|
||||
"telegramChatIdDesc" = "IDs de Chat múltiples separados por comas. Use @userinfobot o use el comando '/id' en el bot para obtener sus IDs de Chat."
|
||||
"telegramNotifyTime" = "Hora de Notificación del Bot de Telegram"
|
||||
"telegramNotifyTimeDesc" = "Usar el formato de tiempo de Crontab."
|
||||
"tgNotifyBackup" = "Respaldo de Base de Datos"
|
||||
"tgNotifyBackupDesc" = "Incluir archivo de respaldo de base de datos con notificación de informe."
|
||||
"tgNotifyLogin" = "Notificación de Inicio de Sesión"
|
||||
"tgNotifyLoginDesc" = "Muestra el nombre de usuario, dirección IP y hora cuando alguien intenta iniciar sesión en su panel."
|
||||
"sessionMaxAge" = "Edad Máxima de Sesión"
|
||||
"sessionMaxAgeDesc" = "La duración de una sesión de inicio de sesión (unidad: minutos)."
|
||||
"expireTimeDiff" = "Umbral de Expiración para Notificación"
|
||||
"expireTimeDiffDesc" = "Reciba notificaciones sobre la expiración de la cuenta antes del umbral (unidad: días)."
|
||||
"trafficDiff" = "Umbral de Tráfico para Notificación"
|
||||
"trafficDiffDesc" = "Reciba notificaciones sobre el agotamiento del tráfico antes de alcanzar el umbral (unidad: GB)."
|
||||
"tgNotifyCpu" = "Umbral de Alerta de Porcentaje de CPU"
|
||||
"tgNotifyCpuDesc" = "Reciba notificaciones si el uso de la CPU supera este umbral (unidad: %)."
|
||||
"timeZone" = "Zona Horaria"
|
||||
"timeZoneDesc" = "Las tareas programadas se ejecutan de acuerdo con la hora en esta zona horaria."
|
||||
"subSettings" = "Suscripción"
|
||||
"subEnable" = "Habilitar Servicio"
|
||||
"subEnableDesc" = "Función de suscripción con configuración separada."
|
||||
"subListen" = "Listening IP"
|
||||
"subListenDesc" = "Dejar en blanco por defecto para monitorear todas las IPs."
|
||||
"subPort" = "Puerto de Suscripción"
|
||||
"subPortDesc" = "El número de puerto para el servicio de suscripción debe estar sin usar en el servidor."
|
||||
"subCertPath" = "Ruta del Archivo de Clave Pública del Certificado de Suscripción"
|
||||
"subCertPathDesc" = "Complete con una ruta absoluta que comience con '/'"
|
||||
"subKeyPath" = "Ruta del Archivo de Clave Privada del Certificado de Suscripción"
|
||||
"subKeyPathDesc" = "Complete con una ruta absoluta que comience con '/'"
|
||||
"subPath" = "Ruta Raíz de la URL de Suscripción"
|
||||
"subPathDesc" = "Debe empezar con '/' y terminar con '/'"
|
||||
"subDomain" = "Dominio de Escucha"
|
||||
"subDomainDesc" = "Dejar en blanco por defecto para monitorear todos los dominios e IPs."
|
||||
"subUpdates" = "Intervalos de Actualización de Suscripción"
|
||||
"subUpdatesDesc" = "Horas de intervalo entre actualizaciones en la aplicación del cliente."
|
||||
"subEncrypt" = "Encriptar configuraciones"
|
||||
"subEncryptDesc" = "Encriptar las configuraciones devueltas en la suscripción."
|
||||
"subShowInfo" = "Mostrar información de uso"
|
||||
"subShowInfoDesc" = "Mostrar tráfico restante y fecha después del nombre de configuración."
|
||||
|
||||
[pages.settings.templates]
|
||||
"title" = "Plantillas"
|
||||
"basicTemplate" = "Plantilla Básica"
|
||||
"advancedTemplate" = "Plantilla Avanzada"
|
||||
"completeTemplate" = "Plantilla Completa"
|
||||
"generalConfigs" = "Configuraciones Generales"
|
||||
"generalConfigsDesc" = "Estas opciones proporcionarán ajustes generales."
|
||||
"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"
|
||||
"blockCountryConfigsDesc" = "Estas opciones evitarán que los usuarios se conecten a dominios de países específicos."
|
||||
"directCountryConfigs" = "Configuraciones de Conexión Directa por País"
|
||||
"directCountryConfigsDesc" = "Estas opciones conectarán a los usuarios directamente a dominios de países específicos."
|
||||
"ipv4Configs" = "Configuraciones IPv4"
|
||||
"ipv4ConfigsDesc" = "Estas opciones solo enrutarán a los dominios objetivo a través de IPv4."
|
||||
"warpConfigs" = "Configuraciones de WARP"
|
||||
"warpConfigsDesc" = "Precaución: Antes de usar estas opciones, instale WARP en modo de proxy socks5 en su servidor siguiendo los pasos en el GitHub del panel. WARP enrutará el tráfico a los sitios web a través de los servidores de Cloudflare."
|
||||
"xrayConfigTemplate" = "Plantilla de Configuración de Xray"
|
||||
"xrayConfigTemplateDesc" = "Genera el archivo de configuración final de Xray basado en esta plantilla."
|
||||
"xrayConfigFreedomStrategy" = "Configurar Estrategia para el Protocolo Freedom"
|
||||
"xrayConfigFreedomStrategyDesc" = "Establece la estrategia de salida de la red en el Protocolo Freedom."
|
||||
"xrayConfigRoutingStrategy" = "Configurar Estrategia de Enrutamiento de Dominios"
|
||||
"xrayConfigRoutingStrategyDesc" = "Establece la estrategia general de enrutamiento para la resolución de DNS."
|
||||
"xrayConfigTorrent" = "Prohibir Uso de BitTorrent"
|
||||
"xrayConfigTorrentDesc" = "Cambia la plantilla de configuración para evitar el uso de BitTorrent por parte de los usuarios."
|
||||
"xrayConfigPrivateIp" = "Prohibir Conexiones a Rangos de IP Privadas"
|
||||
"xrayConfigPrivateIpDesc" = "Cambia la plantilla de configuración para evitar la conexión a rangos de IP privadas."
|
||||
"xrayConfigAds" = "Bloquear Anuncios"
|
||||
"xrayConfigAdsDesc" = "Cambia la plantilla de configuración para bloquear anuncios."
|
||||
"xrayConfigFamily" = "Bloquear Malware y Contenido para Adultos"
|
||||
"xrayConfigFamilyDesc" = "Resolvedores de DNS para bloquear malware y contenido para adultos para protección familiar."
|
||||
"xrayConfigSpeedtest" = "Bloquear Sitios Web de Pruebas de Velocidad"
|
||||
"xrayConfigSpeedtestDesc" = "Cambia la plantilla de configuración para evitar la conexión a sitios web de pruebas de velocidad."
|
||||
"xrayConfigIRIp" = "Desactivar Conexión a Rangos de IP de Irán"
|
||||
"xrayConfigIRIpDesc" = "Cambia la plantilla de configuración para evitar la conexión a rangos de IP de Irán."
|
||||
"xrayConfigIRDomain" = "Desactivar Conexión a Dominios de Irán"
|
||||
"xrayConfigIRDomainDesc" = "Cambia la plantilla de configuración para evitar la conexión a dominios de Irán."
|
||||
"xrayConfigChinaIp" = "Desactivar Conexión a Rangos de IP de China"
|
||||
"xrayConfigChinaIpDesc" = "Cambia la plantilla de configuración para evitar la conexión a rangos de IP de China."
|
||||
"xrayConfigChinaDomain" = "Desactivar Conexión a Dominios de China"
|
||||
"xrayConfigChinaDomainDesc" = "Cambia la plantilla de configuración para evitar la conexión a dominios de China."
|
||||
"xrayConfigRussiaIp" = "Desactivar Conexión a Rangos de IP de Rusia"
|
||||
"xrayConfigRussiaIpDesc" = "Cambia la plantilla de configuración para evitar la conexión a rangos de IP de Rusia."
|
||||
"xrayConfigRussiaDomain" = "Desactivar Conexión a Dominios de Rusia"
|
||||
"xrayConfigRussiaDomainDesc" = "Cambia la plantilla de configuración para evitar la conexión a dominios de Rusia."
|
||||
"xrayConfigDirectIRIp" = "Conexión Directa a Rangos de IP de Irán"
|
||||
"xrayConfigDirectIRIpDesc" = "Cambia la plantilla de configuración para conectarse directamente a rangos de IP de Irán."
|
||||
"xrayConfigDirectIRDomain" = "Conexión Directa a Dominios de Irán"
|
||||
"xrayConfigDirectIRDomainDesc" = "Cambia la plantilla de configuración para conectarse directamente a dominios de Irán."
|
||||
"xrayConfigDirectChinaIp" = "Conexión Directa a Rangos de IP de China"
|
||||
"xrayConfigDirectChinaIpDesc" = "Cambia la plantilla de configuración para conectarse directamente a rangos de IP de China."
|
||||
"xrayConfigDirectChinaDomain" = "Conexión Directa a Dominios de China"
|
||||
"xrayConfigDirectChinaDomainDesc" = "Cambia la plantilla de configuración para conectarse directamente a dominios de China."
|
||||
"xrayConfigDirectRussiaIp" = "Conexión Directa a Rangos de IP de Rusia"
|
||||
"xrayConfigDirectRussiaIpDesc" = "Cambia la plantilla de configuración para conectarse directamente a rangos de IP de Rusia."
|
||||
"xrayConfigDirectRussiaDomain" = "Conexión Directa a Dominios de Rusia"
|
||||
"xrayConfigDirectRussiaDomainDesc" = "Cambia la plantilla de configuración para conectarse directamente a dominios de Rusia."
|
||||
"xrayConfigGoogleIPv4" = "Usar IPv4 para Google"
|
||||
"xrayConfigGoogleIPv4Desc" = "Agregar enrutamiento para que Google se conecte con IPv4."
|
||||
"xrayConfigNetflixIPv4" = "Usar IPv4 para Netflix"
|
||||
"xrayConfigNetflixIPv4Desc" = "Agregar enrutamiento para que Netflix se conecte con IPv4."
|
||||
"xrayConfigGoogleWARP" = "Rutear Google a través de WARP."
|
||||
"xrayConfigGoogleWARPDesc" = "Agregar enrutamiento para Google a través de WARP."
|
||||
"xrayConfigOpenAIWARP" = "Rutear OpenAI (ChatGPT) a través de WARP."
|
||||
"xrayConfigOpenAIWARPDesc" = "Agregar enrutamiento para OpenAI (ChatGPT) a través de WARP."
|
||||
"xrayConfigNetflixWARP" = "Rutear Netflix a través de WARP."
|
||||
"xrayConfigNetflixWARPDesc" = "Agregar enrutamiento para Netflix a través de WARP."
|
||||
"xrayConfigSpotifyWARP" = "Rutear Spotify a través de WARP."
|
||||
"xrayConfigSpotifyWARPDesc" = "Agregar enrutamiento para Spotify a través de WARP."
|
||||
"xrayConfigIRWARP" = "Rutear dominios de Irán a través de WARP."
|
||||
"xrayConfigIRWARPDesc" = "Agregar enrutamiento para dominios de Irán a través de WARP."
|
||||
"xrayConfigInbounds" = "Configuración de Entradas"
|
||||
"xrayConfigInboundsDesc" = "Cambia la plantilla de configuración para aceptar clientes específicos."
|
||||
"xrayConfigOutbounds" = "Configuración de Salidas"
|
||||
"xrayConfigOutboundsDesc" = "Cambia la plantilla de configuración para definir formas de salida para este servidor."
|
||||
"xrayConfigRoutings" = "Configuración de Reglas de Enrutamiento"
|
||||
"xrayConfigRoutingsDesc" = "Cambia la plantilla de configuración para definir reglas de enrutamiento para este servidor."
|
||||
"manualLists" = "Listas Manuales"
|
||||
"manualListsDesc" = "Por favor, utilice el formato de matriz JSON."
|
||||
"manualBlockedIPs" = "Lista de IPs Bloqueadas"
|
||||
"manualBlockedDomains" = "Lista de Dominios Bloqueados"
|
||||
"manualDirectIPs" = "Lista de IPs Directas"
|
||||
"manualDirectDomains" = "Lista de Dominios Directos"
|
||||
"manualIPv4Domains" = "Lista de Dominios IPv4"
|
||||
"manualWARPDomains" = "Lista de Dominios de WARP"
|
||||
|
||||
[pages.settings.security]
|
||||
"admin" = "Administrador"
|
||||
"secret" = "Token Secreto"
|
||||
"loginSecurity" = "Seguridad de Inicio de Sesión"
|
||||
"loginSecurityDesc" = "Habilitar un paso adicional de seguridad para el inicio de sesión de usuarios."
|
||||
"secretToken" = "Token Secreto"
|
||||
"secretTokenDesc" = "Por favor, copia y guarda este token de forma segura en un lugar seguro. Este token es necesario para iniciar sesión y no se puede recuperar con la herramienta de comando x-ui."
|
||||
|
||||
[pages.settings.toasts]
|
||||
"modifySettings" = "Modificar Configuraciones "
|
||||
"getSettings" = "Obtener Configuraciones "
|
||||
"modifyUser" = "Modificar Usuario "
|
||||
"originalUserPassIncorrect" = "Nombre de usuario o contraseña original incorrectos"
|
||||
"userPassMustBeNotEmpty" = "El nuevo nombre de usuario y la nueva contraseña no pueden estar vacíos"
|
||||
|
||||
[tgbot]
|
||||
"keyboardClosed" = "❌ ¡Teclado personalizado cerrado!"
|
||||
"noResult" = "❗ ¡Sin resultados!"
|
||||
"noQuery" = "❌ ¡Consulta no encontrada! ¡Por favor utiliza el comando nuevamente!"
|
||||
"wentWrong" = "❌ ¡Algo salió mal!"
|
||||
"noIpRecord" = "❗ ¡Sin Registro de IP!"
|
||||
"noInbounds" = "❗ ¡No se encontraron entradas!"
|
||||
"unlimited" = "♾ Ilimitado"
|
||||
"month" = "Mes"
|
||||
"months" = "Meses"
|
||||
"day" = "Día"
|
||||
"days" = "Días"
|
||||
"hours" = "Horas"
|
||||
"unknown" = "Desconocido"
|
||||
"inbounds" = "Entradas"
|
||||
"clients" = "Clients"
|
||||
|
||||
[tgbot.commands]
|
||||
"unknown" = "❗ Comando desconocido"
|
||||
"pleaseChoose" = "👇 Por favor elige:\r\n"
|
||||
"help" = "🤖 ¡Bienvenido a este bot! Está diseñado para ofrecerte datos específicos del servidor y te permite hacer modificaciones según sea necesario.\r\n\r\n"
|
||||
"start" = "👋 Hola <i>{{ .Firstname }}</i>.\r\n"
|
||||
"welcome" = "🤖 Bienvenido al bot de gestión de <b>{{ .Hostname }}</b>.\r\n"
|
||||
"status" = "✅ ¡El bot está bien!"
|
||||
"usage" = "❗ ¡Por favor proporciona un texto para buscar!"
|
||||
"getID" = "🆔 Tu ID: <code>{{ .ID }}</code>"
|
||||
"helpAdminCommands" = "Buscar un correo electrónico de cliente:\r\n<code>/usage [Email]</code>\r\n \r\nBuscar entradas (con estadísticas de cliente):\r\n<code>/inbound [Nota]</code>"
|
||||
"helpClientCommands" = "Para buscar estadísticas, simplemente usa el siguiente comando:\r\n \r\n<code>/usage [UUID|Contraseña]</code>\r\n \r\nUsa UUID para vmess/vless y Contraseña para Trojan."
|
||||
|
||||
[tgbot.messages]
|
||||
"cpuThreshold" = "🔴 El uso de CPU {{ .Percent }}% es mayor que el umbral {{ .Threshold }}%"
|
||||
"selectUserFailed" = "❌ ¡Error al seleccionar usuario!"
|
||||
"userSaved" = "✅ Usuario de Telegram guardado."
|
||||
"loginSuccess" = "✅ Has iniciado sesión en el panel con éxito.\r\n"
|
||||
"loginFailed" = "❗️ Falló el inicio de sesión en el panel.\r\n"
|
||||
"report" = "🕰 Informes programados: {{ .RunTime }}\r\n"
|
||||
"datetime" = "⏰ Fecha y Hora: {{ .DateTime }}\r\n"
|
||||
"hostname" = "💻 Nombre del Host: {{ .Hostname }}\r\n"
|
||||
"version" = "🚀 Versión de X-UI: {{ .Version }}\r\n"
|
||||
"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n"
|
||||
"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n"
|
||||
"ip" = "🌐 IP: {{ .IP }}\r\n"
|
||||
"ips" = "🔢 IPs: \r\n{{ .IPs }}\r\n"
|
||||
"serverUpTime" = "⏳ Tiempo de actividad del servidor: {{ .UpTime }} {{ .Unit }}\r\n"
|
||||
"serverLoad" = "📈 Carga del servidor: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
|
||||
"serverMemory" = "📋 Memoria del servidor: {{ .Current }}/{{ .Total }}\r\n"
|
||||
"tcpCount" = "🔹 Conteo de TCP: {{ .Count }}\r\n"
|
||||
"udpCount" = "🔸 Conteo de UDP: {{ .Count }}\r\n"
|
||||
"traffic" = "🚦 Tráfico: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
|
||||
"xrayStatus" = "ℹ️ Estado de Xray: {{ .State }}\r\n"
|
||||
"username" = "👤 Nombre de usuario: {{ .Username }}\r\n"
|
||||
"time" = "⏰ Hora: {{ .Time }}\r\n"
|
||||
"inbound" = "📍 Inbound: {{ .Remark }}\r\n"
|
||||
"port" = "🔌 Puerto: {{ .Port }}\r\n"
|
||||
"expire" = "📅 Fecha de Vencimiento: {{ .Time }}\r\n"
|
||||
"expireIn" = "📅 Vence en: {{ .Time }}\r\n"
|
||||
"active" = "💡 Activo: ✅ Sí\r\n"
|
||||
"inactive" = "💡 Activo: ❌ No\r\n"
|
||||
"email" = "📧 Email: {{ .Email }}\r\n"
|
||||
"upload" = "🔼 Subida: ↑{{ .Upload }}\r\n"
|
||||
"download" = "🔽 Bajada: ↓{{ .Download }}\r\n"
|
||||
"total" = "📊 Total: ↑↓{{ .UpDown }} / {{ .Total }}\r\n"
|
||||
"TGUser" = "👤 Usuario de Telegram: {{ .TelegramID }}\r\n"
|
||||
"exhaustedMsg" = "🚨 Agotado {{ .Type }}: \r\n"
|
||||
"exhaustedCount" = "🚨 Cantidad de Agotados {{ .Type }}: \r\n"
|
||||
"disabled" = "🛑 Desactivado: {{ .Disabled }}\r\n"
|
||||
"depleteSoon" = "🔜 Se agotará pronto: {{ .Deplete }}\r\n \r\n"
|
||||
"backupTime" = "🗄 Hora de la Copia de Seguridad: {{ .Time }}\r\n"
|
||||
"refreshedOn" = "\r\n📋🔄 Actualizado en: {{ .Time }}\r\n \r\n"
|
||||
|
||||
[tgbot.buttons]
|
||||
"closeKeyboard" = "❌ Cerrar Teclado"
|
||||
"cancel" = "❌ Cancelar"
|
||||
"cancelReset" = "❌ Cancelar Reinicio"
|
||||
"cancelIpLimit" = "❌ Cancelar Límite de IP"
|
||||
"confirmResetTraffic" = "✅ ¿Confirmar Reinicio de Tráfico?"
|
||||
"confirmClearIps" = "✅ ¿Confirmar Limpiar IPs?"
|
||||
"confirmRemoveTGUser" = "✅ ¿Confirmar Eliminar Usuario de Telegram?"
|
||||
"dbBackup" = "Obtener Copia de Seguridad de BD"
|
||||
"serverUsage" = "Uso del Servidor"
|
||||
"getInbounds" = "Obtener Entradas"
|
||||
"depleteSoon" = "Pronto se Agotará"
|
||||
"clientUsage" = "Obtener Uso"
|
||||
"commands" = "Comandos"
|
||||
"refresh" = "🔄 Actualizar"
|
||||
"clearIPs" = "❌ Limpiar IPs"
|
||||
"removeTGUser" = "❌ Eliminar Usuario de Telegram"
|
||||
"selectTGUser" = "👤 Seleccionar Usuario de Telegram"
|
||||
"selectOneTGUser" = "👤 Selecciona un usuario de telegram:"
|
||||
"resetTraffic" = "📈 Reiniciar Tráfico"
|
||||
"resetExpire" = "📅 Reiniciar Días de Vencimiento"
|
||||
"ipLog" = "🔢 Registro de IP"
|
||||
"ipLimit" = "🔢 Límite de IP"
|
||||
"setTGUser" = "👤 Establecer Usuario de Telegram"
|
||||
"toggle" = "🔘 Habilitar / Deshabilitar"
|
||||
|
||||
[tgbot.answers]
|
||||
"errorOperation" = "❗ Error en la Operación."
|
||||
"getInboundsFailed" = "❌ Error al obtener las entradas"
|
||||
"canceled" = "❌ {{ .Email }} : Operación cancelada."
|
||||
"clientRefreshSuccess" = "✅ {{ .Email }} : Cliente actualizado exitosamente."
|
||||
"IpRefreshSuccess" = "✅ {{ .Email }} : IPs actualizadas exitosamente."
|
||||
"TGIdRefreshSuccess" = "✅ {{ .Email }} : Usuario de Telegram del cliente actualizado exitosamente."
|
||||
"resetTrafficSuccess" = "✅ {{ .Email }} : Tráfico reiniciado exitosamente."
|
||||
"expireResetSuccess" = "✅ {{ .Email }} : Días de vencimiento reiniciados exitosamente."
|
||||
"resetIpSuccess" = "✅ {{ .Email }} : Límite de IP {{ .Count }} guardado exitosamente."
|
||||
"clearIpSuccess" = "✅ {{ .Email }} : IPs limpiadas exitosamente."
|
||||
"getIpLog" = "✅ {{ .Email }} : Obtener Registro de IP."
|
||||
"getUserInfo" = "✅ {{ .Email }} : Obtener Información de Usuario de Telegram."
|
||||
"removedTGUserSuccess" = "✅ {{ .Email }} : Usuario de Telegram eliminado exitosamente."
|
||||
"enableSuccess" = "✅ {{ .Email }} : Habilitado exitosamente."
|
||||
"disableSuccess" = "✅ {{ .Email }} : Deshabilitado exitosamente."
|
||||
"askToAddUserId" = "¡No se encuentra su configuración!\r\nPor favor, pídale a su administrador que use su ID de usuario de Telegram en su(s) configuración(es).\r\n\r\nSu ID de usuario: <b>{{ .TgUserID }}</b>"
|
||||
"askToAddUserName" = "¡No se encuentra su configuración!\r\nPor favor, pídale a su administrador que use su nombre de usuario o ID de usuario de Telegram en su(s) configuración(es).\r\n\r\nSu nombre de usuario: <b>@{{ .TgUserName }}</b>\r\n\r\nSu ID de usuario: <b>{{ .TgUserID }}</b>"
|
||||
@@ -39,7 +39,6 @@
|
||||
"depleted" = "منقضی"
|
||||
"depletingSoon" = "در حال انقضا"
|
||||
"domainName" = "آدرس دامنه"
|
||||
"additional" = "آی دی جایگزین"
|
||||
"monitor" = "آی پی اتصال"
|
||||
"certificate" = "گواهی دیجیتال"
|
||||
"fail" = "خطا"
|
||||
@@ -49,6 +48,7 @@
|
||||
"clients" = "کاربران"
|
||||
"usage" = "استفاده"
|
||||
"secretToken" = "توکن امنیتی"
|
||||
"remained" = "باقیمانده"
|
||||
|
||||
[menu]
|
||||
"dashboard" = "وضعیت سیستم"
|
||||
@@ -72,17 +72,18 @@
|
||||
"title" = "وضعیت سیستم"
|
||||
"memory" = "حافظه رم"
|
||||
"hard" = "حافظه دیسک"
|
||||
"xrayStatus" = "وضعیت Xray"
|
||||
"xrayStatus" = "وضعیت"
|
||||
"stopXray" = "توقف"
|
||||
"restartXray" = "شروع مجدد"
|
||||
"xraySwitch" = "تغییر ورژن"
|
||||
"xraySwitchClick" = "ورژن مورد نظر را انتخاب کنید"
|
||||
"xraySwitchClickDesk" = "لطفا با دقت انتخاب کنید ، در صورت انتخاب اشتباه امکان قطعی سیستم وجود دارد "
|
||||
"operationHours" = "مدت فعالیت"
|
||||
"operationHoursDesc" = "مدت فعالیت سیستم بعد از روشن شدن"
|
||||
"systemLoad" = "بار روی سیستم"
|
||||
"operationHours" = "آپ تایم سیستم"
|
||||
"systemLoad" = "بار سیستم"
|
||||
"systemLoadDesc" = "میانگین بار سیستم برای 1، 5 و 15 دقیقه گذشته"
|
||||
"connectionTcpCountDesc" = "مجموع اتصالات TCP در تمام کارت های شبکه"
|
||||
"connectionUdpCountDesc" = "مجموع اتصالات UDP در تمام کارت های شبکه"
|
||||
"connectionCount" = "تعداد کانکشن ها"
|
||||
"connectionCountDesc" = "تعداد کانکشن ها برای کل شبکه"
|
||||
"upSpeed" = "سرعت آپلود در حال حاضر سیستم"
|
||||
"downSpeed" = "سرعت دانلود در حال حاضر سیستم"
|
||||
"totalSent" = "جمع کل ترافیک آپلود مصرفی"
|
||||
@@ -126,7 +127,6 @@
|
||||
"network" = "شبکه"
|
||||
"destinationPort" = "پورت مقصد"
|
||||
"targetAddress" = "آدرس مقصد"
|
||||
"disableInsecureEncryption" = "رمزگذاری ناامن را غیرفعال کنید"
|
||||
"monitorDesc" = "به طور پیش فرض خالی بگذارید"
|
||||
"meansNoLimit" = "یعنی بدون محدودیت"
|
||||
"totalFlow" = "کل ترافیک"
|
||||
@@ -167,7 +167,7 @@
|
||||
"setDefaultCert" = "استفاده از گواهی پنل"
|
||||
"xtlsDesc" = "هسته Xray باید 1.7.5 باشد"
|
||||
"realityDesc" = "هسته Xray باید 1.8.0 و بالاتر باشد"
|
||||
"telegramDesc" = "از آیدی تلگرام بدون @ یا آیدی چت استفاده کنید (می توانید آن را از اینجا دریافت کنید @userinfobot)"
|
||||
"telegramDesc" = "از آیدی تلگرام بدون @ یا آیدی چت استفاده کنید (می توانید آن را از اینجا دریافت کنید @userinfobot یا در ربات دستور '/id' را وارد کنید)"
|
||||
"subscriptionDesc" = "می توانید ساب لینک خود را در جزئیات پیدا کنید، همچنین می توانید از همین نام برای چندین کانفیگ استفاده کنید"
|
||||
|
||||
[pages.client]
|
||||
@@ -220,6 +220,8 @@
|
||||
"TGBotSettings" = "تنظیمات ربات تلگرام"
|
||||
"panelListeningIP" = "محدودیت آی پی پنل"
|
||||
"panelListeningIPDesc" = "برای استفاده از تمام آیپیها به طور پیش فرض خالی بگذارید"
|
||||
"panelListeningDomain" = "محدودیت دامین پنل"
|
||||
"panelListeningDomainDesc" = "برای استفاده از تمام دامنهها و آیپیها به طور پیش فرض خالی بگذارید"
|
||||
"panelPort" = "پورت پنل"
|
||||
"panelPortDesc" = "پورت مورد استفاده برای نمایش این پنل"
|
||||
"publicKeyPath" = "مسیر فایل گواهی کلید عمومی پنل"
|
||||
@@ -237,11 +239,13 @@
|
||||
"telegramToken" = "توکن تلگرام"
|
||||
"telegramTokenDesc" = "توکن را باید از مدیر بات های تلگرام دریافت کنید @botfather"
|
||||
"telegramChatId" = "آی دی تلگرام مدیریت"
|
||||
"telegramChatIdDesc" = "از @userinfobot برای دریافت شناسه های چت خود استفاده کنید. با استفاده از کاما میتونید چند آی دی را از هم جدا کنید. "
|
||||
"telegramChatIdDesc" = "از @userinfobot یا دستور '/id' در ربات برای دریافت شناسه های چت خود استفاده کنید. با استفاده از کاما میتونید چند آی دی را از هم جدا کنید. "
|
||||
"telegramNotifyTime" = "مدت زمان نوتیفیکیشن ربات تلگرام"
|
||||
"telegramNotifyTimeDesc" = "از فرمت زمان بندی لینوکس استفاده کنید "
|
||||
"tgNotifyBackup" = "پشتیبان گیری از پایگاه داده"
|
||||
"tgNotifyBackupDesc" = "ارسال کپی فایل پایگاه داده به همراه گزارش دوره ای"
|
||||
"tgNotifyLogin" = "اعلان ورود"
|
||||
"tgNotifyLoginDesc" = "نام کاربری، آدرس ای پی، و زمان وقتی که فردی سعی میکند به پنل شما وارد شود نمایش میدهد"
|
||||
"sessionMaxAge" = "بیشینه زمان جلسه وب"
|
||||
"sessionMaxAgeDesc" = "بیشینه زمانی که میتوانید لاگین بمانید (واحد: دقیقه)"
|
||||
"expireTimeDiff" = "آستانه زمان باقی مانده"
|
||||
@@ -269,6 +273,10 @@
|
||||
"subDomainDesc" = "برای نظارت بر همه دامنه ها و آیپی ها به طور پیش فرض خالی بگذارید"
|
||||
"subUpdates" = "فاصله به روز رسانی های سابسکریپشن"
|
||||
"subUpdatesDesc" = "ساعت های فاصله بین به روز رسانی در برنامه کاربر"
|
||||
"subEncrypt" = "رمزگذاری کانفیگ ها"
|
||||
"subEncryptDesc" = "رمزگذاری کانفیگ های بازگشتی سابسکریپشن"
|
||||
"subShowInfo" = "نمایش اطلاعات مصرف"
|
||||
"subShowInfoDesc" = "ترافیک و زمان باقیمانده را در هر کانفیگ نمایش میدهد"
|
||||
|
||||
[pages.settings.templates]
|
||||
"title" = "الگوها"
|
||||
@@ -383,6 +391,7 @@
|
||||
"months" = "ماهها"
|
||||
"day" = "روز"
|
||||
"days" = "روزها"
|
||||
"hours" = "ساعت ها"
|
||||
"unknown" = "نامشخص"
|
||||
"inbounds" = "ورودیها"
|
||||
"clients" = "کلاینتها"
|
||||
@@ -395,6 +404,7 @@
|
||||
"welcome" = "🤖 به ربات مدیریت <b>{{ .Hostname }}</b> خوش آمدید.\r\n"
|
||||
"status" = "✅ ربات در حالت عادی است!"
|
||||
"usage" = "❗ لطفاً یک متن برای جستجو وارد کنید!"
|
||||
"getID" = "🆔 شناسه شما: <code>{{ .ID }}</code>"
|
||||
"helpAdminCommands" = "برای جستجوی ایمیل مشتری:\r\n<code>/usage [ایمیل]</code>\r\n \r\nبرای جستجوی ورودیها (با آمار مشتری):\r\n<code>/inbound [توضیح]</code>"
|
||||
"helpClientCommands" = "برای جستجوی آمار، فقط از دستور زیر استفاده کنید:\r\n \r\n<code>/usage [UUID|رمز عبور]</code>\r\n \r\nاز UUID برای vmess/vless و از رمز عبور برای Trojan استفاده کنید."
|
||||
|
||||
@@ -423,20 +433,21 @@
|
||||
"time" = "⏰ زمان: {{ .Time }}\r\n"
|
||||
"inbound" = "📍 ورودی: {{ .Remark }}\r\n"
|
||||
"port" = "🔌 پورت: {{ .Port }}\r\n"
|
||||
"expire" = "📅 تاریخ انقضا: {{ .DateTime }}\r\n \r\n"
|
||||
"expireIn" = "📅 باقیمانده از انقضا: {{ .Time }}\r\n \r\n"
|
||||
"active" = "💡 فعال: {{ .Enable }}\r\n"
|
||||
"expire" = "📅 تاریخ انقضا: {{ .Time }}\r\n"
|
||||
"expireIn" = "📅 باقیمانده از انقضا: {{ .Time }}\r\n"
|
||||
"active" = "💡 فعال: ✅\r\n"
|
||||
"inactive" = "💡 فعال: ❌\r\n"
|
||||
"email" = "📧 ایمیل: {{ .Email }}\r\n"
|
||||
"upload" = "🔼 آپلود↑: {{ .Upload }}\r\n"
|
||||
"download" = "🔽 دانلود↓: {{ .Download }}\r\n"
|
||||
"total" = "🔄 کل: {{ .UpDown }} / {{ .Total }}\r\n"
|
||||
"total" = "📊 کل: {{ .UpDown }} / {{ .Total }}\r\n"
|
||||
"TGUser" = "👤 کاربر تلگرام: {{ .TelegramID }}\r\n"
|
||||
"exhaustedMsg" = "🚨 {{ .Type }} به اتمام رسیده است:\r\n"
|
||||
"exhaustedCount" = "🚨 تعداد {{ .Type }} به اتمام رسیده:\r\n"
|
||||
"disabled" = "🛑 غیرفعال: {{ .Disabled }}\r\n"
|
||||
"depleteSoon" = "🔜 به زودی به پایان خواهد رسید: {{ .Deplete }}\r\n \r\n"
|
||||
"backupTime" = "🗄 زمان پشتیبانگیری: {{ .Time }}\r\n"
|
||||
"refreshedOn" = "🔄🕒 تازهسازی شده در: {{ .Time }}\r\n"
|
||||
"refreshedOn" = "\r\n📋🔄 تازهسازی شده در: {{ .Time }}\r\n \r\n"
|
||||
|
||||
[tgbot.buttons]
|
||||
"closeKeyboard" = "❌ بستن کیبورد"
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"indefinite" = "Бессрочно"
|
||||
"unlimited" = "Безлимитно"
|
||||
"none" = "Пусто"
|
||||
"qrCode" = "QR код"
|
||||
"qrCode" = "QR-код"
|
||||
"info" = "Информация"
|
||||
"edit" = "Изменить"
|
||||
"delete" = "Удалить"
|
||||
@@ -39,7 +39,6 @@
|
||||
"depleted" = "Исчерпано"
|
||||
"depletingSoon" = "Почти исчерпано"
|
||||
"domainName" = "Домен"
|
||||
"additional" = "Дополнительно"
|
||||
"monitor" = "Порт IP"
|
||||
"certificate" = "Сертификат"
|
||||
"fail" = "Неудачно"
|
||||
@@ -49,6 +48,7 @@
|
||||
"clients" = "Клиенты"
|
||||
"usage" = "Использование"
|
||||
"secretToken" = "Секретный токен"
|
||||
"remained" = "остались"
|
||||
|
||||
[menu]
|
||||
"dashboard" = "Статус системы"
|
||||
@@ -72,24 +72,25 @@
|
||||
"title" = "Статус системы"
|
||||
"memory" = "Память"
|
||||
"hard" = "Жесткий диск"
|
||||
"xrayStatus" = "Статус Xray"
|
||||
"xrayStatus" = "Статус"
|
||||
"stopXray" = "Остановить Xray"
|
||||
"restartXray" = "Рестарт Xray"
|
||||
"xraySwitch" = "Переключить версию"
|
||||
"xraySwitchClick" = "Выберите желаемую версию"
|
||||
"xraySwitchClickDesk" = "Выбирайте внимательно, так как старые версии могут быть несовместимы с текущими конфигурациями"
|
||||
"operationHours" = "Часы работы"
|
||||
"operationHoursDesc" = "Аптайм системы: время системы в сети"
|
||||
"operationHours" = "Время работы системы"
|
||||
"systemLoad" = "Системная нагрузка"
|
||||
"systemLoadDesc" = "средняя загрузка системы за последние 1, 5 и 15 минут"
|
||||
"connectionTcpCountDesc" = "Всего подключений TCP по всем сетевым картам."
|
||||
"connectionUdpCountDesc" = "Общее количество подключений UDP по всем сетевым картам."
|
||||
"connectionCount" = "Количество соединений"
|
||||
"connectionCountDesc" = "Всего подключений по всем сетям"
|
||||
"upSpeed" = "Общая скорость upload для всех сетей"
|
||||
"downSpeed" = "Общая скорость download для всех сетей"
|
||||
"totalSent" = "Общий объем загруженных данных для всех сетей с момента запуска системы"
|
||||
"totalReceive" = "Общий объем полученных данных для всех сетей с момента запуска системы."
|
||||
"xraySwitchVersionDialog" = "Переключить версию Xray"
|
||||
"xraySwitchVersionDialogDesc" = "Вы точно хотите сменить версию Xray?"
|
||||
"dontRefresh" = "Идёт установка, пожалуйста не обновляйте эту страницу"
|
||||
"dontRefresh" = "Идёт установка. Пожалуйста, не обновляйте эту страницу"
|
||||
"logs" = "Логи"
|
||||
"config" = "Конфигурация"
|
||||
"backup" = "Бэкап и восстановление"
|
||||
@@ -126,7 +127,6 @@
|
||||
"network" = "Сеть"
|
||||
"destinationPort" = "Порт назначения"
|
||||
"targetAddress" = "Целевой адрес"
|
||||
"disableInsecureEncryption" = "Отключить небезопасное шифрование"
|
||||
"monitorDesc" = "Оставьте пустым по умолчанию"
|
||||
"meansNoLimit" = "Значит без ограничений"
|
||||
"totalFlow" = "Общий расход"
|
||||
@@ -138,12 +138,12 @@
|
||||
"publicKeyContent" = "Содержимое публичного ключа"
|
||||
"keyPath" = "Путь к приватному ключу"
|
||||
"keyContent" = "Содержимое приватного ключа"
|
||||
"clickOnQRcode" = "Нажмите на QR код, чтобы скопировать"
|
||||
"clickOnQRcode" = "Нажмите на QR-код, чтобы скопировать"
|
||||
"client" = "Клиент"
|
||||
"export" = "Экспорт ключей"
|
||||
"clone" = "Клонировать"
|
||||
"cloneInbound" = "Клонировать"
|
||||
"cloneInboundContent" = "Все настройки этого подключения, кроме порта, IP прослушки и клиентов, будут клонированы"
|
||||
"cloneInboundContent" = "Все настройки этого подключения, кроме порта, IP-адреса прослушки и клиентов, будут клонированы"
|
||||
"cloneInboundOk" = "Клонировано"
|
||||
"resetAllTraffic" = "Сбросить трафик всех подключений"
|
||||
"resetAllTrafficTitle" = "Сброс трафика всех подключений"
|
||||
@@ -160,15 +160,15 @@
|
||||
"email" = "Email"
|
||||
"emailDesc" = "Пожалуйста, укажите уникальный Email"
|
||||
"IPLimit" = "Ограничение по IP"
|
||||
"IPLimitDesc" = "Сбросить подключение, если подключено больше введенного значения (введите 0, чтобы отключить ограничение IP адресов)"
|
||||
"IPLimitDesc" = "Сбросить подключение, если подключено больше введенного значения (введите 0, чтобы отключить ограничение IP-адресов)"
|
||||
"IPLimitlog" = "IP лог"
|
||||
"IPLimitlogDesc" = "Лог IP адресов (перед включением лога IP адресов, вы должны очистить список)"
|
||||
"IPLimitlogDesc" = "Лог IP-адресов (перед включением лога IP-адресов, вы должны очистить список)"
|
||||
"IPLimitlogclear" = "Очистить лог"
|
||||
"setDefaultCert" = "Установить сертификат с панели"
|
||||
"xtlsDesc" = "Версия Xray должна быть не ниже 1.7.5"
|
||||
"realityDesc" = "Версия Xray должна быть не ниже 1.8.0"
|
||||
"telegramDesc" = "Используйте Telegram ID без @ или ID пользователя (вы можете получить его у @userinfobot)"
|
||||
"subscriptionDesc" = "Вы можете найти свою ссылку подписки в разделе «Подробнее», также вы можете использовать одно и то же имя для нескольких конфигураций"
|
||||
"telegramDesc" = "Используйте идентификатор Telegram без символа @ или идентификатора чата (можно получить его здесь @userinfobot или использовать команду '/id' в боте)"
|
||||
"subscriptionDesc" = "Вы можете найти свою ссылку подписки в разделе 'Подробнее', также вы можете использовать одно и то же имя для нескольких конфигураций"
|
||||
|
||||
[pages.client]
|
||||
"add" = "Добавить пользователя"
|
||||
@@ -182,7 +182,7 @@
|
||||
"last" = "Последний"
|
||||
"prefix" = "Префикс"
|
||||
"postfix" = "Постфикс"
|
||||
"delayedStart" = "Начать со момента первого подключения"
|
||||
"delayedStart" = "Начать с момента первого подключения"
|
||||
"expireDays" = "Срок действия"
|
||||
"days" = "дней"
|
||||
|
||||
@@ -213,13 +213,15 @@
|
||||
"restartPanel" = "Перезапуск панели"
|
||||
"restartPanelDesc" = "Подтвердите перезапуск панели? ОК для перезапуска панели через 3 сек. Если вы не можете пользоваться панелью после перезапуска, пожалуйста, посмотрите лог панели на сервере"
|
||||
"actions" = "Действия"
|
||||
"resetDefaultConfig" = "Сбросить на конфигурацию по-умолчанию"
|
||||
"resetDefaultConfig" = "Сбросить на конфигурацию по умолчанию"
|
||||
"panelSettings" = "Настройки панели"
|
||||
"securitySettings" = "Настройки безопасности"
|
||||
"xrayConfiguration" = "Конфигурация Xray"
|
||||
"TGBotSettings" = "Настройки Telegram бота"
|
||||
"panelListeningIP" = "IP адрес панели"
|
||||
"panelListeningIP" = "IP-адрес панели"
|
||||
"panelListeningIPDesc" = "Оставьте пустым для подключения с любого IP"
|
||||
"panelListeningDomain" = "Домен прослушивания панели"
|
||||
"panelListeningDomainDesc" = "По умолчанию оставьте пустым, чтобы отслеживать все домены и IP-адреса"
|
||||
"panelPort" = "Порт панели"
|
||||
"panelPortDesc" = "Порт, используемый для отображения этой панели"
|
||||
"publicKeyPath" = "Путь к файлу публичного ключа сертификата панели"
|
||||
@@ -227,7 +229,7 @@
|
||||
"privateKeyPath" = "Путь к файлу приватного ключа сертификата панели"
|
||||
"privateKeyPathDesc" = "Введите полный путь, начинающийся с "
|
||||
"panelUrlPath" = "Корневой путь URL адреса панели"
|
||||
"panelUrlPathDesc" = "Должен начинаться с «/» и заканчиваться на "
|
||||
"panelUrlPathDesc" = "Должен начинаться с '/' и заканчиваться на "
|
||||
"oldUsername" = "Текущее имя пользователя"
|
||||
"currentPassword" = "Текущий пароль"
|
||||
"newUsername" = "Новое имя пользователя"
|
||||
@@ -237,11 +239,13 @@
|
||||
"telegramToken" = "Токен Telegram бота"
|
||||
"telegramTokenDesc" = "Необходимо получить токен у менеджера ботов Telegram @botfather"
|
||||
"telegramChatId" = "Telegram ID админа бота"
|
||||
"telegramChatIdDesc" = "Несколько Telegram ID, разделённых запятой. Используйте @userinfobot, чтобы получить Telegram ID"
|
||||
"telegramChatIdDesc" = "Множественные идентификаторы чата, разделенные запятыми. Чтобы получить свои идентификаторы чатов, используйте @userinfobot или команду '/id' в боте."
|
||||
"telegramNotifyTime" = "Частота уведомлений бота Telegram"
|
||||
"telegramNotifyTimeDesc" = "Используйте формат времени Crontab"
|
||||
"tgNotifyBackup" = "Резервное копирование базы данных"
|
||||
"tgNotifyBackupDesc" = "Включать файл резервной копии базы данных с уведомлением об отчете"
|
||||
"tgNotifyLogin" = "Уведомление о входе"
|
||||
"tgNotifyLoginDesc" = "Отображает имя пользователя, IP-адрес и время, когда кто-то пытается войти в вашу панель."
|
||||
"sessionMaxAge" = "Продолжительность сессии"
|
||||
"sessionMaxAgeDesc" = "Продолжительность сессии в системе (значение: минута)"
|
||||
"expireTimeDiff" = "Порог истечения срока сессии для уведомления"
|
||||
@@ -269,6 +273,10 @@
|
||||
"subDomainDesc" = "Оставьте пустым по умолчанию, чтобы отслеживать все домены и IP-адреса"
|
||||
"subUpdates" = "Интервалы обновления подписки"
|
||||
"subUpdatesDesc" = "Часовой интервал между обновлениями в клиентском приложении"
|
||||
"subEncrypt" = "Шифровать конфиги"
|
||||
"subEncryptDesc" = "Шифровать возвращенные конфиги в подписке"
|
||||
"subShowInfo" = "Показать информацию об использовании"
|
||||
"subShowInfoDesc" = "Показывать восстановленный трафик и дату после имени конфигурации"
|
||||
|
||||
[pages.settings.templates]
|
||||
"title" = "Шаблоны"
|
||||
@@ -294,37 +302,37 @@
|
||||
"xrayConfigRoutingStrategy" = "Настройка стратегии маршрутизации доменов"
|
||||
"xrayConfigRoutingStrategyDesc" = "Установка общей стратегии маршрутизации разрешения DNS"
|
||||
"xrayConfigTorrent" = "Запрет использования BitTorrent"
|
||||
"xrayConfigTorrentDesc" = "Изменение шаблона конфигурации, для предупреждения использования BitTorrent пользователями"
|
||||
"xrayConfigPrivateIp" = "Запрет частных диапазонов IP адресов для подключения"
|
||||
"xrayConfigPrivateIpDesc" = "Изменение шаблона конфигурации, для предупреждения подключения к диапазонам частных IP адресов"
|
||||
"xrayConfigTorrentDesc" = "Изменение шаблона конфигурации для предупреждения использования BitTorrent пользователями"
|
||||
"xrayConfigPrivateIp" = "Запрет частных диапазонов IP-адресов для подключения"
|
||||
"xrayConfigPrivateIpDesc" = "Изменение шаблона конфигурации для предупреждения подключения к диапазонам частных IP-адресов"
|
||||
"xrayConfigAds" = "Блокировка рекламы"
|
||||
"xrayConfigAdsDesc" = "Изменение конфигурации, для блокировки рекламы"
|
||||
"xrayConfigAdsDesc" = "Изменение конфигурации для блокировки рекламы"
|
||||
"xrayConfigFamily" = "Блокировать вредоносное ПО и контент для взрослых"
|
||||
"xrayConfigFamilyDesc" = "Резольверы DNS для блокировки вредоносных программ и контента для взрослых для защиты семьи"
|
||||
"xrayConfigSpeedtest" = "Блокировать сайты для проверки скорости"
|
||||
"xrayConfigSpeedtestDesc" = "Изменение шаблона конфигурации, для предупреждения подключения к веб-сайтам для тестирования скорости"
|
||||
"xrayConfigIRIp" = "Заблокировать подключения к диапазонам IP адресов Ирана"
|
||||
"xrayConfigIRIpDesc" = "Изменение конфигурации, чтобы заблокировать подключения к диапазонам IP адресов Ирана"
|
||||
"xrayConfigSpeedtestDesc" = "Изменение шаблона конфигурации для предупреждения подключения к веб-сайтам для тестирования скорости"
|
||||
"xrayConfigIRIp" = "Заблокировать подключения к диапазонам IP-адресов Ирана"
|
||||
"xrayConfigIRIpDesc" = "Изменение конфигурации, чтобы заблокировать подключения к диапазонам IP-адресов Ирана"
|
||||
"xrayConfigIRDomain" = "Заблокировать подключения к доменам Ирана"
|
||||
"xrayConfigIRDomainDesc" = "Изменение конфигурации, чтобы заблокировать подключения к доменам Ирана"
|
||||
"xrayConfigChinaIp" = "Заблокировать подключения к диапазонам IP адресов Китая"
|
||||
"xrayConfigChinaIpDesc" = "Изменение конфигурации, чтобы заблокировать подключения к диапазонам IP адресов Китая"
|
||||
"xrayConfigChinaIp" = "Заблокировать подключения к диапазонам IP-адресов Китая"
|
||||
"xrayConfigChinaIpDesc" = "Изменение конфигурации, чтобы заблокировать подключения к диапазонам IP-адресов Китая"
|
||||
"xrayConfigChinaDomain" = "Заблокировать подключения к доменам Китая"
|
||||
"xrayConfigChinaDomainDesc" = "Изменение конфигурации, чтобы заблокировать подключения к доменам Китая"
|
||||
"xrayConfigRussiaIp" = "Заблокировать подключения к диапазонам IP адресов России"
|
||||
"xrayConfigRussiaIpDesc" = "Изменение конфигурации, чтобы заблокировать подключения к диапазонами IP адресов России"
|
||||
"xrayConfigRussiaIp" = "Заблокировать подключения к диапазонам IP-адресов России"
|
||||
"xrayConfigRussiaIpDesc" = "Изменение конфигурации, чтобы заблокировать подключения к диапазонами IP-адресов России"
|
||||
"xrayConfigRussiaDomain" = "Заблокировать подключения к доменам России"
|
||||
"xrayConfigRussiaDomainDesc" = "Изменение конфигурации, чтобы заблокировать подключения к доменам России"
|
||||
"xrayConfigDirectIRIp" = "Прямое подключения к диапазонам IP адресов Ирана"
|
||||
"xrayConfigDirectIRIpDesc" = "Изменение шаблона конфигурации для прямого подключения к диапазонам IP адресов Ирана"
|
||||
"xrayConfigDirectIRIp" = "Прямое подключения к диапазонам IP-адресов Ирана"
|
||||
"xrayConfigDirectIRIpDesc" = "Изменение шаблона конфигурации для прямого подключения к диапазонам IP-адресов Ирана"
|
||||
"xrayConfigDirectIRDomain" = "Прямое подключение к доменам Ирана"
|
||||
"xrayConfigDirectIRDomainDesc" = "Изменение шаблон конфигурации для прямого подключения к доменам Ирана"
|
||||
"xrayConfigDirectChinaIp" = "Прямое подключение к диапазонам IP адресов Китая"
|
||||
"xrayConfigDirectChinaIpDesc" = "Изменение шаблона конфигурации для прямого подключения к диапазонам IP адресов Китая"
|
||||
"xrayConfigDirectChinaIp" = "Прямое подключение к диапазонам IP-адресов Китая"
|
||||
"xrayConfigDirectChinaIpDesc" = "Изменение шаблона конфигурации для прямого подключения к диапазонам IP-адресов Китая"
|
||||
"xrayConfigDirectChinaDomain" = "Прямое подключение к доменам Китая"
|
||||
"xrayConfigDirectChinaDomainDesc" = "Изменение шаблона конфигурации для прямого подключения к доменам Китая"
|
||||
"xrayConfigDirectRussiaIp" = "Прямое подключение к диапазонам IP адресов России"
|
||||
"xrayConfigDirectRussiaIpDesc" = "Изменение шаблона конфигурации для прямого подключения к диапазонам IP адресов России"
|
||||
"xrayConfigDirectRussiaIp" = "Прямое подключение к диапазонам IP-адресов России"
|
||||
"xrayConfigDirectRussiaIpDesc" = "Изменение шаблона конфигурации для прямого подключения к диапазонам IP-адресов России"
|
||||
"xrayConfigDirectRussiaDomain" = "Прямое подключение к доменам России"
|
||||
"xrayConfigDirectRussiaDomainDesc" = "Изменение шаблона конфигурации для прямого подключения к доменам России"
|
||||
"xrayConfigGoogleIPv4" = "Использовать IPv4 для Google"
|
||||
@@ -342,16 +350,16 @@
|
||||
"xrayConfigIRWARP" = "Маршрутизация доменов Ирана через WARP"
|
||||
"xrayConfigIRWARPDesc" = "Добавить маршрутизацию для доменов Ирана через WARP"
|
||||
"xrayConfigInbounds" = "Конфигурация подключений"
|
||||
"xrayConfigInboundsDesc" = "Изменение шаблона конфигурации, для подключения определенных пользователей"
|
||||
"xrayConfigInboundsDesc" = "Изменение шаблона конфигурации для подключения определенных пользователей"
|
||||
"xrayConfigOutbounds" = "Конфигурация исходящих"
|
||||
"xrayConfigOutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить исходящие пути для этого сервера"
|
||||
"xrayConfigRoutings" = "Настройка правил маршрутизации"
|
||||
"xrayConfigRoutingsDesc" = "Изменение шаблона конфигурации, для определения правил маршрутизации для этого сервера"
|
||||
"xrayConfigRoutingsDesc" = "Изменение шаблона конфигурации для определения правил маршрутизации для этого сервера"
|
||||
"manualLists" = "Ручные списки"
|
||||
"manualListsDesc" = "Пожалуйста, используйте формат массива JSON"
|
||||
"manualBlockedIPs" = "Список заблокированных IP адресов"
|
||||
"manualBlockedIPs" = "Список заблокированных IP-адресов"
|
||||
"manualBlockedDomains" = "Список заблокированных доменов"
|
||||
"manualDirectIPs" = "Список прямых IP адресов"
|
||||
"manualDirectIPs" = "Список прямых IP-адресов"
|
||||
"manualDirectDomains" = "Список прямых доменов"
|
||||
"manualIPv4Domains" = "Список доменов IPv4"
|
||||
"manualWARPDomains" = "Список доменов WARP"
|
||||
@@ -383,6 +391,7 @@
|
||||
"months" = "Месяцев"
|
||||
"day" = "День"
|
||||
"days" = "Дней"
|
||||
"hours" = "Часов"
|
||||
"unknown" = "Неизвестно"
|
||||
"inbounds" = "Входящие"
|
||||
"clients" = "Клиенты"
|
||||
@@ -395,6 +404,7 @@
|
||||
"welcome" = "🤖 Добро пожаловать в бота управления <b>{{ .Hostname }}</b>.\r\n"
|
||||
"status" = "✅ Бот работает нормально!"
|
||||
"usage" = "❗ Пожалуйста, укажите текст для поиска!"
|
||||
"getID" = "🆔 Ваш ID: <code>{{ .ID }}</code>"
|
||||
"helpAdminCommands" = "Поиск по электронной почте клиента:\r\n<code>/usage [Email]</code>\r\n \r\nПоиск входящих соединений (со статистикой клиента):\r\n<code>/inbound [Remark]</code>"
|
||||
"helpClientCommands" = "Для получения статистики используйте следующую команду:\r\n \r\n<code>/usage [UUID|Password]</code>\r\n \r\nИспользуйте UUID для vmess/vless и пароль для Trojan."
|
||||
|
||||
@@ -423,20 +433,21 @@
|
||||
"time" = "⏰ Время: {{ .Time }}\r\n"
|
||||
"inbound" = "📍 Входящий поток: {{ .Remark }}\r\n"
|
||||
"port" = "🔌 Порт: {{ .Port }}\r\n"
|
||||
"expire" = "📅 Дата окончания: {{ .DateTime }}\r\n \r\n"
|
||||
"expireIn" = "📅 Окончание через: {{ .Time }}\r\n \r\n"
|
||||
"active" = "💡 Активен: {{ .Enable }}\r\n"
|
||||
"expire" = "📅 Дата окончания: {{ .Time }}\r\n"
|
||||
"expireIn" = "📅 Окончание через: {{ .Time }}\r\n"
|
||||
"active" = "💡 Активен: ✅ Да\r\n"
|
||||
"inactive" = "💡 Активен: ❌ Нет\r\n"
|
||||
"email" = "📧 Email: {{ .Email }}\r\n"
|
||||
"upload" = "🔼 Загрузка↑: {{ .Upload }}\r\n"
|
||||
"download" = "🔽 Скачивание↓: {{ .Download }}\r\n"
|
||||
"total" = "🔄 Всего: {{ .UpDown }} / {{ .Total }}\r\n"
|
||||
"upload" = "🔼 Исходящий трафик: ↑{{ .Upload }}\r\n"
|
||||
"download" = "🔽 Входящий трафик: ↓{{ .Download }}\r\n"
|
||||
"total" = "📊 Всего: ↑↓{{ .UpDown }} из {{ .Total }}\r\n"
|
||||
"TGUser" = "👤 Пользователь Telegram: {{ .TelegramID }}\r\n"
|
||||
"exhaustedMsg" = "🚨 Исчерпаны {{ .Type }}:\r\n"
|
||||
"exhaustedCount" = "🚨 Количество исчерпанных {{ .Type }}:\r\n"
|
||||
"disabled" = "🛑 Отключено: {{ .Disabled }}\r\n"
|
||||
"depleteSoon" = "🔜 Скоро исчерпание: {{ .Deplete }}\r\n \r\n"
|
||||
"backupTime" = "🗄 Время резервного копирования: {{ .Time }}\r\n"
|
||||
"refreshedOn" = "🔄🕒 Обновлено: {{ .Time }}\r\n"
|
||||
"refreshedOn" = "\r\n📋🔄 Обновлено: {{ .Time }}\r\n \r\n"
|
||||
|
||||
[tgbot.buttons]
|
||||
"closeKeyboard" = "❌ Закрыть клавиатуру"
|
||||
|
||||
495
web/translation/translate.vi_VN.toml
Normal file
@@ -0,0 +1,495 @@
|
||||
"username" = "Tên người dùng"
|
||||
"password" = "Mật khẩu"
|
||||
"login" = "Đăng nhập"
|
||||
"confirm" = "Xác nhận"
|
||||
"cancel" = "Hủy bỏ"
|
||||
"close" = "Đóng"
|
||||
"copy" = "Sao chép"
|
||||
"copied" = "Đã sao chép"
|
||||
"download" = "Tải xuống"
|
||||
"remark" = "Ghi chú"
|
||||
"enable" = "Kích hoạt"
|
||||
"protocol" = "Giao thức"
|
||||
"search" = "Tìm kiếm"
|
||||
"filter" = "Bộ lọc"
|
||||
"loading" = "Đang tải"
|
||||
"second" = "Giây"
|
||||
"minute" = "Phút"
|
||||
"hour" = "Giờ"
|
||||
"day" = "Ngày"
|
||||
"check" = "Kiểm tra"
|
||||
"indefinite" = "Không xác định"
|
||||
"unlimited" = "Không giới hạn"
|
||||
"none" = "Không có"
|
||||
"qrCode" = "Mã QR"
|
||||
"info" = "Thông tin thêm"
|
||||
"edit" = "Chỉnh sửa"
|
||||
"delete" = "Xóa"
|
||||
"reset" = "Đặt lại"
|
||||
"copySuccess" = "Đã sao chép thành công"
|
||||
"sure" = "Chắc chắn"
|
||||
"encryption" = "Mã hóa"
|
||||
"transmission" = "Truyền tải"
|
||||
"host" = "Máy chủ"
|
||||
"path" = "Đường dẫn"
|
||||
"camouflage" = "camouflage"
|
||||
"status" = "Trạng thái"
|
||||
"enabled" = "Đã kích hoạt"
|
||||
"disabled" = "Đã tắt"
|
||||
"depleted" = "Đã cạn kiệt"
|
||||
"depletingSoon" = "Sắp cạn kiệt"
|
||||
"domainName" = "Tên miền"
|
||||
"monitor" = "Listening IP"
|
||||
"certificate" = "Chứng chỉ"
|
||||
"fail" = "Thất bại"
|
||||
"success" = "Thành công"
|
||||
"getVersion" = "Lấy phiên bản"
|
||||
"install" = "Cài đặt"
|
||||
"clients" = "Khách hàng"
|
||||
"usage" = "Sử dụng"
|
||||
"secretToken" = "secretToken"
|
||||
"remained" = "Còn lại"
|
||||
|
||||
[menu]
|
||||
"dashboard" = "Trạng thái hệ thống"
|
||||
"inbounds" = "Inbounds"
|
||||
"settings" = "Cài đặt bảng điều khiển"
|
||||
"logout" = "Đăng xuất"
|
||||
"link" = "Khác"
|
||||
|
||||
[pages.login]
|
||||
"title" = "Đăng nhập"
|
||||
"loginAgain" = "Thời hạn đăng nhập đã hết. Vui lòng đăng nhập lại."
|
||||
|
||||
[pages.login.toasts]
|
||||
"invalidFormData" = "Dạng dữ liệu nhập không hợp lệ."
|
||||
"emptyUsername" = "Vui lòng nhập tên người dùng."
|
||||
"emptyPassword" = "Vui lòng nhập mật khẩu."
|
||||
"wrongUsernameOrPassword" = "Tên người dùng hoặc mật khẩu không đúng."
|
||||
"successLogin" = "Đăng nhập thành công."
|
||||
|
||||
[pages.index]
|
||||
"title" = "Trạng thái hệ thống"
|
||||
"memory" = "Bộ nhớ"
|
||||
"hard" = "Ổ cứng"
|
||||
"xrayStatus" = "Trạng thái của Xray"
|
||||
"stopXray" = "Dừng Xray"
|
||||
"restartXray" = "Khởi động lại Xray"
|
||||
"xraySwitch" = "Chuyển đổi phiên bản"
|
||||
"xraySwitchClick" = "Chọn phiên bản mà bạn muốn chuyển đổi sang."
|
||||
"xraySwitchClickDesk" = "Hãy lựa chọn thận trọng, vì các phiên bản cũ có thể không tương thích với các cấu hình hiện tại."
|
||||
"operationHours" = "Thời gian hoạt động"
|
||||
"systemLoad" = "Tải hệ thống"
|
||||
"systemLoadDesc" = "trung bình tải hệ thống trong 1, 5 và 15 phút qua"
|
||||
"connectionTcpCountDesc" = "Tổng số kết nối TCP trên tất cả các card mạng."
|
||||
"connectionUdpCountDesc" = "Tổng số kết nối UDP trên tất cả các card mạng."
|
||||
"connectionCount" = "Số lượng kết nối"
|
||||
"upSpeed" = "Tổng tốc độ tải lên cho tất cả các card mạng."
|
||||
"downSpeed" = "Tổng tốc độ tải xuống cho tất cả các card mạng."
|
||||
"totalSent" = "Tổng lưu lượng tải lên của tất cả các thẻ mạng kể từ khi hệ thống khởi động."
|
||||
"totalReceive" = "Tổng lưu lượng tải xuống trên tất cả các thẻ mạng kể từ khi hệ thống khởi động."
|
||||
"xraySwitchVersionDialog" = "Chuyển đổi Phiên bản Xray"
|
||||
"xraySwitchVersionDialogDesc" = "Bạn có chắc chắn muốn chuyển đổi phiên bản Xray sang"
|
||||
"dontRefresh" = "Đang tiến hành cài đặt, vui lòng không làm mới trang này."
|
||||
"logs" = "Nhật ký"
|
||||
"config" = "Cấu hình"
|
||||
"backup" = "Sao lưu & Khôi phục"
|
||||
"backupTitle" = "Sao lưu & Khôi phục Cơ sở dữ liệu"
|
||||
"backupDescription" = "Hãy nhớ sao lưu trước khi nhập cơ sở dữ liệu mới."
|
||||
"exportDatabase" = "Tải về Cơ sở dữ liệu"
|
||||
"importDatabase" = "Tải lên Cơ sở dữ liệu"
|
||||
|
||||
[pages.inbounds]
|
||||
"title" = "Điểm vào (Inbounds)"
|
||||
"totalDownUp" = "Tổng tải lên/tải xuống"
|
||||
"totalUsage" = "Tổng sử dụng"
|
||||
"inboundCount" = "Số lượng điểm vào"
|
||||
"operate" = "Thao tác"
|
||||
"enable" = "Kích hoạt"
|
||||
"remark" = "Chú thích"
|
||||
"protocol" = "Giao thức"
|
||||
"port" = "Cổng"
|
||||
"traffic" = "Lưu lượng"
|
||||
"details" = "Chi tiết"
|
||||
"transportConfig" = "Giao vận"
|
||||
"expireDate" = "Ngày hết hạn"
|
||||
"resetTraffic" = "Đặt lại lưu lượng"
|
||||
"addInbound" = "Thêm điểm vào"
|
||||
"generalActions" = "Hành động chung"
|
||||
"create" = "Tạo mới"
|
||||
"update" = "Cập nhật"
|
||||
"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)"
|
||||
"resetTrafficContent" = "Xác nhận đặt lại lưu lượng?"
|
||||
"copyLink" = "Sao chép liên kết"
|
||||
"address" = "Địa chỉ"
|
||||
"network" = "Mạng"
|
||||
"destinationPort" = "Cổng đích"
|
||||
"targetAddress" = "Địa chỉ mục tiêu"
|
||||
"monitorDesc" = "Mặc định để trống"
|
||||
"meansNoLimit" = "Nghĩa là không giới hạn"
|
||||
"totalFlow" = "Tổng lưu lượng"
|
||||
"leaveBlankToNeverExpire" = "Để trống để không bao giờ hết hạn"
|
||||
"noRecommendKeepDefault" = "Không yêu cầu đặc biệt để giữ nguyên cài đặt mặc định"
|
||||
"certificatePath" = "Đường dẫn tập tin chứng chỉ"
|
||||
"certificateContent" = "Nội dung tập tin chứng chỉ"
|
||||
"publicKeyPath" = "Đường dẫn khóa công khai"
|
||||
"publicKeyContent" = "Nội dung khóa công khai"
|
||||
"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" = "Client"
|
||||
"export" = "Xuất liên kết"
|
||||
"clone" = "Sao chép"
|
||||
"cloneInbound" = "Sao chép điểm vào (Inbound)"
|
||||
"cloneInboundContent" = "Tất cả cài đặt của điểm vào này, trừ Cổng, IP nghe và Clients, sẽ được áp dụng cho bản sao."
|
||||
"cloneInboundOk" = "Sao chép"
|
||||
"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 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"
|
||||
"IPLimitDesc" = "Vô hiệu hóa điểm vào nếu số lượng vượt quá giá trị đã nhập (nhập 0 để vô hiệu hóa giới hạn IP)."
|
||||
"IPLimitlog" = "Lịch sử IP"
|
||||
"IPLimitlogDesc" = "Lịch sử đăng nhập IP (trước khi kích hoạt điểm vào sau khi bị vô hiệu hóa bởi giới hạn IP, bạn nên xóa lịch sử)."
|
||||
"IPLimitlogclear" = "Xóa Lịch sử"
|
||||
"setDefaultCert" = "Đặt chứng chỉ từ bảng điều khiển"
|
||||
"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" = "Sử dụng Telegram ID mà không cần ký hiệu @ hoặc chat IDs (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 đă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"
|
||||
|
||||
[pages.client]
|
||||
"add" = "Thêm Client"
|
||||
"edit" = "Chỉnh sửa Client"
|
||||
"submitAdd" = "Thêm Client"
|
||||
"submitEdit" = "Lưu thay đổi"
|
||||
"clientCount" = "Số lượng Client"
|
||||
"bulk" = "Thêm hàng loạt"
|
||||
"method" = "Phương pháp"
|
||||
"first" = "Đầu tiên"
|
||||
"last" = "Cuối cùng"
|
||||
"prefix" = "Tiền tố"
|
||||
"postfix" = "Hậu tố"
|
||||
"delayedStart" = "Bắt đầu sau khi sử dụng lần đầu"
|
||||
"expireDays" = "Số ngày hết hạn"
|
||||
"days" = "ngày"
|
||||
|
||||
[pages.inbounds.toasts]
|
||||
"obtain" = "Nhận"
|
||||
|
||||
[pages.inbounds.stream.general]
|
||||
"requestHeader" = "Tiêu đề yêu cầu"
|
||||
"name" = "Tên"
|
||||
"value" = "Giá trị"
|
||||
|
||||
[pages.inbounds.stream.tcp]
|
||||
"requestVersion" = "Phiên bản yêu cầu"
|
||||
"requestMethod" = "Phương thức yêu cầu"
|
||||
"requestPath" = "Đường dẫn yêu cầu"
|
||||
"responseVersion" = "Phiên bản phản hồi"
|
||||
"responseStatus" = "Trạng thái phản hồi"
|
||||
"responseStatusDescription" = "Mô tả trạng thái phản hồi"
|
||||
"responseHeader" = "Tiêu đề phản hồi"
|
||||
|
||||
[pages.inbounds.stream.quic]
|
||||
"encryption" = "Mã hóa"
|
||||
|
||||
[pages.settings]
|
||||
"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"
|
||||
"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" = "Cài đặt Bảng điều khiển"
|
||||
"securitySettings" = "Cài đặt Bảo mật"
|
||||
"xrayConfiguration" = "Cấu hình Xray"
|
||||
"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"
|
||||
"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 để 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."
|
||||
"oldUsername" = "Tên người dùng hiện tại"
|
||||
"currentPassword" = "Mật khẩu hiện tại"
|
||||
"newUsername" = "Tên người dùng mới"
|
||||
"newPassword" = "Mật khẩu mới"
|
||||
"telegramBotEnable" = "Bật Bot Telegram"
|
||||
"telegramBotEnableDesc" = "Kết nối với các tính năng của bảng điều khiển này thông qua bot Telegram"
|
||||
"telegramToken" = "Token Telegram"
|
||||
"telegramTokenDesc" = "Bạn phải nhận token từ quản lý bot Telegram @botfather"
|
||||
"telegramChatId" = "Chat ID Telegram của quản trị viên"
|
||||
"telegramChatIdDesc" = "Nhiều Chat ID phân tách bằng dấu phẩy. Sử dụng @userinfobot hoặc sử dụng lệnh '/id' trong bot để lấy Chat ID của bạn."
|
||||
"telegramNotifyTime" = "Thời gian thông báo của bot Telegram"
|
||||
"telegramNotifyTimeDesc" = "Sử dụng định dạng thời gian Crontab."
|
||||
"tgNotifyBackup" = "Sao lưu Cơ sở dữ liệu"
|
||||
"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" = "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)"
|
||||
"trafficDiff" = "Ngưỡng lưu lượng cho thông báo"
|
||||
"trafficDiffDesc" = "Nhận thông báo về việc cạn kiệt lưu lượng trước khi đạt đến ngưỡng này (đơn vị: GB)"
|
||||
"tgNotifyCpu" = "Ngưỡng cảnh báo tỷ lệ CPU"
|
||||
"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" = "Đăng ký"
|
||||
"subEnable" = "Bật dịch vụ"
|
||||
"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 Đă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 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 đă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 đă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"
|
||||
|
||||
[pages.settings.templates]
|
||||
"title" = "Mẫu"
|
||||
"basicTemplate" = "Mẫu Cơ bản"
|
||||
"advancedTemplate" = "Mẫu Nâng cao"
|
||||
"completeTemplate" = "Mẫu Đầy đủ"
|
||||
"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."
|
||||
"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"
|
||||
"blockCountryConfigsDesc" = "Những tùy chọn này sẽ ngăn người dùng kết nối đến các tên miền quốc gia cụ thể."
|
||||
"directCountryConfigs" = "Cấu hình Kết nối Trực tiếp Quốc gia"
|
||||
"directCountryConfigsDesc" = "Những tùy chọn này sẽ kết nối người dùng trực tiếp đến các tên miền quốc gia cụ thể."
|
||||
"ipv4Configs" = "Cấu hình IPv4"
|
||||
"ipv4ConfigsDesc" = "Những tùy chọn này sẽ chỉ định kết nối đến các tên miền mục tiêu qua IPv4."
|
||||
"warpConfigs" = "Cấu hình WARP"
|
||||
"warpConfigsDesc" = "Cảnh báo: Trước khi sử dụng những tùy chọn này, hãy cài đặt WARP ở chế độ proxy socks5 trên máy chủ của bạn bằng cách làm theo các bước trên GitHub của bảng điều khiển. WARP sẽ định tuyến lưu lượng đến các trang web qua máy chủ Cloudflare."
|
||||
"xrayConfigTemplate" = "Mẫu Cấu hình Xray"
|
||||
"xrayConfigTemplateDesc" = "Tạo tệp cấu hình Xray cuối cùng dựa trên mẫu này."
|
||||
"xrayConfigFreedomStrategy" = "Cấu hình Chiến lược cho Giao thức Freedom"
|
||||
"xrayConfigFreedomStrategyDesc" = "Đặt chiến lược đầu ra của mạng trong Giao thức Freedom."
|
||||
"xrayConfigRoutingStrategy" = "Cấu hình Chiến lược Định tuyến Tên miền"
|
||||
"xrayConfigRoutingStrategyDesc" = "Đặt chiến lược định tuyến tổng thể cho việc giải quyết DNS."
|
||||
"xrayConfigTorrent" = "Cấu hình sử dụng BitTorrent"
|
||||
"xrayConfigTorrentDesc" = "Thay đổi mẫu cấu hình để tránh việc người dùng sử dụng BitTorrent."
|
||||
"xrayConfigPrivateIp" = "Cấm kết nối đến dải IP Riêng tư"
|
||||
"xrayConfigPrivateIpDesc" = "Thay đổi mẫu cấu hình để tránh kết nối đến dải IP riêng tư."
|
||||
"xrayConfigAds" = "Chặn Quảng cáo"
|
||||
"xrayConfigAdsDesc" = "Thay đổi mẫu cấu hình để chặn quảng cáo."
|
||||
"xrayConfigFamily" = "Chặn Phần mềm độc hại và Nội dung cho Người lớn"
|
||||
"xrayConfigFamilyDesc" = "Các trình giải quyết DNS để chặn phần mềm độc hại và nội dung cho bảo vệ gia đình."
|
||||
"xrayConfigSpeedtest" = "Chặn Trang web Speedtest"
|
||||
"xrayConfigSpeedtestDesc" = "Thay đổi mẫu cấu hình để tránh kết nối đến các trang web Speedtest."
|
||||
"xrayConfigIRIp" = "Vô hiệu hóa kết nối đến dải IP của Iran"
|
||||
"xrayConfigIRIpDesc" = "Thay đổi mẫu cấu hình để tránh kết nối đến dải IP của Iran."
|
||||
"xrayConfigIRDomain" = "Vô hiệu hóa kết nối đến tên miền của Iran"
|
||||
"xrayConfigIRDomainDesc" = "Thay đổi mẫu cấu hình để tránh kết nối đến các tên miền của Iran."
|
||||
"xrayConfigChinaIp" = "Vô hiệu hóa kết nối đến dải IP của Trung Quốc"
|
||||
"xrayConfigChinaIpDesc" = "Thay đổi mẫu cấu hình để tránh kết nối đến dải IP của Trung Quốc."
|
||||
"xrayConfigChinaDomain" = "Vô hiệu hóa kết nối đến tên miền của Trung Quốc"
|
||||
"xrayConfigChinaDomainDesc" = "Thay đổi mẫu cấu hình để tránh kết nối đến các tên miền của Trung Quốc."
|
||||
"xrayConfigRussiaIp" = "Vô hiệu hóa kết nối đến dải IP của Nga"
|
||||
"xrayConfigRussiaIpDesc" = "Thay đổi mẫu cấu hình để tránh kết nối đến dải IP của Nga."
|
||||
"xrayConfigRussiaDomain" = "Vô hiệu hóa kết nối đến tên miền của Nga"
|
||||
"xrayConfigRussiaDomainDesc" = "Thay đổi mẫu cấu hình để tránh kết nối đến các tên miền của Nga."
|
||||
"xrayConfigDirectIRIp" = "Kết nối trực tiếp đến dải IP của Iran"
|
||||
"xrayConfigDirectIRIpDesc" = "Thay đổi mẫu cấu hình cho kết nối trực tiếp đến dải IP của Iran."
|
||||
"xrayConfigDirectIRDomain" = "Kết nối trực tiếp đến tên miền của Iran"
|
||||
"xrayConfigDirectIRDomainDesc" = "Thay đổi mẫu cấu hình cho kết nối trực tiếp đến các tên miền của Iran."
|
||||
"xrayConfigDirectChinaIp" = "Kết nối trực tiếp đến dải IP của Trung Quốc"
|
||||
"xrayConfigDirectChinaIpDesc" = "Thay đổi mẫu cấu hình cho kết nối trực tiếp đến dải IP của Trung Quốc."
|
||||
"xrayConfigDirectChinaDomain" = "Kết nối trực tiếp đến tên miền của Trung Quốc"
|
||||
"xrayConfigDirectChinaDomainDesc" = "Thay đổi mẫu cấu hình cho kết nối trực tiếp đến các tên miền của Trung Quốc."
|
||||
"xrayConfigDirectRussiaIp" = "Kết nối trực tiếp đến dải IP của Nga"
|
||||
"xrayConfigDirectRussiaIpDesc" = "Thay đổi mẫu cấu hình cho kết nối trực tiếp đến dải IP của Nga."
|
||||
"xrayConfigDirectRussiaDomain" = "Kết nối trực tiếp đến tên miền của Nga"
|
||||
"xrayConfigDirectRussiaDomainDesc" = "Thay đổi mẫu cấu hình cho kết nối trực tiếp đến các tên miền của Nga."
|
||||
"xrayConfigGoogleIPv4" = "Sử dụng IPv4 cho Google"
|
||||
"xrayConfigGoogleIPv4Desc" = "Thêm định tuyến cho Google để kết nối qua IPv4."
|
||||
"xrayConfigNetflixIPv4" = "Sử dụng IPv4 cho Netflix"
|
||||
"xrayConfigNetflixIPv4Desc" = "Thêm định tuyến cho Netflix để kết nối qua IPv4."
|
||||
"xrayConfigGoogleWARP" = "Định tuyến Google qua WARP."
|
||||
"xrayConfigGoogleWARPDesc" = "Thêm định tuyến cho Google qua WARP."
|
||||
"xrayConfigOpenAIWARP" = "Định tuyến OpenAI (ChatGPT) qua WARP."
|
||||
"xrayConfigOpenAIWARPDesc" = "Thêm định tuyến cho OpenAI (ChatGPT) qua WARP."
|
||||
"xrayConfigNetflixWARP" = "Định tuyến Netflix qua WARP."
|
||||
"xrayConfigNetflixWARPDesc" = "Thêm định tuyến cho Netflix qua WARP."
|
||||
"xrayConfigSpotifyWARP" = "Định tuyến Spotify qua WARP."
|
||||
"xrayConfigSpotifyWARPDesc" = "Thêm định tuyến cho Spotify qua WARP."
|
||||
"xrayConfigIRWARP" = "Định tuyến tên miền của Iran qua WARP."
|
||||
"xrayConfigIRWARPDesc" = "Thêm định tuyến cho các tên miền của Iran qua WARP."
|
||||
"xrayConfigInbounds" = "Cấu hình của Inbounds"
|
||||
"xrayConfigInboundsDesc" = "Thay đổi mẫu cấu hình để chấp nhận các máy khách cụ thể."
|
||||
"xrayConfigOutbounds" = "Cấu hình của Outbounds"
|
||||
"xrayConfigOutboundsDesc" = "Thay đổi mẫu cấu hình để xác định các cách ra đi cho máy chủ này."
|
||||
"xrayConfigRoutings" = "Cấu hình của Luật Định tuyến."
|
||||
"xrayConfigRoutingsDesc" = "Thay đổi mẫu cấu hình để xác định luật định tuyến cho máy chủ này."
|
||||
"manualLists" = "Danh sách Thủ công"
|
||||
"manualListsDesc" = "Vui lòng sử dụng định dạng mảng JSON."
|
||||
"manualBlockedIPs" = "Danh sách IP bị Chặn"
|
||||
"manualBlockedDomains" = "Danh sách Tên miền bị Chặn"
|
||||
"manualDirectIPs" = "Danh sách IP Trực tiếp"
|
||||
"manualDirectDomains" = "Danh sách Tên miền Trực tiếp"
|
||||
"manualIPv4Domains" = "Danh sách Tên miền IPv4"
|
||||
"manualWARPDomains" = "Danh sách Tên miền WARP"
|
||||
|
||||
[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ã 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 "
|
||||
"getSettings" = "Lấy cài đặt "
|
||||
"modifyUser" = "Chỉnh sửa người dùng "
|
||||
"originalUserPassIncorrect" = "Tên người dùng hoặc mật khẩu gốc không đúng"
|
||||
"userPassMustBeNotEmpty" = "Tên người dùng mới và mật khẩu mới không thể để trống"
|
||||
|
||||
[tgbot]
|
||||
"keyboardClosed" = "❌ Bàn phím tùy chỉnh đã đóng!"
|
||||
"noResult" = "❗ Không có kết quả!"
|
||||
"noQuery" = "❌ Không tìm thấy truy vấn! Vui lòng sử dụng lệnh lại!"
|
||||
"wentWrong" = "❌ Đã xảy ra lỗi!"
|
||||
"noIpRecord" = "❗ Không có bản ghi IP!"
|
||||
"noInbounds" = "❗ Không tìm thấy inbound!"
|
||||
"unlimited" = "♾ Không giới hạn"
|
||||
"month" = "Tháng"
|
||||
"months" = "Tháng"
|
||||
"day" = "Ngày"
|
||||
"days" = "Ngày"
|
||||
"hours" = "Giờ"
|
||||
"unknown" = "Không rõ"
|
||||
"inbounds" = "Inbounds"
|
||||
"clients" = "Khách hàng"
|
||||
|
||||
[tgbot.commands]
|
||||
"unknown" = "❗ Lệnh không rõ"
|
||||
"pleaseChoose" = "👇 Vui lòng chọn:\r\n"
|
||||
"help" = "🤖 Chào mừng bạn đến với bot này! Bot được thiết kế để cung cấp cho bạn dữ liệu cụ thể từ máy chủ và cho phép bạn thực hiện các thay đổi cần thiết.\r\n\r\n"
|
||||
"start" = "👋 Xin chào <i>{{ .Firstname }}</i>.\r\n"
|
||||
"welcome" = "🤖 Chào mừng đến với bot quản lý của <b>{{ .Hostname }}</b>.\r\n"
|
||||
"status" = "✅ Bot hoạt động bình thường!"
|
||||
"usage" = "❗ Vui lòng cung cấp văn bản để tìm kiếm!"
|
||||
"getID" = "🆔 ID của bạn: <code>{{ .ID }}</code>"
|
||||
"helpAdminCommands" = "Tìm kiếm email của khách hàng:\r\n<code>/usage [Email]</code>\r\n \r\nTìm kiếm inbounds (với thống kê của khách hàng):\r\n<code>/inbound [Ghi chú]</code>"
|
||||
"helpClientCommands" = "Để tìm kiếm thống kê, hãy sử dụng lệnh sau:\r\n \r\n<code>/usage [UUID|Mật khẩu]</code>\r\n \r\nSử dụng UUID cho vmess/vless và Mật khẩu cho Trojan."
|
||||
|
||||
[tgbot.messages]
|
||||
"cpuThreshold" = "🔴 Sử dụng CPU {{ .Percent }}% vượt quá ngưỡng {{ .Threshold }}%"
|
||||
"selectUserFailed" = "❌ Lỗi khi chọn người dùng!"
|
||||
"userSaved" = "✅ Người dùng Telegram đã được lưu."
|
||||
"loginSuccess" = "✅ Đăng nhập thành công vào bảng điều khiển.\r\n"
|
||||
"loginFailed" = "❗️ Đăng nhập vào bảng điều khiển thất bại.\r\n"
|
||||
"report" = "🕰 Báo cáo định kỳ: {{ .RunTime }}\r\n"
|
||||
"datetime" = "⏰ Ngày-Giờ: {{ .DateTime }}\r\n"
|
||||
"hostname" = "💻 Tên máy chủ: {{ .Hostname }}\r\n"
|
||||
"version" = "🚀 Phiên bản X-UI: {{ .Version }}\r\n"
|
||||
"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n"
|
||||
"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n"
|
||||
"ip" = "🌐 IP: {{ .IP }}\r\n"
|
||||
"ips" = "🔢 Các IP: \r\n{{ .IPs }}\r\n"
|
||||
"serverUpTime" = "⏳ Thời gian hoạt động của máy chủ: {{ .UpTime }} {{ .Unit }}\r\n"
|
||||
"serverLoad" = "📈 Tải máy chủ: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
|
||||
"serverMemory" = "📋 Bộ nhớ máy chủ: {{ .Current }}/{{ .Total }}\r\n"
|
||||
"tcpCount" = "🔹 Số lượng kết nối TCP: {{ .Count }}\r\n"
|
||||
"udpCount" = "🔸 Số lượng kết nối UDP: {{ .Count }}\r\n"
|
||||
"traffic" = "🚦 Lưu lượng: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
|
||||
"xrayStatus" = "ℹ️ Trạng thái Xray: {{ .State }}\r\n"
|
||||
"username" = "👤 Tên người dùng: {{ .Username }}\r\n"
|
||||
"time" = "⏰ Thời gian: {{ .Time }}\r\n"
|
||||
"inbound" = "📍 Inbound: {{ .Remark }}\r\n"
|
||||
"port" = "🔌 Cổng: {{ .Port }}\r\n"
|
||||
"expire" = "📅 Ngày hết hạn: {{ .Time }}\r\n"
|
||||
"expireIn" = "📅 Hết hạn sau: {{ .Time }}\r\n"
|
||||
"active" = "💡 Hoạt động: ✅ Có\r\n"
|
||||
"inactive" = "💡 Hoạt động: ❌ Không\r\n"
|
||||
"email" = "📧 Email: {{ .Email }}\r\n"
|
||||
"upload" = "🔼 Tải lên: ↑{{ .Upload }}\r\n"
|
||||
"download" = "🔽 Tải xuống: ↓{{ .Download }}\r\n"
|
||||
"total" = "📊 Tổng cộng: ↑↓{{ .UpDown }} / {{ .Total }}\r\n"
|
||||
"TGUser" = "👤 Người dùng Telegram: {{ .TelegramID }}\r\n"
|
||||
"exhaustedMsg" = "🚨 Sự cạn kiệt {{ .Type }}:\r\n"
|
||||
"exhaustedCount" = "🚨 Số lần cạn kiệt {{ .Type }}:\r\n"
|
||||
"disabled" = "🛑 Vô hiệu hóa: {{ .Disabled }}\r\n"
|
||||
"depleteSoon" = "🔜 Sắp cạn kiệt: {{ .Deplete }}\r\n \r\n"
|
||||
"backupTime" = "🗄 Thời gian sao lưu: {{ .Time }}\r\n"
|
||||
"refreshedOn" = "\r\n📋🔄 Đã cập nhật lần cuối vào: {{ .Time }}\r\n \r\n"
|
||||
|
||||
[tgbot.buttons]
|
||||
"closeKeyboard" = "❌ Đóng Bàn Phím"
|
||||
"cancel" = "❌ Hủy"
|
||||
"cancelReset" = "❌ Hủy Đặt Lại"
|
||||
"cancelIpLimit" = "❌ Hủy Giới Hạn IP"
|
||||
"confirmResetTraffic" = "✅ Xác Nhận Đặt Lại Lưu Lượng?"
|
||||
"confirmClearIps" = "✅ Xác Nhận Xóa Các IP?"
|
||||
"confirmRemoveTGUser" = "✅ Xác Nhận Xóa Người Dùng Telegram?"
|
||||
"dbBackup" = "Tải Backup DB"
|
||||
"serverUsage" = "Sử Dụng Máy Chủ"
|
||||
"getInbounds" = "Lấy Inbounds"
|
||||
"depleteSoon" = "Sắp Cạn Kiệt"
|
||||
"clientUsage" = "Lấy Sử Dụng"
|
||||
"commands" = "Lệnh"
|
||||
"refresh" = "🔄 Cập Nhật"
|
||||
"clearIPs" = "❌ Xóa IP"
|
||||
"removeTGUser" = "❌ Xóa Người Dùng Telegram"
|
||||
"selectTGUser" = "👤 Chọn Người Dùng Telegram"
|
||||
"selectOneTGUser" = "👤 Chọn một người dùng telegram:"
|
||||
"resetTraffic" = "📈 Đặt Lại Lưu Lượng"
|
||||
"resetExpire" = "📅 Đặt Lại Ngày Hết Hạn"
|
||||
"ipLog" = "🔢 Log IP"
|
||||
"ipLimit" = "🔢 Giới Hạn IP"
|
||||
"setTGUser" = "👤 Đặt Người Dùng Telegram"
|
||||
"toggle" = "🔘 Bật / Tắt"
|
||||
|
||||
[tgbot.answers]
|
||||
"errorOperation" = "❗ Lỗi Trong Quá Trình Thực Hiện."
|
||||
"getInboundsFailed" = "❌ Không Thể Lấy Được Inbounds"
|
||||
"canceled" = "❌ {{ .Email }} : Thao Tác Đã Bị Hủy."
|
||||
"clientRefreshSuccess" = "✅ {{ .Email }} : Cập Nhật Thành Công Cho Khách Hàng."
|
||||
"IpRefreshSuccess" = "✅ {{ .Email }} : Cập Nhật Thành Công Cho IPs."
|
||||
"TGIdRefreshSuccess" = "✅ {{ .Email }} : Cập Nhật Thành Công Cho Người Dùng Telegram."
|
||||
"resetTrafficSuccess" = "✅ {{ .Email }} : Đặt Lại Lưu Lượng Thành Công."
|
||||
"expireResetSuccess" = "✅ {{ .Email }} : Đặt Lại Ngày Hết Hạn Thành Công."
|
||||
"resetIpSuccess" = "✅ {{ .Email }} : Giới Hạn IP {{ .Count }} Đã Được Lưu Thành Công."
|
||||
"clearIpSuccess" = "✅ {{ .Email }} : IPs Đã Được Xóa Thành Công."
|
||||
"getIpLog" = "✅ {{ .Email }} : Lấy Log IP Thành Công."
|
||||
"getUserInfo" = "✅ {{ .Email }} : Lấy Thông Tin Người Dùng Telegram Thành Công."
|
||||
"removedTGUserSuccess" = "✅ {{ .Email }} : Người Dùng Telegram Đã Được Xóa Thành Công."
|
||||
"enableSuccess" = "✅ {{ .Email }} : Đã Bật Thành Công."
|
||||
"disableSuccess" = "✅ {{ .Email }} : Đã Tắt Thành Công."
|
||||
"askToAddUserId" = "Cấu hình của bạn không được tìm thấy!\r\nVui lòng yêu cầu Quản trị viên sử dụng ID người dùng telegram của bạn trong cấu hình của bạn.\r\n\r\nID người dùng của bạn: <b>{{ .TgUserID }}</b>"
|
||||
"askToAddUserName" = "Cấu hình của bạn không được tìm thấy!\r\nVui lòng yêu cầu Quản trị viên sử dụng tên người dùng hoặc ID người dùng telegram của bạn trong cấu hình của bạn.\r\n\r\nTên người dùng của bạn: <b>@{{ .TgUserName }}</b>\r\n\r\nID người dùng của bạn: <b>{{ .TgUserID }}</b>"
|
||||
@@ -39,7 +39,6 @@
|
||||
"depleted" = "耗尽"
|
||||
"depletingSoon" = "即将耗尽"
|
||||
"domainName" = "域名"
|
||||
"additional" = "额外"
|
||||
"monitor" = "监听"
|
||||
"certificate" = "证书"
|
||||
"fail" = "失败"
|
||||
@@ -49,6 +48,7 @@
|
||||
"clients" = "客户端"
|
||||
"usage" = "用法"
|
||||
"secretToken" = "秘密令牌"
|
||||
"remained" = "仍然存在"
|
||||
|
||||
[menu]
|
||||
"dashboard" = "系统状态"
|
||||
@@ -72,17 +72,18 @@
|
||||
"title" = "系统状态"
|
||||
"memory" = "内存"
|
||||
"hard" = "硬盘"
|
||||
"xrayStatus" = "xray 状态"
|
||||
"xrayStatus" = "状态"
|
||||
"stopXray" = "停止"
|
||||
"restartXray" = "重启"
|
||||
"xraySwitch" = "切换版本"
|
||||
"xraySwitchClick" = "点击你想切换的版本"
|
||||
"xraySwitchClickDesk" = "请谨慎选择,旧版本可能配置不兼容"
|
||||
"operationHours" = "运行时间"
|
||||
"operationHoursDesc" = "系统自启动以来的运行时间"
|
||||
"operationHours" = "系统正常运行时间"
|
||||
"systemLoad" = "系统负载"
|
||||
"systemLoadDesc" = "过去 1、5 和 15 分钟的系统平均负载"
|
||||
"connectionTcpCountDesc" = "所有网卡的总 TCP 连接数。"
|
||||
"connectionUdpCountDesc" = "所有网卡的总 UDP 连接数。"
|
||||
"connectionCount" = "连接数"
|
||||
"connectionCountDesc" = "所有网卡的总连接数"
|
||||
"upSpeed" = "所有网卡的总上传速度"
|
||||
"downSpeed" = "所有网卡的总下载速度"
|
||||
"totalSent" = "系统启动以来所有网卡的总上传流量"
|
||||
@@ -113,7 +114,7 @@
|
||||
"transportConfig" = "传输配置"
|
||||
"expireDate" = "到期时间"
|
||||
"resetTraffic" = "重置流量"
|
||||
"addInbound" = "添加入"
|
||||
"addInbound" = "添加入站"
|
||||
"generalActions" = "通用操作"
|
||||
"create" = "添加"
|
||||
"update" = "修改"
|
||||
@@ -126,7 +127,6 @@
|
||||
"network" = "网络"
|
||||
"destinationPort" = "目标端口"
|
||||
"targetAddress" = "目标地址"
|
||||
"disableInsecureEncryption" = "禁用不安全加密"
|
||||
"monitorDesc" = "默认留空即可"
|
||||
"meansNoLimit" = "表示不限制"
|
||||
"totalFlow" = "总流量"
|
||||
@@ -155,10 +155,10 @@
|
||||
"resetAllClientTrafficTitle" = "重置所有客户端流量"
|
||||
"resetAllClientTrafficContent" = "你确定要重置所有客户端的所有流量吗?"
|
||||
"delDepletedClients" = "删除耗尽的客户端"
|
||||
"delDepletedClientsTitle" = "删除耗尽的客户"
|
||||
"delDepletedClientsTitle" = "删除耗尽的客户端"
|
||||
"delDepletedClientsContent" = "你确定要删除所有耗尽的客户端吗?"
|
||||
"email" = "电子邮件"
|
||||
"emailDesc" = "电子邮件必须完全唯"
|
||||
"emailDesc" = "电子邮件必须完全唯一"
|
||||
"IPLimit" = "IP限制"
|
||||
"IPLimitDesc" = "如果超过输入的计数则禁用入站(0 表示禁用限制 ip)"
|
||||
"IPLimitlog" = "IP日志"
|
||||
@@ -167,12 +167,12 @@
|
||||
"setDefaultCert" = "从面板设置证书"
|
||||
"xtlsDesc" = "Xray核心需要1.7.5"
|
||||
"realityDesc" = "Xray核心需要1.8.0及以上版本"
|
||||
"telegramDesc" = "使用不带@的电报 ID 或聊天 ID(您可以在此处获取 @userinfobot)"
|
||||
"telegramDesc" = "使用 Telegram ID,不包含 @ 符号或聊天 ID(可以在 @userinfobot 处获取,或在机器人中使用'/id'命令)"
|
||||
"subscriptionDesc" = "您可以在详细信息上找到您的子链接,也可以对多个配置使用相同的名称"
|
||||
|
||||
[pages.client]
|
||||
"add" = "添加客户端"
|
||||
"edit" = "编辑客户"
|
||||
"edit" = "编辑客户端"
|
||||
"submitAdd" = "添加客户端"
|
||||
"submitEdit" = "保存修改"
|
||||
"clientCount" = "客户数量"
|
||||
@@ -220,6 +220,8 @@
|
||||
"TGBotSettings" = "TG提醒相关设置"
|
||||
"panelListeningIP" = "面板监听 IP"
|
||||
"panelListeningIPDesc" = "默认留空监听所有 IP"
|
||||
"panelListeningDomain" = "面板监听域名"
|
||||
"panelListeningDomainDesc" = "默认情况下留空以监视所有域名和 IP 地址"
|
||||
"panelPort" = "面板监听端口"
|
||||
"panelPortDesc" = "重启面板生效"
|
||||
"publicKeyPath" = "面板证书公钥文件路径"
|
||||
@@ -237,11 +239,13 @@
|
||||
"telegramToken" = "电报机器人TOKEN"
|
||||
"telegramTokenDesc" = "重启面板生效"
|
||||
"telegramChatId" = "以逗号分隔的多个 chatID 重启面板生效"
|
||||
"telegramChatIdDesc" = "多个聊天 ID 以逗号分隔。使用@userinfobot 获取您的聊天 ID。重新启动面板以应用更改。"
|
||||
"telegramChatIdDesc" = "多个聊天 ID 用逗号分隔。使用 @userinfobot 或在机器人中使用'/id'命令获取您的聊天 ID。"
|
||||
"telegramNotifyTime" = "电报机器人通知时间"
|
||||
"telegramNotifyTimeDesc" = "采用Crontab定时格式,重启面板生效"
|
||||
"tgNotifyBackup" = "数据库备份"
|
||||
"tgNotifyBackupDesc" = "正在发送数据库备份文件和报告通知"
|
||||
"tgNotifyLogin" = "登录通知"
|
||||
"tgNotifyLoginDesc" = "当有人试图登录您的面板时显示用户名、IP 地址和时间"
|
||||
"sessionMaxAge" = "会话最大年龄"
|
||||
"sessionMaxAgeDesc" = "您可以保持登录状态的时间(单位:分钟)"
|
||||
"expireTimeDiff" = "耗尽时间阈值"
|
||||
@@ -265,10 +269,14 @@
|
||||
"subKeyPathDesc" = "填写以'/'开头的绝对路径"
|
||||
"subPath" = "订阅 URL 根路径"
|
||||
"subPathDesc" = "必须以'/'开始并以'/'结束"
|
||||
"subDomain" = "监听域"
|
||||
"subDomain" = "监听域名"
|
||||
"subDomainDesc" = "留空默认监控所有域名和IP"
|
||||
"subUpdates" = "订阅更新间隔"
|
||||
"subUpdatesDesc" = "客户端应用程序更新之间的间隔时间"
|
||||
"subUpdatesDesc" = "客户端应用程序更新订阅的间隔时间"
|
||||
"subEncrypt" = "加密配置"
|
||||
"subEncryptDesc" = "在订阅中加密返回的配置"
|
||||
"subShowInfo" = "显示使用信息"
|
||||
"subShowInfoDesc" = "在配置名称后显示剩余流量和日期"
|
||||
|
||||
[pages.settings.templates]
|
||||
"title" = "模板"
|
||||
@@ -357,12 +365,12 @@
|
||||
"manualWARPDomains" = "WARP域名列表"
|
||||
|
||||
[pages.settings.security]
|
||||
"admin" = "行政"
|
||||
"secret" = "秘密令牌"
|
||||
"admin" = "管理员"
|
||||
"secret" = "密钥"
|
||||
"loginSecurity" = "登录安全"
|
||||
"loginSecurityDesc" = "在用户登录页面中切换附加步骤"
|
||||
"secretToken" = "秘密令牌"
|
||||
"secretTokenDesc" = "复制此秘密令牌并将其保存在安全的地方;没有这个你将无法登录。这也无法从 x-ui 命令工具中恢复"
|
||||
"secretToken" = "密钥"
|
||||
"secretTokenDesc" = "复制此密钥并将其保存在安全的地方;没有这个你将无法登录。这也无法从 x-ui 命令工具中恢复"
|
||||
|
||||
[pages.settings.toasts]
|
||||
"modifySettings" = "修改设置"
|
||||
@@ -383,6 +391,7 @@
|
||||
"months" = "月"
|
||||
"day" = "天"
|
||||
"days" = "天"
|
||||
"hours" = "小时"
|
||||
"unknown" = "未知"
|
||||
"inbounds" = "入站连接"
|
||||
"clients" = "客户端"
|
||||
@@ -395,6 +404,7 @@
|
||||
"welcome" = "🤖 欢迎来到<b>{{ .Hostname }}</b>管理机器人。\r\n"
|
||||
"status" = "✅ 机器人正常运行!"
|
||||
"usage" = "❗ 请输入要搜索的文本!"
|
||||
"getID" = "🆔 您的ID为:<code>{{ .ID }}</code>"
|
||||
"helpAdminCommands" = "搜索客户端邮箱:\r\n<code>/usage [Email]</code>\r\n \r\n搜索入站连接(包含客户端统计信息):\r\n<code>/inbound [Remark]</code>"
|
||||
"helpClientCommands" = "要搜索统计信息,请使用以下命令:\r\n \r\n<code>/usage [UUID|Password]</code>\r\n \r\n对于vmess/vless,请使用UUID;对于Trojan,请使用密码。"
|
||||
|
||||
@@ -423,20 +433,21 @@
|
||||
"time" = "⏰ 时间:{{ .Time }}\r\n"
|
||||
"inbound" = "📍 入站:{{ .Remark }}\r\n"
|
||||
"port" = "🔌 端口:{{ .Port }}\r\n"
|
||||
"expire" = "📅 过期日期:{{ .DateTime }}\r\n \r\n"
|
||||
"expireIn" = "📅 剩余时间:{{ .Time }}\r\n \r\n"
|
||||
"active" = "💡 激活:{{ .Enable }}\r\n"
|
||||
"expire" = "📅 过期日期:{{ .Time }}\r\n"
|
||||
"expireIn" = "📅 剩余时间:{{ .Time }}\r\n"
|
||||
"active" = "💡 激活:✅\r\n"
|
||||
"inactive" = "💡 激活: ❌\r\n"
|
||||
"email" = "📧 邮箱:{{ .Email }}\r\n"
|
||||
"upload" = "🔼 上传↑:{{ .Upload }}\r\n"
|
||||
"download" = "🔽 下载↓:{{ .Download }}\r\n"
|
||||
"total" = "🔄 总计:{{ .UpDown }} / {{ .Total }}\r\n"
|
||||
"total" = "📊 总计:{{ .UpDown }} / {{ .Total }}\r\n"
|
||||
"TGUser" = "👤 电报用户:{{ .TelegramID }}\r\n"
|
||||
"exhaustedMsg" = "🚨 耗尽的{{ .Type }}:\r\n"
|
||||
"exhaustedCount" = "🚨 耗尽的{{ .Type }}数量:\r\n"
|
||||
"disabled" = "🛑 禁用:{{ .Disabled }}\r\n"
|
||||
"depleteSoon" = "🔜 即将耗尽:{{ .Deplete }}\r\n \r\n"
|
||||
"backupTime" = "🗄 备份时间:{{ .Time }}\r\n"
|
||||
"refreshedOn" = "🔄🕒 刷新时间:{{ .Time }}\r\n"
|
||||
"refreshedOn" = "\r\n📋🔄 刷新时间:{{ .Time }}\r\n \r\n"
|
||||
|
||||
[tgbot.buttons]
|
||||
"closeKeyboard" = "❌ 关闭键盘"
|
||||
|
||||
54
web/web.go
@@ -19,11 +19,12 @@ import (
|
||||
"x-ui/web/controller"
|
||||
"x-ui/web/job"
|
||||
"x-ui/web/locale"
|
||||
"x-ui/web/middleware"
|
||||
"x-ui/web/network"
|
||||
"x-ui/web/service"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-contrib/sessions/cookie"
|
||||
sessions "github.com/Calidity/gin-sessions"
|
||||
"github.com/Calidity/gin-sessions/cookie"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
@@ -144,28 +145,6 @@ func (s *Server) getHtmlTemplate(funcMap template.FuncMap) (*template.Template,
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func redirectMiddleware(basePath string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Redirect from old '/xui' path to '/panel'
|
||||
path := c.Request.URL.Path
|
||||
redirects := map[string]string{
|
||||
"panel/API": "panel/api",
|
||||
"xui/API": "panel/api",
|
||||
"xui": "panel",
|
||||
}
|
||||
for from, to := range redirects {
|
||||
from, to = basePath+from, basePath+to
|
||||
if strings.HasPrefix(path, from) {
|
||||
newPath := to + path[len(from):]
|
||||
c.Redirect(http.StatusMovedPermanently, newPath)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
if config.IsDebug() {
|
||||
gin.SetMode(gin.DebugMode)
|
||||
@@ -177,6 +156,15 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
|
||||
engine := gin.Default()
|
||||
|
||||
webDomain, err := s.settingService.GetWebDomain()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if webDomain != "" {
|
||||
engine.Use(middleware.DomainValidatorMiddleware(webDomain))
|
||||
}
|
||||
|
||||
secret, err := s.settingService.GetSecret()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -233,7 +221,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
}
|
||||
|
||||
// Apply the redirect middleware (`/xui` to `/panel`)
|
||||
engine.Use(redirectMiddleware(basePath))
|
||||
engine.Use(middleware.RedirectMiddleware(basePath))
|
||||
|
||||
g := engine.Group(basePath)
|
||||
|
||||
@@ -253,18 +241,28 @@ func (s *Server) startTask() {
|
||||
// Check whether xray is running every 30 seconds
|
||||
s.cron.AddJob("@every 30s", job.NewCheckXrayRunningJob())
|
||||
|
||||
// Check if xray needs to be restarted
|
||||
s.cron.AddFunc("@every 10s", func() {
|
||||
if s.xrayService.IsNeedRestartAndSetFalse() {
|
||||
err := s.xrayService.RestartXray(false)
|
||||
if err != nil {
|
||||
logger.Error("restart xray failed:", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
go func() {
|
||||
time.Sleep(time.Second * 5)
|
||||
// Statistics every 10 seconds, start the delay for 5 seconds for the first time, and staggered with the time to restart xray
|
||||
s.cron.AddJob("@every 10s", job.NewXrayTrafficJob())
|
||||
}()
|
||||
|
||||
// Check the inbound traffic every 30 seconds that the traffic exceeds and expires
|
||||
s.cron.AddJob("@every 30s", job.NewCheckInboundJob())
|
||||
|
||||
// check client ips from log file every 10 sec
|
||||
s.cron.AddJob("@every 10s", job.NewCheckClientIpJob())
|
||||
|
||||
// check client ips from log file every day
|
||||
s.cron.AddJob("@daily", job.NewClearLogsJob())
|
||||
|
||||
// Make a traffic condition every day, 8:30
|
||||
var entry cron.EntryID
|
||||
isTgbotenabled, err := s.settingService.GetTgbotenabled()
|
||||
|
||||
496
x-ui.sh
@@ -54,8 +54,17 @@ elif [[ "${release}" == "debian" ]]; then
|
||||
if [[ ${os_version} -lt 10 ]]; then
|
||||
echo -e "${red} Please use Debian 10 or higher ${plain}\n" && exit 1
|
||||
fi
|
||||
elif [[ "${release}" == "arch" ]]; then
|
||||
echo "OS is ArchLinux"
|
||||
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
|
||||
@@ -289,6 +298,18 @@ show_log() {
|
||||
fi
|
||||
}
|
||||
|
||||
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
|
||||
else
|
||||
echo -e "${red}Log file not found. Please Install Fail2ban and IP Limit first.${plain}\n"
|
||||
fi
|
||||
}
|
||||
|
||||
enable_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 "${green}BBR is already enabled!${plain}"
|
||||
@@ -296,25 +317,28 @@ enable_bbr() {
|
||||
fi
|
||||
|
||||
# Check the OS and install necessary packages
|
||||
if [[ "$(cat /etc/os-release | grep -E '^ID=' | awk -F '=' '{print $2}')" == "ubuntu" ]]; then
|
||||
sudo apt-get update && sudo apt-get install -yqq --no-install-recommends ca-certificates
|
||||
elif [[ "$(cat /etc/os-release | grep -E '^ID=' | awk -F '=' '{print $2}')" == "debian" ]]; then
|
||||
sudo apt-get update && sudo apt-get install -yqq --no-install-recommends ca-certificates
|
||||
elif [[ "$(cat /etc/os-release | grep -E '^ID=' | awk -F '=' '{print $2}')" == "fedora" ]]; then
|
||||
sudo dnf -y update && sudo dnf -y install ca-certificates
|
||||
elif [[ "$(cat /etc/os-release | grep -E '^ID=' | awk -F '=' '{print $2}')" == "centos" ]]; then
|
||||
sudo yum -y update && sudo yum -y install ca-certificates
|
||||
else
|
||||
echo "Unsupported operating system. Please check the script and install the necessary packages manually."
|
||||
exit 1
|
||||
fi
|
||||
case "${release}" in
|
||||
ubuntu|debian)
|
||||
apt-get update && apt-get install -yqq --no-install-recommends ca-certificates
|
||||
;;
|
||||
centos)
|
||||
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
|
||||
echo "net.core.default_qdisc=fq" | sudo tee -a /etc/sysctl.conf
|
||||
echo "net.ipv4.tcp_congestion_control=bbr" | sudo tee -a /etc/sysctl.conf
|
||||
echo "net.core.default_qdisc=fq" | tee -a /etc/sysctl.conf
|
||||
echo "net.ipv4.tcp_congestion_control=bbr" | tee -a /etc/sysctl.conf
|
||||
|
||||
# Apply changes
|
||||
sudo sysctl -p
|
||||
sysctl -p
|
||||
|
||||
# Verify that BBR is enabled
|
||||
if [[ $(sysctl net.ipv4.tcp_congestion_control | awk '{print $3}') == "bbr" ]]; then
|
||||
@@ -434,24 +458,24 @@ show_xray_status() {
|
||||
open_ports() {
|
||||
if ! command -v ufw &>/dev/null; then
|
||||
echo "ufw firewall is not installed. Installing now..."
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y ufw
|
||||
apt-get update
|
||||
apt-get install -y ufw
|
||||
else
|
||||
echo "ufw firewall is already installed"
|
||||
fi
|
||||
|
||||
# Check if the firewall is inactive
|
||||
if sudo ufw status | grep -q "Status: active"; then
|
||||
if ufw status | grep -q "Status: active"; then
|
||||
echo "firewall is already active"
|
||||
else
|
||||
# Open the necessary ports
|
||||
sudo ufw allow ssh
|
||||
sudo ufw allow http
|
||||
sudo ufw allow https
|
||||
sudo ufw allow 2053/tcp
|
||||
ufw allow ssh
|
||||
ufw allow http
|
||||
ufw allow https
|
||||
ufw allow 2053/tcp
|
||||
|
||||
# Enable the firewall
|
||||
sudo ufw --force enable
|
||||
ufw --force enable
|
||||
fi
|
||||
|
||||
# Prompt the user to enter a list of ports
|
||||
@@ -472,15 +496,15 @@ open_ports() {
|
||||
end_port=$(echo $port | cut -d'-' -f2)
|
||||
# Loop through the range and open each port
|
||||
for ((i = start_port; i <= end_port; i++)); do
|
||||
sudo ufw allow $i
|
||||
ufw allow $i
|
||||
done
|
||||
else
|
||||
sudo ufw allow "$port"
|
||||
ufw allow "$port"
|
||||
fi
|
||||
done
|
||||
|
||||
# Confirm that the ports are open
|
||||
sudo ufw status | grep $ports
|
||||
ufw status | grep $ports
|
||||
}
|
||||
|
||||
update_geo() {
|
||||
@@ -495,12 +519,13 @@ update_geo() {
|
||||
|
||||
systemctl stop x-ui
|
||||
cd ${binFolder}
|
||||
rm -f geoip.dat geosite.dat iran.dat
|
||||
rm -f geoip.dat geosite.dat geoip_IR.dat geosite_IR.dat
|
||||
wget -N https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
|
||||
wget -N https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
|
||||
wget -N https://github.com/bootmortis/iran-hosted-domains/releases/latest/download/iran.dat
|
||||
wget -O geoip_IR.dat -N https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
|
||||
wget -O geosite_IR.dat -N https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
|
||||
systemctl start x-ui
|
||||
echo -e "${green}Geosite.dat + Geoip.dat + Iran.dat have been updated successfully in bin folder '${binfolder}'!${plain}"
|
||||
echo -e "${green}Geosite.dat + Geoip.dat + geoip_IR.dat + geosite_IR.dat have been updated successfully in bin folder '${binfolder}'!${plain}"
|
||||
before_show_menu
|
||||
}
|
||||
|
||||
@@ -518,12 +543,16 @@ install_acme() {
|
||||
}
|
||||
|
||||
ssl_cert_issue_main() {
|
||||
echo "1) Get SSL"
|
||||
echo "2) Revoke"
|
||||
echo "3) Force Renew"
|
||||
echo -e "${green}\t1.${plain} Get SSL"
|
||||
echo -e "${green}\t2.${plain} Revoke"
|
||||
echo -e "${green}\t3.${plain} Force Renew"
|
||||
echo -e "${green}\t0.${plain} Back to Main Menu"
|
||||
read -p "Choose an option: " choice
|
||||
case "$choice" in
|
||||
1) ssl_cert_issue ;;
|
||||
0)
|
||||
show_menu ;;
|
||||
1)
|
||||
ssl_cert_issue ;;
|
||||
2)
|
||||
local domain=""
|
||||
read -p "Please enter your domain name to revoke the certificate: " domain
|
||||
@@ -539,7 +568,7 @@ ssl_cert_issue_main() {
|
||||
}
|
||||
|
||||
ssl_cert_issue() {
|
||||
#check for acme.sh first
|
||||
# check for acme.sh first
|
||||
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
|
||||
echo "acme.sh could not be found. we will install it"
|
||||
install_acme
|
||||
@@ -548,24 +577,30 @@ ssl_cert_issue() {
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
#install socat second
|
||||
if [[ "${release}" == "centos" ]] || [[ "${release}" == "fedora" ]]; then
|
||||
yum install socat -y
|
||||
else
|
||||
apt install socat -y
|
||||
fi
|
||||
# install socat second
|
||||
case "${release}" in
|
||||
ubuntu|debian)
|
||||
apt update && apt install socat -y ;;
|
||||
centos)
|
||||
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"
|
||||
LOGE "install socat failed, please check logs"
|
||||
exit 1
|
||||
else
|
||||
LOGI "install socat succeed..."
|
||||
fi
|
||||
|
||||
#get the domain here,and we need verify it
|
||||
# get the domain here,and we need verify it
|
||||
local domain=""
|
||||
read -p "Please enter your domain name:" domain
|
||||
LOGD "your domain is:${domain},check it..."
|
||||
#here we need to judge whether there exists cert already
|
||||
# here we need to judge whether there exists cert already
|
||||
local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}')
|
||||
|
||||
if [ ${currentCert} == ${domain} ]; then
|
||||
@@ -577,7 +612,7 @@ ssl_cert_issue() {
|
||||
LOGI "your domain is ready for issuing cert now..."
|
||||
fi
|
||||
|
||||
#create a directory for install cert
|
||||
# create a directory for install cert
|
||||
certPath="/root/cert/${domain}"
|
||||
if [ ! -d "$certPath" ]; then
|
||||
mkdir -p "$certPath"
|
||||
@@ -586,15 +621,15 @@ ssl_cert_issue() {
|
||||
mkdir -p "$certPath"
|
||||
fi
|
||||
|
||||
#get needed port here
|
||||
# get needed port here
|
||||
local WebPort=80
|
||||
read -p "please choose which port do you use,default will be 80 port:" WebPort
|
||||
if [[ ${WebPort} -gt 65535 || ${WebPort} -lt 1 ]]; then
|
||||
LOGE "your input ${WebPort} is invalid,will use default port"
|
||||
fi
|
||||
LOGI "will use port:${WebPort} to issue certs,please make sure this port is open..."
|
||||
#NOTE:This should be handled by user
|
||||
#open the port and kill the occupied progress
|
||||
# NOTE:This should be handled by user
|
||||
# open the port and kill the occupied progress
|
||||
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt
|
||||
~/.acme.sh/acme.sh --issue -d ${domain} --standalone --httpport ${WebPort}
|
||||
if [ $? -ne 0 ]; then
|
||||
@@ -604,7 +639,7 @@ ssl_cert_issue() {
|
||||
else
|
||||
LOGE "issue certs succeed,installing certs..."
|
||||
fi
|
||||
#install cert
|
||||
# install cert
|
||||
~/.acme.sh/acme.sh --installcert -d ${domain} \
|
||||
--key-file /root/cert/${domain}/privkey.pem \
|
||||
--fullchain-file /root/cert/${domain}/fullchain.pem
|
||||
@@ -628,13 +663,109 @@ ssl_cert_issue() {
|
||||
ls -lah cert/*
|
||||
chmod 755 $certPath/*
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
warp_fixchatgpt() {
|
||||
curl -fsSL https://gist.githubusercontent.com/hamid-gh98/dc5dd9b0cc5b0412af927b1ccdb294c7/raw/install_warp_proxy.sh | bash
|
||||
echo ""
|
||||
before_show_menu
|
||||
ssl_cert_issue_CF() {
|
||||
echo -E ""
|
||||
LOGD "******Instructions for use******"
|
||||
LOGI "This Acme script requires the following data:"
|
||||
LOGI "1.Cloudflare Registered e-mail"
|
||||
LOGI "2.Cloudflare Global API Key"
|
||||
LOGI "3.The domain name that has been resolved dns to the current server by Cloudflare"
|
||||
LOGI "4.The script applies for a certificate. The default installation path is /root/cert "
|
||||
confirm "Confirmed?[y/n]" "y"
|
||||
if [ $? -eq 0 ]; then
|
||||
# check for acme.sh first
|
||||
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
|
||||
echo "acme.sh could not be found. we will install it"
|
||||
install_acme
|
||||
if [ $? -ne 0 ]; then
|
||||
LOGE "install acme failed, please check logs"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
CF_Domain=""
|
||||
CF_GlobalKey=""
|
||||
CF_AccountEmail=""
|
||||
certPath=/root/cert
|
||||
if [ ! -d "$certPath" ]; then
|
||||
mkdir $certPath
|
||||
else
|
||||
rm -rf $certPath
|
||||
mkdir $certPath
|
||||
fi
|
||||
LOGD "Please set a domain name:"
|
||||
read -p "Input your domain here:" CF_Domain
|
||||
LOGD "Your domain name is set to:${CF_Domain}"
|
||||
LOGD "Please set the API key:"
|
||||
read -p "Input your key here:" CF_GlobalKey
|
||||
LOGD "Your API key is:${CF_GlobalKey}"
|
||||
LOGD "Please set up registered email:"
|
||||
read -p "Input your email here:" CF_AccountEmail
|
||||
LOGD "Your registered email address is:${CF_AccountEmail}"
|
||||
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt
|
||||
if [ $? -ne 0 ]; then
|
||||
LOGE "Default CA, Lets'Encrypt fail, script exiting..."
|
||||
exit 1
|
||||
fi
|
||||
export CF_Key="${CF_GlobalKey}"
|
||||
export CF_Email=${CF_AccountEmail}
|
||||
~/.acme.sh/acme.sh --issue --dns dns_cf -d ${CF_Domain} -d *.${CF_Domain} --log
|
||||
if [ $? -ne 0 ]; then
|
||||
LOGE "Certificate issuance failed, script exiting..."
|
||||
exit 1
|
||||
else
|
||||
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
|
||||
if [ $? -ne 0 ]; then
|
||||
LOGE "Certificate installation failed, script exiting..."
|
||||
exit 1
|
||||
else
|
||||
LOGI "Certificate installed Successfully,Turning on automatic updates..."
|
||||
fi
|
||||
~/.acme.sh/acme.sh --upgrade --auto-upgrade
|
||||
if [ $? -ne 0 ]; then
|
||||
LOGE "Auto update setup Failed, script exiting..."
|
||||
ls -lah cert
|
||||
chmod 755 $certPath
|
||||
exit 1
|
||||
else
|
||||
LOGI "The certificate is installed and auto-renewal is turned on, Specific information is as follows"
|
||||
ls -lah cert
|
||||
chmod 755 $certPath
|
||||
fi
|
||||
else
|
||||
show_menu
|
||||
fi
|
||||
}
|
||||
|
||||
warp_cloudflare() {
|
||||
echo -e "${green}\t1.${plain} Install WARP socks5 proxy"
|
||||
echo -e "${green}\t2.${plain} Account Type (free, plus, team)"
|
||||
echo -e "${green}\t3.${plain} Turn on/off WireProxy"
|
||||
echo -e "${green}\t4.${plain} Uninstall WARP"
|
||||
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" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
run_speedtest() {
|
||||
@@ -642,24 +773,19 @@ run_speedtest() {
|
||||
if ! command -v speedtest &> /dev/null; then
|
||||
# If not installed, install it
|
||||
local pkg_manager=""
|
||||
local curl_install_cmd=""
|
||||
local speedtest_install_script=""
|
||||
|
||||
if command -v dnf &> /dev/null; then
|
||||
pkg_manager="dnf"
|
||||
curl_install_cmd="sudo dnf install -y curl"
|
||||
speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh"
|
||||
elif command -v yum &> /dev/null; then
|
||||
pkg_manager="yum"
|
||||
curl_install_cmd="sudo yum install -y curl"
|
||||
speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh"
|
||||
elif command -v apt-get &> /dev/null; then
|
||||
pkg_manager="apt-get"
|
||||
curl_install_cmd="sudo apt-get update && sudo apt-get install -y curl"
|
||||
speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh"
|
||||
elif command -v apt &> /dev/null; then
|
||||
pkg_manager="apt"
|
||||
curl_install_cmd="sudo apt update && sudo apt install -y curl"
|
||||
speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh"
|
||||
fi
|
||||
|
||||
@@ -667,9 +793,8 @@ run_speedtest() {
|
||||
echo "Error: Package manager not found. You may need to install Speedtest manually."
|
||||
return 1
|
||||
else
|
||||
$curl_install_cmd
|
||||
curl -s $speedtest_install_script | sudo bash
|
||||
sudo $pkg_manager install -y speedtest
|
||||
curl -s $speedtest_install_script | bash
|
||||
$pkg_manager install -y speedtest
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -677,6 +802,210 @@ run_speedtest() {
|
||||
speedtest
|
||||
}
|
||||
|
||||
create_iplimit_jails() {
|
||||
# Use default bantime if not passed => 5 minutes
|
||||
local bantime="${1:-5}"
|
||||
|
||||
cat << EOF > /etc/fail2ban/jail.d/3x-ipl.conf
|
||||
[3x-ipl]
|
||||
enabled=true
|
||||
filter=3x-ipl
|
||||
action=3x-ipl
|
||||
logpath=${iplimit_log_path}
|
||||
maxretry=4
|
||||
findtime=60
|
||||
bantime=${bantime}m
|
||||
EOF
|
||||
|
||||
cat << EOF > /etc/fail2ban/filter.d/3x-ipl.conf
|
||||
[Definition]
|
||||
datepattern = ^%%Y/%%m/%%d %%H:%%M:%%S
|
||||
failregex = \[LIMIT_IP\]\s*Email\s*=\s*<F-USER>.+</F-USER>\s*\|\|\s*SRC\s*=\s*<ADDR>
|
||||
ignoreregex =
|
||||
EOF
|
||||
|
||||
cat << EOF > /etc/fail2ban/action.d/3x-ipl.conf
|
||||
[INCLUDES]
|
||||
before = iptables-common.conf
|
||||
|
||||
[Definition]
|
||||
actionstart = <iptables> -N f2b-<name>
|
||||
<iptables> -A f2b-<name> -j <returntype>
|
||||
<iptables> -I <chain> -p <protocol> -j f2b-<name>
|
||||
|
||||
actionstop = <iptables> -D <chain> -p <protocol> -j f2b-<name>
|
||||
<actionflush>
|
||||
<iptables> -X f2b-<name>
|
||||
|
||||
actioncheck = <iptables> -n -L <chain> | grep -q 'f2b-<name>[ \t]'
|
||||
|
||||
actionban = <iptables> -I f2b-<name> 1 -s <ip> -j <blocktype>
|
||||
echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") BAN [Email] = <F-USER> [IP] = <ip> banned for <bantime> seconds." >> ${iplimit_banned_log_path}
|
||||
|
||||
actionunban = <iptables> -D f2b-<name> -s <ip> -j <blocktype>
|
||||
echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") UNBAN [Email] = <F-USER> [IP] = <ip> unbanned." >> ${iplimit_banned_log_path}
|
||||
|
||||
[Init]
|
||||
EOF
|
||||
|
||||
echo -e "${green}Created Ip Limit jail files with a bantime of ${bantime} minutes.${plain}"
|
||||
}
|
||||
|
||||
iplimit_remove_conflicts() {
|
||||
local jail_files=(
|
||||
/etc/fail2ban/jail.conf
|
||||
/etc/fail2ban/jail.local
|
||||
)
|
||||
|
||||
for file in "${jail_files[@]}"; do
|
||||
# Check for [3x-ipl] config in jail file then remove it
|
||||
if test -f "${file}" && grep -qw '3x-ipl' ${file}; then
|
||||
sed -i "/\[3x-ipl\]/,/^$/d" ${file}
|
||||
echo -e "${yellow}Removing conflicts of [3x-ipl] in jail (${file})!${plain}\n"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
iplimit_main() {
|
||||
echo -e "\n${green}\t1.${plain} Install Fail2ban and configure IP Limit"
|
||||
echo -e "${green}\t2.${plain} Change Ban Duration"
|
||||
echo -e "${green}\t3.${plain} Unban Everyone"
|
||||
echo -e "${green}\t4.${plain} Check Logs"
|
||||
echo -e "${green}\t5.${plain} fail2ban status"
|
||||
echo -e "${green}\t6.${plain} Uninstall IP Limit"
|
||||
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 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" ;;
|
||||
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)
|
||||
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
|
||||
echo -e "${green}Fail2ban installed successfully!${plain}\n"
|
||||
else
|
||||
echo -e "${yellow}Fail2ban is already installed.${plain}\n"
|
||||
fi
|
||||
|
||||
echo -e "${green}Configuring IP Limit...${plain}\n"
|
||||
|
||||
# make sure there's no conflict for jail files
|
||||
iplimit_remove_conflicts
|
||||
|
||||
# Check if log file exists
|
||||
if ! test -f "${iplimit_banned_log_path}"; then
|
||||
touch ${iplimit_banned_log_path}
|
||||
fi
|
||||
|
||||
# Check if service log file exists so fail2ban won't return error
|
||||
if ! test -f "${iplimit_log_path}"; then
|
||||
touch ${iplimit_log_path}
|
||||
fi
|
||||
|
||||
# Create the iplimit jail files
|
||||
# we didn't pass the bantime here to use the default value
|
||||
create_iplimit_jails
|
||||
|
||||
# Launching fail2ban
|
||||
if ! systemctl is-active --quiet fail2ban; then
|
||||
systemctl start fail2ban
|
||||
else
|
||||
systemctl restart fail2ban
|
||||
fi
|
||||
systemctl enable fail2ban
|
||||
|
||||
echo -e "${green}IP Limit installed and configured successfully!${plain}\n"
|
||||
before_show_menu
|
||||
}
|
||||
|
||||
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 purge fail2ban -y;;
|
||||
centos)
|
||||
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
|
||||
}
|
||||
|
||||
show_usage() {
|
||||
echo "x-ui control menu usages: "
|
||||
echo "------------------------------------------"
|
||||
@@ -688,6 +1017,7 @@ show_usage() {
|
||||
echo -e "x-ui enable - Enable x-ui on system startup"
|
||||
echo -e "x-ui disable - Disable x-ui on system startup"
|
||||
echo -e "x-ui log - Check x-ui logs"
|
||||
echo -e "x-ui banlog - Check Fail2ban ban logs"
|
||||
echo -e "x-ui update - Update x-ui "
|
||||
echo -e "x-ui install - Install x-ui "
|
||||
echo -e "x-ui uninstall - Uninstall x-ui "
|
||||
@@ -717,15 +1047,18 @@ show_menu() {
|
||||
${green}13.${plain} Enable x-ui On System Startup
|
||||
${green}14.${plain} Disable x-ui On System Startup
|
||||
————————————————
|
||||
${green}15.${plain} Enable BBR
|
||||
${green}16.${plain} SSL Certificate Management
|
||||
${green}17.${plain} Update Geo Files
|
||||
${green}18.${plain} Active Firewall and open ports
|
||||
${green}19.${plain} Install WARP
|
||||
${green}20.${plain} Speedtest by Ookla
|
||||
"
|
||||
${green}15.${plain} SSL Certificate Management
|
||||
${green}16.${plain} Cloudflare SSL Certificate
|
||||
${green}17.${plain} IP Limit Management
|
||||
${green}18.${plain} WARP Management
|
||||
————————————————
|
||||
${green}19.${plain} Enable BBR
|
||||
${green}20.${plain} Update Geo Files
|
||||
${green}21.${plain} Active Firewall and open ports
|
||||
${green}22.${plain} Speedtest by Ookla
|
||||
"
|
||||
show_status
|
||||
echo && read -p "Please enter your selection [0-20]: " num
|
||||
echo && read -p "Please enter your selection [0-22]: " num
|
||||
|
||||
case "${num}" in
|
||||
0)
|
||||
@@ -774,25 +1107,31 @@ show_menu() {
|
||||
check_install && disable
|
||||
;;
|
||||
15)
|
||||
enable_bbr
|
||||
;;
|
||||
16)
|
||||
ssl_cert_issue_main
|
||||
;;
|
||||
16)
|
||||
ssl_cert_issue_CF
|
||||
;;
|
||||
17)
|
||||
update_geo
|
||||
iplimit_main
|
||||
;;
|
||||
18)
|
||||
open_ports
|
||||
warp_cloudflare
|
||||
;;
|
||||
19)
|
||||
warp_fixchatgpt
|
||||
enable_bbr
|
||||
;;
|
||||
20)
|
||||
run_speedtest
|
||||
update_geo
|
||||
;;
|
||||
21)
|
||||
open_ports
|
||||
;;
|
||||
22)
|
||||
run_speedtest
|
||||
;;
|
||||
*)
|
||||
LOGE "Please enter the correct number [0-20]"
|
||||
LOGE "Please enter the correct number [0-22]"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
@@ -820,6 +1159,9 @@ if [[ $# > 0 ]]; then
|
||||
"log")
|
||||
check_install 0 && show_log 0
|
||||
;;
|
||||
"banlog")
|
||||
check_install 0 && show_banlog 0
|
||||
;;
|
||||
"update")
|
||||
check_install 0 && update 0
|
||||
;;
|
||||
|
||||
236
xray/api.go
Normal file
@@ -0,0 +1,236 @@
|
||||
package xray
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"time"
|
||||
"x-ui/logger"
|
||||
"x-ui/util/common"
|
||||
|
||||
"github.com/xtls/xray-core/app/proxyman/command"
|
||||
statsService "github.com/xtls/xray-core/app/stats/command"
|
||||
"github.com/xtls/xray-core/common/protocol"
|
||||
"github.com/xtls/xray-core/common/serial"
|
||||
"github.com/xtls/xray-core/infra/conf"
|
||||
"github.com/xtls/xray-core/proxy/shadowsocks"
|
||||
"github.com/xtls/xray-core/proxy/shadowsocks_2022"
|
||||
"github.com/xtls/xray-core/proxy/trojan"
|
||||
"github.com/xtls/xray-core/proxy/vless"
|
||||
"github.com/xtls/xray-core/proxy/vmess"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
type XrayAPI struct {
|
||||
HandlerServiceClient *command.HandlerServiceClient
|
||||
StatsServiceClient *statsService.StatsServiceClient
|
||||
grpcClient *grpc.ClientConn
|
||||
isConnected bool
|
||||
}
|
||||
|
||||
func (x *XrayAPI) Init(apiPort int) (err error) {
|
||||
if apiPort == 0 {
|
||||
return common.NewError("xray api port wrong:", apiPort)
|
||||
}
|
||||
x.grpcClient, err = grpc.Dial(fmt.Sprintf("127.0.0.1:%v", apiPort), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
x.isConnected = true
|
||||
|
||||
hsClient := command.NewHandlerServiceClient(x.grpcClient)
|
||||
ssClient := statsService.NewStatsServiceClient(x.grpcClient)
|
||||
|
||||
x.HandlerServiceClient = &hsClient
|
||||
x.StatsServiceClient = &ssClient
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (x *XrayAPI) Close() {
|
||||
x.grpcClient.Close()
|
||||
x.HandlerServiceClient = nil
|
||||
x.StatsServiceClient = nil
|
||||
x.isConnected = false
|
||||
}
|
||||
|
||||
func (x *XrayAPI) AddInbound(inbound []byte) error {
|
||||
client := *x.HandlerServiceClient
|
||||
|
||||
conf := new(conf.InboundDetourConfig)
|
||||
err := json.Unmarshal(inbound, conf)
|
||||
if err != nil {
|
||||
logger.Debug("Failed to unmarshal inbound:", err)
|
||||
return err
|
||||
}
|
||||
config, err := conf.Build()
|
||||
if err != nil {
|
||||
logger.Debug("Failed to build inbound Detur:", err)
|
||||
return err
|
||||
}
|
||||
inboundConfig := command.AddInboundRequest{Inbound: config}
|
||||
|
||||
_, err = client.AddInbound(context.Background(), &inboundConfig)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (x *XrayAPI) DelInbound(tag string) error {
|
||||
client := *x.HandlerServiceClient
|
||||
_, err := client.RemoveInbound(context.Background(), &command.RemoveInboundRequest{
|
||||
Tag: tag,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (x *XrayAPI) AddUser(Protocol string, inboundTag string, user map[string]interface{}) error {
|
||||
var account *serial.TypedMessage
|
||||
switch Protocol {
|
||||
case "vmess":
|
||||
account = serial.ToTypedMessage(&vmess.Account{
|
||||
Id: user["id"].(string),
|
||||
})
|
||||
case "vless":
|
||||
account = serial.ToTypedMessage(&vless.Account{
|
||||
Id: user["id"].(string),
|
||||
Flow: user["flow"].(string),
|
||||
})
|
||||
case "trojan":
|
||||
account = serial.ToTypedMessage(&trojan.Account{
|
||||
Password: user["password"].(string),
|
||||
})
|
||||
case "shadowsocks":
|
||||
var ssCipherType shadowsocks.CipherType
|
||||
switch user["cipher"].(string) {
|
||||
case "aes-128-gcm":
|
||||
ssCipherType = shadowsocks.CipherType_AES_128_GCM
|
||||
case "aes-256-gcm":
|
||||
ssCipherType = shadowsocks.CipherType_AES_256_GCM
|
||||
case "chacha20-poly1305", "chacha20-ietf-poly1305":
|
||||
ssCipherType = shadowsocks.CipherType_CHACHA20_POLY1305
|
||||
case "xchacha20-poly1305", "xchacha20-ietf-poly1305":
|
||||
ssCipherType = shadowsocks.CipherType_XCHACHA20_POLY1305
|
||||
default:
|
||||
ssCipherType = shadowsocks.CipherType_NONE
|
||||
}
|
||||
|
||||
if ssCipherType != shadowsocks.CipherType_NONE {
|
||||
account = serial.ToTypedMessage(&shadowsocks.Account{
|
||||
Password: user["password"].(string),
|
||||
CipherType: ssCipherType,
|
||||
})
|
||||
} else {
|
||||
account = serial.ToTypedMessage(&shadowsocks_2022.User{
|
||||
Key: user["password"].(string),
|
||||
Email: user["email"].(string),
|
||||
})
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
client := *x.HandlerServiceClient
|
||||
|
||||
_, err := client.AlterInbound(context.Background(), &command.AlterInboundRequest{
|
||||
Tag: inboundTag,
|
||||
Operation: serial.ToTypedMessage(&command.AddUserOperation{
|
||||
User: &protocol.User{
|
||||
Email: user["email"].(string),
|
||||
Account: account,
|
||||
},
|
||||
}),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (x *XrayAPI) RemoveUser(inboundTag string, email string) error {
|
||||
client := *x.HandlerServiceClient
|
||||
_, err := client.AlterInbound(context.Background(), &command.AlterInboundRequest{
|
||||
Tag: inboundTag,
|
||||
Operation: serial.ToTypedMessage(&command.RemoveUserOperation{
|
||||
Email: email,
|
||||
}),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (x *XrayAPI) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) {
|
||||
if x.grpcClient == nil {
|
||||
return nil, nil, common.NewError("xray api is not initialized")
|
||||
}
|
||||
var trafficRegex = regexp.MustCompile("(inbound|outbound)>>>([^>]+)>>>traffic>>>(downlink|uplink)")
|
||||
var ClientTrafficRegex = regexp.MustCompile("(user)>>>([^>]+)>>>traffic>>>(downlink|uplink)")
|
||||
|
||||
client := *x.StatsServiceClient
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
request := &statsService.QueryStatsRequest{
|
||||
Reset_: reset,
|
||||
}
|
||||
resp, err := client.QueryStats(ctx, request)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
tagTrafficMap := map[string]*Traffic{}
|
||||
emailTrafficMap := map[string]*ClientTraffic{}
|
||||
|
||||
clientTraffics := make([]*ClientTraffic, 0)
|
||||
traffics := make([]*Traffic, 0)
|
||||
for _, stat := range resp.GetStat() {
|
||||
matchs := trafficRegex.FindStringSubmatch(stat.Name)
|
||||
if len(matchs) < 3 {
|
||||
|
||||
matchs := ClientTrafficRegex.FindStringSubmatch(stat.Name)
|
||||
if len(matchs) < 3 {
|
||||
continue
|
||||
} else {
|
||||
|
||||
isUser := matchs[1] == "user"
|
||||
email := matchs[2]
|
||||
isDown := matchs[3] == "downlink"
|
||||
if !isUser {
|
||||
continue
|
||||
}
|
||||
traffic, ok := emailTrafficMap[email]
|
||||
if !ok {
|
||||
traffic = &ClientTraffic{
|
||||
Email: email,
|
||||
}
|
||||
emailTrafficMap[email] = traffic
|
||||
clientTraffics = append(clientTraffics, traffic)
|
||||
}
|
||||
if isDown {
|
||||
traffic.Down = stat.Value
|
||||
} else {
|
||||
traffic.Up = stat.Value
|
||||
}
|
||||
|
||||
}
|
||||
continue
|
||||
}
|
||||
isInbound := matchs[1] == "inbound"
|
||||
tag := matchs[2]
|
||||
isDown := matchs[3] == "downlink"
|
||||
if tag == "api" {
|
||||
continue
|
||||
}
|
||||
traffic, ok := tagTrafficMap[tag]
|
||||
if !ok {
|
||||
traffic = &Traffic{
|
||||
IsInbound: isInbound,
|
||||
Tag: tag,
|
||||
}
|
||||
tagTrafficMap[tag] = traffic
|
||||
traffics = append(traffics, traffic)
|
||||
}
|
||||
if isDown {
|
||||
traffic.Down = stat.Value
|
||||
} else {
|
||||
traffic.Up = stat.Value
|
||||
}
|
||||
}
|
||||
|
||||
return traffics, clientTraffics, nil
|
||||
}
|
||||
151
xray/process.go
@@ -3,30 +3,25 @@ package xray
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"x-ui/config"
|
||||
"x-ui/logger"
|
||||
"x-ui/util/common"
|
||||
|
||||
"github.com/Workiva/go-datastructures/queue"
|
||||
statsservice "github.com/xtls/xray-core/app/stats/command"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
var trafficRegex = regexp.MustCompile("(inbound|outbound)>>>([^>]+)>>>traffic>>>(downlink|uplink)")
|
||||
var ClientTrafficRegex = regexp.MustCompile("(user)>>>([^>]+)>>>traffic>>>(downlink|uplink)")
|
||||
|
||||
func GetBinaryName() string {
|
||||
return fmt.Sprintf("xray-%s-%s", runtime.GOOS, runtime.GOARCH)
|
||||
}
|
||||
@@ -47,12 +42,40 @@ func GetGeoipPath() string {
|
||||
return config.GetBinFolderPath() + "/geoip.dat"
|
||||
}
|
||||
|
||||
func GetIranPath() string {
|
||||
return config.GetBinFolderPath() + "/iran.dat"
|
||||
func GetIPLimitLogPath() string {
|
||||
return config.GetLogFolder() + "/3xipl.log"
|
||||
}
|
||||
|
||||
func GetBlockedIPsPath() string {
|
||||
return config.GetBinFolderPath() + "/blockedIPs"
|
||||
func GetIPLimitBannedLogPath() string {
|
||||
return config.GetLogFolder() + "/3xipl-banned.log"
|
||||
}
|
||||
|
||||
func GetAccessPersistentLogPath() string {
|
||||
return config.GetLogFolder() + "/3xipl-access-persistent.log"
|
||||
}
|
||||
|
||||
func GetAccessLogPath() string {
|
||||
config, err := os.ReadFile(GetConfigPath())
|
||||
if err != nil {
|
||||
logger.Warningf("Something went wrong: %s", err)
|
||||
}
|
||||
|
||||
jsonConfig := map[string]interface{}{}
|
||||
err = json.Unmarshal([]byte(config), &jsonConfig)
|
||||
if err != nil {
|
||||
logger.Warningf("Something went wrong: %s", err)
|
||||
}
|
||||
|
||||
if jsonConfig["log"] != nil {
|
||||
jsonLog := jsonConfig["log"].(map[string]interface{})
|
||||
if jsonLog["access"] != nil {
|
||||
|
||||
accessLogPath := jsonLog["access"].(string)
|
||||
|
||||
return accessLogPath
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func stopProcess(p *Process) {
|
||||
@@ -75,16 +98,18 @@ type process struct {
|
||||
version string
|
||||
apiPort int
|
||||
|
||||
config *Config
|
||||
lines *queue.Queue
|
||||
exitErr error
|
||||
config *Config
|
||||
lines *queue.Queue
|
||||
exitErr error
|
||||
startTime time.Time
|
||||
}
|
||||
|
||||
func newProcess(config *Config) *process {
|
||||
return &process{
|
||||
version: "Unknown",
|
||||
config: config,
|
||||
lines: queue.New(100),
|
||||
version: "Unknown",
|
||||
config: config,
|
||||
lines: queue.New(100),
|
||||
startTime: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,6 +153,10 @@ func (p *Process) GetConfig() *Config {
|
||||
return p.config
|
||||
}
|
||||
|
||||
func (p *Process) GetUptime() uint64 {
|
||||
return uint64(time.Since(p.startTime).Seconds())
|
||||
}
|
||||
|
||||
func (p *process) refreshAPIPort() {
|
||||
for _, inbound := range p.config.InboundConfigs {
|
||||
if inbound.Tag == "api" {
|
||||
@@ -173,7 +202,7 @@ func (p *process) Start() (err error) {
|
||||
return common.NewErrorf("Failed to write configuration file: %v", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command(GetBinaryPath(), "-c", configPath, "-restrictedIPsPath", GetBlockedIPsPath())
|
||||
cmd := exec.Command(GetBinaryPath(), "-c", configPath)
|
||||
p.cmd = cmd
|
||||
|
||||
stdReader, err := cmd.StdoutPipe()
|
||||
@@ -236,87 +265,5 @@ func (p *process) Stop() error {
|
||||
if !p.IsRunning() {
|
||||
return errors.New("xray is not running")
|
||||
}
|
||||
return p.cmd.Process.Kill()
|
||||
}
|
||||
|
||||
func (p *process) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) {
|
||||
if p.apiPort == 0 {
|
||||
return nil, nil, common.NewError("xray api port wrong:", p.apiPort)
|
||||
}
|
||||
conn, err := grpc.Dial(fmt.Sprintf("127.0.0.1:%v", p.apiPort), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
client := statsservice.NewStatsServiceClient(conn)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
request := &statsservice.QueryStatsRequest{
|
||||
Reset_: reset,
|
||||
}
|
||||
resp, err := client.QueryStats(ctx, request)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
tagTrafficMap := map[string]*Traffic{}
|
||||
emailTrafficMap := map[string]*ClientTraffic{}
|
||||
|
||||
clientTraffics := make([]*ClientTraffic, 0)
|
||||
traffics := make([]*Traffic, 0)
|
||||
for _, stat := range resp.GetStat() {
|
||||
matchs := trafficRegex.FindStringSubmatch(stat.Name)
|
||||
if len(matchs) < 3 {
|
||||
|
||||
matchs := ClientTrafficRegex.FindStringSubmatch(stat.Name)
|
||||
if len(matchs) < 3 {
|
||||
continue
|
||||
} else {
|
||||
|
||||
isUser := matchs[1] == "user"
|
||||
email := matchs[2]
|
||||
isDown := matchs[3] == "downlink"
|
||||
if !isUser {
|
||||
continue
|
||||
}
|
||||
traffic, ok := emailTrafficMap[email]
|
||||
if !ok {
|
||||
traffic = &ClientTraffic{
|
||||
Email: email,
|
||||
}
|
||||
emailTrafficMap[email] = traffic
|
||||
clientTraffics = append(clientTraffics, traffic)
|
||||
}
|
||||
if isDown {
|
||||
traffic.Down = stat.Value
|
||||
} else {
|
||||
traffic.Up = stat.Value
|
||||
}
|
||||
|
||||
}
|
||||
continue
|
||||
}
|
||||
isInbound := matchs[1] == "inbound"
|
||||
tag := matchs[2]
|
||||
isDown := matchs[3] == "downlink"
|
||||
if tag == "api" {
|
||||
continue
|
||||
}
|
||||
traffic, ok := tagTrafficMap[tag]
|
||||
if !ok {
|
||||
traffic = &Traffic{
|
||||
IsInbound: isInbound,
|
||||
Tag: tag,
|
||||
}
|
||||
tagTrafficMap[tag] = traffic
|
||||
traffics = append(traffics, traffic)
|
||||
}
|
||||
if isDown {
|
||||
traffic.Down = stat.Value
|
||||
} else {
|
||||
traffic.Up = stat.Value
|
||||
}
|
||||
}
|
||||
|
||||
return traffics, clientTraffics, nil
|
||||
return p.cmd.Process.Signal(syscall.SIGTERM)
|
||||
}
|
||||
|
||||