mirror of
https://github.com/alireza0/x-ui.git
synced 2026-03-14 13:31:41 +00:00
Compare commits
317 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37e90ad587 | ||
|
|
6f15de752c | ||
|
|
81a21b8ae0 | ||
|
|
04aaf94016 | ||
|
|
fc84ddc6c1 | ||
|
|
9f7e2f21f3 | ||
|
|
29078f38e9 | ||
|
|
8a8e6cf741 | ||
|
|
eac1f4d3b3 | ||
|
|
1146c146ae | ||
|
|
3e5a71d65f | ||
|
|
438a15cebc | ||
|
|
025eee20b5 | ||
|
|
4356afebc6 | ||
|
|
7a6f5eef28 | ||
|
|
dbbdbee737 | ||
|
|
bf63d00e3a | ||
|
|
8f4fb47e27 | ||
|
|
8bf6604ed6 | ||
|
|
64c1461a3f | ||
|
|
29d1c616fa | ||
|
|
09a719e179 | ||
|
|
8077307d85 | ||
|
|
06629939be | ||
|
|
754ebbdabb | ||
|
|
3c4fa7b422 | ||
|
|
ebda91b036 | ||
|
|
bc9ab420e6 | ||
|
|
d0d6748d3a | ||
|
|
d2d34e3c7f | ||
|
|
9ddac54320 | ||
|
|
ecee4e4482 | ||
|
|
6f4d5655b2 | ||
|
|
7305953cf1 | ||
|
|
b8eb123d49 | ||
|
|
a05977440c | ||
|
|
0bf6987fbc | ||
|
|
3cdafd600e | ||
|
|
441efa65b5 | ||
|
|
a241a97ef6 | ||
|
|
e4300badc5 | ||
|
|
4b3abe9cdf | ||
|
|
61d11ac66a | ||
|
|
a34496910d | ||
|
|
b5f16a476b | ||
|
|
43dc1e5522 | ||
|
|
dd363d2ead | ||
|
|
068decfb5e | ||
|
|
f77cec681a | ||
|
|
c0e670a5b6 | ||
|
|
afeab09753 | ||
|
|
2990f71fe2 | ||
|
|
60eb61bd60 | ||
|
|
f2394844ff | ||
|
|
d31feba2c8 | ||
|
|
0c189e50ec | ||
|
|
15fdb58433 | ||
|
|
9c170fda26 | ||
|
|
9adc2054a0 | ||
|
|
5efedd3a5e | ||
|
|
b4e2bba934 | ||
|
|
40e4145263 | ||
|
|
2b8c913be9 | ||
|
|
3d1fd65c3a | ||
|
|
5b306e57c9 | ||
|
|
102f8bab51 | ||
|
|
e4a011b6d9 | ||
|
|
7f20b71c45 | ||
|
|
4779b37e6e | ||
|
|
7a20d2c83c | ||
|
|
7ead999f12 | ||
|
|
812c9fbe17 | ||
|
|
5adbfd9528 | ||
|
|
1b93c7c0f2 | ||
|
|
58e7f51313 | ||
|
|
78e97af4db | ||
|
|
7c37319438 | ||
|
|
22605edb20 | ||
|
|
cb4c843dbf | ||
|
|
c2541d65f9 | ||
|
|
a99035bd3d | ||
|
|
57e297d6cf | ||
|
|
97b603c4a7 | ||
|
|
8216de011e | ||
|
|
f3cdc35452 | ||
|
|
a118bae974 | ||
|
|
e01a3ac893 | ||
|
|
f9e2d9f61e | ||
|
|
28233884bb | ||
|
|
a0a4d7571d | ||
|
|
7dde506862 | ||
|
|
1f5a785806 | ||
|
|
df13bf1a97 | ||
|
|
4f289fbbad | ||
|
|
ef0a48de23 | ||
|
|
545d7a73c8 | ||
|
|
58474373d9 | ||
|
|
eb7fda21be | ||
|
|
68b0cb1312 | ||
|
|
520ce4b557 | ||
|
|
63eda1ed27 | ||
|
|
74595b2785 | ||
|
|
b79ff596a2 | ||
|
|
e5f0cf9b76 | ||
|
|
0248b981d7 | ||
|
|
bcb90ac14a | ||
|
|
58ae8fadad | ||
|
|
33e41f1bda | ||
|
|
b87705f50b | ||
|
|
2349c9fdbe | ||
|
|
b00d33830c | ||
|
|
d14c5f4f67 | ||
|
|
c538301d42 | ||
|
|
161c4c950b | ||
|
|
243273defd | ||
|
|
ceb0e0837f | ||
|
|
710c2280a8 | ||
|
|
8a9b3eddfe | ||
|
|
0c4bf6fac8 | ||
|
|
5e8556be33 | ||
|
|
d86c75b925 | ||
|
|
c8c0bbc455 | ||
|
|
6d453fa91b | ||
|
|
abc82cef4c | ||
|
|
14270caa16 | ||
|
|
aa74f05c52 | ||
|
|
953a3ae315 | ||
|
|
a097733ccc | ||
|
|
7edaf89596 | ||
|
|
f8fe396ed5 | ||
|
|
128e027dac | ||
|
|
7a36eda6b4 | ||
|
|
00746c7864 | ||
|
|
c1fb8712d6 | ||
|
|
0bd44a64ea | ||
|
|
d532eb6bd8 | ||
|
|
8960e5450a | ||
|
|
d1d7ee7f7c | ||
|
|
8ddf7963c7 | ||
|
|
5bb0372aee | ||
|
|
788a1b9d6b | ||
|
|
9d1aa0d45f | ||
|
|
b125f1835c | ||
|
|
5e3c0d6ecc | ||
|
|
c197165da7 | ||
|
|
ec1efabcaa | ||
|
|
3ee57fd51d | ||
|
|
26b748338a | ||
|
|
af432828f1 | ||
|
|
cb14d298c0 | ||
|
|
994a9f4907 | ||
|
|
01bfd36316 | ||
|
|
4a332c6723 | ||
|
|
c3d1824ea2 | ||
|
|
57a32ab524 | ||
|
|
2c5bb94894 | ||
|
|
39c1a4276d | ||
|
|
4736786c6f | ||
|
|
c89dbed88e | ||
|
|
402c713f06 | ||
|
|
a371bec2aa | ||
|
|
427d008bd1 | ||
|
|
e69d17be67 | ||
|
|
09c61976ea | ||
|
|
6e17c282e0 | ||
|
|
700973655c | ||
|
|
dcb54267f2 | ||
|
|
19e851d5c8 | ||
|
|
3f7ef07b8e | ||
|
|
89be2c8fec | ||
|
|
dd11585074 | ||
|
|
7ecb73af8c | ||
|
|
ba083ecc7e | ||
|
|
365ec1a704 | ||
|
|
63969d5fd6 | ||
|
|
0449a35409 | ||
|
|
453594ee9e | ||
|
|
a2d8bec80a | ||
|
|
b0ca8a8e6c | ||
|
|
99c9d777c0 | ||
|
|
97d109c900 | ||
|
|
35ad91a9e0 | ||
|
|
8bc16b020b | ||
|
|
6db9bd0ad9 | ||
|
|
5baa397d1c | ||
|
|
76e1243da3 | ||
|
|
7b7a0f9aa7 | ||
|
|
134e2236a6 | ||
|
|
aab672976e | ||
|
|
f7aee9fe18 | ||
|
|
a19b58675b | ||
|
|
7a229b27f5 | ||
|
|
abf68446e5 | ||
|
|
b4b7ec565e | ||
|
|
4b1b920bf4 | ||
|
|
11bff57f23 | ||
|
|
4838b0f6f0 | ||
|
|
bb8807129a | ||
|
|
cb050bfe34 | ||
|
|
76a996cc2d | ||
|
|
7fec1dd5cf | ||
|
|
a24e9089b6 | ||
|
|
b6474eaef6 | ||
|
|
76f90b0a46 | ||
|
|
42abffdaef | ||
|
|
73d518dfe3 | ||
|
|
4592a8d201 | ||
|
|
6ab99326ce | ||
|
|
6c5b098e1d | ||
|
|
fe670ae885 | ||
|
|
f4f1d477b3 | ||
|
|
7455a2baa3 | ||
|
|
4c5576c38b | ||
|
|
18ee201412 | ||
|
|
772a1b341e | ||
|
|
5923c765a9 | ||
|
|
d7074cc3c9 | ||
|
|
f4f8c0d6df | ||
|
|
0c755be648 | ||
|
|
c8dc4a96b4 | ||
|
|
8d9f6ccc11 | ||
|
|
3d7a8e372e | ||
|
|
2fe4f1ad07 | ||
|
|
04095b6ec4 | ||
|
|
6d404e4b27 | ||
|
|
f9f8431dd2 | ||
|
|
f996b9ee2b | ||
|
|
a3e1ee1f35 | ||
|
|
e16391ad05 | ||
|
|
48b543becb | ||
|
|
80f052697c | ||
|
|
fb15248030 | ||
|
|
6a18ba48aa | ||
|
|
c13b7b2a18 | ||
|
|
838bdaf314 | ||
|
|
f6f0cf3657 | ||
|
|
dd5e9a97a6 | ||
|
|
edfbb91dff | ||
|
|
eb239b33af | ||
|
|
4b9940fb06 | ||
|
|
98d5e960a4 | ||
|
|
61fd029e28 | ||
|
|
2be3df59be | ||
|
|
7cf192e179 | ||
|
|
3a002c886d | ||
|
|
4b05c333ca | ||
|
|
bfc5b37bb1 | ||
|
|
07089455b5 | ||
|
|
7add969312 | ||
|
|
501f778c9e | ||
|
|
ac52340c51 | ||
|
|
f5f90d81a3 | ||
|
|
e6c23caa1d | ||
|
|
bf3a64c77d | ||
|
|
e2e88d8652 | ||
|
|
91b453fff7 | ||
|
|
9e955df76a | ||
|
|
58ca5f7a51 | ||
|
|
4f469f9ad5 | ||
|
|
1969120c94 | ||
|
|
cdbf742b66 | ||
|
|
02a4f0c7d4 | ||
|
|
e0a64a8257 | ||
|
|
38db94f507 | ||
|
|
d0ce67a8f0 | ||
|
|
fcc3f67181 | ||
|
|
357630b077 | ||
|
|
1246b54ffa | ||
|
|
2e3119ab9b | ||
|
|
b834ac5d93 | ||
|
|
92df5659c9 | ||
|
|
f85bd39a0a | ||
|
|
67fe79a7eb | ||
|
|
5cd3ee10de | ||
|
|
e3e7b0f736 | ||
|
|
19280cdfbd | ||
|
|
17a483266e | ||
|
|
1e7d2010a8 | ||
|
|
97a0e2cae9 | ||
|
|
e9c3c330f2 | ||
|
|
294894eca0 | ||
|
|
f3df93a950 | ||
|
|
1780f06242 | ||
|
|
50671bfce3 | ||
|
|
7332258dce | ||
|
|
d9523cfd37 | ||
|
|
47f75f6382 | ||
|
|
a6f2dc577a | ||
|
|
36ddde3491 | ||
|
|
f1a87c7d92 | ||
|
|
48f7b88ede | ||
|
|
b7048cdab8 | ||
|
|
df2ef6cdde | ||
|
|
efd1b06593 | ||
|
|
28050aba23 | ||
|
|
27de7b030b | ||
|
|
9ae49ed562 | ||
|
|
cee117e1e8 | ||
|
|
8f0a701d9e | ||
|
|
921a72b5cf | ||
|
|
9e902fc82f | ||
|
|
6ed8ff8e05 | ||
|
|
f333ad23d9 | ||
|
|
3f49aa5541 | ||
|
|
0f487dc10f | ||
|
|
f3c0edac07 | ||
|
|
394c857bb6 | ||
|
|
6080709fc7 | ||
|
|
17c47f0711 | ||
|
|
dac234357a | ||
|
|
2d15073a90 | ||
|
|
1b2e165a65 | ||
|
|
e1f8e8efd2 | ||
|
|
738c7064a9 | ||
|
|
c24483ad44 | ||
|
|
046f071378 | ||
|
|
d40a16e29d |
27
.github/workflows/release.yml
vendored
27
.github/workflows/release.yml
vendored
@@ -8,7 +8,7 @@ on:
|
||||
jobs:
|
||||
linuxamd64build:
|
||||
name: build x-ui amd64 version
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Go
|
||||
@@ -26,18 +26,19 @@ jobs:
|
||||
mv xui-release x-ui
|
||||
mkdir bin
|
||||
cd bin
|
||||
wget https://github.com/xtls/xray-core/releases/latest/download/Xray-linux-64.zip
|
||||
wget https://github.com/XTLS/Xray-core/releases/download/v1.8.1/Xray-linux-64.zip
|
||||
unzip Xray-linux-64.zip
|
||||
rm -f Xray-linux-64.zip geoip.dat geosite.dat
|
||||
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
|
||||
uses: svenstaro/upload-release-action@2.6.1
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ github.ref }}
|
||||
@@ -46,7 +47,7 @@ jobs:
|
||||
prerelease: true
|
||||
linuxarm64build:
|
||||
name: build x-ui arm64 version
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Go
|
||||
@@ -66,18 +67,19 @@ jobs:
|
||||
mv xui-release x-ui
|
||||
mkdir bin
|
||||
cd bin
|
||||
wget https://github.com/xtls/xray-core/releases/latest/download/Xray-linux-arm64-v8a.zip
|
||||
wget https://github.com/xtls/xray-core/releases/download/v1.8.1/Xray-linux-arm64-v8a.zip
|
||||
unzip Xray-linux-arm64-v8a.zip
|
||||
rm -f Xray-linux-arm64-v8a.zip geoip.dat geosite.dat
|
||||
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-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
|
||||
uses: svenstaro/upload-release-action@2.6.1
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ github.ref }}
|
||||
@@ -86,7 +88,7 @@ jobs:
|
||||
prerelease: true
|
||||
linuxs390xbuild:
|
||||
name: build x-ui s390x version
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Go
|
||||
@@ -106,18 +108,19 @@ jobs:
|
||||
mv xui-release x-ui
|
||||
mkdir bin
|
||||
cd bin
|
||||
wget https://github.com/xtls/xray-core/releases/latest/download/Xray-linux-s390x.zip
|
||||
wget https://github.com/xtls/xray-core/releases/download/v1.8.1/Xray-linux-s390x.zip
|
||||
unzip Xray-linux-s390x.zip
|
||||
rm -f Xray-linux-s390x.zip geoip.dat geosite.dat
|
||||
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-s390x
|
||||
cd ..
|
||||
cd ..
|
||||
- name: package
|
||||
run: tar -zcvf x-ui-linux-s390x.tar.gz x-ui
|
||||
- name: upload
|
||||
uses: svenstaro/upload-release-action@2.5.0
|
||||
uses: svenstaro/upload-release-action@2.6.1
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ github.ref }}
|
||||
|
||||
16
.gitignore
vendored
16
.gitignore
vendored
@@ -1,13 +1,15 @@
|
||||
.idea
|
||||
.vscode
|
||||
.cache
|
||||
.sync*
|
||||
*.tar.gz
|
||||
access.log
|
||||
error.log
|
||||
tmp
|
||||
main
|
||||
backup/
|
||||
bin/
|
||||
dist/
|
||||
x-ui-*.tar.gz
|
||||
/x-ui
|
||||
/release.sh
|
||||
.sync*
|
||||
main
|
||||
release/
|
||||
access.log
|
||||
.cache
|
||||
/release.sh
|
||||
/x-ui
|
||||
|
||||
21
DockerInitFiles.sh
Executable file
21
DockerInitFiles.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/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
|
||||
mkdir -p build/bin
|
||||
cd build/bin
|
||||
wget "https://github.com/XTLS/Xray-core/releases/download/v1.8.1/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 ../../
|
||||
12
Dockerfile
12
Dockerfile
@@ -1,16 +1,18 @@
|
||||
FROM golang:1.20-alpine AS builder
|
||||
WORKDIR /app
|
||||
ENV CGO_ENABLED 1
|
||||
RUN apk add gcc && apk --no-cache --update add build-base
|
||||
ARG TARGETARCH
|
||||
RUN apk --no-cache --update add build-base gcc wget unzip
|
||||
COPY . .
|
||||
RUN go build main.go
|
||||
RUN env CGO_ENABLED=1 go build -o build/x-ui main.go
|
||||
RUN ./DockerInitFiles.sh "$TARGETARCH"
|
||||
|
||||
FROM alpine
|
||||
LABEL org.opencontainers.image.authors="alireza7@gmail.com"
|
||||
ENV TZ=Asia/Tehran
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add ca-certificates tzdata && mkdir bin
|
||||
COPY --from=builder /app/main /app/x-ui
|
||||
RUN apk add ca-certificates tzdata
|
||||
|
||||
COPY --from=builder /app/build/ /app/
|
||||
VOLUME [ "/etc/x-ui" ]
|
||||
CMD [ "./x-ui" ]
|
||||
190
README.md
190
README.md
@@ -1,62 +1,62 @@
|
||||
# x-ui
|
||||

|
||||
|
||||

|
||||

|
||||
[](https://goreportcard.com/report/github.com/alireza0/x-ui)
|
||||
[](https://img.shields.io/github/downloads/alireza0/x-ui/total.svg)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
> **Disclaimer: This project is only for personal learning and communication, please do not use it for illegal purposes, please do not use it in a production environment**
|
||||
|
||||
xray panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese)**
|
||||
xray panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese,Russian)**
|
||||
|
||||
| Features | Enable? |
|
||||
| ------------- |:-------------:|
|
||||
| Multi-lang | :heavy_check_mark: |
|
||||
| Dark/Light Theme | :heavy_check_mark: |
|
||||
| Search in deep | :heavy_check_mark: |
|
||||
| Inbound Multi User | :heavy_check_mark: |
|
||||
| Features | Enable? |
|
||||
| ------------------------------------ | :----------------: |
|
||||
| Multi-lang | :heavy_check_mark: |
|
||||
| Dark/Light Theme | :heavy_check_mark: |
|
||||
| Search in deep | :heavy_check_mark: |
|
||||
| Inbound Multi User | :heavy_check_mark: |
|
||||
| Multi User Traffic & Expiration time | :heavy_check_mark: |
|
||||
| REST API | :heavy_check_mark: |
|
||||
| Telegram BOT (admin + clients) | :heavy_check_mark: |
|
||||
| Backup database using Telegram BOT | :heavy_check_mark: |
|
||||
| REST API | :heavy_check_mark: |
|
||||
| Telegram BOT (admin + clients) | :heavy_check_mark: |
|
||||
| Backup database using Telegram BOT | :heavy_check_mark: |
|
||||
| Subscription link + userInfo | :heavy_check_mark: |
|
||||
| Calculate expire date on first usage | :heavy_check_mark: |
|
||||
|
||||
**If you think this project is helpful to you, you may wish to give a** :star2:
|
||||
**If you think this project is helpful to you, you may wish to give a** :star2:
|
||||
|
||||
# Features
|
||||
**Buy Me a Coffee :**
|
||||
|
||||
- System Status Monitoring
|
||||
- Search within all inbounds and clients
|
||||
- Support Dark/Light theme UI
|
||||
- Support multi-user multi-protocol, web page visualization operation
|
||||
- Supported protocols: vmess, vless, trojan, shadowsocks, dokodemo-door, socks, http
|
||||
- Support for configuring more transport configurations
|
||||
- Traffic statistics, limit traffic, limit expiration time
|
||||
- Customizable xray configuration templates
|
||||
- Support https access panel (self-provided domain name + ssl certificate)
|
||||
- Support one-click SSL certificate application and automatic renewal
|
||||
- For more advanced configuration items, please refer to the panel
|
||||
- Tron USDT (TRC20): `TYTq73Gj6dJ67qe58JVPD9zpjW2cc9XgVz`
|
||||
- Tezos (XTZ): tz2Wnh2SsY1eezXrcLChu6idWpgdHzUFQcts
|
||||
|
||||
# Screenshot from Inbouds page
|
||||
# Install & Upgrade to latest version
|
||||
|
||||

|
||||

|
||||
|
||||
# Install & Upgrade
|
||||
|
||||
```
|
||||
```sh
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/alireza0/x-ui/master/install.sh)
|
||||
```
|
||||
|
||||
## Install custom version
|
||||
|
||||
To install your desired version you can add the version to the end of install command. Example for ver `0.5.2`:
|
||||
|
||||
```sh
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/alireza0/x-ui/master/install.sh) 0.5.2
|
||||
```
|
||||
|
||||
## Manual install & upgrade
|
||||
|
||||
1. First download the latest compressed package from https://github.com/alireza0/x-ui/releases , generally choose Architecture `amd64`
|
||||
1. First download the latest compressed package from https://github.com/alireza0/x-ui/releases, generally choose Architecture `amd64`
|
||||
2. Then upload the compressed package to the server's `/root/` directory and `root` rootlog in to the server with user
|
||||
|
||||
> If your server cpu architecture is not `amd64` replace another architecture
|
||||
|
||||
```
|
||||
```sh
|
||||
ARCH=$(uname -m)
|
||||
[[ "${ARCH}" == "s390x" ]] && XUI_ARCH="s390x" || [[ "${ARCH}" == "aarch64" || "${ARCH}" == "arm64" ]] && XUI_ARCH="arm64" || XUI_ARCH="amd64"
|
||||
cd /root/
|
||||
rm x-ui/ /usr/local/x-ui/ /usr/bin/x-ui -rf
|
||||
tar zxvf x-ui-linux-amd64.tar.gz
|
||||
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/
|
||||
@@ -93,11 +93,76 @@ docker run -itd \
|
||||
docker build -t x-ui .
|
||||
```
|
||||
|
||||
# Features
|
||||
|
||||
- System Status Monitoring
|
||||
- Search within all inbounds and clients
|
||||
- Support Dark/Light theme UI
|
||||
- Support multi-user multi-protocol, web page visualization operation
|
||||
- Support multi-domain configuration and multi-certificate inbounds
|
||||
- Supported protocols: vmess, vless, trojan, shadowsocks, dokodemo-door, socks, http
|
||||
- Support for configuring more transport configurations
|
||||
- Traffic statistics, limit traffic, limit expiration time
|
||||
- Customizable xray configuration templates
|
||||
- Support subscription ( multi ) link
|
||||
- Detect users which are expiring or exceed traffic limit soon
|
||||
- Support https access panel (self-provided domain name + ssl certificate)
|
||||
- Support one-click SSL certificate application and automatic renewal
|
||||
- For more advanced configuration items, please refer to the panel
|
||||
- Support export/import database from panel
|
||||
|
||||
## suggestion system
|
||||
|
||||
- CentOS 8+
|
||||
- Ubuntu 20+
|
||||
- Debian 10+
|
||||
- Fedora 36+
|
||||
|
||||
## API routes
|
||||
|
||||
- `/login` with `PUSH` user data: `{username: '', password: ''}` for login
|
||||
- `/xui/API/inbounds` base for following actions:
|
||||
|
||||
| Method | Path | Action |
|
||||
| :----: | ------------------------------- | ----------------------------------------- |
|
||||
| `GET` | `"/"` | Get all inbounds |
|
||||
| `GET` | `"/get/:id"` | Get inbound with inbound.id |
|
||||
| `GET` | `"/createbackup"` | Telegram bot sends backup to admins |
|
||||
| `POST` | `"/add"` | Add inbound |
|
||||
| `POST` | `"/del/:id"` | Delete Inbound |
|
||||
| `POST` | `"/update/:id"` | Update Inbound |
|
||||
| `POST` | `"/addClient/"` | Add Client to inbound |
|
||||
| `POST` | `"/:id/delClient/:clientId"` | Delete Client by clientId\* |
|
||||
| `POST` | `"/updateClient/:clientId"` | Update Client by clientId\* |
|
||||
| `POST` | `"/getClientTraffics/:email"` | Get Client's Traffic |
|
||||
| `POST` | `"/resetAllTraffics"` | Reset traffics of all inbounds |
|
||||
| `POST` | `"/resetAllClientTraffics/:id"` | Reset inbound clients traffics (-1: all) |
|
||||
| `POST` | `"/delDepletedClients/:id"` | Delete inbound depleted clients (-1: all) |
|
||||
|
||||
\*- The field `clientId` should be filled by:
|
||||
|
||||
- `client.id` for VMESS and VLESS
|
||||
- `client.password` for TROJAN
|
||||
- `client.email` for Shadowsocks
|
||||
|
||||
# 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"` |
|
||||
|
||||
# Screenshot from Inbouds page
|
||||
|
||||

|
||||

|
||||
|
||||
## SSL certificate application
|
||||
|
||||
### Cloudflare
|
||||
|
||||
> This feature and tutorial are provided by [FranzKafkaYu](https://github.com/FranzKafkaYu)
|
||||
<details>
|
||||
<summary>Click for details</summary>
|
||||
|
||||
### Certbot
|
||||
|
||||
@@ -109,9 +174,12 @@ ln -s /snap/bin/certbot /usr/bin/certbot
|
||||
certbot certonly --standalone --register-unsafely-without-email --non-interactive --agree-tos -d <Your Domain Name>
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Tg robot use
|
||||
|
||||
> This feature and tutorial are provided by [FranzKafkaYu](https://github.com/FranzKafkaYu)
|
||||
<details>
|
||||
<summary>Click for 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:
|
||||
@@ -126,7 +194,8 @@ Set the robot-related parameters in the panel background, including:
|
||||
|
||||
Reference syntax:
|
||||
|
||||
- 30 * * * * * //Notify at the 30s of each point
|
||||
- 30 \* \* \* \* \* //Notify at the 30s of each point
|
||||
- 0 \*/10 \* \* \* \* //Notify at the first second of each 10 minutes
|
||||
- @hourly // hourly notification
|
||||
- @daily // Daily notification (00:00 in the morning)
|
||||
- @every 8h // notify every 8 hours
|
||||
@@ -137,33 +206,28 @@ Reference syntax:
|
||||
- Login notification
|
||||
- CPU threshold notification
|
||||
- Threshold for Expiration time and Traffic to report in advance
|
||||
- Support client report if client's telegram username is added to the end of `email` like 'test123@telegram_username'
|
||||
- Support telegram traffic report searched with UID (VMESS/VLESS) or Password (TROJAN) - anonymously
|
||||
- Support client report menu if client's telegram username added to the user's configurations
|
||||
- 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 Exhausted users
|
||||
- Check depleted users
|
||||
- Receive backup by request and in periodic reports
|
||||
- Multi language bot
|
||||
</details>
|
||||
|
||||
# Common problem
|
||||
|
||||
|
||||
## suggestion system
|
||||
|
||||
- CentOS 7+
|
||||
- Ubuntu 16+
|
||||
- Debian 8+
|
||||
- Fedora 36+
|
||||
|
||||
# common problem
|
||||
|
||||
<details>
|
||||
<summary>Click for details</summary>
|
||||
## Migrating from v2-ui
|
||||
|
||||
First install the latest version of x-ui on the server where v2-ui is installed, and then use the following command to migrate, which will migrate the native v2-ui `All inbound account data` to x-ui,`Panel settings and username passwords are not migrated`
|
||||
|
||||
> Please `Close v2-ui` and `restart x-ui`, otherwise the inbound of v2-ui will cause a `port conflict with the inbound of x-ui`
|
||||
|
||||
```
|
||||
```sh
|
||||
x-ui v2-ui
|
||||
```
|
||||
|
||||
@@ -171,12 +235,15 @@ x-ui v2-ui
|
||||
|
||||
**If you upgrade from an old version or other forks, for enable traffic for users you should do :**
|
||||
|
||||
find this in config :
|
||||
``` json
|
||||
find this in config :
|
||||
|
||||
```json
|
||||
"policy": {
|
||||
"system": {
|
||||
```
|
||||
**and add this just after ` "policy": {` :**
|
||||
|
||||
**and add this just after ` "policy": {` :**
|
||||
|
||||
```json
|
||||
"levels": {
|
||||
"0": {
|
||||
@@ -186,8 +253,8 @@ find this in config :
|
||||
},
|
||||
```
|
||||
|
||||
|
||||
**the final output is like :**
|
||||
|
||||
```json
|
||||
"policy": {
|
||||
"levels": {
|
||||
@@ -204,12 +271,15 @@ find this in config :
|
||||
},
|
||||
"routing": {
|
||||
```
|
||||
restart panel
|
||||
|
||||
restart panel
|
||||
|
||||
</details>
|
||||
|
||||
# a special thanks to
|
||||
|
||||
- [HexaSoftwareTech](https://github.com/HexaSoftwareTech/)
|
||||
- [FranzKafkaYu](https://github.com/FranzKafkaYu)
|
||||
- [MHSanaei](https://github.com/MHSanaei)
|
||||
- [MHSanaei](https://github.com/MHSanaei)
|
||||
|
||||
## Stargazers over time
|
||||
|
||||
|
||||
@@ -45,6 +45,22 @@ func IsDebug() bool {
|
||||
return os.Getenv("XUI_DEBUG") == "true"
|
||||
}
|
||||
|
||||
func GetDBPath() string {
|
||||
return fmt.Sprintf("/etc/%s/%s.db", GetName(), GetName())
|
||||
func GetBinFolderPath() string {
|
||||
binFolderPath := os.Getenv("XUI_BIN_FOLDER")
|
||||
if binFolderPath == "" {
|
||||
binFolderPath = "bin"
|
||||
}
|
||||
return binFolderPath
|
||||
}
|
||||
|
||||
func GetDBFolderPath() string {
|
||||
dbFolderPath := os.Getenv("XUI_DB_FOLDER")
|
||||
if dbFolderPath == "" {
|
||||
dbFolderPath = "/etc/x-ui"
|
||||
}
|
||||
return dbFolderPath
|
||||
}
|
||||
|
||||
func GetDBPath() string {
|
||||
return fmt.Sprintf("%s/%s.db", GetDBFolderPath(), GetName())
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.4.2
|
||||
1.5.0
|
||||
@@ -1,6 +1,8 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
@@ -98,3 +100,13 @@ func GetDB() *gorm.DB {
|
||||
func IsNotFound(err error) bool {
|
||||
return err == gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
func IsSQLiteDB(file io.Reader) (bool, error) {
|
||||
signature := []byte("SQLite format 3\x00")
|
||||
buf := make([]byte, len(signature))
|
||||
_, err := file.Read(buf)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return bytes.Equal(buf, signature), nil
|
||||
}
|
||||
|
||||
@@ -74,4 +74,7 @@ type Client struct {
|
||||
Email string `json:"email"`
|
||||
TotalGB int64 `json:"totalGB" form:"totalGB"`
|
||||
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
|
||||
Enable bool `json:"enable" form:"enable"`
|
||||
TgID string `json:"tgId" form:"tgId"`
|
||||
SubID string `json:"subId" form:"subId"`
|
||||
}
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
version: '3.9'
|
||||
---
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
xui:
|
||||
image: alireza7/x-ui
|
||||
container_name: x-ui
|
||||
hostname: yourhostname
|
||||
volumes:
|
||||
- $PWD/db/:/etc/x-ui/
|
||||
- $PWD/cert/:/root/cert/
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
XRAY_VMESS_AEAD_FORCED: "false"
|
||||
tty: true
|
||||
network_mode: host
|
||||
restart: unless-stopped
|
||||
|
||||
65
go.mod
65
go.mod
@@ -3,33 +3,37 @@ module x-ui
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/Workiva/go-datastructures v1.0.53
|
||||
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/gin-gonic/gin v1.9.1
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
|
||||
github.com/goccy/go-json v0.10.2
|
||||
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.0.8
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/shirou/gopsutil/v3 v3.23.2
|
||||
github.com/xtls/xray-core v1.8.0
|
||||
go.uber.org/atomic v1.10.0
|
||||
golang.org/x/text v0.8.0
|
||||
google.golang.org/grpc v1.53.0
|
||||
gorm.io/driver/sqlite v1.4.4
|
||||
gorm.io/gorm v1.24.6
|
||||
github.com/shirou/gopsutil/v3 v3.23.5
|
||||
github.com/xtls/xray-core v1.8.1
|
||||
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
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.2.1 // indirect
|
||||
github.com/bytedance/sonic v1.8.0 // indirect
|
||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||
github.com/bytedance/sonic v1.9.1 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gaukas/godicttls v0.0.3 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // 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.11.2 // indirect
|
||||
github.com/goccy/go-json v0.10.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/gorilla/context v1.1.1 // indirect
|
||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||
@@ -37,25 +41,34 @@ require (
|
||||
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/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/kr/pretty v0.1.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/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pires/go-proxyproto v0.6.2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/pires/go-proxyproto v0.7.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect
|
||||
github.com/refraction-networking/utls v1.3.2 // indirect
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // 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/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.9 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
|
||||
golang.org/x/crypto v0.7.0 // indirect
|
||||
golang.org/x/net v0.8.0 // indirect
|
||||
golang.org/x/sys v0.6.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect
|
||||
google.golang.org/protobuf v1.29.0 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e // indirect
|
||||
github.com/xtls/reality v0.0.0-20230331223127-176a94313eda // 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
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
165
go.sum
165
go.sum
@@ -1,32 +1,39 @@
|
||||
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.0.53 h1:J6Y/52yX10Xc5JjXmGtWoSSxs3mZnGSaq37xZZh7Yig=
|
||||
github.com/Workiva/go-datastructures v1.0.53/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A=
|
||||
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/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA=
|
||||
github.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
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/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/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
|
||||
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/gaukas/godicttls v0.0.3/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
|
||||
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/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/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/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
@@ -39,13 +46,13 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+
|
||||
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.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=
|
||||
github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
|
||||
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-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
|
||||
github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
|
||||
github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/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/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
@@ -58,7 +65,7 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20230228050547-1710fef4ab10 h1:CqYfpuYIjnlNxM3msdyPRKabhXZWbKjf3Q8BWROFBso=
|
||||
github.com/google/pprof v0.0.0-20230406165453-00490a63f317 h1:hFhpt7CTmR3DX+b4R19ydQFtofxT0Sv3QsKNMVQYTMQ=
|
||||
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=
|
||||
@@ -70,33 +77,36 @@ github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/z
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
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.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
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.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.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
|
||||
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/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/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||
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/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
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.51 h1:0+Xg7vObnhrz/4ZCZcZh7zPXlmU0aveS2HDBd0m0qSo=
|
||||
github.com/miekg/dns v1.1.53 h1:ZBkuHr5dxHtB1caEOlZTLPo7D3L3TWckgUUs/RHfDxw=
|
||||
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=
|
||||
@@ -105,35 +115,42 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
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.0 h1:Tugw2BKlNHTMfG+CheOITkYvk4LAh6MFOvikhGVnhE8=
|
||||
github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU=
|
||||
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/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/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||
github.com/pires/go-proxyproto v0.6.2 h1:KAZ7UteSOt6urjme6ZldyFm4wDe/z0ZUP0Yv0Dos0d8=
|
||||
github.com/pires/go-proxyproto v0.6.2/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY=
|
||||
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/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 h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||
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.2.1 h1:aJcKNMkH5ASEJB9FXNeZCyTEIHU1J7MmHyz1Q1TSG1A=
|
||||
github.com/quic-go/qtls-go1-20 v0.1.1 h1:KbChDlg82d3IHqaj2bn6GfKRj84Per2VGf5XV3wSwQk=
|
||||
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.2.3-0.20230308205431-4f1df6c200db h1:ULRv/GPW5KYDafE0FACN2no+HTCyQLUtfyOIeyp3GNc=
|
||||
github.com/refraction-networking/utls v1.3.2 h1:o+AkWB57mkcoW36ET7uJ002CpBWHu0KPxi6vzxvPnv8=
|
||||
github.com/refraction-networking/utls v1.3.2/go.mod h1:fmoaOww2bxzzEpIKOebIsnBvjQpqP7L2vcm/9KUfm/E=
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||
github.com/sagernet/sing v0.1.7 h1:g4vjr3q8SUlBZSx97Emz5OBfSMBxxW5Q8C2PfdoSo08=
|
||||
github.com/sagernet/sing-shadowsocks v0.1.1 h1:uFK2rlVeD/b1xhDwSMbUI2goWc6fOKxp+ZeKHZq6C9Q=
|
||||
github.com/sagernet/sing v0.2.3 h1:V50MvZ4c3Iij2lYFWPlzL1PyipwSzjGeN9x+Ox89vpk=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.1 h1:FvdLQOqpvxHBJUcUe4fvgiYP2XLLwH5i1DtXQviVEPw=
|
||||
github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c h1:vK2wyt9aWYHHvNLWniwijBu/n4pySypiKRhN32u/JGo=
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U=
|
||||
github.com/shirou/gopsutil/v3 v3.23.2 h1:PAWSuiAszn7IhPMBtXsbSCafej7PqUOvY6YywlQUExU=
|
||||
github.com/shirou/gopsutil/v3 v3.23.2/go.mod h1:gv0aQw33GLo3pG8SiWKiQrbDzbRY1K80RyZJ7V4Th1M=
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
|
||||
github.com/shirou/gopsutil/v3 v3.23.5 h1:5SgDCeQ0KW0S4N0znjeM/eFHXXOKyv2dVNgRq/c9P6Y=
|
||||
github.com/shirou/gopsutil/v3 v3.23.5/go.mod h1:Ng3Maa27Q2KARVJ0SPZF5NdrQSC3XHKP8IIWrHgMeLY=
|
||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
||||
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
||||
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
|
||||
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=
|
||||
@@ -144,8 +161,9 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
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=
|
||||
@@ -156,38 +174,41 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
||||
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.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU=
|
||||
github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
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/xtls/reality v0.0.0-20230309125256-0d0713b108c8 h1:LLtLxEe3S0Ko+ckqt4t29RLskpNdOZfgjZCC2/Byr50=
|
||||
github.com/xtls/xray-core v1.8.0 h1:/OD0sDv6YIBqvE+cVfnqlKrtbMs0Fm9IP5BR5d8Eu4k=
|
||||
github.com/xtls/xray-core v1.8.0/go.mod h1:i9KWgbLyxg/NT+3+g4nE74Zp3DgTCP3X04YkSfsJeDI=
|
||||
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU=
|
||||
github.com/xtls/reality v0.0.0-20230331223127-176a94313eda h1:psRJD2RrZbnI0OWyHvXfgYCPqlRM5q5SPDcjDoDBWhE=
|
||||
github.com/xtls/reality v0.0.0-20230331223127-176a94313eda/go.mod h1:rkuAY1S9F8eI8gDiPDYvACE8e2uwkyg8qoOTuwWov7Y=
|
||||
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/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
|
||||
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.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
|
||||
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
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/crypto v0.0.0-20190308221718-c2843e01d9a2/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.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/exp v0.0.0-20230307190834-24139beb5833 h1:SChBja7BCQewoTAU7IgvucQKMIXrEpFxNMs0spT3/5s=
|
||||
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/mod v0.3.0/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.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
|
||||
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
|
||||
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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
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-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -203,11 +224,10 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
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.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.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 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||
golang.org/x/sys v0.6.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/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=
|
||||
@@ -215,40 +235,41 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
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.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
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/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
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.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
|
||||
golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y=
|
||||
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-20230306155012-7f2fa6fef1f4 h1:DdoeryqhaXp1LtT/emMP1BRJPHHKFi5akj/nbx/zNTA=
|
||||
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s=
|
||||
google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
|
||||
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
|
||||
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/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.29.0 h1:44S3JjaKmLEE4YIkjzexaP+NzZsudE3Zin5Njn/pYX0=
|
||||
google.golang.org/protobuf v1.29.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
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.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.4.4 h1:gIufGoR0dQzjkyqDyYSCvsYR6fba1Gw5YKDqKeChxFc=
|
||||
gorm.io/driver/sqlite v1.4.4/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
|
||||
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
|
||||
gorm.io/gorm v1.24.6 h1:wy98aq9oFEetsc4CAbKD2SoBCdMzsbSIvSUUFJuHi5s=
|
||||
gorm.io/gorm v1.24.6/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||
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=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
|
||||
36
install.sh
36
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
|
||||
@@ -47,22 +47,22 @@ os_version=""
|
||||
os_version=$(grep -i version_id /etc/os-release | cut -d \" -f2 | cut -d . -f1)
|
||||
|
||||
if [[ "${release}" == "centos" ]]; then
|
||||
if [[ ${os_version} -lt 7 ]]; then
|
||||
echo -e "${red} Please use CentOS 7 or higher ${plain}\n" && exit 1
|
||||
if [[ ${os_version} -lt 8 ]]; then
|
||||
echo -e "${red} Please use CentOS 8 or higher ${plain}\n" && exit 1
|
||||
fi
|
||||
elif [[ "${release}" == "ubuntu" ]]; then
|
||||
if [[ ${os_version} -lt 18 ]]; then
|
||||
echo -e "${red}please use Ubuntu 18 or higher version!${plain}\n" && exit 1
|
||||
if [[ ${os_version} -lt 20 ]]; then
|
||||
echo -e "${red}please use Ubuntu 20 or higher version! ${plain}\n" && exit 1
|
||||
fi
|
||||
|
||||
elif [[ "${release}" == "fedora" ]]; then
|
||||
if [[ ${os_version} -lt 36 ]]; then
|
||||
echo -e "${red}please use Fedora 36 or higher version!${plain}\n" && exit 1
|
||||
echo -e "${red}please use Fedora 36 or higher version! ${plain}\n" && exit 1
|
||||
fi
|
||||
|
||||
elif [[ "${release}" == "debian" ]]; then
|
||||
if [[ ${os_version} -lt 8 ]]; then
|
||||
echo -e "${red} Please use Debian 8 or higher ${plain}\n" && exit 1
|
||||
if [[ ${os_version} -lt 10 ]]; then
|
||||
echo -e "${red} Please use Debian 10 or higher ${plain}\n" && exit 1
|
||||
fi
|
||||
else
|
||||
echo -e "${red}Failed to check the OS version, please contact the author!${plain}" && exit 1
|
||||
@@ -70,7 +70,7 @@ fi
|
||||
|
||||
|
||||
install_base() {
|
||||
if [[ "${release}" == "centos" ]]; then
|
||||
if [[ "${release}" == "centos" ]] || [[ "${release}" == "fedora" ]] ; then
|
||||
yum install wget curl tar -y
|
||||
else
|
||||
apt install wget curl tar -y
|
||||
@@ -81,7 +81,7 @@ install_base() {
|
||||
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
|
||||
if [[ x"${config_confirm}" == x"y" || x"${config_confirm}" == x"Y" ]]; then
|
||||
if [[ "${config_confirm}" == "y" || "${config_confirm}" == "Y" ]]; then
|
||||
read -p "Please set up your username:" config_account
|
||||
echo -e "${yellow}Your username will be:${config_account}${plain}"
|
||||
read -p "Please set up your password:" config_password
|
||||
@@ -94,8 +94,22 @@ config_after_install() {
|
||||
/usr/local/x-ui/x-ui setting -port ${config_port}
|
||||
echo -e "${yellow}Panel port set successfully!${plain}"
|
||||
else
|
||||
echo -e "${red}Canceled, will use the default settings.${plain}"
|
||||
echo -e "${red}cancel...${plain}"
|
||||
if [[ ! -f "/etc/x-ui/x-ui.db" ]]; then
|
||||
local usernameTemp=$(head -c 6 /dev/urandom | base64)
|
||||
local passwordTemp=$(head -c 6 /dev/urandom | base64)
|
||||
/usr/local/x-ui/x-ui setting -username ${usernameTemp} -password ${passwordTemp}
|
||||
echo -e "this is a fresh installation,will generate random login info for security concerns:"
|
||||
echo -e "###############################################"
|
||||
echo -e "${green}username:${usernameTemp}${plain}"
|
||||
echo -e "${green}password:${passwordTemp}${plain}"
|
||||
echo -e "###############################################"
|
||||
echo -e "${red}if you forgot your login info,you can type x-ui and then type 7 to check after installation${plain}"
|
||||
else
|
||||
echo -e "${red} this is your upgrade,will keep old settings,if you forgot your login info,you can type x-ui and then type 7 to check${plain}"
|
||||
fi
|
||||
fi
|
||||
/usr/local/x-ui/x-ui migrate
|
||||
}
|
||||
|
||||
install_x-ui() {
|
||||
|
||||
61
main.go
61
main.go
@@ -11,6 +11,7 @@ import (
|
||||
"x-ui/config"
|
||||
"x-ui/database"
|
||||
"x-ui/logger"
|
||||
"x-ui/sub"
|
||||
"x-ui/v2ui"
|
||||
"x-ui/web"
|
||||
"x-ui/web/global"
|
||||
@@ -50,9 +51,19 @@ func runWebServer() {
|
||||
return
|
||||
}
|
||||
|
||||
var subServer *sub.Server
|
||||
subServer = sub.NewServer()
|
||||
global.SetSubServer(subServer)
|
||||
|
||||
err = subServer.Start()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
//信号量捕获处理
|
||||
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGKILL)
|
||||
// Trap shutdown signals
|
||||
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM)
|
||||
for {
|
||||
sig := <-sigCh
|
||||
|
||||
@@ -62,6 +73,11 @@ func runWebServer() {
|
||||
if err != nil {
|
||||
logger.Warning("stop server err:", err)
|
||||
}
|
||||
err = subServer.Stop()
|
||||
if err != nil {
|
||||
logger.Warning("stop server err:", err)
|
||||
}
|
||||
|
||||
server = web.NewServer()
|
||||
global.SetWebServer(server)
|
||||
err = server.Start()
|
||||
@@ -69,8 +85,18 @@ func runWebServer() {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
subServer = sub.NewServer()
|
||||
global.SetSubServer(subServer)
|
||||
|
||||
err = subServer.Start()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
default:
|
||||
server.Stop()
|
||||
subServer.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -97,7 +123,7 @@ func showSetting(show bool) {
|
||||
settingService := service.SettingService{}
|
||||
port, err := settingService.GetPort()
|
||||
if err != nil {
|
||||
fmt.Println("get current port fialed,error info:", err)
|
||||
fmt.Println("get current port failed,error info:", err)
|
||||
}
|
||||
userService := service.UserService{}
|
||||
userModel, err := userService.GetFirstUser()
|
||||
@@ -109,7 +135,7 @@ func showSetting(show bool) {
|
||||
if (username == "") || (userpasswd == "") {
|
||||
fmt.Println("current username or password is empty")
|
||||
}
|
||||
fmt.Println("current pannel settings as follows:")
|
||||
fmt.Println("current panel settings as follows:")
|
||||
fmt.Println("username:", username)
|
||||
fmt.Println("userpasswd:", userpasswd)
|
||||
fmt.Println("port:", port)
|
||||
@@ -133,7 +159,6 @@ func updateTgbotEnableSts(status bool) {
|
||||
logger.Infof("SetTgbotenabled[%v] success", status)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime string) {
|
||||
@@ -204,6 +229,18 @@ func updateSetting(port int, username string, password string) {
|
||||
}
|
||||
}
|
||||
|
||||
func migrateDb() {
|
||||
inboundService := service.InboundService{}
|
||||
|
||||
err := database.InitDB(config.GetDBPath())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println("Start migrating database...")
|
||||
inboundService.MigrateDB()
|
||||
fmt.Println("Migration done!")
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
runWebServer()
|
||||
@@ -217,7 +254,7 @@ func main() {
|
||||
|
||||
v2uiCmd := flag.NewFlagSet("v2-ui", flag.ExitOnError)
|
||||
var dbPath string
|
||||
v2uiCmd.StringVar(&dbPath, "db", "/etc/v2-ui/v2-ui.db", "set v2-ui db file path")
|
||||
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
|
||||
@@ -234,9 +271,9 @@ func main() {
|
||||
settingCmd.IntVar(&port, "port", 0, "set panel port")
|
||||
settingCmd.StringVar(&username, "username", "", "set login username")
|
||||
settingCmd.StringVar(&password, "password", "", "set login password")
|
||||
settingCmd.StringVar(&tgbottoken, "tgbottoken", "", "set telegrame bot token")
|
||||
settingCmd.StringVar(&tgbotRuntime, "tgbotRuntime", "", "set telegrame bot cron time")
|
||||
settingCmd.StringVar(&tgbotchatid, "tgbotchatid", "", "set telegrame bot chat id")
|
||||
settingCmd.StringVar(&tgbottoken, "tgbottoken", "", "set telegram bot token")
|
||||
settingCmd.StringVar(&tgbotRuntime, "tgbotRuntime", "", "set telegram bot cron time")
|
||||
settingCmd.StringVar(&tgbotchatid, "tgbotchatid", "", "set telegram bot chat id")
|
||||
settingCmd.BoolVar(&enabletgbot, "enabletgbot", false, "enable telegram bot notify")
|
||||
|
||||
oldUsage := flag.Usage
|
||||
@@ -246,6 +283,7 @@ func main() {
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -263,6 +301,8 @@ func main() {
|
||||
return
|
||||
}
|
||||
runWebServer()
|
||||
case "migrate":
|
||||
migrateDb()
|
||||
case "v2-ui":
|
||||
err := v2uiCmd.Parse(os.Args[2:])
|
||||
if err != nil {
|
||||
@@ -290,6 +330,9 @@ func main() {
|
||||
if (tgbottoken != "") || (tgbotchatid != "") || (tgbotRuntime != "") {
|
||||
updateTgbotSetting(tgbottoken, tgbotchatid, tgbotRuntime)
|
||||
}
|
||||
if enabletgbot {
|
||||
updateTgbotEnableSts(enabletgbot)
|
||||
}
|
||||
default:
|
||||
fmt.Println("except 'run' or 'v2-ui' or 'setting' subcommands")
|
||||
fmt.Println()
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 324 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 316 KiB |
162
sub/sub.go
Normal file
162
sub/sub.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package sub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"x-ui/config"
|
||||
"x-ui/logger"
|
||||
"x-ui/util/common"
|
||||
"x-ui/web/middleware"
|
||||
"x-ui/web/network"
|
||||
"x-ui/web/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
httpServer *http.Server
|
||||
listener net.Listener
|
||||
|
||||
sub *SUBController
|
||||
settingService service.SettingService
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func NewServer() *Server {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &Server{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
if config.IsDebug() {
|
||||
gin.SetMode(gin.DebugMode)
|
||||
} else {
|
||||
gin.DefaultWriter = io.Discard
|
||||
gin.DefaultErrorWriter = io.Discard
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
|
||||
engine := gin.Default()
|
||||
|
||||
subPath, err := s.settingService.GetSubPath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
subDomain, err := s.settingService.GetSubDomain()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if subDomain != "" {
|
||||
engine.Use(middleware.DomainValidatorMiddleware(subDomain))
|
||||
}
|
||||
|
||||
g := engine.Group(subPath)
|
||||
|
||||
s.sub = NewSUBController(g)
|
||||
|
||||
return engine, nil
|
||||
}
|
||||
|
||||
func (s *Server) Start() (err error) {
|
||||
//This is an anonymous function, no function name
|
||||
defer func() {
|
||||
if err != nil {
|
||||
s.Stop()
|
||||
}
|
||||
}()
|
||||
|
||||
subEnable, err := s.settingService.GetSubEnable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !subEnable {
|
||||
return nil
|
||||
}
|
||||
|
||||
engine, err := s.initRouter()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
certFile, err := s.settingService.GetSubCertFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
keyFile, err := s.settingService.GetSubKeyFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
listen, err := s.settingService.GetSubListen()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
port, err := s.settingService.GetSubPort()
|
||||
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 {
|
||||
listener.Close()
|
||||
return err
|
||||
}
|
||||
c := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
listener = network.NewAutoHttpsListener(listener)
|
||||
listener = tls.NewListener(listener, c)
|
||||
}
|
||||
|
||||
if certFile != "" || keyFile != "" {
|
||||
logger.Info("Sub server run https on", listener.Addr())
|
||||
} else {
|
||||
logger.Info("Sub server run http on", listener.Addr())
|
||||
}
|
||||
s.listener = listener
|
||||
|
||||
s.httpServer = &http.Server{
|
||||
Handler: engine,
|
||||
}
|
||||
|
||||
go func() {
|
||||
s.httpServer.Serve(listener)
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) Stop() error {
|
||||
s.cancel()
|
||||
|
||||
var err1 error
|
||||
var err2 error
|
||||
if s.httpServer != nil {
|
||||
err1 = s.httpServer.Shutdown(s.ctx)
|
||||
}
|
||||
if s.listener != nil {
|
||||
err2 = s.listener.Close()
|
||||
}
|
||||
return common.Combine(err1, err2)
|
||||
}
|
||||
|
||||
func (s *Server) GetCtx() context.Context {
|
||||
return s.ctx
|
||||
}
|
||||
45
sub/subController.go
Normal file
45
sub/subController.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package sub
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type SUBController struct {
|
||||
subService SubService
|
||||
}
|
||||
|
||||
func NewSUBController(g *gin.RouterGroup) *SUBController {
|
||||
a := &SUBController{}
|
||||
a.initRouter(g)
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *SUBController) initRouter(g *gin.RouterGroup) {
|
||||
g = g.Group("/")
|
||||
|
||||
g.GET("/:subid", a.subs)
|
||||
}
|
||||
|
||||
func (a *SUBController) subs(c *gin.Context) {
|
||||
subId := c.Param("subid")
|
||||
host := strings.Split(c.Request.Host, ":")[0]
|
||||
subs, headers, err := a.subService.GetSubs(subId, host)
|
||||
if err != nil || len(subs) == 0 {
|
||||
c.String(400, "Error!")
|
||||
} else {
|
||||
result := ""
|
||||
for _, sub := range subs {
|
||||
result += sub + "\n"
|
||||
}
|
||||
|
||||
// Add headers
|
||||
c.Writer.Header().Set("Subscription-Userinfo", headers[0])
|
||||
c.Writer.Header().Set("Profile-Update-Interval", headers[1])
|
||||
c.Writer.Header().Set("Profile-Title", headers[2])
|
||||
|
||||
c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
|
||||
}
|
||||
}
|
||||
722
sub/subService.go
Normal file
722
sub/subService.go
Normal file
@@ -0,0 +1,722 @@
|
||||
package sub
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"x-ui/database"
|
||||
"x-ui/database/model"
|
||||
"x-ui/logger"
|
||||
"x-ui/web/service"
|
||||
"x-ui/xray"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
)
|
||||
|
||||
type SubService struct {
|
||||
address string
|
||||
inboundService service.InboundService
|
||||
settingServics service.SettingService
|
||||
}
|
||||
|
||||
func (s *SubService) GetSubs(subId string, host string) ([]string, []string, error) {
|
||||
s.address = host
|
||||
var result []string
|
||||
var headers []string
|
||||
var traffic xray.ClientTraffic
|
||||
var clientTraffics []xray.ClientTraffic
|
||||
inbounds, err := s.getInboundsBySubId(subId)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, inbound := range inbounds {
|
||||
clients, err := s.inboundService.GetClients(inbound)
|
||||
if err != nil {
|
||||
logger.Error("SubService - GetSub: Unable to get clients from inbound")
|
||||
}
|
||||
if clients == nil {
|
||||
continue
|
||||
}
|
||||
if len(inbound.Listen) > 0 && inbound.Listen[0] == '@' {
|
||||
fallbackMaster, err := s.getFallbackMaster(inbound.Listen)
|
||||
if err == nil {
|
||||
inbound.Listen = fallbackMaster.Listen
|
||||
inbound.Port = fallbackMaster.Port
|
||||
var stream map[string]interface{}
|
||||
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
|
||||
var masterStream map[string]interface{}
|
||||
json.Unmarshal([]byte(fallbackMaster.StreamSettings), &masterStream)
|
||||
stream["security"] = masterStream["security"]
|
||||
stream["tlsSettings"] = masterStream["tlsSettings"]
|
||||
modifiedStream, _ := json.MarshalIndent(stream, "", " ")
|
||||
inbound.StreamSettings = string(modifiedStream)
|
||||
}
|
||||
}
|
||||
for _, client := range clients {
|
||||
if client.Enable && client.SubID == subId {
|
||||
link := s.getLink(inbound, client.Email)
|
||||
result = append(result, link)
|
||||
clientTraffics = append(clientTraffics, s.getClientTraffics(inbound.ClientStats, client.Email))
|
||||
}
|
||||
}
|
||||
}
|
||||
for index, clientTraffic := range clientTraffics {
|
||||
if index == 0 {
|
||||
traffic.Up = clientTraffic.Up
|
||||
traffic.Down = clientTraffic.Down
|
||||
traffic.Total = clientTraffic.Total
|
||||
if clientTraffic.ExpiryTime > 0 {
|
||||
traffic.ExpiryTime = clientTraffic.ExpiryTime
|
||||
}
|
||||
} else {
|
||||
traffic.Up += clientTraffic.Up
|
||||
traffic.Down += clientTraffic.Down
|
||||
if traffic.Total == 0 || clientTraffic.Total == 0 {
|
||||
traffic.Total = 0
|
||||
} else {
|
||||
traffic.Total += clientTraffic.Total
|
||||
}
|
||||
if clientTraffic.ExpiryTime != traffic.ExpiryTime {
|
||||
traffic.ExpiryTime = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
headers = append(headers, fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000))
|
||||
updateInterval, _ := s.settingServics.GetSubUpdates()
|
||||
headers = append(headers, fmt.Sprintf("%d", updateInterval))
|
||||
headers = append(headers, subId)
|
||||
return result, headers, nil
|
||||
}
|
||||
|
||||
func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
|
||||
db := database.GetDB()
|
||||
var inbounds []*model.Inbound
|
||||
err := db.Model(model.Inbound{}).Preload("ClientStats").Where("settings like ? and enable = ?", fmt.Sprintf(`%%"subId": "%s"%%`, subId), true).Find(&inbounds).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return inbounds, nil
|
||||
}
|
||||
|
||||
func (s *SubService) getClientTraffics(traffics []xray.ClientTraffic, email string) xray.ClientTraffic {
|
||||
for _, traffic := range traffics {
|
||||
if traffic.Email == email {
|
||||
return traffic
|
||||
}
|
||||
}
|
||||
return xray.ClientTraffic{}
|
||||
}
|
||||
|
||||
func (s *SubService) getFallbackMaster(dest string) (*model.Inbound, error) {
|
||||
db := database.GetDB()
|
||||
var inbound *model.Inbound
|
||||
err := db.Model(model.Inbound{}).
|
||||
Where("JSON_TYPE(settings, '$.fallbacks') = 'array'").
|
||||
Where("EXISTS (SELECT * FROM json_each(settings, '$.fallbacks') WHERE json_extract(value, '$.dest') = ?)", dest).
|
||||
Find(&inbound).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return inbound, nil
|
||||
}
|
||||
|
||||
func (s *SubService) getLink(inbound *model.Inbound, email string) string {
|
||||
switch inbound.Protocol {
|
||||
case "vmess":
|
||||
return s.genVmessLink(inbound, email)
|
||||
case "vless":
|
||||
return s.genVlessLink(inbound, email)
|
||||
case "trojan":
|
||||
return s.genTrojanLink(inbound, email)
|
||||
case "shadowsocks":
|
||||
return s.genShadowsocksLink(inbound, email)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
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",
|
||||
}
|
||||
var stream map[string]interface{}
|
||||
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
|
||||
network, _ := stream["network"].(string)
|
||||
obj["net"] = network
|
||||
switch network {
|
||||
case "tcp":
|
||||
tcp, _ := stream["tcpSettings"].(map[string]interface{})
|
||||
header, _ := tcp["header"].(map[string]interface{})
|
||||
typeStr, _ := header["type"].(string)
|
||||
obj["type"] = typeStr
|
||||
if typeStr == "http" {
|
||||
request := header["request"].(map[string]interface{})
|
||||
requestPath, _ := request["path"].([]interface{})
|
||||
obj["path"] = requestPath[0].(string)
|
||||
headers, _ := request["headers"].(map[string]interface{})
|
||||
obj["host"] = searchHost(headers)
|
||||
}
|
||||
case "kcp":
|
||||
kcp, _ := stream["kcpSettings"].(map[string]interface{})
|
||||
header, _ := kcp["header"].(map[string]interface{})
|
||||
obj["type"], _ = header["type"].(string)
|
||||
obj["path"], _ = kcp["seed"].(string)
|
||||
case "ws":
|
||||
ws, _ := stream["wsSettings"].(map[string]interface{})
|
||||
obj["path"] = ws["path"].(string)
|
||||
headers, _ := ws["headers"].(map[string]interface{})
|
||||
obj["host"] = searchHost(headers)
|
||||
case "http":
|
||||
obj["net"] = "h2"
|
||||
http, _ := stream["httpSettings"].(map[string]interface{})
|
||||
obj["path"], _ = http["path"].(string)
|
||||
obj["host"] = searchHost(http)
|
||||
case "quic":
|
||||
quic, _ := stream["quicSettings"].(map[string]interface{})
|
||||
header := quic["header"].(map[string]interface{})
|
||||
obj["type"], _ = header["type"].(string)
|
||||
obj["host"], _ = quic["security"].(string)
|
||||
obj["path"], _ = quic["key"].(string)
|
||||
case "grpc":
|
||||
grpc, _ := stream["grpcSettings"].(map[string]interface{})
|
||||
obj["path"] = grpc["serviceName"].(string)
|
||||
if grpc["multiMode"].(bool) {
|
||||
obj["type"] = "multi"
|
||||
}
|
||||
}
|
||||
|
||||
security, _ := stream["security"].(string)
|
||||
var domains []interface{}
|
||||
obj["tls"] = security
|
||||
if security == "tls" {
|
||||
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
|
||||
alpns, _ := tlsSetting["alpn"].([]interface{})
|
||||
if len(alpns) > 0 {
|
||||
var alpn []string
|
||||
for _, a := range alpns {
|
||||
alpn = append(alpn, a.(string))
|
||||
}
|
||||
obj["alpn"] = strings.Join(alpn, ",")
|
||||
}
|
||||
tlsSettings, _ := searchKey(tlsSetting, "settings")
|
||||
if tlsSetting != nil {
|
||||
if sniValue, ok := searchKey(tlsSettings, "serverName"); ok {
|
||||
obj["sni"], _ = sniValue.(string)
|
||||
}
|
||||
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
|
||||
obj["fp"], _ = fpValue.(string)
|
||||
}
|
||||
if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
|
||||
obj["allowInsecure"], _ = insecure.(bool)
|
||||
}
|
||||
if domainSettings, ok := searchKey(tlsSettings, "domains"); ok {
|
||||
domains, _ = domainSettings.([]interface{})
|
||||
}
|
||||
}
|
||||
serverName, _ := tlsSetting["serverName"].(string)
|
||||
if serverName != "" {
|
||||
obj["add"] = serverName
|
||||
}
|
||||
}
|
||||
|
||||
clients, _ := s.inboundService.GetClients(inbound)
|
||||
clientIndex := -1
|
||||
for i, client := range clients {
|
||||
if client.Email == email {
|
||||
clientIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
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["add"] = domain["domain"].(string)
|
||||
if index > 0 {
|
||||
links += "\n"
|
||||
}
|
||||
jsonStr, _ := json.MarshalIndent(obj, "", " ")
|
||||
links += "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
|
||||
}
|
||||
return links
|
||||
}
|
||||
|
||||
jsonStr, _ := json.MarshalIndent(obj, "", " ")
|
||||
return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
|
||||
}
|
||||
|
||||
func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||
address := s.address
|
||||
if inbound.Protocol != model.VLESS {
|
||||
return ""
|
||||
}
|
||||
var stream map[string]interface{}
|
||||
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
|
||||
clients, _ := s.inboundService.GetClients(inbound)
|
||||
clientIndex := -1
|
||||
for i, client := range clients {
|
||||
if client.Email == email {
|
||||
clientIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
uuid := clients[clientIndex].ID
|
||||
port := inbound.Port
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
security, _ := stream["security"].(string)
|
||||
var domains []interface{}
|
||||
if security == "tls" {
|
||||
params["security"] = "tls"
|
||||
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
|
||||
alpns, _ := tlsSetting["alpn"].([]interface{})
|
||||
var alpn []string
|
||||
for _, a := range alpns {
|
||||
alpn = append(alpn, a.(string))
|
||||
}
|
||||
if len(alpn) > 0 {
|
||||
params["alpn"] = strings.Join(alpn, ",")
|
||||
}
|
||||
tlsSettings, _ := searchKey(tlsSetting, "settings")
|
||||
if tlsSetting != nil {
|
||||
if sniValue, ok := searchKey(tlsSettings, "serverName"); ok {
|
||||
params["sni"], _ = sniValue.(string)
|
||||
}
|
||||
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
|
||||
params["fp"], _ = fpValue.(string)
|
||||
}
|
||||
if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
|
||||
if insecure.(bool) {
|
||||
params["allowInsecure"] = "1"
|
||||
}
|
||||
}
|
||||
if domainSettings, ok := searchKey(tlsSettings, "domains"); ok {
|
||||
domains, _ = domainSettings.([]interface{})
|
||||
}
|
||||
}
|
||||
|
||||
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
|
||||
params["flow"] = clients[clientIndex].Flow
|
||||
}
|
||||
|
||||
serverName, _ := tlsSetting["serverName"].(string)
|
||||
if serverName != "" {
|
||||
address = serverName
|
||||
}
|
||||
}
|
||||
|
||||
if security == "reality" {
|
||||
params["security"] = "reality"
|
||||
realitySetting, _ := stream["realitySettings"].(map[string]interface{})
|
||||
realitySettings, _ := searchKey(realitySetting, "settings")
|
||||
if realitySetting != nil {
|
||||
if sniValue, ok := searchKey(realitySetting, "serverNames"); ok {
|
||||
sNames, _ := sniValue.([]interface{})
|
||||
params["sni"], _ = sNames[0].(string)
|
||||
}
|
||||
if pbkValue, ok := searchKey(realitySettings, "publicKey"); ok {
|
||||
params["pbk"], _ = pbkValue.(string)
|
||||
}
|
||||
if sidValue, ok := searchKey(realitySetting, "shortIds"); ok {
|
||||
shortIds, _ := sidValue.([]interface{})
|
||||
params["sid"], _ = shortIds[0].(string)
|
||||
}
|
||||
if fpValue, ok := searchKey(realitySettings, "fingerprint"); ok {
|
||||
if fp, ok := fpValue.(string); ok && len(fp) > 0 {
|
||||
params["fp"] = fp
|
||||
}
|
||||
}
|
||||
if spxValue, ok := searchKey(realitySettings, "spiderX"); ok {
|
||||
if spx, ok := spxValue.(string); ok && len(spx) > 0 {
|
||||
params["spx"] = spx
|
||||
}
|
||||
}
|
||||
if serverName, ok := searchKey(realitySettings, "serverName"); ok {
|
||||
if sname, ok := serverName.(string); ok && len(sname) > 0 {
|
||||
address = sname
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
|
||||
params["flow"] = clients[clientIndex].Flow
|
||||
}
|
||||
}
|
||||
|
||||
link := fmt.Sprintf("vless://%s@%s:%d", uuid, address, 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()
|
||||
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.Host = fmt.Sprintf("%s:%d", domain["domain"].(string), port)
|
||||
if index > 0 {
|
||||
links += "\n"
|
||||
}
|
||||
links += url.String()
|
||||
}
|
||||
return links
|
||||
}
|
||||
|
||||
url.Fragment = remark
|
||||
return url.String()
|
||||
}
|
||||
|
||||
func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string {
|
||||
address := s.address
|
||||
if inbound.Protocol != model.Trojan {
|
||||
return ""
|
||||
}
|
||||
var stream map[string]interface{}
|
||||
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
|
||||
clients, _ := s.inboundService.GetClients(inbound)
|
||||
clientIndex := -1
|
||||
for i, client := range clients {
|
||||
if client.Email == email {
|
||||
clientIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
password := clients[clientIndex].Password
|
||||
port := inbound.Port
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
security, _ := stream["security"].(string)
|
||||
var domains []interface{}
|
||||
if security == "tls" {
|
||||
params["security"] = "tls"
|
||||
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
|
||||
alpns, _ := tlsSetting["alpn"].([]interface{})
|
||||
var alpn []string
|
||||
for _, a := range alpns {
|
||||
alpn = append(alpn, a.(string))
|
||||
}
|
||||
if len(alpn) > 0 {
|
||||
params["alpn"] = strings.Join(alpn, ",")
|
||||
}
|
||||
tlsSettings, _ := searchKey(tlsSetting, "settings")
|
||||
if tlsSetting != nil {
|
||||
if sniValue, ok := searchKey(tlsSettings, "serverName"); ok {
|
||||
params["sni"], _ = sniValue.(string)
|
||||
}
|
||||
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
|
||||
params["fp"], _ = fpValue.(string)
|
||||
}
|
||||
if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
|
||||
if insecure.(bool) {
|
||||
params["allowInsecure"] = "1"
|
||||
}
|
||||
}
|
||||
if domainSettings, ok := searchKey(tlsSettings, "domains"); ok {
|
||||
domains, _ = domainSettings.([]interface{})
|
||||
}
|
||||
}
|
||||
|
||||
serverName, _ := tlsSetting["serverName"].(string)
|
||||
if serverName != "" {
|
||||
address = serverName
|
||||
}
|
||||
}
|
||||
|
||||
if security == "reality" {
|
||||
params["security"] = "reality"
|
||||
realitySetting, _ := stream["realitySettings"].(map[string]interface{})
|
||||
realitySettings, _ := searchKey(realitySetting, "settings")
|
||||
if realitySetting != nil {
|
||||
if sniValue, ok := searchKey(realitySetting, "serverNames"); ok {
|
||||
sNames, _ := sniValue.([]interface{})
|
||||
params["sni"], _ = sNames[0].(string)
|
||||
}
|
||||
if pbkValue, ok := searchKey(realitySettings, "publicKey"); ok {
|
||||
params["pbk"], _ = pbkValue.(string)
|
||||
}
|
||||
if sidValue, ok := searchKey(realitySettings, "shortIds"); ok {
|
||||
shortIds, _ := sidValue.([]interface{})
|
||||
params["sid"], _ = shortIds[0].(string)
|
||||
}
|
||||
if fpValue, ok := searchKey(realitySettings, "fingerprint"); ok {
|
||||
if fp, ok := fpValue.(string); ok && len(fp) > 0 {
|
||||
params["fp"] = fp
|
||||
}
|
||||
}
|
||||
if spxValue, ok := searchKey(realitySettings, "spiderX"); ok {
|
||||
if spx, ok := spxValue.(string); ok && len(spx) > 0 {
|
||||
params["spx"] = spx
|
||||
}
|
||||
}
|
||||
if serverName, ok := searchKey(realitySettings, "serverName"); ok {
|
||||
if sname, ok := serverName.(string); ok && len(sname) > 0 {
|
||||
address = sname
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
|
||||
params["flow"] = clients[clientIndex].Flow
|
||||
}
|
||||
}
|
||||
|
||||
link := fmt.Sprintf("trojan://%s@%s:%d", password, address, 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()
|
||||
|
||||
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.Host = fmt.Sprintf("%s:%d", domain["domain"].(string), port)
|
||||
if index > 0 {
|
||||
links += "\n"
|
||||
}
|
||||
links += url.String()
|
||||
}
|
||||
return links
|
||||
}
|
||||
|
||||
url.Fragment = remark
|
||||
return url.String()
|
||||
}
|
||||
|
||||
func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) string {
|
||||
address := s.address
|
||||
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{}
|
||||
json.Unmarshal([]byte(inbound.Settings), &settings)
|
||||
inboundPassword := settings["password"].(string)
|
||||
method := settings["method"].(string)
|
||||
clientIndex := -1
|
||||
for i, client := range clients {
|
||||
if client.Email == email {
|
||||
clientIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
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:%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()
|
||||
|
||||
remark := fmt.Sprintf("%s-%s", inbound.Remark, clients[clientIndex].Email)
|
||||
url.Fragment = remark
|
||||
return url.String()
|
||||
}
|
||||
|
||||
func searchKey(data interface{}, key string) (interface{}, bool) {
|
||||
switch val := data.(type) {
|
||||
case map[string]interface{}:
|
||||
for k, v := range val {
|
||||
if k == key {
|
||||
return v, true
|
||||
}
|
||||
if result, ok := searchKey(v, key); ok {
|
||||
return result, true
|
||||
}
|
||||
}
|
||||
case []interface{}:
|
||||
for _, v := range val {
|
||||
if result, ok := searchKey(v, key); ok {
|
||||
return result, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func searchHost(headers interface{}) string {
|
||||
data, _ := headers.(map[string]interface{})
|
||||
for k, v := range data {
|
||||
if strings.EqualFold(k, "host") {
|
||||
switch v.(type) {
|
||||
case []interface{}:
|
||||
hosts, _ := v.([]interface{})
|
||||
if len(hosts) > 0 {
|
||||
return hosts[0].(string)
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
case interface{}:
|
||||
return v.(string)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
@@ -6,8 +6,6 @@ import (
|
||||
"x-ui/logger"
|
||||
)
|
||||
|
||||
var CtxDone = errors.New("context done")
|
||||
|
||||
func NewErrorf(format string, a ...interface{}) error {
|
||||
msg := fmt.Sprintf(format, a...)
|
||||
return errors.New(msg)
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package common
|
||||
|
||||
import "sort"
|
||||
|
||||
func IsSubString(target string, str_array []string) bool {
|
||||
sort.Strings(str_array)
|
||||
index := sort.SearchStrings(str_array, target)
|
||||
return index < len(str_array) && str_array[index] == target
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package util
|
||||
|
||||
import "context"
|
||||
|
||||
func IsDone(ctx context.Context) bool {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
type RawMessage []byte
|
||||
|
||||
// MarshalJSON 自定义 json.RawMessage 默认行为
|
||||
// MarshalJSON: Customize json.RawMessage default behavior
|
||||
func (m RawMessage) MarshalJSON() ([]byte, error) {
|
||||
if len(m) == 0 {
|
||||
return []byte("null"), nil
|
||||
@@ -14,7 +14,7 @@ func (m RawMessage) MarshalJSON() ([]byte, error) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON sets *m to a copy of data.
|
||||
// UnmarshalJSON: sets *m to a copy of data.
|
||||
func (m *RawMessage) UnmarshalJSON(data []byte) error {
|
||||
if m == nil {
|
||||
return errors.New("json.RawMessage: UnmarshalJSON on nil pointer")
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
html,
|
||||
body {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.ant-space {
|
||||
@@ -10,6 +27,12 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.ant-layout-sider {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-card {
|
||||
border-radius: 30px;
|
||||
}
|
||||
@@ -142,6 +165,11 @@
|
||||
transform: translateY(-30px)
|
||||
}
|
||||
|
||||
.ant-list-item-meta-title {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.ant-progress-inner {
|
||||
background-color: #EBEEF5;
|
||||
}
|
||||
@@ -156,6 +184,12 @@
|
||||
padding:16px;
|
||||
}
|
||||
|
||||
.ant-table-expand-icon-th,
|
||||
.ant-table-row-expand-icon-cell {
|
||||
width: 30px;
|
||||
min-width: 30px;
|
||||
}
|
||||
|
||||
.ant-menu-dark,
|
||||
.ant-menu-dark .ant-menu-sub,
|
||||
.ant-layout-header,
|
||||
@@ -163,7 +197,11 @@
|
||||
.ant-layout-sider-zero-width-trigger,
|
||||
.ant-dropdown-menu-dark,.ant-dropdown-menu-dark .ant-dropdown-menu,
|
||||
.ant-menu-dark.ant-menu-horizontal>.ant-menu-item,.ant-menu-dark.ant-menu-horizontal>.ant-menu-submenu {
|
||||
background:#161b22
|
||||
background:#1a212a
|
||||
}
|
||||
|
||||
.ant-tabs:not(.ant-card-dark) {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.ant-card-dark {
|
||||
@@ -174,6 +212,19 @@
|
||||
|
||||
.ant-card-dark:hover {
|
||||
border-color: #e8e8e8;
|
||||
box-shadow: 0 2px 8px rgba(255,255,255,.15);
|
||||
}
|
||||
|
||||
.ant-setting-textarea {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.ant-card-dark-box-nohover{
|
||||
padding: 0 20px 20px !important;
|
||||
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 0%) !important;
|
||||
}
|
||||
.ant-card-dark-box-nohover:hover{
|
||||
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 0%) !important;
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-table-thead th {
|
||||
@@ -188,9 +239,11 @@
|
||||
|
||||
.ant-card-dark .ant-collapse-content,
|
||||
.ant-card-dark .ant-calendar,
|
||||
.ant-card-dark .ant-table-placeholder {
|
||||
.ant-card-dark .ant-table-placeholder,
|
||||
.ant-card-dark .ant-input-group-addon {
|
||||
color: hsla(0,0%,100%,.65);
|
||||
background-color: #262f3d;
|
||||
border: 1px solid rgb(0 150 112 / 0%);
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-list-item-meta-title,
|
||||
@@ -204,32 +257,49 @@
|
||||
.ant-card-dark .ant-modal-close,
|
||||
.ant-card-dark i,
|
||||
.ant-card-dark .ant-select-dropdown-menu-item,
|
||||
.ant-card-dark .ant-calendar-day-select,
|
||||
.ant-card-dark .ant-calendar-month-select,
|
||||
.ant-card-dark .ant-calendar-year-select,
|
||||
.ant-card-dark .ant-calendar-date,
|
||||
.ant-card-dark .ant-collapse>.ant-collapse-item>.ant-collapse-header,
|
||||
.ant-card-dark .ant-empty-normal {
|
||||
.ant-card-dark .ant-empty-normal,
|
||||
.ant-card-dark .ant-checkbox+span {
|
||||
color: hsla(0,0%,100%,.65);
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-table-tbody>tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)>td,
|
||||
.ant-card-dark .ant-select-dropdown-menu-item:hover:not(.ant-select-dropdown-menu-item-disabled),
|
||||
.ant-card-dark .ant-calendar-date:hover {
|
||||
background-color: #004488;
|
||||
.ant-card-dark .ant-calendar-date:hover,
|
||||
.ant-card-dark .ant-select-dropdown-menu-item-active,
|
||||
.ant-card-dark li.ant-calendar-time-picker-select-option-selected {
|
||||
background-color: #11314d;
|
||||
}
|
||||
|
||||
.ant-card-dark tbody .ant-table-expanded-row {
|
||||
.ant-card-dark tbody .ant-table-expanded-row,
|
||||
.ant-card-dark .ant-calendar-time-picker-inner {
|
||||
color: hsla(0,0%,100%,.65);
|
||||
background-color: #1a212a;
|
||||
}
|
||||
|
||||
.ant-input-number {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-input,
|
||||
.ant-card-dark .ant-input-number,
|
||||
.ant-card-dark .ant-input-number-handler-wrap,
|
||||
.ant-card-dark .ant-calendar-input,
|
||||
.ant-card-dark .ant-select-dropdown-menu-item-selected,
|
||||
.ant-card-dark .ant-select-selection {
|
||||
.ant-card-dark .ant-select-selection,
|
||||
.ant-card-dark .ant-calendar-picker-clear {
|
||||
color: hsla(0,0%,100%,.65);
|
||||
background-color: #2e3b52;
|
||||
background-color: #193752;
|
||||
border: 1px solid rgba(0, 65, 150, 0);
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-select-disabled .ant-select-selection {
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
background-color: #242c3a;
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-collapse-item {
|
||||
@@ -237,14 +307,27 @@
|
||||
background-color: #161b22;
|
||||
}
|
||||
|
||||
.ant-dropdown-menu-dark,
|
||||
.ant-card-dark .ant-modal-content {
|
||||
border: 1px solid rgba(255, 255, 255, 0.65);
|
||||
box-shadow: 0 2px 8px rgba(255,255,255,.15);
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-modal-content,
|
||||
.ant-card-dark .ant-modal-body,
|
||||
.ant-card-dark .ant-modal-header,
|
||||
.ant-card-dark .ant-calendar-selected-day .ant-calendar-date {
|
||||
.ant-card-dark .ant-modal-header {
|
||||
color: hsla(0,0%,100%,.65);
|
||||
background-color: #222a37;
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-calendar-selected-day .ant-calendar-date {
|
||||
background-color: #1668dc;
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-calendar-time-picker-select li:hover {
|
||||
background: #1668dc;
|
||||
}
|
||||
|
||||
.client-table-header {
|
||||
background-color: #f0f2f5;
|
||||
}
|
||||
@@ -278,6 +361,12 @@
|
||||
border: 1px solid hsla(0,0%,100%,.30);
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-tag {
|
||||
color: hsla(0,0%,100%,.65);
|
||||
background: rgba(255,255,255,.04);
|
||||
border-color: #434343;
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-tag-blue {
|
||||
color: #3c9ae8;
|
||||
background: #111d2c;
|
||||
@@ -308,7 +397,8 @@
|
||||
border-color: #593815;
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-table-row-expand-icon {
|
||||
.ant-card-dark .ant-table-row-expand-icon,
|
||||
.ant-card-dark .ant-checkbox-inner {
|
||||
background: none;
|
||||
}
|
||||
|
||||
@@ -316,16 +406,44 @@
|
||||
background-color: #0c61b0;
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-btn {
|
||||
.ant-card-dark .ant-btn,
|
||||
.ant-card-dark .ant-radio-button-wrapper {
|
||||
color: hsla(0,0%,100%,.65);
|
||||
background: none;
|
||||
border: 1px solid hsla(0,0%,100%,.65);
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-radio-button-wrapper:hover {
|
||||
color: #177ddc;
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-btn-primary {
|
||||
color: hsla(0,0%,100%,.65);
|
||||
background-color: #073763;
|
||||
border-color: #1890ff;
|
||||
text-shadow: 0 -1px 0 rgba(0,0,0,.12);
|
||||
box-shadow: 0 2px 0 rgba(0,0,0,.045);
|
||||
text-shadow: 0 -1px 0 rgba(255,255,255,.12);
|
||||
box-shadow: 0 2px 0 rgba(255,255,255,.045);
|
||||
}
|
||||
.ant-card-dark .ant-btn-primary:hover {
|
||||
background-color: #40a9ff;
|
||||
border-color: #40a9ff;
|
||||
}
|
||||
|
||||
.ant-dark .ant-popover-content {
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(255,255,255,.15);
|
||||
}
|
||||
|
||||
.ant-dark .ant-popover-inner {
|
||||
background: #222a37;
|
||||
}
|
||||
|
||||
.ant-dark .ant-popover-title,
|
||||
.ant-dark .ant-popover-inner-content {
|
||||
color: hsla(0,0%,100%,.65);
|
||||
}
|
||||
|
||||
.ant-dark .ant-popover-placement-top>.ant-popover-content>.ant-popover-arrow {
|
||||
border-color: transparent #2e3b52 #2e3b52 transparent;
|
||||
}
|
||||
BIN
web/assets/favicon.ico
Normal file
BIN
web/assets/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
@@ -2,11 +2,15 @@ axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded
|
||||
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
|
||||
axios.interceptors.request.use(
|
||||
config => {
|
||||
config.data = Qs.stringify(config.data, {
|
||||
arrayFormat: 'repeat'
|
||||
});
|
||||
(config) => {
|
||||
if (config.data instanceof FormData) {
|
||||
config.headers['Content-Type'] = 'multipart/form-data';
|
||||
} else {
|
||||
config.data = Qs.stringify(config.data, {
|
||||
arrayFormat: 'repeat',
|
||||
});
|
||||
}
|
||||
return config;
|
||||
},
|
||||
error => Promise.reject(error)
|
||||
);
|
||||
(error) => Promise.reject(error),
|
||||
);
|
||||
|
||||
@@ -1,36 +1,41 @@
|
||||
supportLangs = [
|
||||
const supportLangs = [
|
||||
{
|
||||
name : "English",
|
||||
value : "en-US",
|
||||
icon : "🇺🇸"
|
||||
name: 'English',
|
||||
value: 'en-US',
|
||||
icon: '🇺🇸',
|
||||
},
|
||||
{
|
||||
name : "Farsi",
|
||||
value : "fa_IR",
|
||||
icon : "🇮🇷"
|
||||
name: 'فارسی',
|
||||
value: 'fa_IR',
|
||||
icon: '🇮🇷',
|
||||
},
|
||||
{
|
||||
name : "汉语",
|
||||
value : "zh-Hans",
|
||||
icon : "🇨🇳"
|
||||
name: '汉语',
|
||||
value: 'zh-Hans',
|
||||
icon: '🇨🇳',
|
||||
},
|
||||
]
|
||||
{
|
||||
name: 'Русский',
|
||||
value: 'ru_RU',
|
||||
icon: '🇷🇺',
|
||||
},
|
||||
];
|
||||
|
||||
function getLang(){
|
||||
let lang = getCookie('lang')
|
||||
function getLang() {
|
||||
let lang = getCookie('lang');
|
||||
|
||||
if (! lang){
|
||||
if (window.navigator){
|
||||
if (!lang) {
|
||||
if (window.navigator) {
|
||||
lang = window.navigator.language || window.navigator.userLanguage;
|
||||
|
||||
if (isSupportLang(lang)){
|
||||
setCookie('lang' , lang , 150)
|
||||
}else{
|
||||
setCookie('lang' , 'en-US' , 150)
|
||||
if (isSupportLang(lang)) {
|
||||
setCookie('lang', lang, 150);
|
||||
} else {
|
||||
setCookie('lang', 'en-US', 150);
|
||||
window.location.reload();
|
||||
}
|
||||
}else{
|
||||
setCookie('lang' , 'en-US' , 150)
|
||||
} else {
|
||||
setCookie('lang', 'en-US', 150);
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
@@ -38,47 +43,21 @@ function getLang(){
|
||||
return lang;
|
||||
}
|
||||
|
||||
function setLang(lang){
|
||||
|
||||
if (!isSupportLang(lang)){
|
||||
function setLang(lang) {
|
||||
if (!isSupportLang(lang)) {
|
||||
lang = 'en-US';
|
||||
}
|
||||
|
||||
setCookie('lang' , lang , 150)
|
||||
setCookie('lang', lang, 150);
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
function isSupportLang(lang){
|
||||
for (l of supportLangs){
|
||||
if (l.value === lang){
|
||||
function isSupportLang(lang) {
|
||||
for (l of supportLangs) {
|
||||
if (l.value === lang) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function getCookie(cname) {
|
||||
let name = cname + "=";
|
||||
let decodedCookie = decodeURIComponent(document.cookie);
|
||||
let ca = decodedCookie.split(';');
|
||||
for(let i = 0; i <ca.length; i++) {
|
||||
let c = ca[i];
|
||||
while (c.charAt(0) == ' ') {
|
||||
c = c.substring(1);
|
||||
}
|
||||
if (c.indexOf(name) == 0) {
|
||||
return c.substring(name.length, c.length);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function setCookie(cname, cvalue, exdays) {
|
||||
const d = new Date();
|
||||
d.setTime(d.getTime() + (exdays*24*60*60*1000));
|
||||
let expires = "expires="+ d.toUTCString();
|
||||
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
|
||||
}
|
||||
@@ -151,9 +151,9 @@ class DBInbound {
|
||||
}
|
||||
}
|
||||
|
||||
genLink(clientIndex) {
|
||||
genLink(address=this.address, remark=this.remark, clientIndex=0) {
|
||||
const inbound = this.toInbound();
|
||||
return inbound.genLink(this.address, this.remark, clientIndex);
|
||||
return inbound.genLink(address, remark, clientIndex);
|
||||
}
|
||||
|
||||
get genInboundLinks() {
|
||||
@@ -166,19 +166,30 @@ class AllSetting {
|
||||
|
||||
constructor(data) {
|
||||
this.webListen = "";
|
||||
this.webDomain = "";
|
||||
this.webPort = 54321;
|
||||
this.webCertFile = "";
|
||||
this.webKeyFile = "";
|
||||
this.webBasePath = "/";
|
||||
this.sessionMaxAge = "";
|
||||
this.expireDiff = "";
|
||||
this.trafficDiff = "";
|
||||
this.tgBotEnable = false;
|
||||
this.tgBotToken = "";
|
||||
this.tgBotChatId = "";
|
||||
this.tgRunTime = "@daily";
|
||||
this.tgBotBackup = false;
|
||||
this.tgExpireDiff = "";
|
||||
this.tgTrafficDiff = "";
|
||||
this.tgCpu = "";
|
||||
this.tgLang = "";
|
||||
this.xrayTemplateConfig = "";
|
||||
this.subEnable = false;
|
||||
this.subListen = "";
|
||||
this.subPort = "2096";
|
||||
this.subPath = "/sub/";
|
||||
this.subDomain = "";
|
||||
this.subCertFile = "";
|
||||
this.subKeyFile = "";
|
||||
this.subUpdates = 0;
|
||||
|
||||
this.timeLocation = "Asia/Tehran";
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -56,14 +56,79 @@ function toFixed(num, n) {
|
||||
return Math.round(num * n) / n;
|
||||
}
|
||||
|
||||
function debounce (fn, delay) {
|
||||
var timeoutID = null
|
||||
function debounce(fn, delay) {
|
||||
var timeoutID = null;
|
||||
return function () {
|
||||
clearTimeout(timeoutID)
|
||||
var args = arguments
|
||||
var that = this
|
||||
timeoutID = setTimeout(function () {
|
||||
fn.apply(that, args)
|
||||
}, delay)
|
||||
clearTimeout(timeoutID);
|
||||
var args = arguments;
|
||||
var that = this;
|
||||
timeoutID = setTimeout(function () {
|
||||
fn.apply(that, args);
|
||||
}, delay);
|
||||
};
|
||||
}
|
||||
|
||||
function getCookie(cname) {
|
||||
let name = cname + "=";
|
||||
let decodedCookie = decodeURIComponent(document.cookie);
|
||||
let ca = decodedCookie.split(";");
|
||||
for (let i = 0; i < ca.length; i++) {
|
||||
let c = ca[i];
|
||||
while (c.charAt(0) == " ") {
|
||||
c = c.substring(1);
|
||||
}
|
||||
if (c.indexOf(name) == 0) {
|
||||
return c.substring(name.length, c.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function setCookie(cname, cvalue, exdays) {
|
||||
const d = new Date();
|
||||
d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000);
|
||||
let expires = "expires=" + d.toUTCString();
|
||||
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
|
||||
}
|
||||
|
||||
function usageColor(data, threshold, total) {
|
||||
switch (true) {
|
||||
case data === null:
|
||||
return "blue";
|
||||
case total <= 0:
|
||||
return "blue";
|
||||
case data < total - threshold:
|
||||
return "cyan";
|
||||
case data < total:
|
||||
return "orange";
|
||||
default:
|
||||
return "red";
|
||||
}
|
||||
}
|
||||
|
||||
function doAllItemsExist(array1, array2) {
|
||||
for (let i = 0; i < array1.length; i++) {
|
||||
if (!array2.includes(array1[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
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}`;
|
||||
}
|
||||
|
||||
@@ -1,67 +1,67 @@
|
||||
const oneMinute = 1000 * 60; // 一分钟的毫秒数
|
||||
const oneHour = oneMinute * 60; // 一小时的毫秒数
|
||||
const oneDay = oneHour * 24; // 一天的毫秒数
|
||||
const oneWeek = oneDay * 7; // 一星期的毫秒数
|
||||
const oneMonth = oneDay * 30; // 一个月的毫秒数
|
||||
const oneMinute = 1000 * 60; // 一The millise times of minutes
|
||||
const oneHour = oneMinute * 60; // 一Hours of millise times
|
||||
const oneDay = oneHour * 24; // 一Day's milliseconds
|
||||
const oneWeek = oneDay * 7; // 一Number of millise times on week
|
||||
const oneMonth = oneDay * 30; // 一Number of millise times a month
|
||||
|
||||
/**
|
||||
* 按天数减少
|
||||
* Decrease by day
|
||||
*
|
||||
* @param days 要减少的天数
|
||||
* @param days A few days to reduce
|
||||
*/
|
||||
Date.prototype.minusDays = function (days) {
|
||||
return this.minusMillis(oneDay * days);
|
||||
};
|
||||
|
||||
/**
|
||||
* 按天数增加
|
||||
* Increase by day
|
||||
*
|
||||
* @param days 要增加的天数
|
||||
* @param days The number of days to be increased
|
||||
*/
|
||||
Date.prototype.plusDays = function (days) {
|
||||
return this.plusMillis(oneDay * days);
|
||||
};
|
||||
|
||||
/**
|
||||
* 按小时减少
|
||||
* Reduced
|
||||
*
|
||||
* @param hours 要减少的小时数
|
||||
* @param hours The number of hours to be reduced
|
||||
*/
|
||||
Date.prototype.minusHours = function (hours) {
|
||||
return this.minusMillis(oneHour * hours);
|
||||
};
|
||||
|
||||
/**
|
||||
* 按小时增加
|
||||
* Increase
|
||||
*
|
||||
* @param hours 要增加的小时数
|
||||
* @param hours Increase the number of hours
|
||||
*/
|
||||
Date.prototype.plusHours = function (hours) {
|
||||
return this.plusMillis(oneHour * hours);
|
||||
};
|
||||
|
||||
/**
|
||||
* 按分钟减少
|
||||
* Decrease by minute
|
||||
*
|
||||
* @param minutes 要减少的分钟数
|
||||
* @param minutes The number of minutes to be reduced
|
||||
*/
|
||||
Date.prototype.minusMinutes = function (minutes) {
|
||||
return this.minusMillis(oneMinute * minutes);
|
||||
};
|
||||
|
||||
/**
|
||||
* 按分钟增加
|
||||
* Increase
|
||||
*
|
||||
* @param minutes 要增加的分钟数
|
||||
* @param minutes The number of minutes to be increased
|
||||
*/
|
||||
Date.prototype.plusMinutes = function (minutes) {
|
||||
return this.plusMillis(oneMinute * minutes);
|
||||
};
|
||||
|
||||
/**
|
||||
* 按毫秒减少
|
||||
* Decrease by millisecond
|
||||
*
|
||||
* @param millis 要减少的毫秒数
|
||||
* @param millis Number of milliligues to be reduced
|
||||
*/
|
||||
Date.prototype.minusMillis = function(millis) {
|
||||
let time = this.getTime() - millis;
|
||||
@@ -71,9 +71,9 @@ Date.prototype.minusMillis = function(millis) {
|
||||
};
|
||||
|
||||
/**
|
||||
* 按毫秒增加
|
||||
* Add in milliseconds
|
||||
*
|
||||
* @param millis 要增加的毫秒数
|
||||
* @param millis To increase the millimeter number
|
||||
*/
|
||||
Date.prototype.plusMillis = function(millis) {
|
||||
let time = this.getTime() + millis;
|
||||
@@ -83,7 +83,7 @@ Date.prototype.plusMillis = function(millis) {
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置时间为当天的 00:00:00.000
|
||||
* Setting time is the day 00:00:00.000
|
||||
*/
|
||||
Date.prototype.setMinTime = function () {
|
||||
this.setHours(0);
|
||||
@@ -94,7 +94,7 @@ Date.prototype.setMinTime = function () {
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置时间为当天的 23:59:59.999
|
||||
* Setting time is the day 23:59:59.999
|
||||
*/
|
||||
Date.prototype.setMaxTime = function () {
|
||||
this.setHours(23);
|
||||
@@ -105,37 +105,36 @@ Date.prototype.setMaxTime = function () {
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
* Formatting date
|
||||
*/
|
||||
Date.prototype.formatDate = function () {
|
||||
return this.getFullYear() + "-" + addZero(this.getMonth() + 1) + "-" + addZero(this.getDate());
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化时间
|
||||
* Formatting time
|
||||
*/
|
||||
Date.prototype.formatTime = function () {
|
||||
return addZero(this.getHours()) + ":" + addZero(this.getMinutes()) + ":" + addZero(this.getSeconds());
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化日期加时间
|
||||
* Formatting date plus time
|
||||
*
|
||||
* @param split 日期和时间之间的分隔符,默认是一个空格
|
||||
* @param split Division between date and time, the default is a space
|
||||
*/
|
||||
Date.prototype.formatDateTime = function (split = ' ') {
|
||||
return this.formatDate() + split + this.formatTime();
|
||||
};
|
||||
|
||||
class DateUtil {
|
||||
|
||||
// 字符串转 Date 对象
|
||||
// String string to date object
|
||||
static parseDate(str) {
|
||||
return new Date(str.replace(/-/g, '/'));
|
||||
}
|
||||
|
||||
static formatMillis(millis) {
|
||||
return moment(millis).format('YYYY-M-D H:m:s')
|
||||
return moment(millis).format('YYYY-M-D H:m:s');
|
||||
}
|
||||
|
||||
static firstDayOfMonth() {
|
||||
@@ -144,4 +143,4 @@ class DateUtil {
|
||||
date.setMinTime();
|
||||
return date;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,13 +68,11 @@ class HttpUtil {
|
||||
}
|
||||
|
||||
class PromiseUtil {
|
||||
|
||||
static async sleep(timeout) {
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, timeout)
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const seq = [
|
||||
@@ -90,7 +88,6 @@ const seq = [
|
||||
];
|
||||
|
||||
class RandomUtil {
|
||||
|
||||
static randomIntRange(min, max) {
|
||||
return parseInt(Math.random() * (max - min) + min, 10);
|
||||
}
|
||||
@@ -137,19 +134,35 @@ class RandomUtil {
|
||||
});
|
||||
}
|
||||
|
||||
static randomText() {
|
||||
static randomText(minLen = 6, varLen = 5) {
|
||||
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
|
||||
var string = '';
|
||||
var len = 6 + Math.floor(Math.random() * 5)
|
||||
for(var ii=0; ii<len; ii++){
|
||||
var len = minLen + Math.floor(Math.random() * varLen);
|
||||
for (var ii = 0; ii < len; ii++) {
|
||||
string += chars[Math.floor(Math.random() * chars.length)];
|
||||
}
|
||||
return string;
|
||||
}
|
||||
|
||||
static randomShadowsocksPassword() {
|
||||
let array = new Uint8Array(32);
|
||||
window.crypto.getRandomValues(array);
|
||||
return btoa(String.fromCharCode.apply(null, array));
|
||||
}
|
||||
|
||||
static randomShortId() {
|
||||
let shortIds = ['','','',''];
|
||||
for (var ii = 0; ii < 4; ii++) {
|
||||
for (var jj = 0; jj < this.randomInt(8); jj++){
|
||||
let randomNum = this.randomInt(256);
|
||||
shortIds[ii] += ('0' + randomNum.toString(16)).slice(-2)
|
||||
}
|
||||
}
|
||||
return shortIds;
|
||||
}
|
||||
}
|
||||
|
||||
class ObjectUtil {
|
||||
|
||||
static getPropIgnoreCase(obj, prop) {
|
||||
for (const name in obj) {
|
||||
if (!obj.hasOwnProperty(name)) {
|
||||
@@ -297,5 +310,4 @@ class ObjectUtil {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"x-ui/web/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type APIController struct {
|
||||
BaseController
|
||||
|
||||
inboundController *InboundController
|
||||
settingController *SettingController
|
||||
Tgbot service.Tgbot
|
||||
}
|
||||
|
||||
func NewAPIController(g *gin.RouterGroup) *APIController {
|
||||
@@ -23,9 +24,18 @@ func (a *APIController) initRouter(g *gin.RouterGroup) {
|
||||
|
||||
g.GET("/", a.inbounds)
|
||||
g.GET("/get/:id", a.inbound)
|
||||
g.GET("/getClientTraffics/:email", a.getClientTraffics)
|
||||
g.POST("/add", a.addInbound)
|
||||
g.POST("/del/:id", a.delInbound)
|
||||
g.POST("/update/:id", a.updateInbound)
|
||||
g.POST("/addClient", a.addInboundClient)
|
||||
g.POST("/:id/delClient/:clientId", a.delInboundClient)
|
||||
g.POST("/updateClient/:clientId", a.updateInboundClient)
|
||||
g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
|
||||
g.POST("/resetAllTraffics", a.resetAllTraffics)
|
||||
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
|
||||
g.POST("/delDepletedClients/:id", a.delDepletedClients)
|
||||
g.GET("/createbackup", a.createBackup)
|
||||
|
||||
a.inboundController = NewInboundController(g)
|
||||
}
|
||||
@@ -33,15 +43,55 @@ func (a *APIController) initRouter(g *gin.RouterGroup) {
|
||||
func (a *APIController) inbounds(c *gin.Context) {
|
||||
a.inboundController.getInbounds(c)
|
||||
}
|
||||
|
||||
func (a *APIController) inbound(c *gin.Context) {
|
||||
a.inboundController.getInbound(c)
|
||||
}
|
||||
|
||||
func (a *APIController) getClientTraffics(c *gin.Context) {
|
||||
a.inboundController.getClientTraffics(c)
|
||||
}
|
||||
|
||||
func (a *APIController) addInbound(c *gin.Context) {
|
||||
a.inboundController.addInbound(c)
|
||||
}
|
||||
|
||||
func (a *APIController) delInbound(c *gin.Context) {
|
||||
a.inboundController.delInbound(c)
|
||||
}
|
||||
|
||||
func (a *APIController) updateInbound(c *gin.Context) {
|
||||
a.inboundController.updateInbound(c)
|
||||
}
|
||||
|
||||
func (a *APIController) addInboundClient(c *gin.Context) {
|
||||
a.inboundController.addInboundClient(c)
|
||||
}
|
||||
|
||||
func (a *APIController) delInboundClient(c *gin.Context) {
|
||||
a.inboundController.delInboundClient(c)
|
||||
}
|
||||
|
||||
func (a *APIController) updateInboundClient(c *gin.Context) {
|
||||
a.inboundController.updateInboundClient(c)
|
||||
}
|
||||
|
||||
func (a *APIController) resetClientTraffic(c *gin.Context) {
|
||||
a.inboundController.resetClientTraffic(c)
|
||||
}
|
||||
|
||||
func (a *APIController) resetAllTraffics(c *gin.Context) {
|
||||
a.inboundController.resetAllTraffics(c)
|
||||
}
|
||||
|
||||
func (a *APIController) resetAllClientTraffics(c *gin.Context) {
|
||||
a.inboundController.resetAllClientTraffics(c)
|
||||
}
|
||||
|
||||
func (a *APIController) delDepletedClients(c *gin.Context) {
|
||||
a.inboundController.delDepletedClients(c)
|
||||
}
|
||||
|
||||
func (a *APIController) createBackup(c *gin.Context) {
|
||||
a.Tgbot.SendBackupToAdmins()
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"x-ui/logger"
|
||||
"x-ui/web/locale"
|
||||
"x-ui/web/session"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type BaseController struct {
|
||||
@@ -12,7 +15,7 @@ type BaseController struct {
|
||||
func (a *BaseController) checkLogin(c *gin.Context) {
|
||||
if !session.IsLogin(c) {
|
||||
if isAjax(c) {
|
||||
pureJsonMsg(c, false, I18n(c, "pages.login.loginAgain"))
|
||||
pureJsonMsg(c, false, I18nWeb(c, "pages.login.loginAgain"))
|
||||
} else {
|
||||
c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
|
||||
}
|
||||
@@ -22,11 +25,13 @@ func (a *BaseController) checkLogin(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func I18n(c *gin.Context, name string) string {
|
||||
anyfunc, _ := c.Get("I18n")
|
||||
i18n, _ := anyfunc.(func(key string, params ...string) (string, error))
|
||||
|
||||
message, _ := i18n(name)
|
||||
|
||||
return message
|
||||
func I18nWeb(c *gin.Context, name string, params ...string) string {
|
||||
anyfunc, funcExists := c.Get("I18n")
|
||||
if !funcExists {
|
||||
logger.Warning("I18n function not exists in gin context!")
|
||||
return ""
|
||||
}
|
||||
i18nFunc, _ := anyfunc.(func(i18nType locale.I18nType, key string, keyParams ...string) string)
|
||||
msg := i18nFunc(locale.Web, name, params...)
|
||||
return msg
|
||||
}
|
||||
|
||||
@@ -31,10 +31,13 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
|
||||
g.POST("/add", a.addInbound)
|
||||
g.POST("/del/:id", a.delInbound)
|
||||
g.POST("/update/:id", a.updateInbound)
|
||||
g.POST("/addClient/", a.addInboundClient)
|
||||
g.POST("/delClient/:email", a.delInboundClient)
|
||||
g.POST("/updateClient/:index", a.updateInboundClient)
|
||||
g.POST("/addClient", a.addInboundClient)
|
||||
g.POST("/:id/delClient/:clientId", a.delInboundClient)
|
||||
g.POST("/updateClient/:clientId", a.updateInboundClient)
|
||||
g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
|
||||
g.POST("/resetAllTraffics", a.resetAllTraffics)
|
||||
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
|
||||
g.POST("/delDepletedClients/:id", a.delDepletedClients)
|
||||
|
||||
}
|
||||
|
||||
@@ -55,30 +58,40 @@ func (a *InboundController) getInbounds(c *gin.Context) {
|
||||
user := session.GetLoginUser(c)
|
||||
inbounds, err := a.inboundService.GetInbounds(user.Id)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.inbounds.toasts.obtain"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, inbounds, nil)
|
||||
}
|
||||
|
||||
func (a *InboundController) getInbound(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "get"), err)
|
||||
jsonMsg(c, I18nWeb(c, "get"), err)
|
||||
return
|
||||
}
|
||||
inbound, err := a.inboundService.GetInbound(id)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.inbounds.toasts.obtain"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, inbound, nil)
|
||||
}
|
||||
func (a *InboundController) getClientTraffics(c *gin.Context) {
|
||||
email := c.Param("email")
|
||||
clientTraffics, err := a.inboundService.GetClientTrafficByEmail(email)
|
||||
if err != nil {
|
||||
jsonMsg(c, "Error getting traffics", err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, clientTraffics, nil)
|
||||
}
|
||||
|
||||
func (a *InboundController) addInbound(c *gin.Context) {
|
||||
inbound := &model.Inbound{}
|
||||
err := c.ShouldBind(inbound)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.inbounds.addTo"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.create"), err)
|
||||
return
|
||||
}
|
||||
user := session.GetLoginUser(c)
|
||||
@@ -86,7 +99,7 @@ func (a *InboundController) addInbound(c *gin.Context) {
|
||||
inbound.Enable = true
|
||||
inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
|
||||
inbound, err = a.inboundService.AddInbound(inbound)
|
||||
jsonMsgObj(c, I18n(c, "pages.inbounds.addTo"), inbound, err)
|
||||
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.create"), inbound, err)
|
||||
if err == nil {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
@@ -95,11 +108,11 @@ func (a *InboundController) addInbound(c *gin.Context) {
|
||||
func (a *InboundController) delInbound(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "delete"), err)
|
||||
jsonMsg(c, I18nWeb(c, "delete"), err)
|
||||
return
|
||||
}
|
||||
err = a.inboundService.DelInbound(id)
|
||||
jsonMsgObj(c, I18n(c, "delete"), id, err)
|
||||
jsonMsgObj(c, I18nWeb(c, "delete"), id, err)
|
||||
if err == nil {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
@@ -108,7 +121,7 @@ func (a *InboundController) delInbound(c *gin.Context) {
|
||||
func (a *InboundController) updateInbound(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
|
||||
return
|
||||
}
|
||||
inbound := &model.Inbound{
|
||||
@@ -116,76 +129,77 @@ func (a *InboundController) updateInbound(c *gin.Context) {
|
||||
}
|
||||
err = c.ShouldBind(inbound)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
|
||||
return
|
||||
}
|
||||
inbound, err = a.inboundService.UpdateInbound(inbound)
|
||||
jsonMsgObj(c, I18n(c, "pages.inbounds.revise"), inbound, err)
|
||||
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.update"), inbound, err)
|
||||
if err == nil {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *InboundController) addInboundClient(c *gin.Context) {
|
||||
inbound := &model.Inbound{}
|
||||
err := c.ShouldBind(inbound)
|
||||
data := &model.Inbound{}
|
||||
err := c.ShouldBind(data)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
|
||||
return
|
||||
}
|
||||
|
||||
err = a.inboundService.AddInboundClient(inbound)
|
||||
needRestart := false
|
||||
|
||||
needRestart, err = a.inboundService.AddInboundClient(data)
|
||||
if err != nil {
|
||||
jsonMsg(c, "something worng!", err)
|
||||
jsonMsg(c, "Something went wrong!", err)
|
||||
return
|
||||
}
|
||||
jsonMsg(c, "Client added", nil)
|
||||
if err == nil {
|
||||
jsonMsg(c, "Client(s) added", nil)
|
||||
if err == nil && needRestart {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *InboundController) delInboundClient(c *gin.Context) {
|
||||
email := c.Param("email")
|
||||
inbound := &model.Inbound{}
|
||||
err := c.ShouldBind(inbound)
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
|
||||
return
|
||||
}
|
||||
clientId := c.Param("clientId")
|
||||
|
||||
err = a.inboundService.DelInboundClient(inbound, email)
|
||||
needRestart := false
|
||||
|
||||
needRestart, err = a.inboundService.DelInboundClient(id, clientId)
|
||||
if err != nil {
|
||||
jsonMsg(c, "something worng!", err)
|
||||
jsonMsg(c, "Something went wrong!", err)
|
||||
return
|
||||
}
|
||||
jsonMsg(c, "Client deleted", nil)
|
||||
if err == nil {
|
||||
if err == nil && needRestart {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *InboundController) updateInboundClient(c *gin.Context) {
|
||||
index, err := strconv.Atoi(c.Param("index"))
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
|
||||
return
|
||||
}
|
||||
clientId := c.Param("clientId")
|
||||
|
||||
inbound := &model.Inbound{}
|
||||
err = c.ShouldBind(inbound)
|
||||
err := c.ShouldBind(inbound)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
|
||||
return
|
||||
}
|
||||
|
||||
err = a.inboundService.UpdateInboundClient(inbound, index)
|
||||
needRestart := false
|
||||
|
||||
needRestart, err = a.inboundService.UpdateInboundClient(inbound, clientId)
|
||||
if err != nil {
|
||||
jsonMsg(c, "something worng!", err)
|
||||
jsonMsg(c, "Something went wrong!", err)
|
||||
return
|
||||
}
|
||||
jsonMsg(c, "Client updated", nil)
|
||||
if err == nil {
|
||||
if err == nil && needRestart {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
@@ -193,18 +207,62 @@ func (a *InboundController) updateInboundClient(c *gin.Context) {
|
||||
func (a *InboundController) resetClientTraffic(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
|
||||
return
|
||||
}
|
||||
email := c.Param("email")
|
||||
|
||||
err = a.inboundService.ResetClientTraffic(id, email)
|
||||
needRestart := false
|
||||
|
||||
needRestart, err = a.inboundService.ResetClientTraffic(id, email)
|
||||
if err != nil {
|
||||
jsonMsg(c, "something worng!", err)
|
||||
jsonMsg(c, "Something went wrong!", err)
|
||||
return
|
||||
}
|
||||
jsonMsg(c, "traffic reseted", nil)
|
||||
if err == nil {
|
||||
if err == nil && needRestart {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *InboundController) resetAllTraffics(c *gin.Context) {
|
||||
err := a.inboundService.ResetAllTraffics()
|
||||
if err != nil {
|
||||
jsonMsg(c, "Something went wrong!", err)
|
||||
return
|
||||
} else {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
jsonMsg(c, "All traffics reseted", nil)
|
||||
}
|
||||
|
||||
func (a *InboundController) resetAllClientTraffics(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
|
||||
return
|
||||
}
|
||||
|
||||
err = a.inboundService.ResetAllClientTraffics(id)
|
||||
if err != nil {
|
||||
jsonMsg(c, "Something went wrong!", err)
|
||||
return
|
||||
} else {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
jsonMsg(c, "All traffics of client reseted", nil)
|
||||
}
|
||||
|
||||
func (a *InboundController) delDepletedClients(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
|
||||
return
|
||||
}
|
||||
err = a.inboundService.DelDepletedClients(id)
|
||||
if err != nil {
|
||||
jsonMsg(c, "Something went wrong!", err)
|
||||
return
|
||||
}
|
||||
jsonMsg(c, "All delpeted clients are deleted", nil)
|
||||
}
|
||||
|
||||
@@ -18,8 +18,9 @@ type LoginForm struct {
|
||||
type IndexController struct {
|
||||
BaseController
|
||||
|
||||
userService service.UserService
|
||||
tgbot service.Tgbot
|
||||
settingService service.SettingService
|
||||
userService service.UserService
|
||||
tgbot service.Tgbot
|
||||
}
|
||||
|
||||
func NewIndexController(g *gin.RouterGroup) *IndexController {
|
||||
@@ -46,32 +47,45 @@ func (a *IndexController) login(c *gin.Context) {
|
||||
var form LoginForm
|
||||
err := c.ShouldBind(&form)
|
||||
if err != nil {
|
||||
pureJsonMsg(c, false, I18n(c, "pages.login.toasts.invalidFormData"))
|
||||
pureJsonMsg(c, false, I18nWeb(c, "pages.login.toasts.invalidFormData"))
|
||||
return
|
||||
}
|
||||
if form.Username == "" {
|
||||
pureJsonMsg(c, false, I18n(c, "pages.login.toasts.emptyUsername"))
|
||||
pureJsonMsg(c, false, I18nWeb(c, "pages.login.toasts.emptyUsername"))
|
||||
return
|
||||
}
|
||||
if form.Password == "" {
|
||||
pureJsonMsg(c, false, I18n(c, "pages.login.toasts.emptyPassword"))
|
||||
pureJsonMsg(c, false, I18nWeb(c, "pages.login.toasts.emptyPassword"))
|
||||
return
|
||||
}
|
||||
|
||||
user := a.userService.CheckUser(form.Username, form.Password)
|
||||
timeStr := time.Now().Format("2006-01-02 15:04:05")
|
||||
if user == nil {
|
||||
a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0)
|
||||
logger.Infof("wrong username or password: \"%s\" \"%s\"", form.Username, form.Password)
|
||||
pureJsonMsg(c, false, I18n(c, "pages.login.toasts.wrongUsernameOrPassword"))
|
||||
a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0)
|
||||
pureJsonMsg(c, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword"))
|
||||
return
|
||||
} else {
|
||||
logger.Infof("%s login success,Ip Address:%s\n", form.Username, getRemoteIp(c))
|
||||
logger.Infof("%s login success ,Ip Address: %s\n", form.Username, getRemoteIp(c))
|
||||
a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 1)
|
||||
}
|
||||
|
||||
sessionMaxAge, err := a.settingService.GetSessionMaxAge()
|
||||
if err != nil {
|
||||
logger.Infof("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")
|
||||
}
|
||||
}
|
||||
|
||||
err = session.SetLoginUser(c, user)
|
||||
logger.Info("user", user.Id, "login success")
|
||||
jsonMsg(c, I18n(c, "pages.login.toasts.successLogin"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.login.toasts.successLogin"), err)
|
||||
}
|
||||
|
||||
func (a *IndexController) logout(c *gin.Context) {
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"time"
|
||||
"x-ui/web/global"
|
||||
"x-ui/web/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var filenameRegex = regexp.MustCompile(`^[a-zA-Z0-9_\-.]+$`)
|
||||
|
||||
type ServerController struct {
|
||||
BaseController
|
||||
|
||||
@@ -37,6 +43,11 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
|
||||
g.POST("/stopXrayService", a.stopXrayService)
|
||||
g.POST("/restartXrayService", a.restartXrayService)
|
||||
g.POST("/installXray/:version", a.installXray)
|
||||
g.POST("/logs/:count", a.getLogs)
|
||||
g.POST("/getConfigJson", a.getConfigJson)
|
||||
g.GET("/getDb", a.getDb)
|
||||
g.POST("/importDB", a.importDB)
|
||||
g.POST("/getNewX25519Cert", a.getNewX25519Cert)
|
||||
}
|
||||
|
||||
func (a *ServerController) refreshStatus() {
|
||||
@@ -70,7 +81,7 @@ func (a *ServerController) getXrayVersion(c *gin.Context) {
|
||||
|
||||
versions, err := a.serverService.GetXrayVersions()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "getVersion"), err)
|
||||
jsonMsg(c, I18nWeb(c, "getVersion"), err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -83,25 +94,96 @@ func (a *ServerController) getXrayVersion(c *gin.Context) {
|
||||
func (a *ServerController) installXray(c *gin.Context) {
|
||||
version := c.Param("version")
|
||||
err := a.serverService.UpdateXray(version)
|
||||
jsonMsg(c, I18n(c, "install")+" xray", err)
|
||||
jsonMsg(c, I18nWeb(c, "install")+" xray", err)
|
||||
}
|
||||
|
||||
func (a *ServerController) stopXrayService(c *gin.Context) {
|
||||
a.lastGetStatusTime = time.Now()
|
||||
a.lastGetStatusTime = time.Now()
|
||||
err := a.serverService.StopXrayService()
|
||||
if err != nil {
|
||||
jsonMsg(c, "", err)
|
||||
return
|
||||
}
|
||||
jsonMsg(c, "Xray stoped",err)
|
||||
|
||||
jsonMsg(c, "Xray stoped", err)
|
||||
}
|
||||
|
||||
func (a *ServerController) restartXrayService(c *gin.Context) {
|
||||
err := a.serverService.RestartXrayService()
|
||||
if err != nil {
|
||||
jsonMsg(c, "", err)
|
||||
return
|
||||
}
|
||||
jsonMsg(c, "Xray restarted",err)
|
||||
jsonMsg(c, "Xray restarted", err)
|
||||
}
|
||||
|
||||
}
|
||||
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
|
||||
}
|
||||
jsonObj(c, logs, nil)
|
||||
}
|
||||
|
||||
func (a *ServerController) getConfigJson(c *gin.Context) {
|
||||
configJson, err := a.serverService.GetConfigJson()
|
||||
if err != nil {
|
||||
jsonMsg(c, "get config.json", err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, configJson, nil)
|
||||
}
|
||||
|
||||
func (a *ServerController) getDb(c *gin.Context) {
|
||||
db, err := a.serverService.GetDb()
|
||||
if err != nil {
|
||||
jsonMsg(c, "get Database", err)
|
||||
return
|
||||
}
|
||||
|
||||
filename := "x-ui.db"
|
||||
|
||||
if !filenameRegex.MatchString(filename) {
|
||||
c.AbortWithError(http.StatusBadRequest, fmt.Errorf("invalid filename"))
|
||||
return
|
||||
}
|
||||
|
||||
// Set the headers for the response
|
||||
c.Header("Content-Type", "application/octet-stream")
|
||||
c.Header("Content-Disposition", "attachment; filename="+filename)
|
||||
|
||||
// Write the file contents to the response
|
||||
c.Writer.Write(db)
|
||||
}
|
||||
|
||||
func (a *ServerController) importDB(c *gin.Context) {
|
||||
// Get the file from the request body
|
||||
file, _, err := c.Request.FormFile("db")
|
||||
if err != nil {
|
||||
jsonMsg(c, "Error reading db file", err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
// Always restart Xray before return
|
||||
defer a.serverService.RestartXrayService()
|
||||
defer func() {
|
||||
a.lastGetStatusTime = time.Now()
|
||||
}()
|
||||
// Import it
|
||||
err = a.serverService.ImportDB(file)
|
||||
if err != nil {
|
||||
jsonMsg(c, "", err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, "Import DB", nil)
|
||||
}
|
||||
|
||||
func (a *ServerController) getNewX25519Cert(c *gin.Context) {
|
||||
cert, err := a.serverService.GetNewX25519Cert()
|
||||
if err != nil {
|
||||
jsonMsg(c, "get x25519 certificate", err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, cert, nil)
|
||||
}
|
||||
|
||||
@@ -33,45 +33,87 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) {
|
||||
g = g.Group("/setting")
|
||||
|
||||
g.POST("/all", a.getAllSetting)
|
||||
g.POST("/defaultSettings", a.getDefaultSettings)
|
||||
g.POST("/update", a.updateSetting)
|
||||
g.POST("/updateUser", a.updateUser)
|
||||
g.POST("/restartPanel", a.restartPanel)
|
||||
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
|
||||
}
|
||||
|
||||
func (a *SettingController) getAllSetting(c *gin.Context) {
|
||||
allSetting, err := a.settingService.GetAllSetting()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.setting.toasts.getSetting"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, allSetting, nil)
|
||||
}
|
||||
|
||||
func (a *SettingController) getDefaultSettings(c *gin.Context) {
|
||||
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() },
|
||||
}
|
||||
|
||||
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 result["subKeyFile"] != "" || result["subCertFile"] != "" {
|
||||
subTLS = true
|
||||
}
|
||||
result["subTLS"] = subTLS
|
||||
|
||||
delete(result, "subKeyFile")
|
||||
delete(result, "subCertFile")
|
||||
|
||||
jsonObj(c, result, nil)
|
||||
}
|
||||
|
||||
func (a *SettingController) updateSetting(c *gin.Context) {
|
||||
allSetting := &entity.AllSetting{}
|
||||
err := c.ShouldBind(allSetting)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.setting.toasts.modifySetting"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
|
||||
return
|
||||
}
|
||||
err = a.settingService.UpdateAllSetting(allSetting)
|
||||
jsonMsg(c, I18n(c, "pages.setting.toasts.modifySetting"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
|
||||
}
|
||||
|
||||
func (a *SettingController) updateUser(c *gin.Context) {
|
||||
form := &updateUserForm{}
|
||||
err := c.ShouldBind(form)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.setting.toasts.modifySetting"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
|
||||
return
|
||||
}
|
||||
user := session.GetLoginUser(c)
|
||||
if user.Username != form.OldUsername || user.Password != form.OldPassword {
|
||||
jsonMsg(c, I18n(c, "pages.setting.toasts.modifyUser"), errors.New(I18n(c, "pages.setting.toasts.originalUserPassIncorrect")))
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), errors.New(I18nWeb(c, "pages.settings.toasts.originalUserPassIncorrect")))
|
||||
return
|
||||
}
|
||||
if form.NewUsername == "" || form.NewPassword == "" {
|
||||
jsonMsg(c, I18n(c, "pages.setting.toasts.modifyUser"), errors.New(I18n(c, "pages.setting.toasts.userPassMustBeNotEmpty")))
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), errors.New(I18nWeb(c, "pages.settings.toasts.userPassMustBeNotEmpty")))
|
||||
return
|
||||
}
|
||||
err = a.userService.UpdateUser(user.Id, form.NewUsername, form.NewPassword)
|
||||
@@ -80,10 +122,19 @@ func (a *SettingController) updateUser(c *gin.Context) {
|
||||
user.Password = form.NewPassword
|
||||
session.SetLoginUser(c, user)
|
||||
}
|
||||
jsonMsg(c, I18n(c, "pages.setting.toasts.modifyUser"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err)
|
||||
}
|
||||
|
||||
func (a *SettingController) restartPanel(c *gin.Context) {
|
||||
err := a.panelService.RestartPanel(time.Second * 3)
|
||||
jsonMsg(c, I18n(c, "pages.setting.restartPanel"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.restartPanel"), err)
|
||||
}
|
||||
|
||||
func (a *SettingController) getDefaultXrayConfig(c *gin.Context) {
|
||||
defaultJsonConfig, err := a.settingService.GetDefaultXrayConfig()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, defaultJsonConfig, nil)
|
||||
}
|
||||
|
||||
@@ -1,24 +1,16 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"x-ui/config"
|
||||
"x-ui/logger"
|
||||
"x-ui/web/entity"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func getUriId(c *gin.Context) int64 {
|
||||
s := struct {
|
||||
Id int64 `uri:"id"`
|
||||
}{}
|
||||
|
||||
_ = c.BindUri(&s)
|
||||
return s.Id
|
||||
}
|
||||
|
||||
func getRemoteIp(c *gin.Context) string {
|
||||
value := c.GetHeader("X-Forwarded-For")
|
||||
if value != "" {
|
||||
@@ -46,12 +38,12 @@ func jsonMsgObj(c *gin.Context, msg string, obj interface{}, err error) {
|
||||
if err == nil {
|
||||
m.Success = true
|
||||
if msg != "" {
|
||||
m.Msg = msg + I18n(c, "success")
|
||||
m.Msg = msg + I18nWeb(c, "success")
|
||||
}
|
||||
} else {
|
||||
m.Success = false
|
||||
m.Msg = msg + I18n(c, "fail") + ": " + err.Error()
|
||||
logger.Warning(msg+I18n(c, "fail")+": ", err)
|
||||
m.Msg = msg + I18nWeb(c, "fail") + ": " + err.Error()
|
||||
logger.Warning(msg+I18nWeb(c, "fail")+": ", err)
|
||||
}
|
||||
c.JSON(http.StatusOK, m)
|
||||
}
|
||||
@@ -75,6 +67,7 @@ func html(c *gin.Context, name string, title string, data gin.H) {
|
||||
data = gin.H{}
|
||||
}
|
||||
data["title"] = title
|
||||
data["host"] = strings.Split(c.Request.Host, ":")[0]
|
||||
data["request_uri"] = c.Request.RequestURI
|
||||
data["base_path"] = c.GetString("base_path")
|
||||
c.HTML(http.StatusOK, name, getContext(data))
|
||||
@@ -84,10 +77,8 @@ func getContext(h gin.H) gin.H {
|
||||
a := gin.H{
|
||||
"cur_ver": config.GetVersion(),
|
||||
}
|
||||
if h != nil {
|
||||
for key, value := range h {
|
||||
a[key] = value
|
||||
}
|
||||
for key, value := range h {
|
||||
a[key] = value
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) {
|
||||
|
||||
g.GET("/", a.index)
|
||||
g.GET("/inbounds", a.inbounds)
|
||||
g.GET("/setting", a.setting)
|
||||
g.GET("/settings", a.settings)
|
||||
|
||||
a.inboundController = NewInboundController(g)
|
||||
a.settingController = NewSettingController(g)
|
||||
@@ -37,6 +37,6 @@ func (a *XUIController) inbounds(c *gin.Context) {
|
||||
html(c, "inbounds.html", "pages.inbounds.title", nil)
|
||||
}
|
||||
|
||||
func (a *XUIController) setting(c *gin.Context) {
|
||||
html(c, "setting.html", "pages.setting.title", nil)
|
||||
func (a *XUIController) settings(c *gin.Context) {
|
||||
html(c, "settings.html", "pages.settings.title", nil)
|
||||
}
|
||||
|
||||
@@ -28,20 +28,31 @@ 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"`
|
||||
WebBasePath string `json:"webBasePath" form:"webBasePath"`
|
||||
SessionMaxAge int `json:"sessionMaxAge" form:"sessionMaxAge"`
|
||||
ExpireDiff int `json:"expireDiff" form:"expireDiff"`
|
||||
TrafficDiff int `json:"trafficDiff" form:"trafficDiff"`
|
||||
TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"`
|
||||
TgBotToken string `json:"tgBotToken" form:"tgBotToken"`
|
||||
TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"`
|
||||
TgRunTime string `json:"tgRunTime" form:"tgRunTime"`
|
||||
TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"`
|
||||
TgExpireDiff int `json:"tgExpireDiff" form:"tgExpireDiff"`
|
||||
TgTrafficDiff int `json:"tgTrafficDiff" form:"tgTrafficDiff"`
|
||||
TgCpu int `json:"tgCpu" form:"tgCpu"`
|
||||
TgLang string `json:"tgLang" form:"tgLang"`
|
||||
XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"`
|
||||
TimeLocation string `json:"timeLocation" form:"timeLocation"`
|
||||
SubEnable bool `json:"subEnable" form:"subEnable"`
|
||||
SubListen string `json:"subListen" form:"subListen"`
|
||||
SubPort int `json:"subPort" form:"subPort"`
|
||||
SubPath string `json:"subPath" form:"subPath"`
|
||||
SubDomain string `json:"subDomain" form:"subDomain"`
|
||||
SubCertFile string `json:"subCertFile" form:"subCertFile"`
|
||||
SubKeyFile string `json:"subKeyFile" form:"subKeyFile"`
|
||||
SubUpdates int `json:"subUpdates" form:"subUpdates"`
|
||||
}
|
||||
|
||||
func (s *AllSetting) CheckValid() error {
|
||||
@@ -52,10 +63,25 @@ func (s *AllSetting) CheckValid() error {
|
||||
}
|
||||
}
|
||||
|
||||
if s.SubListen != "" {
|
||||
ip := net.ParseIP(s.SubListen)
|
||||
if ip == nil {
|
||||
return common.NewError("Sub listen is not valid ip:", s.SubListen)
|
||||
}
|
||||
}
|
||||
|
||||
if s.WebPort <= 0 || s.WebPort > 65535 {
|
||||
return common.NewError("web port is not a valid port:", s.WebPort)
|
||||
}
|
||||
|
||||
if s.SubPort <= 0 || s.SubPort > 65535 {
|
||||
return common.NewError("Sub port is not a valid port:", s.SubPort)
|
||||
}
|
||||
|
||||
if s.SubPort == s.WebPort {
|
||||
return common.NewError("Sub and Web could not use same port:", s.SubPort)
|
||||
}
|
||||
|
||||
if s.WebCertFile != "" || s.WebKeyFile != "" {
|
||||
_, err := tls.LoadX509KeyPair(s.WebCertFile, s.WebKeyFile)
|
||||
if err != nil {
|
||||
@@ -63,6 +89,13 @@ func (s *AllSetting) CheckValid() error {
|
||||
}
|
||||
}
|
||||
|
||||
if s.SubCertFile != "" || s.SubKeyFile != "" {
|
||||
_, err := tls.LoadX509KeyPair(s.SubCertFile, s.SubKeyFile)
|
||||
if err != nil {
|
||||
return common.NewErrorf("cert file <%v> or key file <%v> invalid: %v", s.SubCertFile, s.SubKeyFile, err)
|
||||
}
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(s.WebBasePath, "/") {
|
||||
s.WebBasePath = "/" + s.WebBasePath
|
||||
}
|
||||
|
||||
@@ -2,17 +2,23 @@ package global
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/robfig/cron/v3"
|
||||
_ "unsafe"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
var webServer WebServer
|
||||
var subServer SubServer
|
||||
|
||||
type WebServer interface {
|
||||
GetCron() *cron.Cron
|
||||
GetCtx() context.Context
|
||||
}
|
||||
|
||||
type SubServer interface {
|
||||
GetCtx() context.Context
|
||||
}
|
||||
|
||||
func SetWebServer(s WebServer) {
|
||||
webServer = s
|
||||
}
|
||||
@@ -20,3 +26,11 @@ func SetWebServer(s WebServer) {
|
||||
func GetWebServer() WebServer {
|
||||
return webServer
|
||||
}
|
||||
|
||||
func SetSubServer(s SubServer) {
|
||||
subServer = s
|
||||
}
|
||||
|
||||
func GetSubServer() SubServer {
|
||||
return subServer
|
||||
}
|
||||
|
||||
@@ -7,11 +7,13 @@
|
||||
<link rel="stylesheet" href="{{ .base_path }}assets/ant-design-vue@1.7.2/antd.min.css">
|
||||
<link rel="stylesheet" href="{{ .base_path }}assets/element-ui@2.15.0/theme-chalk/display.css">
|
||||
<link rel="stylesheet" href="{{ .base_path }}assets/css/custom.css?{{ .cur_ver }}">
|
||||
<link rel=”icon” type=”image/x-icon” href="{{ .base_path }}assets/favicon.ico">
|
||||
<link rel="shortcut icon" type="image/x-icon" href="{{ .base_path }}assets/favicon.ico">
|
||||
<style>
|
||||
[v-cloak] {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<title>{{ i18n .title}}</title>
|
||||
<title>{{ .host }}-{{ i18n .title}}</title>
|
||||
</head>
|
||||
{{end}}
|
||||
@@ -1,7 +1,7 @@
|
||||
{{define "promptModal"}}
|
||||
<a-modal id="prompt-modal" v-model="promptModal.visible" :title="promptModal.title"
|
||||
:closable="true" @ok="promptModal.ok" :mask-closable="false"
|
||||
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||
:class="themeSwitcher.darkCardClass"
|
||||
:ok-text="promptModal.okText" cancel-text='{{ i18n "cancel" }}'>
|
||||
<a-input id="prompt-modal-input" :type="promptModal.type"
|
||||
v-model="promptModal.value"
|
||||
@@ -36,11 +36,11 @@
|
||||
},
|
||||
confirm() {},
|
||||
open({
|
||||
title='',
|
||||
type='text',
|
||||
value='',
|
||||
okText='{{ i18n "sure"}}',
|
||||
confirm=() => {},
|
||||
title = '',
|
||||
type = 'text',
|
||||
value = '',
|
||||
okText = '{{ i18n "sure"}}',
|
||||
confirm = () => {},
|
||||
}) {
|
||||
this.title = title;
|
||||
this.type = type;
|
||||
|
||||
@@ -1,54 +1,58 @@
|
||||
{{define "qrcodeModal"}}
|
||||
<a-modal id="qrcode-modal" v-model="qrModal.visible" :title="qrModal.title"
|
||||
:closable="true" width="300px" :ok-text="qrModal.okText"
|
||||
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||
cancel-text='{{ i18n "close" }}' :ok-button-props="{attrs:{id:'qr-modal-ok-btn'}}">
|
||||
<a-tag color="green" style="margin-bottom: 10px;display: block;text-align: center;" >{{ i18n "pages.inbounds.clickOnQRcode" }}</a-tag>
|
||||
<canvas @click="copyToClipboard()" id="qrCode" style="width: 100%; height: 100%;"></canvas>
|
||||
:closable="true"
|
||||
:class="themeSwitcher.darkCardClass"
|
||||
:footer="null"
|
||||
width="300px">
|
||||
<a-tag color="green" style="margin-bottom: 10px;display: block;text-align: center;" >{{ i18n "pages.inbounds.clickOnQRcode" }}</a-tag>
|
||||
<template v-if="app.subSettings.enable && qrModal.subId">
|
||||
<a-divider>Subscription</a-divider>
|
||||
<canvas @click="copyToClipboard('qrCode-sub',genSubLink(qrModal.client.subId))" id="qrCode-sub" style="width: 100%; height: 100%;"></canvas>
|
||||
</template>
|
||||
<a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
|
||||
<template v-for="(row, index) in qrModal.qrcodes">
|
||||
<a-tag color="orange" style="margin-top: 10px;display: block;text-align: center;">[[ row.remark ]]</a-tag>
|
||||
<canvas @click="copyToClipboard('qrCode-'+index, row.link)" :id="'qrCode-'+index" style="width: 100%; height: 100%;"></canvas>
|
||||
</template>
|
||||
</a-modal>
|
||||
|
||||
<script>
|
||||
|
||||
const qrModal = {
|
||||
title: '',
|
||||
content: '',
|
||||
clientIndex: 0,
|
||||
inbound: new Inbound(),
|
||||
dbInbound: new DBInbound(),
|
||||
okText: '',
|
||||
copyText: '',
|
||||
qrcode: null,
|
||||
client: null,
|
||||
qrcodes: [],
|
||||
clipboard: null,
|
||||
visible: false,
|
||||
show: function (title='', content='', dbInbound=new DBInbound(),okText='{{ i18n "copy" }}', copyText='') {
|
||||
subId: '',
|
||||
show: function (title = '', dbInbound = new DBInbound(), clientIndex = 0) {
|
||||
this.title = title;
|
||||
this.content = content;
|
||||
this.clientIndex = clientIndex;
|
||||
this.dbInbound = dbInbound;
|
||||
this.inbound = dbInbound.toInbound();
|
||||
this.okText = okText;
|
||||
if (ObjectUtil.isEmpty(copyText)) {
|
||||
this.copyText = content;
|
||||
settings = JSON.parse(this.inbound.settings);
|
||||
this.client = settings.clients[clientIndex];
|
||||
remark = this.dbInbound.remark + "-" + this.client.email;
|
||||
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) => {
|
||||
this.qrcodes.push({
|
||||
remark: remark + "-" + domain.remark,
|
||||
link: this.inbound.genLink(domain.domain, remark + "-" + domain.remark, clientIndex)
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.copyText = copyText;
|
||||
this.qrcodes.push({
|
||||
remark: remark,
|
||||
link: this.inbound.genLink(address, remark, clientIndex)
|
||||
});
|
||||
}
|
||||
this.visible = true;
|
||||
qrModalApp.$nextTick(() => {
|
||||
this.clipboard = new ClipboardJS('#qr-modal-ok-btn', {
|
||||
text: () => this.copyText,
|
||||
});
|
||||
this.clipboard.on('success', () => {
|
||||
app.$message.success('{{ i18n "copied" }}')
|
||||
this.clipboard.destroy();
|
||||
});
|
||||
if (this.qrcode === null) {
|
||||
this.qrcode = new QRious({
|
||||
element: document.querySelector('#qrCode'),
|
||||
size: 260,
|
||||
value: content,
|
||||
});
|
||||
} else {
|
||||
this.qrcode.value = content;
|
||||
}
|
||||
});
|
||||
},
|
||||
close: function () {
|
||||
this.visible = false;
|
||||
@@ -56,21 +60,42 @@
|
||||
};
|
||||
|
||||
const qrModalApp = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#qrcode-modal',
|
||||
data: {
|
||||
qrModal: qrModal,
|
||||
},
|
||||
methods: {
|
||||
copyToClipboard() {
|
||||
this.qrModal.clipboard = new ClipboardJS('#qrCode', {
|
||||
text: () => this.qrModal.copyText,
|
||||
copyToClipboard(elmentId, content) {
|
||||
this.qrModal.clipboard = new ClipboardJS('#' + elmentId, {
|
||||
text: () => content,
|
||||
});
|
||||
this.qrModal.clipboard.on('success', () => {
|
||||
app.$message.success('{{ i18n "copied" }}')
|
||||
this.qrModal.clipboard.destroy();
|
||||
});
|
||||
},
|
||||
setQrCode(elmentId, content) {
|
||||
new QRious({
|
||||
element: document.querySelector('#' + elmentId),
|
||||
size: 260,
|
||||
value: content,
|
||||
});
|
||||
},
|
||||
genSubLink(subID) {
|
||||
const { domain: host, port, tls: isTLS, path: base } = app.subSettings;
|
||||
return buildURL({ host, port, isTLS, base, path: subID });
|
||||
}
|
||||
},
|
||||
updated() {
|
||||
if (qrModal.client && qrModal.client.subId) {
|
||||
qrModal.subId = qrModal.client.subId;
|
||||
this.setQrCode("qrCode-sub", this.genSubLink(qrModal.subId));
|
||||
}
|
||||
qrModal.qrcodes.forEach((element, index) => {
|
||||
this.setQrCode("qrCode-" + index, element.link);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
{{define "textModal"}}
|
||||
<a-modal id="text-modal" v-model="txtModal.visible" :title="txtModal.title"
|
||||
:closable="true" ok-text='{{ i18n "copy" }}' cancel-text='{{ i18n "close" }}'
|
||||
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||
:class="themeSwitcher.darkCardClass"
|
||||
:ok-button-props="{attrs:{id:'txt-modal-ok-btn'}}">
|
||||
<a-button v-if="!ObjectUtil.isEmpty(txtModal.fileName)" type="primary" style="margin-bottom: 10px;"
|
||||
:href="'data:application/text;charset=utf-8,' + encodeURIComponent(txtModal.content)" :download="txtModal.fileName">
|
||||
:href="'data:application/text;charset=utf-8,' + encodeURIComponent(txtModal.content)"
|
||||
:download="txtModal.fileName">
|
||||
{{ i18n "download" }} [[ txtModal.fileName ]]
|
||||
</a-button>
|
||||
<a-input type="textarea" v-model="txtModal.content"
|
||||
@@ -20,7 +21,7 @@
|
||||
qrcode: null,
|
||||
clipboard: null,
|
||||
visible: false,
|
||||
show: function (title='', content='', fileName='') {
|
||||
show: function (title = '', content = '', fileName = '') {
|
||||
this.title = title;
|
||||
this.content = content;
|
||||
this.fileName = fileName;
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
margin: 20px 0 50px 0;
|
||||
}
|
||||
|
||||
@@ -18,6 +17,12 @@
|
||||
border-radius: 30px;
|
||||
}
|
||||
|
||||
.ant-input-group-addon {
|
||||
border-radius: 0 30px 30px 0;
|
||||
width: 50px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.ant-input-affix-wrapper .ant-input-prefix {
|
||||
left: 23px;
|
||||
}
|
||||
@@ -26,20 +31,26 @@
|
||||
padding-left: 50px;
|
||||
}
|
||||
|
||||
.selectLang{
|
||||
.centered {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
</style>
|
||||
<body>
|
||||
<a-layout id="app" v-cloak>
|
||||
<a-layout id="app" v-cloak :class="themeSwitcher.darkCardClass">
|
||||
<transition name="list" appear>
|
||||
<a-layout-content>
|
||||
<a-row type="flex" justify="center">
|
||||
<a-col :xs="22" :sm="20" :md="16" :lg="12" :xl="8">
|
||||
<h1>{{ i18n "pages.login.title" }}</h1>
|
||||
<h1 class="title" :style="themeSwitcher.textStyle">{{ i18n "pages.login.title" }}</h1>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row type="flex" justify="center">
|
||||
@@ -48,40 +59,38 @@
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="user.username" placeholder='{{ i18n "username" }}'
|
||||
@keydown.enter.native="login" autofocus>
|
||||
<a-icon slot="prefix" type="user" style="color: rgba(0,0,0,.25)"/>
|
||||
<a-icon slot="prefix" type="user" :style="'font-size: 16px;' + themeSwitcher.textStyle"/>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-input type="password" v-model.trim="user.password"
|
||||
placeholder='{{ i18n "password" }}' @keydown.enter.native="login">
|
||||
<a-icon slot="prefix" type="lock" style="color: rgba(0,0,0,.25)"/>
|
||||
</a-input>
|
||||
<password-input icon="lock" v-model.trim="user.password"
|
||||
placeholder='{{ i18n "password" }}' @keydown.enter.native="login">
|
||||
</password-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button block @click="login" :loading="loading">{{ i18n "login" }}</a-button>
|
||||
<a-row justify="center" class="centered">
|
||||
<a-button type="primary" :loading="loading" @click="login" :icon="loading ? 'poweroff' : undefined"
|
||||
:style="loading ? { width: '50px' } : { display: 'block', width: '100%' }">
|
||||
[[ loading ? '' : '{{ i18n "login" }}' ]]
|
||||
</a-button>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
|
||||
<a-row justify="center" class="selectLang">
|
||||
<a-col :span="4"><span>Language : </span></a-col>
|
||||
|
||||
<a-col :span="6">
|
||||
<a-select
|
||||
ref="selectLang"
|
||||
v-model="lang"
|
||||
@change="setLang(lang)"
|
||||
>
|
||||
<a-select-option :value="l.value" label="China" v-for="l in supportLangs" >
|
||||
<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>
|
||||
<span v-text="l.name"></span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
|
||||
</a-row>
|
||||
|
||||
|
||||
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-row justify="center" class="centered">
|
||||
<theme-switch />
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-col>
|
||||
@@ -90,22 +99,21 @@
|
||||
</transition>
|
||||
</a-layout>
|
||||
{{template "js" .}}
|
||||
{{template "component/themeSwitcher" .}}
|
||||
{{template "component/password" .}}
|
||||
<script>
|
||||
const leftColor = RandomUtil.randomIntRange(0x222222, 0xFFFFFF / 2).toString(16);
|
||||
const rightColor = RandomUtil.randomIntRange(0xFFFFFF / 2, 0xDDDDDD).toString(16);
|
||||
const deg = RandomUtil.randomIntRange(0, 360);
|
||||
const background = `linear-gradient(${deg}deg, #${leftColor} 10%, #${rightColor} 100%)`;
|
||||
document.querySelector('#app').style.background = background;
|
||||
const app = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#app',
|
||||
data: {
|
||||
themeSwitcher,
|
||||
loading: false,
|
||||
user: new User(),
|
||||
lang : ""
|
||||
lang: ""
|
||||
},
|
||||
created(){
|
||||
this.lang = getLang();
|
||||
created() {
|
||||
this.updateBackground();
|
||||
this.lang = getLang();
|
||||
},
|
||||
methods: {
|
||||
async login() {
|
||||
@@ -115,8 +123,16 @@
|
||||
if (msg.success) {
|
||||
location.href = basePath + 'xui/';
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
updateBackground() {
|
||||
document.querySelector('#app').style.background = colors[this.themeSwitcher.currentTheme].bg;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'themeSwitcher.isDarkTheme'(newVal, oldVal) {
|
||||
this.updateBackground();
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -1,67 +1,143 @@
|
||||
{{define "clientsBulkModal"}}
|
||||
<a-modal id="client-bulk-modal" v-model="clientsBulkModal.visible" :title="clientsBulkModal.title" @ok="clientsBulkModal.ok"
|
||||
:confirm-loading="clientsBulkModal.confirmLoading" :closable="true" :mask-closable="false"
|
||||
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||
:class="themeSwitcher.darkCardClass"
|
||||
:ok-text="clientsBulkModal.okText" cancel-text='{{ i18n "close" }}'>
|
||||
<a-form layout="inline">
|
||||
<a-form-item label='{{ i18n "pages.client.method" }}'>
|
||||
<a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid" style="width: 350px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||
<a-select-option :value="0">Random</a-select-option>
|
||||
<a-select-option :value="1">Random+Prefix</a-select-option>
|
||||
<a-select-option :value="2">Random+Prefix+Num</a-select-option>
|
||||
<a-select-option :value="3">Random+Prefix+Num+Postfix</a-select-option>
|
||||
<a-select-option :value="4">Random+Prefix+Num@Telegram Username</a-select-option>
|
||||
<a-select-option :value="5">Prefix+Num+Postfix [ BE CAREFUL! ]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item><br />
|
||||
<a-form-item v-if="clientsBulkModal.emailMethod>1">
|
||||
<span slot="label">{{ i18n "pages.client.first" }}</span>
|
||||
<a-input-number v-model="clientsBulkModal.firstNum" :min="1"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="clientsBulkModal.emailMethod>1">
|
||||
<span slot="label">{{ i18n "pages.client.last" }}</span>
|
||||
<a-input-number v-model="clientsBulkModal.lastNum" :min="clientsBulkModal.firstNum"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="clientsBulkModal.emailMethod>0">
|
||||
<span slot="label">{{ i18n "pages.client.prefix" }}</span>
|
||||
<a-input v-model="clientsBulkModal.emailPrefix" style="width: 120px"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="clientsBulkModal.emailMethod>2">
|
||||
<span slot="label" v-if="clientsBulkModal.emailMethod == 4">tg_uname</span>
|
||||
<span slot="label" v-else>{{ i18n "pages.client.postfix" }}</span>
|
||||
<a-input v-model="clientsBulkModal.emailPostfix" style="width: 120px"></a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-if="clientsBulkModal.emailMethod < 2">
|
||||
<span slot="label">{{ i18n "pages.client.clientCount" }}</span>
|
||||
<a-input-number v-model="clientsBulkModal.quantity" :min="1" :max="100"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-input-number v-model="clientsBulkModal.totalGB" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
||||
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
|
||||
v-model="clientsBulkModal.expiryTime" style="width: 300px;"></a-date-picker>
|
||||
</a-form-item>
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>{{ i18n "pages.client.method" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid" style="width: 250px"
|
||||
:dropdown-class-name="themeSwitcher.darkCardClass">
|
||||
<a-select-option :value="0">Random</a-select-option>
|
||||
<a-select-option :value="1">Random+Prefix</a-select-option>
|
||||
<a-select-option :value="2">Random+Prefix+Num</a-select-option>
|
||||
<a-select-option :value="3">Random+Prefix+Num+Postfix</a-select-option>
|
||||
<a-select-option :value="4">Prefix+Num+Postfix</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="clientsBulkModal.emailMethod>1">
|
||||
<td>{{ i18n "pages.client.first" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model="clientsBulkModal.firstNum" :min="1"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="clientsBulkModal.emailMethod>1">
|
||||
<td>{{ i18n "pages.client.last" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model="clientsBulkModal.lastNum" :min="clientsBulkModal.firstNum"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="clientsBulkModal.emailMethod>0">
|
||||
<td>{{ i18n "pages.client.prefix" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model="clientsBulkModal.emailPrefix" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="clientsBulkModal.emailMethod>2">
|
||||
<td>{{ i18n "pages.client.postfix" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model="clientsBulkModal.emailPostfix" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="clientsBulkModal.emailMethod < 2">
|
||||
<td>{{ i18n "pages.client.clientCount" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model="clientsBulkModal.quantity" :min="1" :max="100"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="clientsBulkModal.inbound.canEnableTlsFlow()">
|
||||
<td>Flow</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select v-model="clientsBulkModal.flow" style="width: 250px" :dropdown-class-name="themeSwitcher.darkCardClass">
|
||||
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
|
||||
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="app.subSettings.enable">
|
||||
<td>Subscription</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="clientsBulkModal.subId" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="app.tgBotEnable">
|
||||
<td>Telegram Username</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="clientsBulkModal.tgId" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span>{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model="clientsBulkModal.totalGB" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.client.delayedStart" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-switch v-model="clientsBulkModal.delayedStart" @click="clientsBulkModal.expiryTime=0"></a-switch>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="clientsBulkModal.delayedStart">
|
||||
<td>{{ i18n "pages.client.expireDays" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-else>
|
||||
<td>
|
||||
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
|
||||
:dropdown-class-name="themeSwitcher.darkCardClass"
|
||||
v-model="clientsBulkModal.expiryTime" style="width: 250px;"></a-date-picker>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
<script>
|
||||
@@ -74,7 +150,6 @@
|
||||
confirm: null,
|
||||
dbInbound: new DBInbound(),
|
||||
inbound: new Inbound(),
|
||||
clients: [],
|
||||
quantity: 1,
|
||||
totalGB: 0,
|
||||
expiryTime: '',
|
||||
@@ -83,7 +158,12 @@
|
||||
lastNum: 1,
|
||||
emailPrefix: "",
|
||||
emailPostfix: "",
|
||||
subId: "",
|
||||
tgId: "",
|
||||
flow: "",
|
||||
delayedStart: false,
|
||||
ok() {
|
||||
clients = [];
|
||||
method=clientsBulkModal.emailMethod;
|
||||
if(method>1){
|
||||
start=clientsBulkModal.firstNum;
|
||||
@@ -94,16 +174,21 @@
|
||||
}
|
||||
prefix = (method>0 && clientsBulkModal.emailPrefix.length>0) ? clientsBulkModal.emailPrefix : "";
|
||||
useNum=(method>1);
|
||||
postfix = (method>2 && clientsBulkModal.emailPostfix.length>0) ? (method == 4 ? "@" : "") + clientsBulkModal.emailPostfix : "";
|
||||
postfix = (method>2 && clientsBulkModal.emailPostfix.length>0) ? clientsBulkModal.emailPostfix : "";
|
||||
for (let i = start; i < end; i++) {
|
||||
newClient = clientsBulkModal.newClient(clientsBulkModal.dbInbound.protocol);
|
||||
if(method==5) newClient.email = "";
|
||||
if(method==4) newClient.email = "";
|
||||
newClient.email += useNum ? prefix + i.toString() + postfix : prefix + postfix;
|
||||
if (clientsBulkModal.subId.length > 0) newClient.subId = clientsBulkModal.subId;
|
||||
if (clientsBulkModal.tgId.length > 0) newClient.tgId = clientsBulkModal.tgId;
|
||||
newClient._totalGB = clientsBulkModal.totalGB;
|
||||
newClient._expiryTime = clientsBulkModal.expiryTime;
|
||||
clientsBulkModal.clients.push(newClient);
|
||||
if(clientsBulkModal.inbound.canEnableTlsFlow()){
|
||||
newClient.flow = clientsBulkModal.flow;
|
||||
}
|
||||
clients.push(newClient);
|
||||
}
|
||||
ObjectUtil.execute(clientsBulkModal.confirm, clientsBulkModal.inbound, clientsBulkModal.dbInbound);
|
||||
ObjectUtil.execute(clientsBulkModal.confirm, clients, clientsBulkModal.dbInbound.id);
|
||||
},
|
||||
show({ title='', okText='{{ i18n "sure" }}', dbInbound=null, confirm=(inbound, dbInbound)=>{} }) {
|
||||
this.visible = true;
|
||||
@@ -112,22 +197,25 @@
|
||||
this.confirm = confirm;
|
||||
this.quantity = 1;
|
||||
this.totalGB = 0;
|
||||
this.expiryTime = '';
|
||||
this.expiryTime = 0;
|
||||
this.emailMethod= 0;
|
||||
this.firstNum= 1;
|
||||
this.lastNum= 1;
|
||||
this.emailPrefix= "";
|
||||
this.emailPostfix= "";
|
||||
|
||||
this.subId= "";
|
||||
this.tgId= "";
|
||||
this.flow= "";
|
||||
this.dbInbound = new DBInbound(dbInbound);
|
||||
this.inbound = dbInbound.toInbound();
|
||||
this.clients = this.getClients(this.inbound.protocol, this.inbound.settings);
|
||||
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;
|
||||
}
|
||||
},
|
||||
@@ -136,6 +224,7 @@
|
||||
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();
|
||||
default: return null;
|
||||
}
|
||||
},
|
||||
@@ -156,6 +245,12 @@
|
||||
get inbound() {
|
||||
return this.clientsBulkModal.inbound;
|
||||
},
|
||||
get delayedExpireDays() {
|
||||
return this.clientsBulkModal.expiryTime < 0 ? this.clientsBulkModal.expiryTime / -86400000 : 0;
|
||||
},
|
||||
set delayedExpireDays(days){
|
||||
this.clientsBulkModal.expiryTime = -86400000 * days;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{{define "clientsModal"}}
|
||||
<a-modal id="client-modal" v-model="clientModal.visible" :title="clientModal.title" @ok="clientModal.ok"
|
||||
:confirm-loading="clientModal.confirmLoading" :closable="true" :mask-closable="false"
|
||||
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||
:class="themeSwitcher.darkCardClass"
|
||||
:ok-text="clientModal.okText" cancel-text='{{ i18n "close" }}'>
|
||||
{{template "form/client"}}
|
||||
</a-modal>
|
||||
@@ -12,16 +12,22 @@
|
||||
confirmLoading: false,
|
||||
title: '',
|
||||
okText: '',
|
||||
isEdit: false,
|
||||
dbInbound: new DBInbound(),
|
||||
inbound: new Inbound(),
|
||||
clients: [],
|
||||
clientStats: [],
|
||||
oldClientId: "",
|
||||
index: null,
|
||||
isExpired: false,
|
||||
delayedStart: false,
|
||||
ok() {
|
||||
ObjectUtil.execute(clientModal.confirm, clientModal.inbound, clientModal.dbInbound, clientModal.index);
|
||||
if(clientModal.isEdit){
|
||||
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id, clientModal.oldClientId);
|
||||
} else {
|
||||
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id);
|
||||
}
|
||||
},
|
||||
show({ title='', okText='{{ i18n "sure" }}', index=null, dbInbound=null, confirm=(index, dbInbound)=>{}, isEdit=false }) {
|
||||
show({ title='', okText='{{ i18n "sure" }}', index=null, dbInbound=null, confirm=()=>{}, isEdit=false }) {
|
||||
this.visible = true;
|
||||
this.title = title;
|
||||
this.okText = okText;
|
||||
@@ -30,8 +36,13 @@
|
||||
this.inbound = dbInbound.toInbound();
|
||||
this.clients = this.getClients(this.inbound.protocol, this.inbound.settings);
|
||||
this.index = index === null ? this.clients.length : index;
|
||||
this.isExpired = isEdit ? this.inbound.isExpiry(this.index) : false;
|
||||
if (!isEdit){
|
||||
this.delayedStart = false;
|
||||
if (isEdit){
|
||||
if (this.clients[index].expiryTime < 0){
|
||||
this.delayedStart = true;
|
||||
}
|
||||
this.oldClientId = this.getClientId(dbInbound.protocol,clients[index]);
|
||||
} else {
|
||||
this.addClient(this.inbound.protocol, this.clients);
|
||||
}
|
||||
this.clientStats = this.dbInbound.clientStats.find(row => row.email === this.clients[this.index].email);
|
||||
@@ -42,14 +53,23 @@
|
||||
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;
|
||||
}
|
||||
},
|
||||
getClientId(protocol, client) {
|
||||
switch(protocol){
|
||||
case Protocols.TROJAN: return client.password;
|
||||
case Protocols.SHADOWSOCKS: return client.email;
|
||||
default: return client.id;
|
||||
}
|
||||
},
|
||||
addClient(protocol, clients) {
|
||||
switch (protocol) {
|
||||
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());
|
||||
default: return null;
|
||||
}
|
||||
},
|
||||
@@ -81,19 +101,22 @@
|
||||
},
|
||||
get isTrafficExhausted() {
|
||||
if(!clientStats) return false
|
||||
if(clientStats.total == 0) return false
|
||||
if(clientStats.total <= 0) return false
|
||||
if(clientStats.up + clientStats.down < clientStats.total) return false
|
||||
return true
|
||||
},
|
||||
get isExpiry() {
|
||||
return this.clientModal.isExpired
|
||||
return this.clientModal.isEdit && this.client.expiryTime >0 ? (this.client.expiryTime < new Date().getTime()) : false;
|
||||
},
|
||||
get statsColor() {
|
||||
if(!clientStats) return 'blue'
|
||||
if(clientStats.total === 0) return 'blue'
|
||||
else if(clientStats.total > 0 && (clientStats.down+clientStats.up) < clientStats.total) return 'cyan'
|
||||
else return 'red'
|
||||
}
|
||||
return usageColor(clientStats.up + clientStats.down, app.trafficDiff, this.client.totalGB);
|
||||
},
|
||||
get delayedExpireDays() {
|
||||
return this.client && this.client.expiryTime < 0 ? this.client.expiryTime / -86400000 : 0;
|
||||
},
|
||||
set delayedExpireDays(days){
|
||||
this.client.expiryTime = -86400000 * days;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getNewEmail(client) {
|
||||
@@ -105,6 +128,24 @@
|
||||
}
|
||||
client.email = string;
|
||||
},
|
||||
resetClientTraffic(email,dbInboundId,iconElement) {
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
|
||||
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
|
||||
class: themeSwitcher.darkCardClass,
|
||||
okText: '{{ i18n "reset"}}',
|
||||
cancelText: '{{ i18n "cancel"}}',
|
||||
onOk: async () => {
|
||||
iconElement.disabled = true;
|
||||
const msg = await HttpUtil.postWithModal('/xui/inbound/' + dbInboundId + '/resetClientTraffic/'+ email);
|
||||
if (msg.success) {
|
||||
this.clientModal.clientStats.up = 0;
|
||||
this.clientModal.clientStats.down = 0;
|
||||
}
|
||||
iconElement.disabled = false;
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -7,24 +7,10 @@
|
||||
<a-icon type="user"></a-icon>
|
||||
<span>{{ i18n "menu.inbounds"}}</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="{{ .base_path }}xui/setting">
|
||||
<a-menu-item key="{{ .base_path }}xui/settings">
|
||||
<a-icon type="setting"></a-icon>
|
||||
<span>{{ i18n "menu.setting"}}</span>
|
||||
<span>{{ i18n "menu.settings"}}</span>
|
||||
</a-menu-item>
|
||||
<!--<a-menu-item key="{{ .base_path }}xui/clients">-->
|
||||
<!-- <a-icon type="laptop"></a-icon>-->
|
||||
<!-- <span>Client</span>-->
|
||||
<!--</a-menu-item>-->
|
||||
<a-sub-menu>
|
||||
<template slot="title">
|
||||
<a-icon type="link"></a-icon>
|
||||
<span>{{ i18n "menu.link"}}</span>
|
||||
</template>
|
||||
<a-menu-item key="https://github.com/alireza0/x-ui/">
|
||||
<a-icon type="github"></a-icon>
|
||||
<span>Github</span>
|
||||
</a-menu-item>
|
||||
</a-sub-menu>
|
||||
<a-menu-item key="{{ .base_path }}logout">
|
||||
<a-icon type="logout"></a-icon>
|
||||
<span>{{ i18n "menu.logout"}}</span>
|
||||
@@ -33,17 +19,14 @@
|
||||
|
||||
|
||||
{{define "commonSider"}}
|
||||
<a-layout-sider :theme="siderDrawer.theme" id="sider" collapsible breakpoint="md" collapsed-width="0">
|
||||
<a-menu :theme="siderDrawer.theme" mode="inline" selected-keys="">
|
||||
<a-layout-sider :theme="themeSwitcher.currentTheme" id="sider" collapsible breakpoint="md" collapsed-width="0">
|
||||
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" selected-keys="">
|
||||
<a-menu-item mode="inline">
|
||||
<a-icon type="bg-colors"></a-icon>
|
||||
<a-switch :default-checked="siderDrawer.isDarkTheme"
|
||||
checked-children="☀"
|
||||
un-checked-children="🌙"
|
||||
@change="siderDrawer.changeTheme()"></a-switch>
|
||||
<theme-switch />
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
<a-menu :theme="siderDrawer.theme" mode="inline" :selected-keys="['{{ .request_uri }}']"
|
||||
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" :selected-keys="['{{ .request_uri }}']"
|
||||
@click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key">
|
||||
{{template "menuItems" .}}
|
||||
</a-menu>
|
||||
@@ -51,32 +34,25 @@
|
||||
<a-drawer id="sider-drawer" placement="left" :closable="false"
|
||||
@close="siderDrawer.close()"
|
||||
:visible="siderDrawer.visible"
|
||||
:wrap-class-name="siderDrawer.isDarkTheme ? 'ant-drawer-dark' : ''"
|
||||
:wrap-class-name="themeSwitcher.darkDrawerClass"
|
||||
:wrap-style="{ padding: 0 }">
|
||||
<div class="drawer-handle" @click="siderDrawer.change()" slot="handle">
|
||||
<a-icon :type="siderDrawer.visible ? 'close' : 'menu-fold'"></a-icon>
|
||||
</div>
|
||||
<a-menu :theme="siderDrawer.theme" mode="inline" selected-keys="">
|
||||
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" selected-keys="">
|
||||
<a-menu-item mode="inline">
|
||||
<a-icon type="bg-colors"></a-icon>
|
||||
<a-switch :default-checked="siderDrawer.isDarkTheme"
|
||||
checked-children="☀"
|
||||
un-checked-children="🌙"
|
||||
@change="siderDrawer.changeTheme()"></a-switch>
|
||||
<theme-switch />
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
<a-menu :theme="siderDrawer.theme" mode="inline" :selected-keys="['{{ .request_uri }}']"
|
||||
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" :selected-keys="['{{ .request_uri }}']"
|
||||
@click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key">
|
||||
{{template "menuItems" .}}
|
||||
</a-menu>
|
||||
</a-drawer>
|
||||
<script>
|
||||
const darkClass = "ant-card-dark";
|
||||
const bgDarkStyle = "background-color: #242c3a";
|
||||
const siderDrawer = {
|
||||
visible: false,
|
||||
collapsed: false,
|
||||
isDarkTheme: localStorage.getItem("dark-mode") === 'true' ? true : false,
|
||||
show() {
|
||||
this.visible = true;
|
||||
},
|
||||
@@ -85,16 +61,6 @@
|
||||
},
|
||||
change() {
|
||||
this.visible = !this.visible;
|
||||
},
|
||||
toggleCollapsed() {
|
||||
this.collapsed = !this.collapsed;
|
||||
},
|
||||
changeTheme() {
|
||||
this.isDarkTheme = ! this.isDarkTheme;
|
||||
localStorage.setItem("dark-mode", this.isDarkTheme);
|
||||
},
|
||||
get theme() {
|
||||
return this.isDarkTheme ? 'dark' : 'light';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
35
web/html/xui/component/password.html
Normal file
35
web/html/xui/component/password.html
Normal file
@@ -0,0 +1,35 @@
|
||||
{{define "component/passwordInput"}}
|
||||
<template>
|
||||
<a-input :value="value" :type="showPassword ? 'text' : 'password'"
|
||||
:placeholder="placeholder"
|
||||
@input="$emit('input', $event.target.value)">
|
||||
<template v-if="icon" #prefix>
|
||||
<a-icon :type="icon" :style="'font-size: 16px;' + themeSwitcher.textStyle" />
|
||||
</template>
|
||||
<template #addonAfter>
|
||||
<a-icon :type="showPassword ? 'eye-invisible' : 'eye'"
|
||||
@click="toggleShowPassword"
|
||||
:style="'font-size: 16px;' + themeSwitcher.textStyle" />
|
||||
</template>
|
||||
</a-input>
|
||||
</template>
|
||||
{{end}}
|
||||
|
||||
{{define "component/password"}}
|
||||
<script>
|
||||
Vue.component('password-input', {
|
||||
props: ["title", "value", "placeholder", "icon"],
|
||||
template: `{{template "component/passwordInput"}}`,
|
||||
data() {
|
||||
return {
|
||||
showPassword: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
toggleShowPassword() {
|
||||
this.showPassword = !this.showPassword;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
@@ -1,6 +1,12 @@
|
||||
{{define "component/settingListItem"}}
|
||||
<a-list-item style="padding: 20px">
|
||||
<a-row>
|
||||
<a-row v-if="type === 'textarea'">
|
||||
<a-col>
|
||||
<a-list-item-meta :title="title" :description="desc"/>
|
||||
<a-textarea class="ant-setting-textarea" :value="value" @input="$emit('input', $event.target.value)" :auto-size="{ minRows: 5 }"></a-textarea>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row v-else>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta :title="title" :description="desc"/>
|
||||
</a-col>
|
||||
@@ -9,10 +15,7 @@
|
||||
<a-input :value="value" @input="$emit('input', $event.target.value)"></a-input>
|
||||
</template>
|
||||
<template v-else-if="type === 'number'">
|
||||
<a-input type="number" :value="value" @input="$emit('input', $event.target.value)"></a-input>
|
||||
</template>
|
||||
<template v-else-if="type === 'textarea'">
|
||||
<a-textarea :value="value" @input="$emit('input', $event.target.value)" :auto-size="{ minRows: 10, maxRows: 10 }"></a-textarea>
|
||||
<a-input-number :value="value" @change="value => $emit('input', value)" :min="min" style="width: 100%;"></a-input-number>
|
||||
</template>
|
||||
<template v-else-if="type === 'switch'">
|
||||
<a-switch :checked="value" @change="value => $emit('input', value)"></a-switch>
|
||||
@@ -25,7 +28,7 @@
|
||||
{{define "component/setting"}}
|
||||
<script>
|
||||
Vue.component('setting-list-item', {
|
||||
props: ["type", "title", "desc", "value"],
|
||||
props: ["type", "title", "desc", "value", "min"],
|
||||
template: `{{template "component/settingListItem"}}`,
|
||||
});
|
||||
</script>
|
||||
|
||||
58
web/html/xui/component/themeSwitch.html
Normal file
58
web/html/xui/component/themeSwitch.html
Normal file
@@ -0,0 +1,58 @@
|
||||
{{define "component/themeSwitchTemplate"}}
|
||||
<template>
|
||||
<a-switch :default-checked="themeSwitcher.isDarkTheme"
|
||||
checked-children="☀"
|
||||
un-checked-children="🌙"
|
||||
@change="themeSwitcher.toggleTheme()">
|
||||
</a-switch>
|
||||
</template>
|
||||
{{end}}
|
||||
|
||||
{{define "component/themeSwitcher"}}
|
||||
<script>
|
||||
const colors = {
|
||||
dark: {
|
||||
bg: "#242c3a",
|
||||
text: "hsla(0,0%,100%,.65)"
|
||||
},
|
||||
light: {
|
||||
bg: '#f0f2f5',
|
||||
text: "rgba(0, 0, 0, 0.7)",
|
||||
}
|
||||
}
|
||||
|
||||
function createThemeSwitcher() {
|
||||
const isDarkTheme = localStorage.getItem('dark-mode') === 'true';
|
||||
const theme = isDarkTheme ? 'dark' : 'light';
|
||||
return {
|
||||
isDarkTheme,
|
||||
bgStyle: `background: ${colors[theme].bg};`,
|
||||
textStyle: `color: ${colors[theme].text};`,
|
||||
darkClass: isDarkTheme ? 'ant-dark' : '',
|
||||
darkCardClass: isDarkTheme ? 'ant-card-dark' : '',
|
||||
darkDrawerClass: isDarkTheme ? 'ant-drawer-dark' : '',
|
||||
get currentTheme() {
|
||||
return this.isDarkTheme ? 'dark' : 'light';
|
||||
},
|
||||
toggleTheme() {
|
||||
this.isDarkTheme = !this.isDarkTheme;
|
||||
this.theme = this.isDarkTheme ? 'dark' : 'light';
|
||||
localStorage.setItem('dark-mode', this.isDarkTheme);
|
||||
this.bgStyle = `background: ${colors[this.theme].bg};`;
|
||||
this.textStyle = `color: ${colors[this.theme].text};`;
|
||||
this.darkClass = this.isDarkTheme ? 'ant-dark' : '';
|
||||
this.darkCardClass = this.isDarkTheme ? 'ant-card-dark' : '';
|
||||
this.darkDrawerClass = this.isDarkTheme ? 'ant-drawer-dark' : '';
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const themeSwitcher = createThemeSwitcher();
|
||||
|
||||
Vue.component('theme-switch', {
|
||||
props: [],
|
||||
template: `{{template "component/themeSwitchTemplate"}}`,
|
||||
data: () => ({ themeSwitcher }),
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
@@ -3,73 +3,149 @@
|
||||
<template v-if="isEdit">
|
||||
<a-tag v-if="isExpiry || isTrafficExhausted" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag>
|
||||
</template>
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
Email
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
The email must be completely unique
|
||||
</template>
|
||||
<a-icon type="sync" @click="getNewEmail(client)"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-input v-model.trim="client.email"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="password" v-if="inbound.protocol === Protocols.TROJAN">
|
||||
<a-input v-model.trim="client.password"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="id" v-if="inbound.protocol === Protocols.VMESS || inbound.protocol === Protocols.VLESS">
|
||||
<a-input v-model.trim="client.id"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "additional" }} ID' v-if="inbound.protocol === Protocols.VMESS">
|
||||
<a-input type="number" v-model.number="client.alterId"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="inbound.xtls" label="flow">
|
||||
<a-select v-model="client.flow" style="width: 150px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||
<a-select-option value="">{{ i18n "none" }}</a-select-option>
|
||||
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item v-else-if="inbound.canEnableTlsFlow()" label="flow" layout="inline">
|
||||
<a-select v-model="client.flow" style="width: 150px">
|
||||
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
|
||||
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
|
||||
<template v-if="isEdit && clientStats">
|
||||
<span>{{ i18n "usage" }}:</span>
|
||||
<a-tag :color="statsColor">
|
||||
[[ sizeFormat(clientStats.up) ]] /
|
||||
[[ sizeFormat(clientStats.down) ]]
|
||||
([[ sizeFormat(clientStats.up + clientStats.down) ]])
|
||||
</a-tag>
|
||||
</template>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
||||
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
|
||||
v-model="client._expiryTime" style="width: 300px;"></a-date-picker>
|
||||
<a-tag color="red" v-if="isExpiry">Expired</a-tag>
|
||||
</a-form-item>
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.enable" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-switch v-model="client.enable"></a-switch>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span>{{ i18n "pages.inbounds.email" }}</span>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
|
||||
</template>
|
||||
<a-icon type="sync" @click="getNewEmail(client)"></a-icon>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.email" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="inbound.protocol === Protocols.TROJAN || inbound.protocol === Protocols.SHADOWSOCKS">
|
||||
<td>password
|
||||
<a-icon v-if="inbound.protocol === Protocols.SHADOWSOCKS" @click="client.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.password" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="inbound.protocol === Protocols.VMESS || inbound.protocol === Protocols.VLESS">
|
||||
<td>ID <a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"></a-icon></td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.id" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="inbound.protocol === Protocols.VMESS">
|
||||
<td>{{ i18n "additional" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model.number="client.alterId"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="client.email && app.subSettings.enable">
|
||||
<td>Subscription <a-icon @click="client.subId = RandomUtil.randomText(16,16)" type="sync"></a-icon></td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.subId" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="client.email && app.tgBotEnable">
|
||||
<td>Telegram Username</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.tgId" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="inbound.canEnableTlsFlow()">
|
||||
<td>Flow</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select v-model="client.flow" style="width: 250px" :dropdown-class-name="themeSwitcher.darkCardClass">
|
||||
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
|
||||
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span>{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="isEdit && clientStats">
|
||||
<td>{{ i18n "usage" }}</td>
|
||||
<td>
|
||||
<a-tag :color="statsColor">
|
||||
[[ sizeFormat(clientStats.up) ]] /
|
||||
[[ sizeFormat(clientStats.down) ]]
|
||||
([[ sizeFormat(clientStats.up + clientStats.down) ]])
|
||||
</a-tag>
|
||||
<a-tooltip>
|
||||
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
|
||||
<a-icon type="retweet" @click="resetClientTraffic(client.email,clientStats.inboundId,$event.target)" v-if="client.email.length > 0"></a-icon>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.client.delayedStart" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-switch v-model="clientModal.delayedStart" @click="client._expiryTime=0"></a-switch>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="clientModal.delayedStart">
|
||||
<td>{{ i18n "pages.client.expireDays" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-else>
|
||||
<td>
|
||||
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
|
||||
:dropdown-class-name="themeSwitcher.darkCardClass"
|
||||
v-model="client._expiryTime" style="width: 250px;"></a-date-picker>
|
||||
<a-tag color="red" v-if="isExpiry">Expired</a-tag>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-form>
|
||||
{{end}}
|
||||
@@ -1,58 +1,91 @@
|
||||
{{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>
|
||||
<a-form-item label='{{ i18n "protocol" }}'>
|
||||
<a-select v-model="inbound.protocol" style="width: 160px;" :disabled="isEdit" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||
<a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
{{ i18n "monitor" }}
|
||||
<a-tooltip>
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>{{ i18n "enable" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-switch v-model="dbInbound.enable"></a-switch>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "remark" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="dbInbound.remark" style="width: 250px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "protocol" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select v-model="inbound.protocol" style="width: 250px;" :disabled="isEdit" :dropdown-class-name="themeSwitcher.darkCardClass">
|
||||
<a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "monitor" }}
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.monitorDesc" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-input v-model.trim="inbound.listen"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.port" }}'>
|
||||
<a-input type="number" v-model.number="inbound.port"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-input-number v-model="dbInbound.totalGB" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
||||
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
|
||||
v-model="dbInbound._expiryTime" style="width: 300px;"></a-date-picker>
|
||||
</a-form-item>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.listen" style="width: 250px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.port" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model.number="inbound.port"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span>{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model="dbInbound.totalGB" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
|
||||
:dropdown-class-name="themeSwitcher.darkCardClass"
|
||||
v-model="dbInbound._expiryTime" style="width: 250px;"></a-date-picker>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-form>
|
||||
|
||||
<!-- vmess settings -->
|
||||
|
||||
@@ -1,20 +1,42 @@
|
||||
{{define "form/dokodemo"}}
|
||||
<a-form layout="inline">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.targetAddress"}}'>
|
||||
<a-input v-model.trim="inbound.settings.address"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.destinationPort"}}'>
|
||||
<a-input type="number" v-model.number="inbound.settings.port"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.network"}}'>
|
||||
<a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||
<a-select-option value="tcp,udp">tcp+udp</a-select-option>
|
||||
<a-select-option value="tcp">tcp</a-select-option>
|
||||
<a-select-option value="udp">udp</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="FollowRedirect">
|
||||
<a-switch v-model="inbound.settings.followRedirect"></a-switch>
|
||||
</a-form-item>
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.targetAddress"}}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.settings.address"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.destinationPort"}}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model.number="inbound.settings.port"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.network"}}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="themeSwitcher.darkCardClass">
|
||||
<a-select-option value="tcp,udp">tcp+udp</a-select-option>
|
||||
<a-select-option value="tcp">tcp</a-select-option>
|
||||
<a-select-option value="udp">udp</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>FollowRedirect</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-switch v-model="inbound.settings.followRedirect"></a-switch>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-form>
|
||||
{{end}}
|
||||
@@ -1,10 +1,22 @@
|
||||
{{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>
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>{{ i18n "username"}}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.settings.accounts[0].user"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "password" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.settings.accounts[0].pass"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-form>
|
||||
{{end}}
|
||||
@@ -1,19 +1,148 @@
|
||||
{{define "form/shadowsocks"}}
|
||||
<a-form layout="inline">
|
||||
<a-form-item label='{{ i18n "encryption" }}'>
|
||||
<a-select v-model="inbound.settings.method" style="width: 165px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||
<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-input v-model.trim="inbound.settings.password"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.network" }}'>
|
||||
<a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||
<a-select-option value="tcp,udp">tcp+udp</a-select-option>
|
||||
<a-select-option value="tcp">tcp</a-select-option>
|
||||
<a-select-option value="udp">udp</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<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" }}'>
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>
|
||||
<span>{{ i18n "pages.inbounds.email" }}</span>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
|
||||
</template>
|
||||
<a-icon @click="getNewEmail(client)" type="sync"> </a-icon>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>password
|
||||
<a-icon @click="client.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.password" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="client.email && app.subSettings.enable">
|
||||
<td>Subscription <a-icon @click="client.subId = RandomUtil.randomText(16,16)" type="sync"></a-icon></td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.subId" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="client.email && app.tgBotEnable">
|
||||
<td>Telegram Username</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.tgId" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB)
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.client.delayedStart" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="delayedStart">
|
||||
<td>{{ i18n "pages.client.expireDays" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-else>
|
||||
<td>
|
||||
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
|
||||
:dropdown-class-name="themeSwitcher.darkCardClass"
|
||||
v-model="client._expiryTime" style="width: 200px;"></a-date-picker>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
<a-collapse v-else>
|
||||
<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>
|
||||
</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>
|
||||
</tr>
|
||||
</table>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>{{ i18n "encryption" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select v-model="inbound.settings.method" style="width: 250px;" :dropdown-class-name="themeSwitcher.darkCardClass">
|
||||
<a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "password" }}
|
||||
<a-icon @click="inbound.settings.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.settings.password" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.network" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="themeSwitcher.darkCardClass">
|
||||
<a-select-option value="tcp,udp">tcp+udp</a-select-option>
|
||||
<a-select-option value="tcp">tcp</a-select-option>
|
||||
<a-select-option value="udp">udp</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-form>
|
||||
{{end}}
|
||||
@@ -1,24 +1,49 @@
|
||||
{{define "form/socks"}}
|
||||
<a-form layout="inline">
|
||||
<!-- <a-form-item label="Password authentication">-->
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
<a-switch :checked="inbound.settings.auth === 'password'"
|
||||
@change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"></a-switch>
|
||||
</a-form-item>
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>{{ i18n "password" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-switch :checked="inbound.settings.auth === 'password'"
|
||||
@change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"></a-switch>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<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>
|
||||
<tr>
|
||||
<td>{{ i18n "username" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.settings.accounts[0].user"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "password" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.settings.accounts[0].pass"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.enable" }} udp'>
|
||||
<a-switch v-model="inbound.settings.udp"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="inbound.settings.udp"
|
||||
label="IP">
|
||||
<a-input v-model.trim="inbound.settings.ip"></a-input>
|
||||
</a-form-item>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.enable" }} udp</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-switch v-model="inbound.settings.udp"></a-switch>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>IP</td>
|
||||
<td>
|
||||
<a-form-item v-if="inbound.settings.udp">
|
||||
<a-input v-model.trim="inbound.settings.ip"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-form>
|
||||
{{end}}
|
||||
@@ -1,56 +1,99 @@
|
||||
{{define "form/trojan"}}
|
||||
<a-form layout="inline">
|
||||
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.trojans.slice(0,1)" v-if="!isEdit">
|
||||
<a-collapse-panel header="{{ i18n "pages.inbounds.client" }}">
|
||||
<a-form layout="inline">
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
Email
|
||||
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>
|
||||
<span>{{ i18n "pages.inbounds.email" }}</span>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
The email must be completely unique
|
||||
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
|
||||
</template>
|
||||
<a-icon @click="getNewEmail(client)" type="sync"> </a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-input v-model.trim="client.email"></a-input>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-form-item label="password">
|
||||
<a-input v-model.trim="client.password"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="inbound.xtls" label="flow">
|
||||
<a-select v-model="client.flow" style="width: 150px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||
<a-select-option value="">{{ i18n "none" }}</a-select-option>
|
||||
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
||||
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
|
||||
v-model="client._expiryTime" style="width: 300px;"></a-date-picker>
|
||||
</a-form-item>
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>password</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.password" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="client.email && app.subSettings.enable">
|
||||
<td>Subscription <a-icon @click="client.subId = RandomUtil.randomText(16,16)" type="sync"></a-icon></td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.subId" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="client.email && app.tgBotEnable">
|
||||
<td>Telegram Username</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.tgId" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB)
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.client.delayedStart" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="delayedStart">
|
||||
<td>{{ i18n "pages.client.expireDays" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-else>
|
||||
<td>
|
||||
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
|
||||
:dropdown-class-name="themeSwitcher.darkCardClass"
|
||||
v-model="client._expiryTime" style="width: 200px;"></a-date-picker>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
<a-collapse v-else>
|
||||
@@ -97,7 +140,7 @@
|
||||
<a-input v-model="fallback.dest"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="xver">
|
||||
<a-input type="number" v-model.number="fallback.xver"></a-input>
|
||||
<a-input-number v-model.number="fallback.xver"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
|
||||
</a-form>
|
||||
|
||||
@@ -2,61 +2,109 @@
|
||||
<a-form layout="inline">
|
||||
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit">
|
||||
<a-collapse-panel header="{{ i18n "pages.inbounds.client" }}">
|
||||
<a-form layout="inline">
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
Email
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>
|
||||
<span>{{ i18n "pages.inbounds.email" }}</span>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
The email must be completely unique
|
||||
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
|
||||
</template>
|
||||
<a-icon type="sync" @click="getNewEmail(client)"></a-icon>
|
||||
<a-icon @click="getNewEmail(client)" type="sync"> </a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-input v-model.trim="client.email"></a-input>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-form-item label="id">
|
||||
<a-input v-model.trim="client.id"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="inbound.xtls" label="flow">
|
||||
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 150px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
|
||||
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item v-else-if="inbound.canEnableTlsFlow()" label="flow" layout="inline">
|
||||
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 150px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
|
||||
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
||||
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
|
||||
v-model="client._expiryTime" style="width: 300px;"></a-date-picker>
|
||||
</a-form-item>
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ID <a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"></a-icon></td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.id" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="inbound.canEnableTlsFlow()">
|
||||
<td>flow</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass">
|
||||
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
|
||||
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="client.email && app.subSettings.enable">
|
||||
<td>Subscription <a-icon @click="client.subId = RandomUtil.randomText(16,16)" type="sync"></a-icon></td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.subId" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="client.email && app.tgBotEnable">
|
||||
<td>Telegram Username</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.tgId" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span>{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.client.delayedStart" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="delayedStart">
|
||||
<td>{{ i18n "pages.client.expireDays" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-else>
|
||||
<td>
|
||||
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
|
||||
:dropdown-class-name="themeSwitcher.darkCardClass"
|
||||
v-model="client._expiryTime" style="width: 200px;"></a-date-picker>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
<a-collapse v-else>
|
||||
@@ -103,7 +151,7 @@
|
||||
<a-input v-model="fallback.dest"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="xver">
|
||||
<a-input type="number" v-model.number="fallback.xver"></a-input>
|
||||
<a-input-number v-model.number="fallback.xver"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
|
||||
</a-form>
|
||||
|
||||
@@ -2,52 +2,106 @@
|
||||
<a-form layout="inline">
|
||||
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vmesses.slice(0,1)" v-if="!isEdit">
|
||||
<a-collapse-panel header="{{ i18n "pages.inbounds.client" }}">
|
||||
<a-form layout="inline">
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
Email
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>
|
||||
<span>{{ i18n "pages.inbounds.email" }}</span>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
The email must be completely unique
|
||||
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
|
||||
</template>
|
||||
<a-icon type="sync" @click="getNewEmail(client)"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-input v-model.trim="client.email"></a-input>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-form-item label="id">
|
||||
<a-input v-model.trim="client.id"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "additional" }} ID'>
|
||||
<a-input type="number" v-model.number="client.alterId"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
||||
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
|
||||
v-model="client._expiryTime" style="width: 300px;"></a-date-picker>
|
||||
</a-form-item>
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ID <a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"></a-icon></td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.id" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "additional" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model.number="client.alterId"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="client.email && app.subSettings.enable">
|
||||
<td>Subscription <a-icon @click="client.subId = RandomUtil.randomText(16,16)" type="sync"></a-icon></td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.subId" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="client.email && app.tgBotEnable">
|
||||
<td>Telegram Username</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.tgId" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span>{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.client.delayedStart" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="delayedStart">
|
||||
<td>{{ i18n "pages.client.expireDays" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-else>
|
||||
<td>
|
||||
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
|
||||
:dropdown-class-name="themeSwitcher.darkCardClass"
|
||||
v-model="client._expiryTime" style="width: 200px;"></a-date-picker>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
<a-collapse v-else>
|
||||
@@ -64,8 +118,7 @@
|
||||
</a-collapse>
|
||||
<a-form layout="inline">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.disableInsecureEncryption" }}'>
|
||||
<a-switch v-model.number="inbound.settings.disableInsecure"></a-switch>
|
||||
<a-switch v-model="inbound.settings.disableInsecure"></a-switch>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
{{end}}
|
||||
|
||||
@@ -5,12 +5,17 @@
|
||||
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>
|
||||
</span>
|
||||
<a-switch v-model="inbound.sniffing.enabled"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-checkbox-group v-model="inbound.sniffing.destOverride" v-if="inbound.sniffing.enabled">
|
||||
<a-checkbox v-for="key,value in SNIFFING_OPTION" :value="key">[[ value ]]</a-checkbox>
|
||||
</a-checkbox-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
@@ -1,7 +1,22 @@
|
||||
{{define "form/streamGRPC"}}
|
||||
<a-form layout="inline">
|
||||
<a-form-item label="serviceName">
|
||||
<a-input v-model.trim="inbound.stream.grpc.serviceName"></a-input>
|
||||
</a-form-item>
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>serviceName</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.stream.grpc.serviceName" style="width: 250px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MultiMode</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-switch v-model="inbound.stream.grpc.multiMode"></a-switch>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-form>
|
||||
{{end}}
|
||||
@@ -1,12 +1,24 @@
|
||||
{{define "form/streamHTTP"}}
|
||||
<a-form layout="inline">
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
<a-input v-model.trim="inbound.stream.http.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="host">
|
||||
<a-row v-for="(host, index) in inbound.stream.http.host">
|
||||
<a-input v-model.trim="inbound.stream.http.host[index]"></a-input>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>{{ i18n "path" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.stream.http.path" style="width: 250px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>host</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-row v-for="(host, index) in inbound.stream.http.host">
|
||||
<a-input v-model.trim="inbound.stream.http.host[index]" style="width: 250px;"></a-input>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-form>
|
||||
{{end}}
|
||||
@@ -1,38 +1,85 @@
|
||||
{{define "form/streamKCP"}}
|
||||
<a-form layout="inline">
|
||||
<a-form-item label='{{ i18n "camouflage" }}'>
|
||||
<a-select v-model="inbound.stream.kcp.type" style="width: 280px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||
<a-select-option value="none">none(not camouflage)</a-select-option>
|
||||
<a-select-option value="srtp">srtp(camouflage video call)</a-select-option>
|
||||
<a-select-option value="utp">utp(camouflage BT download)</a-select-option>
|
||||
<a-select-option value="wechat-video">wechat-video(camouflage WeChat video)</a-select-option>
|
||||
<a-select-option value="dtls">dtls(camouflage DTLS 1.2 packages)</a-select-option>
|
||||
<a-select-option value="wireguard">wireguard(camouflage wireguard packages)</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
<a-input v-model.number="inbound.stream.kcp.seed"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="mtu">
|
||||
<a-input type="number" v-model.number="inbound.stream.kcp.mtu"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="tti (ms)">
|
||||
<a-input type="number" v-model.number="inbound.stream.kcp.tti"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="uplink capacity (MB/S)">
|
||||
<a-input type="number" v-model.number="inbound.stream.kcp.upCap"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="downlink capacity (MB/S)">
|
||||
<a-input type="number" v-model.number="inbound.stream.kcp.downCap"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="congestion">
|
||||
<a-switch v-model="inbound.stream.kcp.congestion"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="read buffer size (MB)">
|
||||
<a-input type="number" v-model.number="inbound.stream.kcp.readBuffer"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="write buffer size (MB)">
|
||||
<a-input type="number" v-model.number="inbound.stream.kcp.writeBuffer"></a-input>
|
||||
</a-form-item>
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>{{ i18n "camouflage" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select v-model="inbound.stream.kcp.type" style="width: 250px;" :dropdown-class-name="themeSwitcher.darkCardClass">
|
||||
<a-select-option value="none">none (not camouflage)</a-select-option>
|
||||
<a-select-option value="srtp">srtp (video call)</a-select-option>
|
||||
<a-select-option value="utp">utp (BT download)</a-select-option>
|
||||
<a-select-option value="wechat-video">wechat-video (WeChat video)</a-select-option>
|
||||
<a-select-option value="dtls">dtls (DTLS 1.2 packages)</a-select-option>
|
||||
<a-select-option value="wireguard">wireguard (wireguard packages)</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "password" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model="inbound.stream.kcp.seed" style="width: 250px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>mtu</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.mtu"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>tti (ms)</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.tti"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>uplink capacity (MB/S)</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.upCap"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>downlink capacity (MB/S)</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.downCap"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>congestion</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-switch v-model="inbound.stream.kcp.congestion"></a-switch>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>read buffer size (MB)</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.readBuffer"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>write buffer size (MB)</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.writeBuffer"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-form>
|
||||
{{end}}
|
||||
@@ -1,24 +1,41 @@
|
||||
{{define "form/streamQUIC"}}
|
||||
<a-form layout="inline">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.quic.encryption" }}'>
|
||||
<a-select v-model="inbound.stream.quic.security" style="width: 165px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||
<a-select-option value="none">none</a-select-option>
|
||||
<a-select-option value="aes-128-gcm">aes-128-gcm</a-select-option>
|
||||
<a-select-option value="chacha20-poly1305">chacha20-poly1305</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
<a-input v-model.trim="inbound.stream.quic.key"></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="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||
<a-select-option value="none">none(not camouflage)</a-select-option>
|
||||
<a-select-option value="srtp">srtp(camouflage video call)</a-select-option>
|
||||
<a-select-option value="utp">utp(camouflage BT download)</a-select-option>
|
||||
<a-select-option value="wechat-video">wechat-video(camouflage WeChat video)</a-select-option>
|
||||
<a-select-option value="dtls">dtls(camouflage DTLS 1.2 packages)</a-select-option>
|
||||
<a-select-option value="wireguard">wireguard(camouflage wireguard packages)</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.stream.quic.encryption" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select v-model="inbound.stream.quic.security" style="width: 200px;" :dropdown-class-name="themeSwitcher.darkCardClass">
|
||||
<a-select-option value="none">none</a-select-option>
|
||||
<a-select-option value="aes-128-gcm">aes-128-gcm</a-select-option>
|
||||
<a-select-option value="chacha20-poly1305">chacha20-poly1305</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "password" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.stream.quic.key" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "camouflage" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select v-model="inbound.stream.quic.type" style="width: 200px;" :dropdown-class-name="themeSwitcher.darkCardClass">
|
||||
<a-select-option value="none">none (not camouflage)</a-select-option>
|
||||
<a-select-option value="srtp">srtp (video call)</a-select-option>
|
||||
<a-select-option value="utp">utp (BT download)</a-select-option>
|
||||
<a-select-option value="wechat-video">wechat-video (WeChat video)</a-select-option>
|
||||
<a-select-option value="dtls">dtls (DTLS 1.2 packages)</a-select-option>
|
||||
<a-select-option value="wireguard">wireguard (wireguard packages)</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-form>
|
||||
{{end}}
|
||||
@@ -1,8 +1,9 @@
|
||||
{{define "form/streamSettings"}}
|
||||
<!-- select stream network -->
|
||||
<a-form layout="inline">
|
||||
<a-form-item label='{{ i18n "transmission" }}'>
|
||||
<a-select v-model="inbound.stream.network" @change="streamNetworkChange" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||
<a-form-item label="{{ i18n "transmission" }}">
|
||||
<a-select v-model="inbound.stream.network" @change="streamNetworkChange"
|
||||
:dropdown-class-name="themeSwitcher.darkCardClass">
|
||||
<a-select-option value="tcp">tcp</a-select-option>
|
||||
<a-select-option value="kcp">kcp</a-select-option>
|
||||
<a-select-option value="ws">ws</a-select-option>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{{define "form/streamTCP"}}
|
||||
<!-- tcp type -->
|
||||
<a-form layout="inline">
|
||||
<a-form-item label="acceptProxyProtocol">
|
||||
<a-form-item label="Accept Proxy Protocol">
|
||||
<a-switch v-model="inbound.stream.tcp.acceptProxyProtocol"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="http {{ i18n "camouflage" }}">
|
||||
@@ -13,74 +13,109 @@
|
||||
</a-form>
|
||||
|
||||
<!-- tcp request -->
|
||||
<a-form v-if="inbound.stream.tcp.type === 'http'"
|
||||
layout="inline">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestVersion" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tcp.request.version"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestMethod" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tcp.request.method"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestPath" }}'>
|
||||
<a-row v-for="(path, index) in inbound.stream.tcp.request.path">
|
||||
<a-input v-model.trim="inbound.stream.tcp.request.path[index]"></a-input>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.general.requestHeader" }}'>
|
||||
<a-row>
|
||||
<a-button size="small"
|
||||
@click="inbound.stream.tcp.request.addHeader('Host', 'xxx.com')">
|
||||
+
|
||||
</a-button>
|
||||
</a-row>
|
||||
<a-input-group v-for="(header, index) in inbound.stream.tcp.request.headers">
|
||||
<a-input style="width: 50%" v-model.trim="header.name"
|
||||
addon-before='{{ i18n "pages.inbounds.stream.general.name" }}'></a-input>
|
||||
<a-input style="width: 50%" v-model.trim="header.value"
|
||||
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
|
||||
<template slot="addonAfter">
|
||||
<a-button size="small"
|
||||
@click="inbound.stream.tcp.request.removeHeader(index)">
|
||||
-
|
||||
</a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- tcp response -->
|
||||
<a-form v-if="inbound.stream.tcp.type === 'http'"
|
||||
layout="inline">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseVersion" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tcp.response.version"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseStatus" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tcp.response.status"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseStatusDescription" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tcp.response.reason"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseHeader" }}'>
|
||||
<a-row>
|
||||
<a-button size="small"
|
||||
@click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')">
|
||||
+
|
||||
</a-button>
|
||||
</a-row>
|
||||
<a-input-group v-for="(header, index) in inbound.stream.tcp.response.headers">
|
||||
<a-input style="width: 50%" v-model.trim="header.name"
|
||||
addon-before='{{ i18n "pages.inbounds.stream.general.name" }}'></a-input>
|
||||
<a-input style="width: 50%" v-model.trim="header.value"
|
||||
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
|
||||
<template slot="addonAfter">
|
||||
<a-button size="small"
|
||||
@click="inbound.stream.tcp.response.removeHeader(index)">
|
||||
-
|
||||
</a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
<a-form v-if="inbound.stream.tcp.type === 'http'" layout="inline">
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.stream.tcp.requestVersion" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.stream.tcp.request.version" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.stream.tcp.requestMethod" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.stream.tcp.request.method" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.stream.tcp.requestPath" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-row v-for="(path, index) in inbound.stream.tcp.request.path">
|
||||
<a-input v-model.trim="inbound.stream.tcp.request.path[index]" style="width: 200px;"></a-input>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.general.requestHeader" }}'>
|
||||
<a-row>
|
||||
<a-button size="small"
|
||||
@click="inbound.stream.tcp.request.addHeader('Host', 'xxx.com')">
|
||||
+
|
||||
</a-button>
|
||||
</a-row>
|
||||
<a-input-group v-for="(header, index) in inbound.stream.tcp.request.headers">
|
||||
<a-input style="width: 50%" v-model.trim="header.name"
|
||||
addon-before='{{ i18n "pages.inbounds.stream.general.name" }}'></a-input>
|
||||
<a-input style="width: 50%" v-model.trim="header.value"
|
||||
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
|
||||
<template slot="addonAfter">
|
||||
<a-button size="small"
|
||||
@click="inbound.stream.tcp.request.removeHeader(index)">
|
||||
-
|
||||
</a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- tcp response -->
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.stream.tcp.responseVersion" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.stream.tcp.response.version" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.stream.tcp.responseStatus" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.stream.tcp.response.status" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.stream.tcp.responseStatusDescription" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.stream.tcp.response.reason" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseHeader" }}'>
|
||||
<a-row>
|
||||
<a-button size="small"
|
||||
@click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')">
|
||||
+
|
||||
</a-button>
|
||||
</a-row>
|
||||
<a-input-group v-for="(header, index) in inbound.stream.tcp.response.headers">
|
||||
<a-input style="width: 50%" v-model.trim="header.name"
|
||||
addon-before='{{ i18n "pages.inbounds.stream.general.name" }}'></a-input>
|
||||
<a-input style="width: 50%" v-model.trim="header.value"
|
||||
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
|
||||
<template slot="addonAfter">
|
||||
<a-button size="small"
|
||||
@click="inbound.stream.tcp.response.removeHeader(index)">
|
||||
-
|
||||
</a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-form>
|
||||
{{end}}
|
||||
@@ -1,33 +1,47 @@
|
||||
{{define "form/streamWS"}}
|
||||
<a-form layout="inline">
|
||||
<a-form-item label="acceptProxyProtocol">
|
||||
<a-switch v-model="inbound.stream.ws.acceptProxyProtocol"></a-switch>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-form layout="inline">
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
<a-input v-model.trim="inbound.stream.ws.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.general.requestHeader" }}'>
|
||||
<a-row>
|
||||
<a-button size="small"
|
||||
@click="inbound.stream.ws.addHeader('Host', '')">
|
||||
+
|
||||
</a-button>
|
||||
</a-row>
|
||||
<a-input-group v-for="(header, index) in inbound.stream.ws.headers">
|
||||
<a-input style="width: 50%" v-model.trim="header.name"
|
||||
addon-before='{{ i18n "pages.inbounds.stream.general.name"}}'></a-input>
|
||||
<a-input style="width: 50%" v-model.trim="header.value"
|
||||
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
|
||||
<template slot="addonAfter">
|
||||
<a-button size="small"
|
||||
@click="inbound.stream.ws.removeHeader(index)">
|
||||
-
|
||||
</a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>Accept Proxy Protocol</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-switch v-model="inbound.stream.ws.acceptProxyProtocol"></a-switch>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "path" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.stream.ws.path" style="width: 250px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.general.requestHeader" }}'>
|
||||
<a-row>
|
||||
<a-button size="small"
|
||||
@click="inbound.stream.ws.addHeader('Host', '')">
|
||||
+
|
||||
</a-button>
|
||||
</a-row>
|
||||
<a-input-group v-for="(header, index) in inbound.stream.ws.headers">
|
||||
<a-input style="width: 50%" v-model.trim="header.name"
|
||||
addon-before='{{ i18n "pages.inbounds.stream.general.name"}}'></a-input>
|
||||
<a-input style="width: 50%" v-model.trim="header.value"
|
||||
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
|
||||
<template slot="addonAfter">
|
||||
<a-button size="small"
|
||||
@click="inbound.stream.ws.removeHeader(index)">
|
||||
-
|
||||
</a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-form>
|
||||
{{end}}
|
||||
@@ -1,72 +1,269 @@
|
||||
{{define "form/tlsSettings"}}
|
||||
<!-- tls enable -->
|
||||
<a-form layout="inline" v-if="inbound.canSetTls()">
|
||||
<a-form-item label="tls">
|
||||
<a-form v-if="inbound.canSetTls()" layout="inline">
|
||||
<a-form-item label="TLS">
|
||||
<a-switch v-model="inbound.tls">
|
||||
</a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="inbound.canEnableXTls()" label="xtls">
|
||||
<a-switch v-model="inbound.xtls"></a-switch>
|
||||
<a-form-item v-if="inbound.canEnableReality()" label="Reality">
|
||||
<a-switch v-model="inbound.reality"></a-switch>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- tls settings -->
|
||||
<a-form v-if="inbound.tls || inbound.xtls" layout="inline">
|
||||
<a-form-item label="SNI" placeholder="Server Name Indication" v-if="inbound.tls">
|
||||
<a-input v-model.trim="inbound.stream.tls.settings[0].serverName"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="CipherSuites">
|
||||
<a-select v-model="inbound.stream.tls.cipherSuites" style="width: 300px">
|
||||
<a-select-option value="">auto</a-select-option>
|
||||
<a-select-option v-for="key in TLS_CIPHER_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="MinVersion">
|
||||
<a-select v-model="inbound.stream.tls.minVersion" style="width: 60px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="MaxVersion">
|
||||
<a-select v-model="inbound.stream.tls.maxVersion" style="width: 60px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="uTLS" v-if="inbound.tls" >
|
||||
<a-select v-model="inbound.stream.tls.settings[0].fingerprint" style="width: 135px">
|
||||
<a-select-option value=''>None</a-select-option>
|
||||
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "domainName" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tls.server"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Alpn" v-if="inbound.tls">
|
||||
<a-select v-model="inbound.stream.tls.alpn[0]" style="width:200px">
|
||||
<a-select-option value=''>auto</a-select-option>
|
||||
<a-select-option v-for="key in ALPN_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "certificate" }}'>
|
||||
<a-radio-group v-model="inbound.stream.tls.certs[0].useFile" button-style="solid">
|
||||
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
|
||||
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<template v-if="inbound.stream.tls.certs[0].useFile">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.publicKeyPath" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tls.certs[0].certFile" style="width:300px;"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tls.certs[0].keyFile" style="width:300px;"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
|
||||
<a-input type="textarea" :rows="3" style="width:300px;" v-model="inbound.stream.tls.certs[0].cert"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.keyContent" }}'>
|
||||
<a-input type="textarea" :rows="3" style="width:300px;" v-model="inbound.stream.tls.certs[0].key"></a-input>
|
||||
</a-form-item>
|
||||
<a-form v-if="inbound.tls" layout="inline">
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>SNI</td>
|
||||
<td>
|
||||
<a-form-item placeholder="Server Name Indication" v-if="inbound.tls">
|
||||
<a-input v-model.trim="inbound.stream.tls.settings.serverName" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CipherSuites</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select v-model="inbound.stream.tls.cipherSuites" style="width: 250px" :dropdown-class-name="themeSwitcher.darkCardClass">
|
||||
<a-select-option value="">auto</a-select-option>
|
||||
<a-select-option v-for="key in TLS_CIPHER_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MinVersion</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select v-model="inbound.stream.tls.minVersion" :dropdown-class-name="themeSwitcher.darkCardClass">
|
||||
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MaxVersion</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select v-model="inbound.stream.tls.maxVersion" :dropdown-class-name="themeSwitcher.darkCardClass">
|
||||
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>uTLS</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select v-model="inbound.stream.tls.settings.fingerprint"
|
||||
style="width: 250px" :dropdown-class-name="themeSwitcher.darkCardClass">
|
||||
<a-select-option value=''>None</a-select-option>
|
||||
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Multi Domain</td>
|
||||
<td>
|
||||
<a-switch v-model="multiDomain"></a-switch>
|
||||
<a-button v-if="multiDomain" size="small" @click="inbound.stream.tls.settings.domains.push({remark: '', domain: ''})">+</a-button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="multiDomain">
|
||||
<td colspan="2">
|
||||
<a-form-item>
|
||||
<a-input-group v-for="(row, index) in inbound.stream.tls.settings.domains">
|
||||
<a-input style="width: 40%" v-model.trim="row.remark" addon-before='{{ i18n "remark" }}'></a-input>
|
||||
<a-input style="width: 60%" v-model.trim="row.domain" addon-before='{{ i18n "host" }}'>
|
||||
<template slot="addonAfter">
|
||||
<a-button size="small" style="margin: 0px" @click="inbound.stream.tls.settings.domains.splice(index, 1)">-</a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-else>
|
||||
<td>{{ i18n "domainName" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.stream.tls.server" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Alpn</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-checkbox-group v-model="inbound.stream.tls.alpn">
|
||||
<a-checkbox v-for="key,value in ALPN_OPTION" :value="key">[[ value ]]</a-checkbox>
|
||||
</a-checkbox-group>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Allow insecure</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-switch v-model="inbound.stream.tls.settings.allowInsecure"></a-switch>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<template v-for="cert,index in inbound.stream.tls.certs">
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<a-form-item label='{{ i18n "certificate" }}'>
|
||||
<a-radio-group v-model="cert.useFile" button-style="solid">
|
||||
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
|
||||
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
|
||||
</a-radio-group>
|
||||
<a-button v-if="index === 0" type="primary" size="small" @click="inbound.stream.tls.addCert()" style="margin-left: 10px">+</a-button>
|
||||
<a-button v-if="inbound.stream.tls.certs.length>1" type="primary" size="small" @click="inbound.stream.tls.removeCert(index)" style="margin-left: 10px">-</a-button>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<template v-if="cert.useFile">
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.publicKeyPath" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="cert.certFile" style="width:250px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.keyPath" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="cert.keyFile" style="width:250px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<a-button type="primary" icon="import" @click="setDefaultCertData(index)">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template v-else>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.publicKeyContent" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input type="textarea" :rows="3" style="width:250px;" v-model="cert.cert"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.keyContent" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input type="textarea" :rows="3" style="width:250px;" v-model="cert.key"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</template>
|
||||
</table>
|
||||
</a-form>
|
||||
|
||||
<!-- reality settings -->
|
||||
<a-form v-if="inbound.reality" layout="inline">
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>{{ i18n "domainName" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.stream.reality.settings.serverName" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Show</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-switch v-model="inbound.stream.reality.show"></a-switch>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Xver</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model.number="inbound.stream.reality.xver" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>uTLS</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select v-model="inbound.stream.reality.settings.fingerprint"
|
||||
style="width: 250px" :dropdown-class-name="themeSwitcher.darkCardClass">
|
||||
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Dest</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.stream.reality.dest" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Server Names</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.stream.reality.serverNames" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Short Ids
|
||||
<a-icon @click="inbound.stream.reality.shortIds = RandomUtil.randomShortId().join(',')" type="sync">
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.stream.reality.shortIds" style="width:250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SpiderX</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.stream.reality.settings.spiderX" style="width:250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Private Key</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.stream.reality.privateKey" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Public Key</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.stream.reality.settings.publicKey" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<a-button type="primary" icon="import" @click="getNewX25519Cert">Get new cert</a-button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-form>
|
||||
{{end}}
|
||||
@@ -2,7 +2,7 @@
|
||||
<template slot="actions" slot-scope="text, client, index">
|
||||
<a-tooltip>
|
||||
<template slot="title">{{ i18n "qrCode" }}</template>
|
||||
<a-icon style="font-size: 24px;" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record,index);"></a-icon>
|
||||
<a-icon style="font-size: 24px;" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record,index,'',client.email);"></a-icon>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template slot="title">{{ i18n "pages.client.edit" }}</template>
|
||||
@@ -21,24 +21,27 @@
|
||||
<a-icon style="font-size: 24px;" type="delete" v-if="isRemovable(record.id)" @click="delClient(record.id,client)"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<template slot="enable" slot-scope="text, client, index">
|
||||
<a-switch v-model="client.enable" @change="switchEnableClient(record.id,client)"></a-switch>
|
||||
</template>
|
||||
<template slot="client" slot-scope="text, client">
|
||||
[[ client.email ]]
|
||||
<a-tag v-if="!isClientEnabled(record, client.email)" color="red">{{ i18n "disabled" }}</a-tag>
|
||||
<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="blue">[[ sizeFormat(getUpStats(record, client.email)) ]] / [[ sizeFormat(getDownStats(record, client.email)) ]]</a-tag>
|
||||
<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 v-if="isTrafficExhausted(record, client.email)" color="red">[[client._totalGB]]GB</a-tag>
|
||||
<a-tag v-else color="cyan">[[client._totalGB]]GB</a-tag>
|
||||
<a-tag :color="statsColor(record, client.email)">[[client._totalGB]]GB</a-tag>
|
||||
</template>
|
||||
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
|
||||
</template>
|
||||
<template slot="expiryTime" slot-scope="text, client, index">
|
||||
<template v-if="client._expiryTime > 0">
|
||||
<a-tag :color="isExpiry(record, index)? 'red' : 'blue'">
|
||||
<template v-if="client.expiryTime > 0">
|
||||
<a-tag :color="usageColor(new Date().getTime(), app.expireDiff, client.expiryTime)">
|
||||
[[ DateUtil.formatMillis(client._expiryTime) ]]
|
||||
</a-tag>
|
||||
</template>
|
||||
<a-tag v-else-if="client.expiryTime < 0" color="cyan">[[ client._expiryTime ]] {{ i18n "pages.client.days" }}</a-tag>
|
||||
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
|
||||
</template>
|
||||
{{end}}
|
||||
@@ -3,7 +3,7 @@
|
||||
v-model="infoModal.visible" title='{{ i18n "pages.inbounds.details"}}'
|
||||
:closable="true"
|
||||
:mask-closable="true"
|
||||
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||
:class="themeSwitcher.darkCardClass"
|
||||
:footer="null"
|
||||
width="600px"
|
||||
>
|
||||
@@ -41,6 +41,7 @@
|
||||
|
||||
<template v-if="inbound.isGrpc">
|
||||
<tr><td>grpc serviceName</td><td><a-tag color="green">[[ inbound.serviceName ]]</a-tag></td></tr>
|
||||
<tr><td>grpc multiMode</td><td><a-tag color="green">[[ inbound.stream.grpc.multiMode ]]</a-tag></td></tr>
|
||||
</template>
|
||||
</table>
|
||||
</td></tr>
|
||||
@@ -49,9 +50,9 @@
|
||||
tls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
|
||||
tls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
|
||||
</td>
|
||||
<td v-else-if="inbound.xtls">
|
||||
xtls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
|
||||
xtls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
|
||||
<td v-else-if="inbound.reality">
|
||||
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>
|
||||
@@ -59,18 +60,28 @@
|
||||
</table>
|
||||
<template v-if="infoModal.clientSettings">
|
||||
<a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
|
||||
<table style="margin-bottom: 10px; width: 100%;">
|
||||
<tr>
|
||||
<th v-for="col in Object.keys(infoModal.clientSettings).slice(0, 3)">[[ col ]]</th>
|
||||
<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 v-for="col in Object.values(infoModal.clientSettings).slice(0, 3)"><a-tag color="green">[[ col ]]</a-tag></td>
|
||||
<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%;">
|
||||
<tr><th>{{ i18n "usage" }}</th><th>{{ i18n "pages.inbounds.totalFlow" }}</th><th>{{ i18n "pages.inbounds.expireDate" }}</th><th>{{ i18n "enable" }}</th></tr>
|
||||
<tr>
|
||||
<th>{{ i18n "usage" }}</th>
|
||||
<th>{{ i18n "pages.inbounds.totalFlow" }}</th>
|
||||
<th>{{ i18n "pages.inbounds.expireDate" }}</th>
|
||||
<tr>
|
||||
<td>
|
||||
<a-tag v-if="infoModal.clientStats" :color="statsColor(infoModal.clientStats)">
|
||||
<a-tag v-if="infoModal.clientStats" color="green">
|
||||
[[ sizeFormat(infoModal.clientStats['up']) ]] /
|
||||
[[ sizeFormat(infoModal.clientStats['down']) ]]
|
||||
([[ sizeFormat(infoModal.clientStats['up'] + infoModal.clientStats['down']) ]])
|
||||
@@ -82,18 +93,25 @@
|
||||
</td>
|
||||
<td>
|
||||
<template v-if="infoModal.clientSettings.expiryTime > 0">
|
||||
<a-tag :color="infoModal.isExpired ? 'red' : 'blue'">
|
||||
<a-tag :color="usageColor(new Date().getTime(), app.expireDiff, infoModal.clientSettings.expiryTime)">
|
||||
[[ DateUtil.formatMillis(infoModal.clientSettings.expiryTime) ]]
|
||||
</a-tag>
|
||||
</template>
|
||||
<a-tag v-else-if="infoModal.clientSettings.expiryTime < 0" color="cyan">[[ infoModal.clientSettings.expiryTime / -86400000 ]] {{ i18n "pages.client.days" }}</a-tag>
|
||||
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
|
||||
</td>
|
||||
<td>
|
||||
<a-tag v-if="isEnable" color="blue">{{ i18n "enabled" }}</a-tag>
|
||||
<a-tag v-else color="red">{{ i18n "disabled" }}</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
</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>
|
||||
</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>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-divider></a-divider>
|
||||
@@ -157,47 +175,73 @@
|
||||
</template>
|
||||
<div v-if="dbInbound.hasLink()">
|
||||
<a-divider>URL</a-divider>
|
||||
<p>[[ infoModal.link ]]</p>
|
||||
<button class="ant-btn ant-btn-primary" id="copy-url-link"><a-icon type="snippets"></a-icon>{{ i18n "copy" }}</button>
|
||||
<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(),
|
||||
dbInbound: new DBInbound(),
|
||||
settings: null,
|
||||
clientSettings: new Inbound.Settings(),
|
||||
clientSettings: null,
|
||||
clientStats: [],
|
||||
upStats: 0,
|
||||
downStats: 0,
|
||||
clipboard: null,
|
||||
link: null,
|
||||
links: [],
|
||||
index: null,
|
||||
isExpired: false,
|
||||
subLink: '',
|
||||
tgLink: '',
|
||||
show(dbInbound, index) {
|
||||
this.index = index;
|
||||
this.inbound = dbInbound.toInbound();
|
||||
this.dbInbound = new DBInbound(dbInbound);
|
||||
this.link = dbInbound.genLink(index);
|
||||
this.settings = JSON.parse(this.inbound.settings);
|
||||
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) : [];
|
||||
this.visible = true;
|
||||
infoModalApp.$nextTick(() => {
|
||||
if (this.clipboard === null) {
|
||||
this.clipboard = new ClipboardJS('#copy-url-link', {
|
||||
text: () => this.link,
|
||||
remark = this.dbInbound.remark + "-" + this.clientSettings.email;
|
||||
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) => {
|
||||
this.links.push({
|
||||
remark: remark + "-" + domain.remark,
|
||||
link: this.inbound.genLink(domain.domain, remark + "-" + domain.remark, index)
|
||||
});
|
||||
this.clipboard.on('success', () => app.$message.success('{{ i18n "copied" }}'));
|
||||
});
|
||||
} else {
|
||||
this.links.push({
|
||||
remark: remark,
|
||||
link: this.inbound.genLink(address, remark, index)
|
||||
});
|
||||
}
|
||||
if (this.clientSettings) {
|
||||
if (this.clientSettings.subId) {
|
||||
this.subLink = this.genSubLink(this.clientSettings.subId);
|
||||
}
|
||||
});
|
||||
if (this.clientSettings.tgId) {
|
||||
this.tgLink = "https://t.me/" + this.clientSettings.tgId;
|
||||
}
|
||||
}
|
||||
this.visible = true;
|
||||
},
|
||||
close() {
|
||||
infoModal.visible = false;
|
||||
},
|
||||
genSubLink(subID) {
|
||||
const { domain: host, port, tls: isTLS, path: base } = app.subSettings;
|
||||
return buildURL({ host, port, isTLS, base, path: subID });
|
||||
}
|
||||
};
|
||||
|
||||
const infoModalApp = new Vue({
|
||||
@@ -211,15 +255,21 @@
|
||||
get inbound() {
|
||||
return this.infoModal.inbound;
|
||||
},
|
||||
get isEnable() {
|
||||
get isActive() {
|
||||
if(infoModal.clientStats){
|
||||
return infoModal.clientStats.enable;
|
||||
}
|
||||
return infoModal.dbInbound.isEnable;
|
||||
}
|
||||
},
|
||||
get isEnable() {
|
||||
if(infoModal.clientSettings){
|
||||
return infoModal.clientSettings.enable;
|
||||
}
|
||||
return infoModal.dbInbound.isEnable;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
copyTextToClipboard(elmentId,content) {
|
||||
copyToClipboard(elmentId,content) {
|
||||
this.infoModal.clipboard = new ClipboardJS('#' + elmentId, {
|
||||
text: () => content,
|
||||
});
|
||||
@@ -229,10 +279,7 @@
|
||||
});
|
||||
},
|
||||
statsColor(stats) {
|
||||
if(!stats) return 'blue'
|
||||
if(stats['total'] === 0) return 'blue'
|
||||
else if(stats['total'] > 0 && (stats['down']+stats['up']) < stats['total']) return 'cyan'
|
||||
else return 'red'
|
||||
return usageColor(stats.up + stats.down, app.trafficDiff, stats.total);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{{define "inboundModal"}}
|
||||
<a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title" @ok="inModal.ok"
|
||||
:confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false"
|
||||
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||
:class="themeSwitcher.darkCardClass"
|
||||
:ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
|
||||
{{template "form/inbound"}}
|
||||
</a-modal>
|
||||
@@ -43,6 +43,15 @@
|
||||
loading(loading) {
|
||||
inModal.confirmLoading = loading;
|
||||
},
|
||||
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;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const protocols = {
|
||||
@@ -62,6 +71,7 @@
|
||||
inModal: inModal,
|
||||
Protocols: protocols,
|
||||
SSMethods: SSMethods,
|
||||
delayedStart: false,
|
||||
get inbound() {
|
||||
return inModal.inbound;
|
||||
},
|
||||
@@ -70,31 +80,51 @@
|
||||
},
|
||||
get isEdit() {
|
||||
return inModal.isEdit;
|
||||
},
|
||||
get client() {
|
||||
return inModal.getClients(this.inbound.protocol, this.inbound.settings)[0];
|
||||
},
|
||||
get delayedExpireDays() {
|
||||
return this.client && this.client.expiryTime < 0 ? this.client.expiryTime / -86400000 : 0;
|
||||
},
|
||||
set delayedExpireDays(days){
|
||||
this.client.expiryTime = -86400000 * days;
|
||||
},
|
||||
get multiDomain() {
|
||||
return this.inbound.stream.tls.settings.domains.length > 0;
|
||||
},
|
||||
set multiDomain(value) {
|
||||
if (value) {
|
||||
inModal.inbound.stream.tls.server = "";
|
||||
inModal.inbound.stream.tls.settings.domains = [{ remark: "", domain: window.location.hostname }];
|
||||
} else {
|
||||
inModal.inbound.stream.tls.server = "";
|
||||
inModal.inbound.stream.tls.settings.domains = [];
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
streamNetworkChange(oldValue) {
|
||||
if (oldValue === 'kcp') {
|
||||
this.inModal.inbound.tls = false;
|
||||
streamNetworkChange() {
|
||||
if (!inModal.inbound.canSetTls()) {
|
||||
this.inModal.inbound.stream.security = 'none';
|
||||
}
|
||||
if (!inModal.inbound.canEnableReality()) {
|
||||
this.inModal.inbound.reality = false;
|
||||
}
|
||||
},
|
||||
addClient(protocol, clients) {
|
||||
switch (protocol) {
|
||||
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());
|
||||
default: return null;
|
||||
setDefaultCertData(index){
|
||||
inModal.inbound.stream.tls.certs[index].certFile = app.defaultCert;
|
||||
inModal.inbound.stream.tls.certs[index].keyFile = app.defaultKey;
|
||||
},
|
||||
async getNewX25519Cert(){
|
||||
inModal.loading(true);
|
||||
const msg = await HttpUtil.post('/server/getNewX25519Cert');
|
||||
inModal.loading(false);
|
||||
if (!msg.success) {
|
||||
return;
|
||||
}
|
||||
},
|
||||
removeClient(index, clients) {
|
||||
clients.splice(index, 1);
|
||||
},
|
||||
isExpiry(index) {
|
||||
return this.inbound.isExpiry(index)
|
||||
},
|
||||
isClientEnable(email) {
|
||||
clientStats = this.dbInbound.clientStats ? this.dbInbound.clientStats.find(stats => stats.email === email) : null
|
||||
return clientStats ? clientStats['enable'] : true
|
||||
inModal.inbound.stream.reality.privateKey = msg.obj.privateKey;
|
||||
inModal.inbound.stream.reality.settings.publicKey = msg.obj.publicKey;
|
||||
},
|
||||
getNewEmail(client) {
|
||||
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
|
||||
|
||||
@@ -12,10 +12,11 @@
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<body>
|
||||
<a-layout id="app" v-cloak>
|
||||
{{ template "commonSider" . }}
|
||||
<a-layout id="content-layout" :style="siderDrawer.isDarkTheme ? bgDarkStyle : ''">
|
||||
<a-layout id="content-layout" :style="themeSwitcher.bgStyle">
|
||||
<a-layout-content>
|
||||
<a-spin :spinning="spinning" :delay="500" tip="loading">
|
||||
<transition name="list" appear>
|
||||
@@ -24,7 +25,7 @@
|
||||
</a-tag>
|
||||
</transition>
|
||||
<transition name="list" appear>
|
||||
<a-card hoverable style="margin-bottom: 20px;" :class="siderDrawer.isDarkTheme ? darkClass : ''">
|
||||
<a-card hoverable style="margin-bottom: 20px;" :class="themeSwitcher.darkCardClass">
|
||||
<a-row>
|
||||
<a-col :xs="24" :sm="24" :lg="12">
|
||||
{{ i18n "pages.inbounds.totalDownUp" }}:
|
||||
@@ -41,19 +42,79 @@
|
||||
<a-col :xs="24" :sm="24" :lg="12">
|
||||
{{ i18n "clients" }}:
|
||||
<a-tag color="green">[[ total.clients ]]</a-tag>
|
||||
<a-tag color="blue">{{ i18n "enabled" }} [[ total.active ]]</a-tag>
|
||||
<a-tag color="red">{{ i18n "disabled" }} [[ total.deactive ]]</a-tag>
|
||||
<a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.darkClass">
|
||||
<template slot="content">
|
||||
<p v-for="clientEmail in total.deactive">[[ clientEmail ]]</p>
|
||||
</template>
|
||||
<a-tag v-if="total.deactive.length">[[ total.deactive.length ]]</a-tag>
|
||||
</a-popover>
|
||||
<a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.darkClass">
|
||||
<template slot="content">
|
||||
<p v-for="clientEmail in total.depleted">[[ clientEmail ]]</p>
|
||||
</template>
|
||||
<a-tag color="red" v-if="total.depleted.length">[[ total.depleted.length ]]</a-tag>
|
||||
</a-popover>
|
||||
<a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.darkClass">
|
||||
<template slot="content">
|
||||
<p v-for="clientEmail in total.expiring">[[ clientEmail ]]</p>
|
||||
</template>
|
||||
<a-tag color="orange" v-if="total.expiring.length">[[ total.expiring.length ]]</a-tag>
|
||||
</a-popover>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</transition>
|
||||
<transition name="list" appear>
|
||||
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
|
||||
<a-card hoverable :class="themeSwitcher.darkCardClass">
|
||||
<div slot="title">
|
||||
<a-button type="primary" icon="plus" @click="openAddInbound">{{ i18n "pages.inbounds.addInbound" }}</a-button>
|
||||
<a-button type="primary" icon="export" @click="exportAllLinks">{{ i18n "pages.inbounds.export" }}</a-button>
|
||||
<a-row>
|
||||
<a-col :xs="24" :sm="24" :lg="12">
|
||||
<a-button type="primary" icon="plus" @click="openAddInbound">{{ i18n "pages.inbounds.addInbound" }}</a-button>
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-button type="primary" icon="menu">{{ i18n "pages.inbounds.generalActions" }}</a-button>
|
||||
<a-menu slot="overlay" @click="a => generalActions(a)" :theme="themeSwitcher.currentTheme">
|
||||
<a-menu-item key="export">
|
||||
<a-icon type="export"></a-icon>
|
||||
{{ i18n "pages.inbounds.export" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="resetInbounds">
|
||||
<a-icon type="reload"></a-icon>
|
||||
{{ i18n "pages.inbounds.resetAllTraffic" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="resetClients">
|
||||
<a-icon type="file-done"></a-icon>
|
||||
{{ i18n "pages.inbounds.resetAllClientTraffics" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="delDepletedClients">
|
||||
<a-icon type="rest"></a-icon>
|
||||
{{ i18n "pages.inbounds.delDepletedClients" }}
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-dropdown>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="24" :lg="12" style="text-align: right;">
|
||||
<a-select v-model="refreshInterval"
|
||||
v-if="isRefreshEnabled"
|
||||
@change="changeRefreshInterval"
|
||||
:dropdown-class-name="themeSwitcher.darkCardClass">
|
||||
<a-select-option v-for="key in [5,10,30,60]" :value="key*1000">[[ key ]]s</a-select-option>
|
||||
</a-select>
|
||||
<a-icon type="sync" :spin="refreshing" @click="manualRefresh" style="margin: 0 5px;"></a-icon>
|
||||
<a-switch v-model="isRefreshEnabled" @change="toggleRefresh"></a-switch>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
<a-input v-model.lazy="searchKey" placeholder="{{ i18n "search" }}" autofocus style="max-width: 300px"></a-input>
|
||||
<a-switch v-model="enableFilter"
|
||||
checked-children='{{ i18n "search" }}' un-checked-children='{{ i18n "filter" }}'
|
||||
@change="toggleFilter" style="margin-right: 10px;">
|
||||
</a-switch>
|
||||
<a-input v-if="!enableFilter" v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus style="max-width: 300px"></a-input>
|
||||
<a-radio-group v-if="enableFilter" v-model="filterBy" @change="filterInbounds" button-style="solid">
|
||||
<a-radio-button value="">{{ i18n "none" }}</a-radio-button>
|
||||
<a-radio-button value="deactive">{{ i18n "disabled" }}</a-radio-button>
|
||||
<a-radio-button value="depleted">{{ i18n "depleted" }}</a-radio-button>
|
||||
<a-radio-button value="expiring">{{ i18n "depletingSoon" }}</a-radio-button>
|
||||
</a-radio-group>
|
||||
<a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
|
||||
:data-source="searchedInbounds"
|
||||
:loading="spinning" :scroll="{ x: 1300 }"
|
||||
@@ -63,28 +124,32 @@
|
||||
<template slot="action" slot-scope="text, dbInbound">
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-icon @click="e => e.preventDefault()" type="menu"></a-icon>
|
||||
<a-menu slot="overlay" @click="a => clickAction(a, dbInbound)" :theme="siderDrawer.theme" style="border: 1px solid rgba(255, 255, 255, 0.65);">
|
||||
<a-menu-item v-if="dbInbound.isSS" key="qrcode">
|
||||
<a-icon type="qrcode"></a-icon>
|
||||
{{ i18n "qrCode" }}
|
||||
</a-menu-item>
|
||||
<a-menu slot="overlay" @click="a => clickAction(a, dbInbound)" :theme="themeSwitcher.currentTheme">
|
||||
<a-menu-item key="edit">
|
||||
<a-icon type="edit"></a-icon>
|
||||
{{ i18n "edit" }}
|
||||
</a-menu-item>
|
||||
<template v-if="dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess">
|
||||
<template v-if="dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess || dbInbound.isSS">
|
||||
<a-menu-item key="addClient">
|
||||
<a-icon type="user"></a-icon>
|
||||
<a-icon type="user-add"></a-icon>
|
||||
{{ i18n "pages.client.add"}}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="addBulkClient">
|
||||
<a-icon type="team"></a-icon>
|
||||
<a-icon type="usergroup-add"></a-icon>
|
||||
{{ i18n "pages.client.bulk"}}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="resetClients">
|
||||
<a-icon type="file-done"></a-icon>
|
||||
{{ i18n "pages.inbounds.resetInboundClientTraffics"}}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="export">
|
||||
<a-icon type="export"></a-icon>
|
||||
{{ i18n "pages.inbounds.export"}}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="delDepletedClients">
|
||||
<a-icon type="rest"></a-icon>
|
||||
{{ i18n "pages.inbounds.delDepletedClients" }}
|
||||
</a-menu-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-menu-item key="showInfo">
|
||||
@@ -95,6 +160,9 @@
|
||||
<a-menu-item key="resetTraffic">
|
||||
<a-icon type="retweet"></a-icon> {{ i18n "pages.inbounds.resetTraffic" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="clone">
|
||||
<a-icon type="block"></a-icon> {{ i18n "pages.inbounds.clone"}}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="delete">
|
||||
<span style="color: #FF4D4F">
|
||||
<a-icon type="delete"></a-icon> {{ i18n "delete"}}
|
||||
@@ -104,7 +172,35 @@
|
||||
</a-dropdown>
|
||||
</template>
|
||||
<template slot="protocol" slot-scope="text, dbInbound">
|
||||
<a-tag color="blue">[[ dbInbound.protocol ]]</a-tag>
|
||||
<a-tag style="margin:0;" color="blue">[[ dbInbound.protocol ]]</a-tag>
|
||||
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
|
||||
<a-tag style="margin:0;" color="green">[[ dbInbound.toInbound().stream.network ]]</a-tag>
|
||||
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isTls" color="cyan">tls</a-tag>
|
||||
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isReality" color="cyan">reality</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
<template slot="clients" slot-scope="text, dbInbound">
|
||||
<template v-if="clientCount[dbInbound.id]">
|
||||
<a-tag style="margin:0;" color="green">[[ clientCount[dbInbound.id].clients ]]</a-tag>
|
||||
<a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.darkClass">
|
||||
<template slot="content">
|
||||
<p v-for="clientEmail in clientCount[dbInbound.id].deactive">[[ clientEmail ]]</p>
|
||||
</template>
|
||||
<a-tag style="margin:0; padding: 0 2px;" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag>
|
||||
</a-popover>
|
||||
<a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.darkClass">
|
||||
<template slot="content">
|
||||
<p v-for="clientEmail in clientCount[dbInbound.id].depleted">[[ clientEmail ]]</p>
|
||||
</template>
|
||||
<a-tag style="margin:0; padding: 0 2px;" color="red" v-if="clientCount[dbInbound.id].depleted.length">[[ clientCount[dbInbound.id].depleted.length ]]</a-tag>
|
||||
</a-popover>
|
||||
<a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.darkClass">
|
||||
<template slot="content">
|
||||
<p v-for="clientEmail in clientCount[dbInbound.id].expiring">[[ clientEmail ]]</p>
|
||||
</template>
|
||||
<a-tag style="margin:0; padding: 0 2px;" color="orange" v-if="clientCount[dbInbound.id].expiring.length">[[ clientCount[dbInbound.id].expiring.length ]]</a-tag>
|
||||
</a-popover>
|
||||
</template>
|
||||
</template>
|
||||
<template slot="traffic" slot-scope="text, dbInbound">
|
||||
<a-tag color="blue">[[ sizeFormat(dbInbound.up) ]] / [[ sizeFormat(dbInbound.down) ]]</a-tag>
|
||||
@@ -114,14 +210,6 @@
|
||||
</template>
|
||||
<a-tag v-else color="green">{{ i18n "unlimited" }}</a-tag>
|
||||
</template>
|
||||
<template slot="stream" slot-scope="text, dbInbound, index">
|
||||
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
|
||||
<a-tag color="green">[[ inbounds[index].stream.network ]]</a-tag>
|
||||
<a-tag v-if="inbounds[index].stream.isTls" color="blue">tls</a-tag>
|
||||
<a-tag v-if="inbounds[index].stream.isXTls" color="blue">xtls</a-tag>
|
||||
</template>
|
||||
<template v-else>{{ i18n "none" }}</template>
|
||||
</template>
|
||||
<template slot="enable" slot-scope="text, dbInbound">
|
||||
<a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound.id)"></a-switch>
|
||||
</template>
|
||||
@@ -138,20 +226,20 @@
|
||||
</template>
|
||||
<template slot="expandedRowRender" slot-scope="record">
|
||||
<a-table
|
||||
v-if="(record.protocol === Protocols.VLESS) || (record.protocol === Protocols.VMESS)"
|
||||
:row-key="client => client.id"
|
||||
:columns="innerColumns"
|
||||
:data-source="getInboundClients(record)"
|
||||
:pagination="false"
|
||||
v-if="(record.protocol === Protocols.VLESS) || (record.protocol === Protocols.VMESS)"
|
||||
:row-key="client => client.id"
|
||||
:columns="innerColumns"
|
||||
:data-source="getInboundClients(record)"
|
||||
:pagination="false"
|
||||
>
|
||||
{{template "client_table"}}
|
||||
</a-table>
|
||||
<a-table
|
||||
v-else-if="record.protocol === Protocols.TROJAN"
|
||||
:row-key="client => client.id"
|
||||
:columns="innerTrojanColumns"
|
||||
:data-source="getInboundClients(record)"
|
||||
:pagination="false"
|
||||
v-else-if="record.protocol === Protocols.TROJAN || record.protocol === Protocols.SHADOWSOCKS"
|
||||
:row-key="client => client.id"
|
||||
:columns="innerTrojanColumns"
|
||||
:data-source="getInboundClients(record)"
|
||||
:pagination="false"
|
||||
>
|
||||
{{template "client_table"}}
|
||||
</a-table>
|
||||
@@ -164,12 +252,12 @@
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
{{template "js" .}}
|
||||
{{template "component/themeSwitcher" .}}
|
||||
<script>
|
||||
|
||||
const columns = [{
|
||||
title: '{{ i18n "pages.inbounds.operate" }}',
|
||||
align: 'center',
|
||||
width: 30,
|
||||
width: 40,
|
||||
scopedSlots: { customRender: 'action' },
|
||||
}, {
|
||||
title: '{{ i18n "pages.inbounds.enable" }}',
|
||||
@@ -177,35 +265,35 @@
|
||||
width: 40,
|
||||
scopedSlots: { customRender: 'enable' },
|
||||
}, {
|
||||
title: "Id",
|
||||
title: "ID",
|
||||
align: 'center',
|
||||
dataIndex: "id",
|
||||
width: 30,
|
||||
}, {
|
||||
title: '{{ i18n "pages.inbounds.remark" }}',
|
||||
align: 'center',
|
||||
width: 60,
|
||||
width: 50,
|
||||
dataIndex: "remark",
|
||||
}, {
|
||||
title: '{{ i18n "pages.inbounds.protocol" }}',
|
||||
align: 'center',
|
||||
width: 40,
|
||||
scopedSlots: { customRender: 'protocol' },
|
||||
}, {
|
||||
title: '{{ i18n "pages.inbounds.port" }}',
|
||||
align: 'center',
|
||||
dataIndex: "port",
|
||||
width: 40,
|
||||
}, {
|
||||
title: '{{ i18n "pages.inbounds.protocol" }}',
|
||||
align: 'left',
|
||||
width: 70,
|
||||
scopedSlots: { customRender: 'protocol' },
|
||||
}, {
|
||||
title: '{{ i18n "clients" }}',
|
||||
align: 'left',
|
||||
width: 50,
|
||||
scopedSlots: { customRender: 'clients' },
|
||||
}, {
|
||||
title: '{{ i18n "pages.inbounds.traffic" }}↑|↓',
|
||||
align: 'center',
|
||||
width: 120,
|
||||
scopedSlots: { customRender: 'traffic' },
|
||||
}, {
|
||||
title: '{{ i18n "pages.inbounds.transportConfig" }}',
|
||||
align: 'center',
|
||||
width: 60,
|
||||
scopedSlots: { customRender: 'stream' },
|
||||
}, {
|
||||
title: '{{ i18n "pages.inbounds.expireDate" }}',
|
||||
align: 'center',
|
||||
@@ -214,15 +302,17 @@
|
||||
}];
|
||||
|
||||
const innerColumns = [
|
||||
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 80, scopedSlots: { customRender: 'actions' } },
|
||||
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } },
|
||||
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 30, scopedSlots: { customRender: 'enable' } },
|
||||
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
|
||||
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 70, scopedSlots: { customRender: 'traffic' } },
|
||||
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
|
||||
{ title: 'UID', width: 120, dataIndex: "id" },
|
||||
{ title: 'UUID', width: 120, dataIndex: "id" },
|
||||
];
|
||||
|
||||
const innerTrojanColumns = [
|
||||
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 80, scopedSlots: { customRender: 'actions' } },
|
||||
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } },
|
||||
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 30, scopedSlots: { customRender: 'enable' } },
|
||||
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
|
||||
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 70, scopedSlots: { customRender: 'traffic' } },
|
||||
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
|
||||
@@ -234,36 +324,117 @@
|
||||
el: '#app',
|
||||
data: {
|
||||
siderDrawer,
|
||||
themeSwitcher,
|
||||
spinning: false,
|
||||
inbounds: [],
|
||||
dbInbounds: [],
|
||||
searchKey: '',
|
||||
enableFilter: false,
|
||||
filterBy: '',
|
||||
searchedInbounds: [],
|
||||
expireDiff: 0,
|
||||
trafficDiff: 0,
|
||||
defaultCert: '',
|
||||
defaultKey: '',
|
||||
clientCount: {},
|
||||
isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false,
|
||||
refreshing: false,
|
||||
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
|
||||
subSettings: {
|
||||
enable : false,
|
||||
port: 0,
|
||||
path: '',
|
||||
domain: '',
|
||||
tls: false
|
||||
},
|
||||
tgBotEnable: false
|
||||
},
|
||||
methods: {
|
||||
loading(spinning=true) {
|
||||
loading(spinning = true) {
|
||||
this.spinning = spinning;
|
||||
},
|
||||
async getDBInbounds() {
|
||||
this.loading();
|
||||
this.refreshing = true;
|
||||
const msg = await HttpUtil.post('/xui/inbound/list');
|
||||
this.loading(false);
|
||||
if (!msg.success) {
|
||||
this.refreshing = false;
|
||||
return;
|
||||
}
|
||||
this.setInbounds(msg.obj);
|
||||
this.searchKey = '';
|
||||
setTimeout(() => {
|
||||
this.refreshing = false;
|
||||
}, 500);
|
||||
},
|
||||
async getDefaultSettings() {
|
||||
const msg = await HttpUtil.post('/xui/setting/defaultSettings');
|
||||
if (!msg.success) {
|
||||
return;
|
||||
}
|
||||
with(msg.obj){
|
||||
this.expireDiff = expireDiff * 86400000;
|
||||
this.trafficDiff = trafficDiff * 1073741824;
|
||||
this.defaultCert = defaultCert;
|
||||
this.defaultKey = defaultKey;
|
||||
this.tgBotEnable = tgBotEnable;
|
||||
this.subSettings = {
|
||||
enable : subEnable,
|
||||
port: subPort,
|
||||
path: subPath,
|
||||
domain: subDomain,
|
||||
tls: subTLS
|
||||
};
|
||||
}
|
||||
},
|
||||
setInbounds(dbInbounds) {
|
||||
this.inbounds.splice(0);
|
||||
this.dbInbounds.splice(0);
|
||||
this.searchedInbounds.splice(0);
|
||||
for (const inbound of dbInbounds) {
|
||||
const dbInbound = new DBInbound(inbound);
|
||||
this.inbounds.push(dbInbound.toInbound());
|
||||
to_inbound = dbInbound.toInbound()
|
||||
this.inbounds.push(to_inbound);
|
||||
this.dbInbounds.push(dbInbound);
|
||||
this.searchedInbounds.push(dbInbound);
|
||||
if ([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(inbound.protocol)) {
|
||||
this.clientCount[inbound.id] = this.getClientCounts(inbound, to_inbound);
|
||||
}
|
||||
}
|
||||
if(this.enableFilter){
|
||||
this.filterInbounds();
|
||||
} else {
|
||||
this.searchInbounds(this.searchKey);
|
||||
}
|
||||
},
|
||||
getClientCounts(dbInbound, inbound) {
|
||||
let clientCount = 0, active = [], deactive = [], depleted = [], expiring = [];
|
||||
clients = this.getClients(dbInbound.protocol, inbound.settings);
|
||||
clientStats = dbInbound.clientStats
|
||||
now = new Date().getTime()
|
||||
if (clients) {
|
||||
clientCount = clients.length;
|
||||
if (dbInbound.enable) {
|
||||
clients.forEach(client => {
|
||||
client.enable ? active.push(client.email) : deactive.push(client.email);
|
||||
});
|
||||
clientStats.forEach(client => {
|
||||
if (!client.enable) {
|
||||
depleted.push(client.email);
|
||||
} else {
|
||||
if ((client.expiryTime > 0 && (client.expiryTime - now < this.expireDiff)) ||
|
||||
(client.total > 0 && (client.total - (client.up + client.down) < this.trafficDiff))) expiring.push(client.email);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
clients.forEach(client => {
|
||||
deactive.push(client.email);
|
||||
});
|
||||
}
|
||||
}
|
||||
return {
|
||||
clients: clientCount,
|
||||
active: active,
|
||||
deactive: deactive,
|
||||
depleted: depleted,
|
||||
expiring: expiring,
|
||||
};
|
||||
},
|
||||
searchInbounds(key) {
|
||||
if (ObjectUtil.isEmpty(key)) {
|
||||
@@ -274,10 +445,10 @@
|
||||
if (ObjectUtil.deepSearch(inbound, key)) {
|
||||
const newInbound = new DBInbound(inbound);
|
||||
const inboundSettings = JSON.parse(inbound.settings);
|
||||
if (inboundSettings.hasOwnProperty('clients')){
|
||||
if (inboundSettings.hasOwnProperty('clients')) {
|
||||
const searchedSettings = { "clients": [] };
|
||||
inboundSettings.clients.forEach(client => {
|
||||
if (ObjectUtil.deepSearch(client, key)){
|
||||
if (ObjectUtil.deepSearch(client, key)) {
|
||||
searchedSettings.clients.push(client);
|
||||
}
|
||||
});
|
||||
@@ -288,6 +459,54 @@
|
||||
});
|
||||
}
|
||||
},
|
||||
filterInbounds() {
|
||||
if (ObjectUtil.isEmpty(this.filterBy)) {
|
||||
this.searchedInbounds = this.dbInbounds.slice();
|
||||
} else {
|
||||
this.searchedInbounds.splice(0, this.searchedInbounds.length);
|
||||
this.dbInbounds.forEach(inbound => {
|
||||
const newInbound = new DBInbound(inbound);
|
||||
const inboundSettings = JSON.parse(inbound.settings);
|
||||
if (this.clientCount[inbound.id] && this.clientCount[inbound.id].hasOwnProperty(this.filterBy)){
|
||||
const list = this.clientCount[inbound.id][this.filterBy];
|
||||
if (list.length > 0) {
|
||||
const filteredSettings = { "clients": [] };
|
||||
inboundSettings.clients.forEach(client => {
|
||||
if (list.includes(client.email)) {
|
||||
filteredSettings.clients.push(client);
|
||||
}
|
||||
});
|
||||
newInbound.settings = Inbound.Settings.fromJson(inbound.protocol, filteredSettings);
|
||||
this.searchedInbounds.push(newInbound);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
toggleFilter(){
|
||||
if(this.enableFilter) {
|
||||
this.searchKey = '';
|
||||
} else {
|
||||
this.filterBy = '';
|
||||
this.searchedInbounds = this.dbInbounds.slice();
|
||||
}
|
||||
},
|
||||
generalActions(action) {
|
||||
switch (action.key) {
|
||||
case "export":
|
||||
this.exportAllLinks();
|
||||
break;
|
||||
case "resetInbounds":
|
||||
this.resetAllTraffic();
|
||||
break;
|
||||
case "resetClients":
|
||||
this.resetAllClientTraffics(-1);
|
||||
break;
|
||||
case "delDepletedClients":
|
||||
this.delDepletedClients(-1)
|
||||
break;
|
||||
}
|
||||
},
|
||||
clickAction(action, dbInbound) {
|
||||
switch (action.key) {
|
||||
case "qrcode":
|
||||
@@ -311,15 +530,24 @@
|
||||
case "resetTraffic":
|
||||
this.resetTraffic(dbInbound.id);
|
||||
break;
|
||||
case "resetClients":
|
||||
this.resetAllClientTraffics(dbInbound.id);
|
||||
break;
|
||||
case "clone":
|
||||
this.openCloneInbound(dbInbound);
|
||||
break;
|
||||
case "delete":
|
||||
this.delInbound(dbInbound.id);
|
||||
break;
|
||||
case "delDepletedClients":
|
||||
this.delDepletedClients(dbInbound.id)
|
||||
break;
|
||||
}
|
||||
},
|
||||
openAddInbound() {
|
||||
inModal.show({
|
||||
title: '{{ i18n "pages.inbounds.addInbound"}}',
|
||||
okText: '{{ i18n "pages.inbounds.addTo"}}',
|
||||
okText: '{{ i18n "pages.inbounds.create"}}',
|
||||
cancelText: '{{ i18n "close" }}',
|
||||
confirm: async (inbound, dbInbound) => {
|
||||
inModal.loading();
|
||||
@@ -334,7 +562,7 @@
|
||||
const inbound = dbInbound.toInbound();
|
||||
inModal.show({
|
||||
title: '{{ i18n "pages.inbounds.modifyInbound"}}',
|
||||
okText: '{{ i18n "pages.inbounds.revise"}}',
|
||||
okText: '{{ i18n "pages.inbounds.update"}}',
|
||||
cancelText: '{{ i18n "close" }}',
|
||||
inbound: inbound,
|
||||
dbInbound: dbInbound,
|
||||
@@ -345,6 +573,38 @@
|
||||
},
|
||||
isEdit: true
|
||||
});
|
||||
},
|
||||
openCloneInbound(dbInbound) {
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.inbounds.cloneInbound"}} \"' + dbInbound.remark + '\"',
|
||||
content: '{{ i18n "pages.inbounds.cloneInboundContent"}}',
|
||||
okText: '{{ i18n "pages.inbounds.update"}}',
|
||||
cancelText: '{{ i18n "cancel" }}',
|
||||
onOk: () => {
|
||||
const baseInbound = dbInbound.toInbound();
|
||||
dbInbound.up = 0;
|
||||
dbInbound.down = 0;
|
||||
this.cloneInbound(baseInbound, dbInbound);
|
||||
},
|
||||
});
|
||||
},
|
||||
async cloneInbound(baseInbound, dbInbound) {
|
||||
const data = {
|
||||
up: dbInbound.up,
|
||||
down: dbInbound.down,
|
||||
total: dbInbound.total,
|
||||
remark: dbInbound.remark + " - Cloned",
|
||||
enable: dbInbound.enable,
|
||||
expiryTime: dbInbound.expiryTime,
|
||||
|
||||
listen: '',
|
||||
port: RandomUtil.randomIntRange(10000, 60000),
|
||||
protocol: baseInbound.protocol,
|
||||
settings: Inbound.Settings.getSettings(baseInbound.protocol).toString(),
|
||||
streamSettings: baseInbound.stream.toString(),
|
||||
sniffing: baseInbound.canSniffing() ? baseInbound.sniffing.toString() : '{}',
|
||||
};
|
||||
await this.submit('/xui/inbound/add', data, inModal);
|
||||
},
|
||||
async addInbound(inbound, dbInbound) {
|
||||
const data = {
|
||||
@@ -390,9 +650,9 @@
|
||||
title: '{{ i18n "pages.client.add"}}',
|
||||
okText: '{{ i18n "pages.client.submitAdd"}}',
|
||||
dbInbound: dbInbound,
|
||||
confirm: async (inbound, dbInbound, index) => {
|
||||
confirm: async (clients, dbInboundId) => {
|
||||
clientModal.loading();
|
||||
await this.addClient(inbound, dbInbound);
|
||||
await this.addClient(clients, dbInboundId);
|
||||
clientModal.close();
|
||||
},
|
||||
isEdit: false
|
||||
@@ -404,9 +664,9 @@
|
||||
title: '{{ i18n "pages.client.bulk"}} ' + dbInbound.remark,
|
||||
okText: '{{ i18n "pages.client.bulk"}}',
|
||||
dbInbound: dbInbound,
|
||||
confirm: async (inbound, dbInbound) => {
|
||||
confirm: async (clients, dbInboundId) => {
|
||||
clientsBulkModal.loading();
|
||||
await this.addClient(inbound, dbInbound);
|
||||
await this.addClient(clients, dbInboundId);
|
||||
clientsBulkModal.close();
|
||||
},
|
||||
});
|
||||
@@ -420,38 +680,38 @@
|
||||
okText: '{{ i18n "pages.client.submitEdit"}}',
|
||||
dbInbound: dbInbound,
|
||||
index: index,
|
||||
confirm: async (inbound, dbInbound, index) => {
|
||||
confirm: async (client, dbInboundId, clientId) => {
|
||||
clientModal.loading();
|
||||
await this.updateClient(inbound, dbInbound, index);
|
||||
await this.updateClient(client, dbInboundId, clientId);
|
||||
clientModal.close();
|
||||
},
|
||||
isEdit: true
|
||||
});
|
||||
},
|
||||
findIndexOfClient(clients,client) {
|
||||
findIndexOfClient(clients, client) {
|
||||
firstKey = Object.keys(client)[0];
|
||||
return clients.findIndex(c => c[firstKey] === client[firstKey]);
|
||||
},
|
||||
async addClient(inbound, dbInbound) {
|
||||
async addClient(clients, dbInboundId) {
|
||||
const data = {
|
||||
id: dbInbound.id,
|
||||
settings: inbound.settings.toString(),
|
||||
id: dbInboundId,
|
||||
settings: '{"clients": [' + clients.toString() + ']}',
|
||||
};
|
||||
await this.submit('/xui/inbound/addClient', data);
|
||||
await this.submit(`/xui/inbound/addClient`, data);
|
||||
},
|
||||
async updateClient(inbound, dbInbound, index) {
|
||||
async updateClient(client, dbInboundId, clientId) {
|
||||
const data = {
|
||||
id: dbInbound.id,
|
||||
settings: inbound.settings.toString(),
|
||||
id: dbInboundId,
|
||||
settings: '{"clients": [' + client.toString() + ']}',
|
||||
};
|
||||
await this.submit(`/xui/inbound/updateClient/${index}`, data);
|
||||
await this.submit(`/xui/inbound/updateClient/${clientId}`, data);
|
||||
},
|
||||
resetTraffic(dbInboundId) {
|
||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
|
||||
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
|
||||
class: siderDrawer.isDarkTheme ? darkClass : '',
|
||||
class: themeSwitcher.darkCardClass,
|
||||
okText: '{{ i18n "reset"}}',
|
||||
cancelText: '{{ i18n "cancel"}}',
|
||||
onOk: () => {
|
||||
@@ -466,51 +726,82 @@
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.inbounds.deleteInbound"}}',
|
||||
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
|
||||
class: siderDrawer.isDarkTheme ? darkClass : '',
|
||||
class: themeSwitcher.darkCardClass,
|
||||
okText: '{{ i18n "delete"}}',
|
||||
cancelText: '{{ i18n "cancel"}}',
|
||||
onOk: () => this.submit('/xui/inbound/del/' + dbInboundId),
|
||||
});
|
||||
},
|
||||
delClient(dbInboundId,client) {
|
||||
delClient(dbInboundId, client) {
|
||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
newDbInbound = new DBInbound(dbInbound);
|
||||
inbound = newDbInbound.toInbound();
|
||||
clients = this.getClients(dbInbound.protocol, inbound.settings);
|
||||
index = this.findIndexOfClient(clients, client);
|
||||
clients.splice(index, 1);
|
||||
const data = {
|
||||
id: dbInboundId,
|
||||
settings: inbound.settings.toString(),
|
||||
};
|
||||
clientId = this.getClientId(dbInbound.protocol, client);
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.inbounds.deleteInbound"}}',
|
||||
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
|
||||
class: siderDrawer.isDarkTheme ? darkClass : '',
|
||||
class: themeSwitcher.darkCardClass,
|
||||
okText: '{{ i18n "delete"}}',
|
||||
cancelText: '{{ i18n "cancel"}}',
|
||||
onOk: () => this.submit('/xui/inbound/delClient/' + client.email, data),
|
||||
onOk: () => this.submit(`/xui/inbound/${dbInboundId}/delClient/${clientId}`),
|
||||
});
|
||||
},
|
||||
getClients(protocol, clientSettings) {
|
||||
switch(protocol){
|
||||
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;
|
||||
}
|
||||
},
|
||||
getClientId(protocol, client) {
|
||||
switch (protocol) {
|
||||
case Protocols.TROJAN: return client.password;
|
||||
case Protocols.SHADOWSOCKS: return client.email;
|
||||
default: return client.id;
|
||||
}
|
||||
},
|
||||
checkFallback(dbInbound) {
|
||||
newDbInbound = new DBInbound(dbInbound);
|
||||
if (dbInbound.listen.startsWith("@")){
|
||||
rootInbound = this.inbounds.find((i) =>
|
||||
i.tls &&
|
||||
['trojan','vless'].includes(i.protocol) &&
|
||||
i.settings.fallbacks.find(f => f.dest === dbInbound.listen)
|
||||
);
|
||||
if (rootInbound) {
|
||||
newDbInbound.listen = rootInbound.listen;
|
||||
newDbInbound.port = rootInbound.port;
|
||||
newInbound = newDbInbound.toInbound();
|
||||
newInbound.stream.security = 'tls';
|
||||
newInbound.stream.tls = rootInbound.stream.tls;
|
||||
newDbInbound.streamSettings = newInbound.stream.toString();
|
||||
}
|
||||
}
|
||||
return newDbInbound;
|
||||
},
|
||||
showQrcode(dbInbound, clientIndex) {
|
||||
const link = dbInbound.genLink(clientIndex);
|
||||
qrModal.show('{{ i18n "qrCode"}}', link, dbInbound);
|
||||
newDbInbound = this.checkFallback(dbInbound);
|
||||
qrModal.show('{{ i18n "qrCode"}}', newDbInbound, clientIndex);
|
||||
},
|
||||
showInfo(dbInbound, index) {
|
||||
infoModal.show(dbInbound, index);
|
||||
newDbInbound = this.checkFallback(dbInbound);
|
||||
infoModal.show(newDbInbound, index);
|
||||
},
|
||||
switchEnable(dbInboundId) {
|
||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
this.submit(`/xui/inbound/update/${dbInboundId}`, dbInbound);
|
||||
},
|
||||
async switchEnableClient(dbInboundId, client) {
|
||||
this.loading()
|
||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
inbound = dbInbound.toInbound();
|
||||
clients = this.getClients(dbInbound.protocol, inbound.settings);
|
||||
index = this.findIndexOfClient(clients, client);
|
||||
clients[index].enable = !clients[index].enable;
|
||||
clientId = this.getClientId(dbInbound.protocol, clients[index]);
|
||||
await this.updateClient(clients[index], dbInboundId, clientId);
|
||||
this.loading(false);
|
||||
},
|
||||
async submit(url, data) {
|
||||
const msg = await HttpUtil.postWithModal(url, data);
|
||||
if (msg.success) {
|
||||
@@ -518,59 +809,118 @@
|
||||
}
|
||||
},
|
||||
getInboundClients(dbInbound) {
|
||||
if(dbInbound.protocol == Protocols.VLESS) {
|
||||
return dbInbound.toInbound().settings.vlesses
|
||||
} else if(dbInbound.protocol == Protocols.VMESS) {
|
||||
return dbInbound.toInbound().settings.vmesses
|
||||
} else if(dbInbound.protocol == Protocols.TROJAN) {
|
||||
return dbInbound.toInbound().settings.trojans
|
||||
if (dbInbound.protocol == Protocols.VLESS) {
|
||||
return dbInbound.toInbound().settings.vlesses;
|
||||
} else if (dbInbound.protocol == Protocols.VMESS) {
|
||||
return dbInbound.toInbound().settings.vmesses;
|
||||
} else if (dbInbound.protocol == Protocols.TROJAN) {
|
||||
return dbInbound.toInbound().settings.trojans;
|
||||
} else if (dbInbound.protocol == Protocols.SHADOWSOCKS) {
|
||||
return dbInbound.toInbound().settings.shadowsockses;
|
||||
}
|
||||
},
|
||||
resetClientTraffic(client,dbInboundId) {
|
||||
resetClientTraffic(client, dbInboundId) {
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
|
||||
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
|
||||
class: siderDrawer.isDarkTheme ? darkClass : '',
|
||||
class: themeSwitcher.darkCardClass,
|
||||
okText: '{{ i18n "reset"}}',
|
||||
cancelText: '{{ i18n "cancel"}}',
|
||||
onOk: () => this.submit('/xui/inbound/' + dbInboundId + '/resetClientTraffic/'+ client.email),
|
||||
onOk: () => this.submit('/xui/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email),
|
||||
})
|
||||
},
|
||||
resetAllTraffic() {
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.inbounds.resetAllTrafficTitle"}}',
|
||||
content: '{{ i18n "pages.inbounds.resetAllTrafficContent"}}',
|
||||
class: themeSwitcher.darkCardClass,
|
||||
okText: '{{ i18n "reset"}}',
|
||||
cancelText: '{{ i18n "cancel"}}',
|
||||
onOk: () => this.submit('/xui/inbound/resetAllTraffics'),
|
||||
});
|
||||
},
|
||||
resetAllClientTraffics(dbInboundId) {
|
||||
this.$confirm({
|
||||
title: dbInboundId > 0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficTitle"}}' : '{{ i18n "pages.inbounds.resetAllClientTrafficTitle"}}',
|
||||
content: dbInboundId > 0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficContent"}}' : '{{ i18n "pages.inbounds.resetAllClientTrafficContent"}}',
|
||||
class: themeSwitcher.darkCardClass,
|
||||
okText: '{{ i18n "reset"}}',
|
||||
cancelText: '{{ i18n "cancel"}}',
|
||||
onOk: () => this.submit('/xui/inbound/resetAllClientTraffics/' + dbInboundId),
|
||||
})
|
||||
},
|
||||
delDepletedClients(dbInboundId) {
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.inbounds.delDepletedClientsTitle"}}',
|
||||
content: '{{ i18n "pages.inbounds.delDepletedClientsContent"}}',
|
||||
class: themeSwitcher.darkCardClass,
|
||||
okText: '{{ i18n "reset"}}',
|
||||
cancelText: '{{ i18n "cancel"}}',
|
||||
onOk: () => this.submit('/xui/inbound/delDepletedClients/' + dbInboundId),
|
||||
})
|
||||
},
|
||||
isExpiry(dbInbound, index) {
|
||||
return dbInbound.toInbound().isExpiry(index)
|
||||
},
|
||||
getUpStats(dbInbound, email) {
|
||||
if(email.length == 0) return 0
|
||||
if (email.length == 0) return 0
|
||||
clientStats = dbInbound.clientStats.find(stats => stats.email === email)
|
||||
return clientStats ? clientStats.up : 0
|
||||
},
|
||||
getDownStats(dbInbound, email) {
|
||||
if(email.length == 0) return 0
|
||||
if (email.length == 0) return 0
|
||||
clientStats = dbInbound.clientStats.find(stats => stats.email === email)
|
||||
return clientStats ? clientStats.down : 0
|
||||
},
|
||||
isTrafficExhausted(dbInbound, email) {
|
||||
if(email.length == 0) return false
|
||||
clientStats = dbInbound.clientStats.find(stats => stats.email === email)
|
||||
return clientStats ? clientStats.down + clientStats.up > clientStats.total : false
|
||||
statsColor(dbInbound, email) {
|
||||
if (email.length == 0) return 'blue';
|
||||
clientStats = dbInbound.clientStats.find(stats => stats.email === email);
|
||||
return usageColor(clientStats.down + clientStats.up, this.trafficDiff, clientStats.total);
|
||||
},
|
||||
isClientEnabled(dbInbound, email) {
|
||||
clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null
|
||||
return clientStats ? clientStats['enable'] : true
|
||||
},
|
||||
isRemovable(dbInbound_id){
|
||||
isRemovable(dbInbound_id) {
|
||||
return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInbound_id)).length > 1
|
||||
},
|
||||
inboundLinks(dbInboundId) {
|
||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
txtModal.show('{{ i18n "pages.inbounds.export"}}',dbInbound.genInboundLinks,dbInbound.remark);
|
||||
newDbInbound = this.checkFallback(dbInbound);
|
||||
txtModal.show('{{ i18n "pages.inbounds.export"}}', newDbInbound.genInboundLinks, newDbInbound.remark);
|
||||
},
|
||||
exportAllLinks() {
|
||||
let copyText = '';
|
||||
for (const dbInbound of this.dbInbounds) {
|
||||
copyText += dbInbound.genInboundLinks
|
||||
}
|
||||
txtModal.show('{{ i18n "pages.inbounds.export"}}',copyText,'All-Inbounds');
|
||||
txtModal.show('{{ i18n "pages.inbounds.export"}}', copyText, 'All-Inbounds');
|
||||
},
|
||||
async startDataRefreshLoop() {
|
||||
while (this.isRefreshEnabled) {
|
||||
try {
|
||||
await this.getDBInbounds();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
await PromiseUtil.sleep(this.refreshInterval);
|
||||
}
|
||||
},
|
||||
toggleRefresh() {
|
||||
localStorage.setItem("isRefreshEnabled", this.isRefreshEnabled);
|
||||
if (this.isRefreshEnabled) {
|
||||
this.startDataRefreshLoop();
|
||||
}
|
||||
},
|
||||
changeRefreshInterval() {
|
||||
localStorage.setItem("refreshInterval", this.refreshInterval);
|
||||
},
|
||||
async manualRefresh() {
|
||||
if (!this.refreshing) {
|
||||
this.spinning = true;
|
||||
await this.getDBInbounds();
|
||||
this.spinning = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
@@ -579,42 +929,41 @@
|
||||
}, 500)
|
||||
},
|
||||
mounted() {
|
||||
this.getDBInbounds();
|
||||
this.loading();
|
||||
this.getDefaultSettings();
|
||||
if (this.isRefreshEnabled) {
|
||||
this.startDataRefreshLoop();
|
||||
}
|
||||
else {
|
||||
this.getDBInbounds();
|
||||
}
|
||||
this.loading(false);
|
||||
},
|
||||
computed: {
|
||||
total() {
|
||||
let down = 0, up = 0;
|
||||
let clients = 0, active = 0, deactive = 0;
|
||||
let clients = 0, deactive = [], depleted = [], expiring = [];
|
||||
this.dbInbounds.forEach(dbInbound => {
|
||||
down += dbInbound.down;
|
||||
up += dbInbound.up;
|
||||
inbound = dbInbound.toInbound();
|
||||
clients = this.getClients(dbInbound.protocol, inbound.settings);
|
||||
if(clients){
|
||||
if(dbInbound.enable){
|
||||
isClientEnable = false;
|
||||
clients.forEach(client => {
|
||||
isClientEnable = client.email == "" ? true: this.isClientEnabled(dbInbound,client.email);
|
||||
isClientEnable ? active++ : deactive++;
|
||||
});
|
||||
} else {
|
||||
deactive += clients.length;
|
||||
}
|
||||
} else {
|
||||
dbInbound.enable ? active++ : deactive++;
|
||||
if (this.clientCount[dbInbound.id]) {
|
||||
clients += this.clientCount[dbInbound.id].clients;
|
||||
deactive = deactive.concat(this.clientCount[dbInbound.id].deactive);
|
||||
depleted = depleted.concat(this.clientCount[dbInbound.id].depleted);
|
||||
expiring = expiring.concat(this.clientCount[dbInbound.id].expiring);
|
||||
}
|
||||
});
|
||||
return {
|
||||
down: down,
|
||||
up: up,
|
||||
clients: active + deactive,
|
||||
active: active,
|
||||
clients: clients,
|
||||
deactive: deactive,
|
||||
depleted: depleted,
|
||||
expiring: expiring,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
{{template "inboundModal"}}
|
||||
|
||||
@@ -19,26 +19,26 @@
|
||||
<body>
|
||||
<a-layout id="app" v-cloak>
|
||||
{{ template "commonSider" . }}
|
||||
<a-layout id="content-layout" :style="siderDrawer.isDarkTheme ? bgDarkStyle : ''">
|
||||
<a-layout id="content-layout" :style="themeSwitcher.bgStyle">
|
||||
<a-layout-content>
|
||||
<a-spin :spinning="spinning" :delay="200" :tip="loadingTip"/>
|
||||
<transition name="list" appear>
|
||||
<a-row>
|
||||
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
|
||||
<a-card hoverable :class="themeSwitcher.darkCardClass">
|
||||
<a-row>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-row>
|
||||
<a-col :span="12" style="text-align: center">
|
||||
<a-progress type="dashboard" status="normal"
|
||||
:stroke-color="status.cpu.color"
|
||||
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||
:class="themeSwitcher.darkCardClass"
|
||||
:percent="status.cpu.percent"></a-progress>
|
||||
<div>CPU</div>
|
||||
</a-col>
|
||||
<a-col :span="12" style="text-align: center">
|
||||
<a-progress type="dashboard" status="normal"
|
||||
:stroke-color="status.mem.color"
|
||||
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||
:class="themeSwitcher.darkCardClass"
|
||||
:percent="status.mem.percent"></a-progress>
|
||||
<div>
|
||||
{{ i18n "pages.index.memory"}}: [[ sizeFormat(status.mem.current) ]] / [[ sizeFormat(status.mem.total) ]]
|
||||
@@ -51,7 +51,7 @@
|
||||
<a-col :span="12" style="text-align: center">
|
||||
<a-progress type="dashboard" status="normal"
|
||||
:stroke-color="status.swap.color"
|
||||
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||
:class="themeSwitcher.darkCardClass"
|
||||
:percent="status.swap.percent"></a-progress>
|
||||
<div>
|
||||
swap: [[ sizeFormat(status.swap.current) ]] / [[ sizeFormat(status.swap.total) ]]
|
||||
@@ -60,7 +60,7 @@
|
||||
<a-col :span="12" style="text-align: center">
|
||||
<a-progress type="dashboard" status="normal"
|
||||
:stroke-color="status.disk.color"
|
||||
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||
:class="themeSwitcher.darkCardClass"
|
||||
:percent="status.disk.percent"></a-progress>
|
||||
<div>
|
||||
{{ i18n "pages.index.hard"}}: [[ sizeFormat(status.disk.current) ]] / [[ sizeFormat(status.disk.total) ]]
|
||||
@@ -75,23 +75,13 @@
|
||||
<transition name="list" appear>
|
||||
<a-row>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
|
||||
{{ i18n "pages.index.xrayStatus" }}:
|
||||
<a-tag :color="status.xray.color">[[ status.xray.state ]]</a-tag>
|
||||
<a-tooltip v-if="status.xray.state === State.Error">
|
||||
<template slot="title">
|
||||
<p v-for="line in status.xray.errorMsg.split('\n')">[[ line ]]</p>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
<a-tag color="green" style="cursor: pointer;" @click="openSelectV2rayVersion">[[ status.xray.version ]]</a-tag>
|
||||
<a-tag color="blue" style="cursor: pointer;" @click="stopXrayService">{{ i18n "pages.index.stopXray" }}</a-tag>
|
||||
<a-tag color="blue" style="cursor: pointer;" @click="restartXrayService">{{ i18n "pages.index.restartXray" }}</a-tag>
|
||||
<a-tag color="blue" style="cursor: pointer;" @click="openSelectV2rayVersion">{{ i18n "pages.index.xraySwitch" }}</a-tag>
|
||||
<a-card hoverable :class="themeSwitcher.darkCardClass">
|
||||
x-ui: <a href="https://github.com/alireza0/x-ui/releases" target="_blank"><a-tag color="green">{{ .cur_ver }}</a-tag></a>
|
||||
xray: <a-tag color="green" style="cursor: pointer;" @click="openSelectV2rayVersion">[[ status.xray.version ]]</a-tag>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
|
||||
<a-card hoverable :class="themeSwitcher.darkCardClass">
|
||||
{{ i18n "pages.index.operationHours" }}:
|
||||
<a-tag color="green">[[ formatSecond(status.uptime) ]]</a-tag>
|
||||
<a-tooltip>
|
||||
@@ -103,12 +93,35 @@
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
|
||||
<a-card hoverable :class="themeSwitcher.darkCardClass">
|
||||
{{ i18n "pages.index.xrayStatus" }}:
|
||||
<a-tag :color="status.xray.color">[[ status.xray.state ]]</a-tag>
|
||||
<a-tooltip v-if="status.xray.state === State.Error">
|
||||
<template slot="title">
|
||||
<p v-for="line in status.xray.errorMsg.split('\n')">[[ line ]]</p>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
<a-tag color="blue" style="cursor: pointer;" @click="stopXrayService">{{ i18n "pages.index.stopXray" }}</a-tag>
|
||||
<a-tag color="blue" style="cursor: pointer;" @click="restartXrayService">{{ i18n "pages.index.restartXray" }}</a-tag>
|
||||
<a-tag color="blue" style="cursor: pointer;" @click="openSelectV2rayVersion">{{ i18n "pages.index.xraySwitch" }}</a-tag>
|
||||
</a-card>
|
||||
</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>
|
||||
</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="siderDrawer.isDarkTheme ? darkClass : ''">
|
||||
<a-card hoverable :class="themeSwitcher.darkCardClass">
|
||||
tcp / udp {{ i18n "pages.index.connectionCount" }}: [[ status.tcpCount ]] / [[ status.udpCount ]]
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
@@ -119,7 +132,7 @@
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
|
||||
<a-card hoverable :class="themeSwitcher.darkCardClass">
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
<a-icon type="arrow-up"></a-icon>
|
||||
@@ -145,7 +158,7 @@
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
|
||||
<a-card hoverable :class="themeSwitcher.darkCardClass">
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
<a-icon type="cloud-upload"></a-icon>
|
||||
@@ -174,10 +187,11 @@
|
||||
</transition>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
|
||||
<a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}'
|
||||
:closable="true" @ok="() => versionModal.visible = false"
|
||||
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||
ok-text='{{ i18n "confirm" }}' cancel-text='{{ i18n "cancel"}}'>
|
||||
:class="themeSwitcher.darkCardClass"
|
||||
footer="">
|
||||
<h2>{{ i18n "pages.index.xraySwitchClick"}}</h2>
|
||||
<h2>{{ i18n "pages.index.xraySwitchClickDesk"}}</h2>
|
||||
<template v-for="version, index in versionModal.versions">
|
||||
@@ -187,8 +201,59 @@
|
||||
</a-tag>
|
||||
</template>
|
||||
</a-modal>
|
||||
|
||||
<a-modal id="log-modal" v-model="logModal.visible" title="X-UI logs"
|
||||
:closable="true" @ok="() => logModal.visible = false" @cancel="() => logModal.visible = false"
|
||||
:class="themeSwitcher.darkCardClass"
|
||||
width="800px"
|
||||
footer="">
|
||||
<a-form layout="inline">
|
||||
<a-form-item label="Count">
|
||||
<a-select v-model="logModal.rows"
|
||||
style="width: 80px"
|
||||
@change="openLogs(logModal.rows)"
|
||||
:dropdown-class-name="themeSwitcher.darkCardClass">
|
||||
<a-select-option value="10">10</a-select-option>
|
||||
<a-select-option value="20">20</a-select-option>
|
||||
<a-select-option value="50">50</a-select-option>
|
||||
<a-select-option value="100">100</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<button class="ant-btn ant-btn-primary" @click="openLogs(logModal.rows)"><a-icon type="sync"></a-icon> Reload</button>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" style="margin-bottom: 10px;"
|
||||
:href="'data:application/text;charset=utf-8,' + encodeURIComponent(logModal.logs)" download="x-ui.log">
|
||||
{{ i18n "download" }} x-ui.log
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-input type="textarea" v-model="logModal.logs" disabled="true"
|
||||
:autosize="{ minRows: 10, maxRows: 22}"></a-input>
|
||||
</a-modal>
|
||||
|
||||
<a-modal id="backup-modal" v-model="backupModal.visible" :title="backupModal.title"
|
||||
:closable="true" :class="themeSwitcher.darkCardClass"
|
||||
@ok="() => backupModal.hide()" @cancel="() => backupModal.hide()">
|
||||
<p style="color: inherit; font-size: 16px; padding: 4px 2px;">
|
||||
<a-icon type="warning" style="color: inherit; font-size: 20px;"></a-icon>
|
||||
[[ backupModal.description ]]
|
||||
</p>
|
||||
<a-space direction="horizontal" align="center" style="margin-bottom: 10px;">
|
||||
<a-button type="primary" @click="exportDatabase()">
|
||||
[[ backupModal.exportText ]]
|
||||
</a-button>
|
||||
<a-button type="primary" @click="importDatabase()">
|
||||
[[ backupModal.importText ]]
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-modal>
|
||||
|
||||
</a-layout>
|
||||
{{template "js" .}}
|
||||
{{template "component/themeSwitcher" .}}
|
||||
{{template "textModal"}}
|
||||
<script>
|
||||
|
||||
const State = {
|
||||
@@ -280,13 +345,53 @@
|
||||
},
|
||||
};
|
||||
|
||||
const logModal = {
|
||||
visible: false,
|
||||
logs: '',
|
||||
rows: 20,
|
||||
show(logs, rows) {
|
||||
this.visible = true;
|
||||
this.rows = rows;
|
||||
this.logs = logs.join("\n");
|
||||
},
|
||||
hide() {
|
||||
this.visible = false;
|
||||
},
|
||||
};
|
||||
|
||||
const backupModal = {
|
||||
visible: false,
|
||||
title: '',
|
||||
description: '',
|
||||
exportText: '',
|
||||
importText: '',
|
||||
show({
|
||||
title = '{{ i18n "pages.index.backupTitle" }}',
|
||||
description = '{{ i18n "pages.index.backupDescription" }}',
|
||||
exportText = '{{ i18n "pages.index.exportDatabase" }}',
|
||||
importText = '{{ i18n "pages.index.importDatabase" }}',
|
||||
}) {
|
||||
this.title = title;
|
||||
this.description = description;
|
||||
this.exportText = exportText;
|
||||
this.importText = importText;
|
||||
this.visible = true;
|
||||
},
|
||||
hide() {
|
||||
this.visible = false;
|
||||
},
|
||||
};
|
||||
|
||||
const app = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#app',
|
||||
data: {
|
||||
siderDrawer,
|
||||
themeSwitcher,
|
||||
status: new Status(),
|
||||
versionModal,
|
||||
logModal,
|
||||
backupModal,
|
||||
spinning: false,
|
||||
loadingTip: '{{ i18n "loading"}}',
|
||||
},
|
||||
@@ -318,17 +423,16 @@
|
||||
title: '{{ i18n "pages.index.xraySwitchVersionDialog"}}',
|
||||
content: '{{ i18n "pages.index.xraySwitchVersionDialogDesc"}}' + ` ${version}?`,
|
||||
okText: '{{ i18n "confirm"}}',
|
||||
class: siderDrawer.isDarkTheme ? darkClass : '',
|
||||
class: themeSwitcher.darkCardClass,
|
||||
cancelText: '{{ i18n "cancel"}}',
|
||||
onOk: async () => {
|
||||
versionModal.hide();
|
||||
this.loading(true, '{{ i18n "pages.index.dontRefreshh"}}');
|
||||
this.loading(true, '{{ i18n "pages.index.dontRefresh"}}');
|
||||
await HttpUtil.post(`/server/installXray/${version}`);
|
||||
this.loading(false);
|
||||
},
|
||||
});
|
||||
},
|
||||
//here add stop xray function
|
||||
async stopXrayService() {
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.post('server/stopXrayService');
|
||||
@@ -337,7 +441,6 @@
|
||||
return;
|
||||
}
|
||||
},
|
||||
//here add restart xray function
|
||||
async restartXrayService() {
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.post('server/restartXrayService');
|
||||
@@ -346,6 +449,67 @@
|
||||
return;
|
||||
}
|
||||
},
|
||||
async openLogs(rows){
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.post('server/logs/'+rows);
|
||||
this.loading(false);
|
||||
if (!msg.success) {
|
||||
return;
|
||||
}
|
||||
logModal.show(msg.obj, rows);
|
||||
},
|
||||
async openConfig() {
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.post('server/getConfigJson');
|
||||
this.loading(false);
|
||||
if (!msg.success) {
|
||||
return;
|
||||
}
|
||||
txtModal.show('config.json', JSON.stringify(msg.obj, null, 2), 'config.json');
|
||||
},
|
||||
openBackup() {
|
||||
backupModal.show({
|
||||
title: '{{ i18n "pages.index.backupTitle" }}',
|
||||
description: '{{ i18n "pages.index.backupDescription" }}',
|
||||
exportText: '{{ i18n "pages.index.exportDatabase" }}',
|
||||
importText: '{{ i18n "pages.index.importDatabase" }}',
|
||||
});
|
||||
},
|
||||
exportDatabase() {
|
||||
window.location = basePath + 'server/getDb';
|
||||
},
|
||||
importDatabase() {
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = '.db';
|
||||
fileInput.addEventListener('change', async (event) => {
|
||||
const dbFile = event.target.files[0];
|
||||
if (dbFile) {
|
||||
const formData = new FormData();
|
||||
formData.append('db', dbFile);
|
||||
backupModal.hide();
|
||||
this.loading(true);
|
||||
const uploadMsg = await HttpUtil.post('server/importDB', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
}
|
||||
});
|
||||
this.loading(false);
|
||||
if (!uploadMsg.success) {
|
||||
return;
|
||||
}
|
||||
this.loading(true);
|
||||
const restartMsg = await HttpUtil.post("/xui/setting/restartPanel");
|
||||
this.loading(false);
|
||||
if (restartMsg.success) {
|
||||
this.loading(true);
|
||||
await PromiseUtil.sleep(5000);
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
});
|
||||
fileInput.click();
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
while (true) {
|
||||
|
||||
@@ -1,312 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
{{template "head" .}}
|
||||
<style>
|
||||
@media (min-width: 769px) {
|
||||
.ant-layout-content {
|
||||
margin: 24px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-col-sm-24 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.ant-tabs-bar {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ant-list-item {
|
||||
display: block;
|
||||
}
|
||||
|
||||
:not(.ant-card-dark)>.ant-tabs-top-bar {
|
||||
background: white;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<a-layout id="app" v-cloak>
|
||||
{{ template "commonSider" . }}
|
||||
<a-layout id="content-layout" :style="siderDrawer.isDarkTheme ? bgDarkStyle : ''">
|
||||
<a-layout-content>
|
||||
<a-spin :spinning="spinning" :delay="500" tip="loading">
|
||||
<a-space direction="vertical">
|
||||
<a-space direction="horizontal">
|
||||
<a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n "pages.setting.save" }}</a-button>
|
||||
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.setting.restartPanel" }}</a-button>
|
||||
</a-space>
|
||||
<a-tabs default-active-key="1" :class="siderDrawer.isDarkTheme ? darkClass : ''">
|
||||
<a-tab-pane key="1" tab='{{ i18n "pages.setting.panelConfig"}}'>
|
||||
|
||||
<a-list item-layout="horizontal" :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65);': 'background: white;'">
|
||||
<setting-list-item type="text" title='{{ i18n "pages.setting.panelListeningIP"}}' desc='{{ i18n "pages.setting.panelListeningIPDesc"}}' v-model="allSetting.webListen"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.setting.panelPort"}}' desc='{{ i18n "pages.setting.panelPortDesc"}}' v-model.number="allSetting.webPort"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.setting.publicKeyPath"}}' desc='{{ i18n "pages.setting.publicKeyPathDesc"}}' v-model="allSetting.webCertFile"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.setting.privateKeyPath"}}' desc='{{ i18n "pages.setting.privateKeyPathDesc"}}' v-model="allSetting.webKeyFile"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.setting.panelUrlPath"}}' desc='{{ i18n "pages.setting.panelUrlPathDesc"}}' v-model="allSetting.webBasePath"></setting-list-item>
|
||||
<a-list-item>
|
||||
<a-row style="padding: 20px">
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta title="Language"/>
|
||||
</a-col>
|
||||
|
||||
<a-col :lg="24" :xl="12">
|
||||
<temlate>
|
||||
<a-select
|
||||
ref="selectLang"
|
||||
v-model="lang"
|
||||
@change="setLang(lang)"
|
||||
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
|
||||
style="width: 100%"
|
||||
>
|
||||
<a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs">
|
||||
<span role="img" aria-label="l.name" v-text="l.icon"></span>
|
||||
<span v-text="l.name"></span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</temlate>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2" tab='{{ i18n "pages.setting.userSetting"}}'>
|
||||
<a-form :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65); padding: 20px;': 'background: white; padding: 20px;'">
|
||||
<a-form-item label='{{ i18n "pages.setting.oldUsername"}}'>
|
||||
<a-input v-model="user.oldUsername" style="max-width: 300px"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.setting.currentPassword"}}'>
|
||||
<a-input type="password" v-model="user.oldPassword"
|
||||
style="max-width: 300px"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.setting.newUsername"}}'>
|
||||
<a-input v-model="user.newUsername" style="max-width: 300px"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.setting.newPassword"}}'>
|
||||
<a-input type="password" v-model="user.newPassword"
|
||||
style="max-width: 300px"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<!-- <a-button type="primary" @click="updateUser">Revise</a-button>-->
|
||||
<a-button type="primary" @click="updateUser">{{ i18n "confirm" }}</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="3" tab='{{ i18n "pages.setting.xrayConfiguration"}}'>
|
||||
<a-list item-layout="horizontal" :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65);': 'background: white;'">
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigTorrent"}}' desc='{{ i18n "pages.setting.xrayConfigTorrentDesc"}}' v-model="torrentSettings"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigPrivateIp"}}' desc='{{ i18n "pages.setting.xrayConfigPrivateIpDesc"}}' v-model="privateIpSettings"></setting-list-item>
|
||||
<a-divider>{{ i18n "pages.setting.advancedTemplate"}}</a-divider>
|
||||
<a-collapse>
|
||||
<a-collapse-panel header="{{ i18n "pages.setting.xrayConfigInbounds"}}">
|
||||
<setting-list-item type="textarea" title='{{ i18n "pages.setting.xrayConfigInbounds"}}' desc='{{ i18n "pages.setting.xrayConfigInboundsDesc"}}' v-model ="inboundSettings"></setting-list-item>
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel header="{{ i18n "pages.setting.xrayConfigOutbounds"}}">
|
||||
<setting-list-item type="textarea" title='{{ i18n "pages.setting.xrayConfigOutbounds"}}' desc='{{ i18n "pages.setting.xrayConfigOutboundsDesc"}}' v-model ="outboundSettings"></setting-list-item>
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel header="{{ i18n "pages.setting.xrayConfigRoutings"}}">
|
||||
<setting-list-item type="textarea" title='{{ i18n "pages.setting.xrayConfigRoutings"}}' desc='{{ i18n "pages.setting.xrayConfigRoutingsDesc"}}' v-model ="routingRuleSettings"></setting-list-item>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
<a-divider>{{ i18n "pages.setting.completeTemplate"}}</a-divider>
|
||||
<setting-list-item type="textarea" title='{{ i18n "pages.setting.xrayConfigTemplate"}}' desc='{{ i18n "pages.setting.xrayConfigTemplateDesc"}}' v-model="allSetting.xrayTemplateConfig"></setting-list-item>
|
||||
</a-list>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="4" tab='{{ i18n "pages.setting.TGReminder"}}'>
|
||||
<a-list item-layout="horizontal" :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65);': 'background: white;'">
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.setting.telegramBotEnable" }}' desc='{{ i18n "pages.setting.telegramBotEnableDesc" }}' v-model="allSetting.tgBotEnable"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.setting.telegramToken"}}' desc='{{ i18n "pages.setting.telegramTokenDesc"}}' v-model="allSetting.tgBotToken"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.setting.telegramChatId"}}' desc='{{ i18n "pages.setting.telegramChatIdDesc"}}' v-model.number="allSetting.tgBotChatId"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.setting.telegramNotifyTime"}}' desc='{{ i18n "pages.setting.telegramNotifyTimeDesc"}}' v-model="allSetting.tgRunTime"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.setting.tgNotifyBackup" }}' desc='{{ i18n "pages.setting.tgNotifyBackupDesc" }}' v-model="allSetting.tgBotBackup"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.setting.tgNotifyExpireTimeDiff" }}' desc='{{ i18n "pages.setting.tgNotifyExpireTimeDiffDesc" }}' v-model="allSetting.tgExpireDiff" :min="0"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.setting.tgNotifyTrafficDiff" }}' desc='{{ i18n "pages.setting.tgNotifyTrafficDiffDesc" }}' v-model="allSetting.tgTrafficDiff" :min="0"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.setting.tgNotifyCpu" }}' desc='{{ i18n "pages.setting.tgNotifyCpuDesc" }}' v-model="allSetting.tgCpu" :min="0" :max="100"></setting-list-item>
|
||||
</a-list>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="5" tab='{{ i18n "pages.setting.otherSetting"}}'>
|
||||
<a-list item-layout="horizontal" :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65);': 'background: white;'">
|
||||
<setting-list-item type="text" title='{{ i18n "pages.setting.timeZonee"}}' desc='{{ i18n "pages.setting.timeZoneDesc"}}' v-model="allSetting.timeLocation"></setting-list-item>
|
||||
</a-list>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-space>
|
||||
</a-spin>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
{{template "js" .}}
|
||||
{{template "component/setting"}}
|
||||
<script>
|
||||
|
||||
const app = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#app',
|
||||
data: {
|
||||
siderDrawer,
|
||||
spinning: false,
|
||||
oldAllSetting: new AllSetting(),
|
||||
allSetting: new AllSetting(),
|
||||
saveBtnDisable: true,
|
||||
user: {},
|
||||
lang : getLang()
|
||||
},
|
||||
methods: {
|
||||
loading(spinning = true) {
|
||||
this.spinning = spinning;
|
||||
},
|
||||
async getAllSetting() {
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.post("/xui/setting/all");
|
||||
this.loading(false);
|
||||
if (msg.success) {
|
||||
this.oldAllSetting = new AllSetting(msg.obj);
|
||||
this.allSetting = new AllSetting(msg.obj);
|
||||
this.saveBtnDisable = true;
|
||||
}
|
||||
},
|
||||
async updateAllSetting() {
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.post("/xui/setting/update", this.allSetting);
|
||||
this.loading(false);
|
||||
if (msg.success) {
|
||||
await this.getAllSetting();
|
||||
}
|
||||
},
|
||||
async updateUser() {
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.post("/xui/setting/updateUser", this.user);
|
||||
this.loading(false);
|
||||
if (msg.success) {
|
||||
this.user = {};
|
||||
}
|
||||
},
|
||||
async restartPanel() {
|
||||
await new Promise(resolve => {
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.setting.restartPanel" }}',
|
||||
content: '{{ i18n "pages.setting.restartPanelDesc" }}',
|
||||
okText: '{{ i18n "sure" }}',
|
||||
cancelText: '{{ i18n "cancel" }}',
|
||||
onOk: () => resolve(),
|
||||
});
|
||||
});
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.post("/xui/setting/restartPanel");
|
||||
this.loading(false);
|
||||
if (msg.success) {
|
||||
this.loading(true);
|
||||
await PromiseUtil.sleep(5000);
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.getAllSetting();
|
||||
while (true) {
|
||||
await PromiseUtil.sleep(1000);
|
||||
this.saveBtnDisable = this.oldAllSetting.equals(this.allSetting);
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
templateSettings: {
|
||||
get: function () { return this.allSetting.xrayTemplateConfig ? JSON.parse(this.allSetting.xrayTemplateConfig) : null ; },
|
||||
set: function (newValue) { this.allSetting.xrayTemplateConfig = JSON.stringify(newValue, null, 2) },
|
||||
},
|
||||
inboundSettings: {
|
||||
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.inbounds, null, 2) : null; },
|
||||
set: function (newValue) {
|
||||
newTemplateSettings = this.templateSettings;
|
||||
newTemplateSettings.inbounds = JSON.parse(newValue)
|
||||
this.templateSettings = newTemplateSettings
|
||||
},
|
||||
},
|
||||
outboundSettings: {
|
||||
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.outbounds, null, 2) : null; },
|
||||
set: function (newValue) {
|
||||
newTemplateSettings = this.templateSettings;
|
||||
newTemplateSettings.outbounds = JSON.parse(newValue)
|
||||
this.templateSettings = newTemplateSettings
|
||||
},
|
||||
},
|
||||
routingRuleSettings: {
|
||||
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.routing.rules, null, 2) : null; },
|
||||
set: function (newValue) {
|
||||
newTemplateSettings = this.templateSettings;
|
||||
newTemplateSettings.routing.rules = JSON.parse(newValue)
|
||||
this.templateSettings = newTemplateSettings
|
||||
},
|
||||
},
|
||||
torrentSettings: {
|
||||
get: function () {
|
||||
torrentFilter = false
|
||||
if(this.templateSettings != null){
|
||||
this.templateSettings.routing.rules.forEach(routingRule => {
|
||||
if(routingRule.hasOwnProperty("protocol")){
|
||||
if (routingRule.protocol[0] === "bittorrent" && routingRule.outboundTag == "blocked"){
|
||||
torrentFilter = true
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return torrentFilter
|
||||
},
|
||||
set: function (newValue) {
|
||||
newTemplateSettings = JSON.parse(this.allSetting.xrayTemplateConfig);
|
||||
if (newValue){
|
||||
newTemplateSettings.routing.rules.push(JSON.parse("{\"outboundTag\": \"blocked\",\"protocol\": [\"bittorrent\"],\"type\": \"field\"}"))
|
||||
}
|
||||
else {
|
||||
newTemplateSettings.routing.rules = [];
|
||||
this.templateSettings.routing.rules.forEach(routingRule => {
|
||||
if (routingRule.hasOwnProperty('protocol')){
|
||||
if (routingRule.protocol[0] === "bittorrent" && routingRule.outboundTag == "blocked"){
|
||||
return;
|
||||
}
|
||||
}
|
||||
newTemplateSettings.routing.rules.push(routingRule);
|
||||
});
|
||||
}
|
||||
this.templateSettings = newTemplateSettings
|
||||
},
|
||||
},
|
||||
privateIpSettings: {
|
||||
get: function () {
|
||||
localIpFilter = false
|
||||
if(this.templateSettings != null){
|
||||
this.templateSettings.routing.rules.forEach(routingRule => {
|
||||
if(routingRule.hasOwnProperty("ip")){
|
||||
if (routingRule.ip[0] === "geoip:private" && routingRule.outboundTag == "blocked"){
|
||||
localIpFilter = true
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return localIpFilter
|
||||
},
|
||||
set: function (newValue) {
|
||||
newTemplateSettings = JSON.parse(this.allSetting.xrayTemplateConfig);
|
||||
if (newValue){
|
||||
newTemplateSettings.routing.rules.push(JSON.parse("{\"outboundTag\": \"blocked\",\"ip\": [\"geoip:private\"],\"type\": \"field\"}"))
|
||||
}
|
||||
else {
|
||||
newTemplateSettings.routing.rules = [];
|
||||
this.templateSettings.routing.rules.forEach(routingRule => {
|
||||
if (routingRule.hasOwnProperty('ip')){
|
||||
if (routingRule.ip[0] === "geoip:private" && routingRule.outboundTag == "blocked"){
|
||||
return;
|
||||
}
|
||||
}
|
||||
newTemplateSettings.routing.rules.push(routingRule);
|
||||
});
|
||||
}
|
||||
this.templateSettings = newTemplateSettings
|
||||
},
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
923
web/html/xui/settings.html
Normal file
923
web/html/xui/settings.html
Normal file
@@ -0,0 +1,923 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
{{template "head" .}}
|
||||
<style>
|
||||
@media (min-width: 769px) {
|
||||
.ant-layout-content {
|
||||
margin: 24px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-col-sm-24 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.ant-tabs-bar {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ant-list-item {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.alert-msg {
|
||||
color: inherit;
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
padding: 20px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.alert-msg > i {
|
||||
color: inherit;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.collapse-title {
|
||||
color: inherit;
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
padding: 10px 20px;
|
||||
border-bottom: 2px solid;
|
||||
}
|
||||
|
||||
.collapse-title > i {
|
||||
color: inherit;
|
||||
font-size: 24px;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<a-layout id="app" v-cloak>
|
||||
{{ template "commonSider" . }}
|
||||
<a-layout id="content-layout" :style="themeSwitcher.bgStyle">
|
||||
<a-layout-content>
|
||||
<a-spin :spinning="spinning" :delay="500" tip="loading">
|
||||
<a-space direction="vertical">
|
||||
<a-space direction="horizontal">
|
||||
<a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n "pages.settings.save" }}</a-button>
|
||||
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.settings.restartPanel" }}</a-button>
|
||||
</a-space>
|
||||
<a-tabs default-active-key="1" :class="themeSwitcher.darkCardClass">
|
||||
<a-tab-pane key="1" tab='{{ i18n "pages.settings.panelConfig"}}'>
|
||||
<a-row :xs="24" :sm="24" :lg="12">
|
||||
<h2 class="alert-msg">
|
||||
<a-icon type="warning"></a-icon>
|
||||
{{ i18n "pages.settings.infoDesc" }}
|
||||
</h2>
|
||||
</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.number="allSetting.webPort"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.publicKeyPath"}}' desc='{{ i18n "pages.settings.publicKeyPathDesc"}}' v-model="allSetting.webCertFile"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.privateKeyPath"}}' desc='{{ i18n "pages.settings.privateKeyPathDesc"}}' v-model="allSetting.webKeyFile"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.panelUrlPath"}}' desc='{{ i18n "pages.settings.panelUrlPathDesc"}}' v-model="allSetting.webBasePath"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.settings.sessionMaxAge" }}' desc='{{ i18n "pages.settings.sessionMaxAgeDesc" }}' v-model="allSetting.sessionMaxAge" :min="0"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.settings.expireTimeDiff" }}' desc='{{ i18n "pages.settings.expireTimeDiffDesc" }}' v-model="allSetting.expireDiff" :min="0"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.settings.trafficDiff" }}' desc='{{ i18n "pages.settings.trafficDiffDesc" }}' v-model="allSetting.trafficDiff" :min="0"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.timeZone"}}' desc='{{ i18n "pages.settings.timeZoneDesc"}}' v-model="allSetting.timeLocation"></setting-list-item>
|
||||
<a-list-item>
|
||||
<a-row style="padding: 20px">
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta title="Language"/>
|
||||
</a-col>
|
||||
|
||||
<a-col :lg="24" :xl="12">
|
||||
<template>
|
||||
<a-select
|
||||
ref="selectLang"
|
||||
v-model="lang"
|
||||
@change="setLang(lang)"
|
||||
:dropdown-class-name="themeSwitcher.darkCardClass"
|
||||
style="width: 100%"
|
||||
>
|
||||
<a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs">
|
||||
<span role="img" aria-label="l.name" v-text="l.icon"></span>
|
||||
<span v-text="l.name"></span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2" tab='{{ i18n "pages.settings.userSettings"}}'>
|
||||
<a-form :style="'padding: 20px;' + themeSwitcher.textStyle">
|
||||
<a-form-item label='{{ i18n "pages.settings.oldUsername"}}'>
|
||||
<a-input v-model="user.oldUsername" style="max-width: 300px"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.settings.currentPassword"}}'>
|
||||
<password-input v-model="user.oldPassword" style="max-width: 300px"></password-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.settings.newUsername"}}'>
|
||||
<a-input v-model="user.newUsername" style="max-width: 300px"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.settings.newPassword"}}'>
|
||||
<password-input v-model="user.newPassword" style="max-width: 300px"></password-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="updateUser">{{ i18n "confirm" }}</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="3" tab='{{ i18n "pages.settings.xrayConfiguration"}}'>
|
||||
<a-row :xs="24" :sm="24" :lg="12">
|
||||
<h2 class="alert-msg">
|
||||
<a-icon type="warning"></a-icon>
|
||||
{{ i18n "pages.settings.infoDesc" }}
|
||||
</h2>
|
||||
</a-row>
|
||||
<a-list item-layout="horizontal" :style="themeSwitcher.textStyle">
|
||||
<a-tabs class="ant-card-dark-box-nohover" default-active-key="tpl-1" :class="themeSwitcher.darkCardClass" style="padding: 20px 20px;">
|
||||
<a-tab-pane key="tpl-1" tab='{{ i18n "pages.settings.templates.basicTemplate"}}' style="padding-top: 20px;">
|
||||
<a-collapse>
|
||||
<a-collapse-panel header='{{ i18n "pages.settings.templates.generalConfigs"}}'>
|
||||
<a-row :xs="24" :sm="24" :lg="12">
|
||||
<h2 class="collapse-title">
|
||||
<a-icon type="warning"></a-icon>
|
||||
{{ i18n "pages.settings.templates.generalConfigsDesc" }}
|
||||
</h2>
|
||||
</a-row>
|
||||
<a-list-item>
|
||||
<a-row style="padding: 20px">
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta
|
||||
title='{{ i18n "pages.settings.templates.xrayConfigFreedomStrategy" }}'
|
||||
description='{{ i18n "pages.settings.templates.xrayConfigFreedomStrategyDesc" }}'/>
|
||||
</a-col>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<template>
|
||||
<a-select
|
||||
v-model="freedomStrategy"
|
||||
:dropdown-class-name="themeSwitcher.darkCardClass"
|
||||
style="width: 100%">
|
||||
<a-select-option v-for="s in outboundDomainStrategies" :value="s">[[ s ]]</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-list-item>
|
||||
<a-row style="padding: 20px">
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta
|
||||
title='{{ i18n "pages.settings.templates.xrayConfigRoutingStrategy" }}'
|
||||
description='{{ i18n "pages.settings.templates.xrayConfigRoutingStrategyDesc" }}'/>
|
||||
</a-col>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<template>
|
||||
<a-select
|
||||
v-model="routingStrategy"
|
||||
:dropdown-class-name="themeSwitcher.darkCardClass"
|
||||
style="width: 100%">
|
||||
<a-select-option v-for="s in routingDomainStrategies" :value="s">[[ s ]]</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-list-item>
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel header='{{ i18n "pages.settings.templates.blockConfigs"}}'>
|
||||
<a-row :xs="24" :sm="24" :lg="12">
|
||||
<h2 class="collapse-title">
|
||||
<a-icon type="warning"></a-icon>
|
||||
{{ i18n "pages.settings.templates.blockConfigsDesc" }}
|
||||
</h2>
|
||||
</a-row>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigTorrent"}}' desc='{{ i18n "pages.settings.templates.xrayConfigTorrentDesc"}}' v-model="torrentSettings"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigPrivateIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigPrivateIpDesc"}}' v-model="privateIpSettings"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigAds"}}' desc='{{ i18n "pages.settings.templates.xrayConfigAdsDesc"}}' v-model="AdsSettings"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigFamily"}}' desc='{{ i18n "pages.settings.templates.xrayConfigFamilyDesc"}}' v-model="familyProtectSettings"></setting-list-item>
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel header='{{ i18n "pages.settings.templates.blockCountryConfigs"}}'>
|
||||
<a-row :xs="24" :sm="24" :lg="12">
|
||||
<h2 class="collapse-title">
|
||||
<a-icon type="warning"></a-icon>
|
||||
{{ i18n "pages.settings.templates.blockCountryConfigsDesc" }}
|
||||
</h2>
|
||||
</a-row>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigIRIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigIRIpDesc"}}' v-model="IRIpSettings"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigIRDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigIRDomainDesc"}}' v-model="IRDomainSettings"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigChinaIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigChinaIpDesc"}}' v-model="ChinaIpSettings"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigChinaDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigChinaDomainDesc"}}' v-model="ChinaDomainSettings"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigRussiaIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigRussiaIpDesc"}}' v-model="RussiaIpSettings"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigRussiaDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigRussiaDomainDesc"}}' v-model="RussiaDomainSettings"></setting-list-item>
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel header='{{ i18n "pages.settings.templates.directCountryConfigs"}}'>
|
||||
<a-row :xs="24" :sm="24" :lg="12">
|
||||
<h2 class="collapse-title">
|
||||
<a-icon type="warning"></a-icon>
|
||||
{{ i18n "pages.settings.templates.directCountryConfigsDesc" }}
|
||||
</h2>
|
||||
</a-row>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectIRIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectIRIpDesc"}}' v-model="IRIpDirectSettings"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectIRDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectIRDomainDesc"}}' v-model="IRDomainDirectSettings"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectChinaIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectChinaIpDesc"}}' v-model="ChinaIpDirectSettings"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectChinaDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectChinaDomainDesc"}}' v-model="ChinaDomainDirectSettings"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectRussiaIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectRussiaIpDesc"}}' v-model="RussiaIpDirectSettings"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectRussiaDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectRussiaDomainDesc"}}' v-model="RussiaDomainDirectSettings"></setting-list-item>
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel header='{{ i18n "pages.settings.templates.ipv4Configs"}}'>
|
||||
<a-row :xs="24" :sm="24" :lg="12">
|
||||
<h2 class="collapse-title">
|
||||
<a-icon type="warning"></a-icon>
|
||||
{{ i18n "pages.settings.templates.ipv4ConfigsDesc" }}
|
||||
</h2>
|
||||
</a-row>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigGoogleIPv4"}}' desc='{{ i18n "pages.settings.templates.xrayConfigGoogleIPv4Desc"}}' v-model="GoogleIPv4Settings"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigNetflixIPv4"}}' desc='{{ i18n "pages.settings.templates.xrayConfigNetflixIPv4Desc"}}' v-model="NetflixIPv4Settings"></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>
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel header='{{ i18n "pages.settings.resetDefaultConfig"}}'>
|
||||
<a-space direction="horizontal" style="padding: 0 20px">
|
||||
<a-button type="primary" @click="resetXrayConfigToDefault">{{ i18n "pages.settings.resetDefaultConfig" }}</a-button>
|
||||
</a-space>
|
||||
</a-collapse-panel>
|
||||
</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>
|
||||
</a-tab-pane>
|
||||
<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>
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel header='{{ i18n "pages.settings.templates.xrayConfigOutbounds"}}'>
|
||||
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigOutbounds"}}' desc='{{ i18n "pages.settings.templates.xrayConfigOutboundsDesc"}}' v-model="outboundSettings"></setting-list-item>
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel header='{{ i18n "pages.settings.templates.xrayConfigRoutings"}}'>
|
||||
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigRoutings"}}' desc='{{ i18n "pages.settings.templates.xrayConfigRoutingsDesc"}}' v-model="routingRuleSettings"></setting-list-item>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</a-tab-pane>
|
||||
<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>
|
||||
</a-list>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="4" tab='{{ i18n "pages.settings.TGBotSettings"}}'>
|
||||
<a-row :xs="24" :sm="24" :lg="12">
|
||||
<h2 class="alert-msg">
|
||||
<a-icon type="warning"></a-icon>
|
||||
{{ i18n "pages.settings.infoDesc" }}
|
||||
</h2>
|
||||
</a-row>
|
||||
<a-list item-layout="horizontal" :style="themeSwitcher.textStyle">
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.telegramBotEnable" }}' desc='{{ i18n "pages.settings.telegramBotEnableDesc" }}' v-model="allSetting.tgBotEnable"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.telegramToken"}}' desc='{{ i18n "pages.settings.telegramTokenDesc"}}' v-model="allSetting.tgBotToken"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.telegramChatId"}}' desc='{{ i18n "pages.settings.telegramChatIdDesc"}}' v-model="allSetting.tgBotChatId"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.telegramNotifyTime"}}' desc='{{ i18n "pages.settings.telegramNotifyTimeDesc"}}' v-model="allSetting.tgRunTime"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.tgNotifyBackup" }}' desc='{{ i18n "pages.settings.tgNotifyBackupDesc" }}' v-model="allSetting.tgBotBackup"></setting-list-item>
|
||||
<setting-list-item type="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">
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta title="Telegram Bot Language" />
|
||||
</a-col>
|
||||
|
||||
<a-col :lg="24" :xl="12">
|
||||
<template>
|
||||
<a-select
|
||||
ref="selectBotLang"
|
||||
v-model="allSetting.tgLang"
|
||||
:dropdown-class-name="themeSwitcher.darkCardClass"
|
||||
style="width: 100%"
|
||||
>
|
||||
<a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs">
|
||||
<span role="img" aria-label="l.name" v-text="l.icon"></span>
|
||||
<span v-text="l.name"></span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="5" tab='{{ i18n "pages.settings.subSettings" }}'>
|
||||
<a-row :xs="24" :sm="24" :lg="12">
|
||||
<h2 class="alert-msg">
|
||||
<a-icon type="warning"></a-icon>
|
||||
{{ i18n "pages.settings.infoDesc" }}
|
||||
</h2>
|
||||
</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="text" title='{{ i18n "pages.settings.subListen"}}' desc='{{ i18n "pages.settings.subListenDesc"}}' v-model="allSetting.subListen"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subDomain"}}' desc='{{ i18n "pages.settings.subDomainDesc"}}' v-model="allSetting.subDomain"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.settings.subPort"}}' desc='{{ i18n "pages.settings.subPortDesc"}}' v-model.number="allSetting.subPort"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subPath"}}' desc='{{ i18n "pages.settings.subPathDesc"}}' v-model="allSetting.subPath"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subCertPath"}}' desc='{{ i18n "pages.settings.subCertPathDesc"}}' v-model="allSetting.subCertFile"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subKeyPath"}}' desc='{{ i18n "pages.settings.subKeyPathDesc"}}' v-model="allSetting.subKeyFile"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.settings.subUpdates"}}' desc='{{ i18n "pages.settings.subUpdatesDesc"}}' v-model="allSetting.subUpdates"></setting-list-item>
|
||||
</a-list>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-space>
|
||||
</a-spin>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
{{template "js" .}}
|
||||
{{template "component/themeSwitcher" .}}
|
||||
{{template "component/password" .}}
|
||||
{{template "component/setting"}}
|
||||
<script>
|
||||
|
||||
const app = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#app',
|
||||
data: {
|
||||
siderDrawer,
|
||||
themeSwitcher,
|
||||
spinning: false,
|
||||
oldAllSetting: new AllSetting(),
|
||||
allSetting: new AllSetting(),
|
||||
saveBtnDisable: true,
|
||||
user: {},
|
||||
lang: getLang(),
|
||||
ipv4Settings: {
|
||||
tag: "IPv4",
|
||||
protocol: "freedom",
|
||||
settings: {
|
||||
domainStrategy: "UseIPv4"
|
||||
}
|
||||
},
|
||||
directSettings: {
|
||||
tag: "direct",
|
||||
protocol: "freedom"
|
||||
},
|
||||
outboundDomainStrategies: ["AsIs", "UseIP", "UseIPv4", "UseIPv6"],
|
||||
routingDomainStrategies: ["AsIs", "IPIfNonMatch", "IPOnDemand"],
|
||||
settingsData: {
|
||||
protocols: {
|
||||
bittorrent: ["bittorrent"],
|
||||
},
|
||||
ips: {
|
||||
local: ["geoip:private"],
|
||||
cn: ["geoip:cn"],
|
||||
ir: ["geoip:ir"],
|
||||
ru: ["geoip:ru"],
|
||||
},
|
||||
domains: {
|
||||
ads: [
|
||||
"geosite:category-ads-all",
|
||||
"ext:iran.dat:ads"
|
||||
],
|
||||
google: ["geosite:google"],
|
||||
netflix: ["geosite:netflix"],
|
||||
cn: [
|
||||
"geosite:cn",
|
||||
"regexp:.*\\.cn$"
|
||||
],
|
||||
ru: [
|
||||
"geosite:category-gov-ru",
|
||||
"regexp:.*\\.ru$"
|
||||
],
|
||||
ir: [
|
||||
"regexp:.*\\.ir$",
|
||||
"ext:iran.dat:ir",
|
||||
"ext:iran.dat:other",
|
||||
"geosite:category-ir"
|
||||
]
|
||||
},
|
||||
familyProtectDNS: {
|
||||
"servers": [
|
||||
"1.1.1.3",
|
||||
"1.0.0.3",
|
||||
"94.140.14.15",
|
||||
"94.140.15.16"
|
||||
],
|
||||
"queryStrategy": "UseIPv4"
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loading(spinning = true) {
|
||||
this.spinning = spinning;
|
||||
},
|
||||
async getAllSetting() {
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.post("/xui/setting/all");
|
||||
this.loading(false);
|
||||
if (msg.success) {
|
||||
this.oldAllSetting = new AllSetting(msg.obj);
|
||||
this.allSetting = new AllSetting(msg.obj);
|
||||
this.saveBtnDisable = true;
|
||||
}
|
||||
},
|
||||
async updateAllSetting() {
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.post("/xui/setting/update", this.allSetting);
|
||||
this.loading(false);
|
||||
if (msg.success) {
|
||||
await this.getAllSetting();
|
||||
}
|
||||
},
|
||||
async updateUser() {
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.post("/xui/setting/updateUser", this.user);
|
||||
this.loading(false);
|
||||
if (msg.success) {
|
||||
this.user = {};
|
||||
window.location.replace(basePath + "logout");
|
||||
}
|
||||
},
|
||||
async restartPanel() {
|
||||
await new Promise(resolve => {
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.settings.restartPanel" }}',
|
||||
content: '{{ i18n "pages.settings.restartPanelDesc" }}',
|
||||
okText: '{{ i18n "sure" }}',
|
||||
cancelText: '{{ i18n "cancel" }}',
|
||||
onOk: () => resolve(),
|
||||
});
|
||||
});
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.post("/xui/setting/restartPanel");
|
||||
this.loading(false);
|
||||
if (msg.success) {
|
||||
this.loading(true);
|
||||
await PromiseUtil.sleep(5000);
|
||||
const { webCertFile, webKeyFile, webDomain: host, webPort: port, webBasePath: base } = this.allSetting;
|
||||
const isTLS = webCertFile !== "" || webKeyFile !== "";
|
||||
const url = buildURL({ host, port, isTLS, base, path: "xui/settings" });
|
||||
window.location.replace(url);
|
||||
}
|
||||
},
|
||||
async resetXrayConfigToDefault() {
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.get("/xui/setting/getDefaultJsonConfig");
|
||||
this.loading(false);
|
||||
if (msg.success) {
|
||||
this.templateSettings = JSON.parse(JSON.stringify(msg.obj, null, 2));
|
||||
this.saveBtnDisable = true;
|
||||
}
|
||||
},
|
||||
syncRulesWithOutbound(tag, setting) {
|
||||
const newTemplateSettings = this.templateSettings;
|
||||
const haveRules = newTemplateSettings.routing.rules.some((r) => r?.outboundTag === tag);
|
||||
const outboundIndex = newTemplateSettings.outbounds.findIndex((o) => o.tag === tag);
|
||||
if (!haveRules && outboundIndex > 0) {
|
||||
newTemplateSettings.outbounds.splice(outboundIndex);
|
||||
}
|
||||
if (haveRules && outboundIndex < 0) {
|
||||
newTemplateSettings.outbounds.push(setting);
|
||||
}
|
||||
this.templateSettings = newTemplateSettings;
|
||||
},
|
||||
templateRuleGetter(routeSettings) {
|
||||
const { property, outboundTag } = routeSettings;
|
||||
let result = [];
|
||||
if (this.templateSettings != null) {
|
||||
this.templateSettings.routing.rules.forEach(
|
||||
(routingRule) => {
|
||||
if (
|
||||
routingRule.hasOwnProperty(property) &&
|
||||
routingRule.hasOwnProperty("outboundTag") &&
|
||||
routingRule.outboundTag === outboundTag
|
||||
) {
|
||||
result.push(...routingRule[property]);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
templateRuleSetter(routeSettings) {
|
||||
const { data, property, outboundTag } = routeSettings;
|
||||
const oldTemplateSettings = this.templateSettings;
|
||||
const newTemplateSettings = oldTemplateSettings;
|
||||
currentProperty = this.templateRuleGetter({ outboundTag, property })
|
||||
if (currentProperty.length == 0) {
|
||||
const propertyRule = {
|
||||
type: "field",
|
||||
outboundTag,
|
||||
[property]: data
|
||||
};
|
||||
newTemplateSettings.routing.rules.push(propertyRule);
|
||||
}
|
||||
else {
|
||||
const newRules = [];
|
||||
insertedOnce = false;
|
||||
newTemplateSettings.routing.rules.forEach(
|
||||
(routingRule) => {
|
||||
if (
|
||||
routingRule.hasOwnProperty(property) &&
|
||||
routingRule.hasOwnProperty("outboundTag") &&
|
||||
routingRule.outboundTag === outboundTag
|
||||
) {
|
||||
if (!insertedOnce && data.length > 0) {
|
||||
insertedOnce = true;
|
||||
routingRule[property] = data;
|
||||
newRules.push(routingRule);
|
||||
}
|
||||
}
|
||||
else {
|
||||
newRules.push(routingRule);
|
||||
}
|
||||
}
|
||||
);
|
||||
newTemplateSettings.routing.rules = newRules;
|
||||
}
|
||||
this.templateSettings = newTemplateSettings;
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.getAllSetting();
|
||||
while (true) {
|
||||
await PromiseUtil.sleep(1000);
|
||||
this.saveBtnDisable = this.oldAllSetting.equals(this.allSetting);
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
templateSettings: {
|
||||
get: function () { return this.allSetting.xrayTemplateConfig ? JSON.parse(this.allSetting.xrayTemplateConfig) : null; },
|
||||
set: function (newValue) { this.allSetting.xrayTemplateConfig = JSON.stringify(newValue, null, 2); },
|
||||
},
|
||||
inboundSettings: {
|
||||
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.inbounds, null, 2) : null; },
|
||||
set: function (newValue) {
|
||||
newTemplateSettings = this.templateSettings;
|
||||
newTemplateSettings.inbounds = JSON.parse(newValue);
|
||||
this.templateSettings = newTemplateSettings;
|
||||
},
|
||||
},
|
||||
outboundSettings: {
|
||||
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.outbounds, null, 2) : null; },
|
||||
set: function (newValue) {
|
||||
newTemplateSettings = this.templateSettings;
|
||||
newTemplateSettings.outbounds = JSON.parse(newValue);
|
||||
this.templateSettings = newTemplateSettings;
|
||||
},
|
||||
},
|
||||
routingRuleSettings: {
|
||||
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.routing.rules, null, 2) : null; },
|
||||
set: function (newValue) {
|
||||
newTemplateSettings = this.templateSettings;
|
||||
newTemplateSettings.routing.rules = JSON.parse(newValue);
|
||||
this.templateSettings = newTemplateSettings;
|
||||
},
|
||||
},
|
||||
freedomStrategy: {
|
||||
get: function () {
|
||||
if (!this.templateSettings) return "AsIs";
|
||||
freedomOutbound = this.templateSettings.outbounds.find((o) => o.protocol === "freedom" && !o.tag);
|
||||
if (!freedomOutbound) return "AsIs";
|
||||
if (!freedomOutbound.settings || !freedomOutbound.settings.domainStrategy) return "AsIs";
|
||||
return freedomOutbound.settings.domainStrategy;
|
||||
},
|
||||
set: function (newValue) {
|
||||
newTemplateSettings = this.templateSettings;
|
||||
freedomOutboundIndex = newTemplateSettings.outbounds.findIndex((o) => o.protocol === "freedom" && !o.tag);
|
||||
if (!newTemplateSettings.outbounds[freedomOutboundIndex].settings) {
|
||||
newTemplateSettings.outbounds[freedomOutboundIndex].settings = { "domainStrategy": newValue };
|
||||
} else {
|
||||
newTemplateSettings.outbounds[freedomOutboundIndex].settings.domainStrategy = newValue;
|
||||
}
|
||||
this.templateSettings = newTemplateSettings;
|
||||
}
|
||||
},
|
||||
routingStrategy: {
|
||||
get: function () {
|
||||
if (!this.templateSettings || !this.templateSettings.routing || !this.templateSettings.routing.domainStrategy) return "AsIs";
|
||||
return this.templateSettings.routing.domainStrategy;
|
||||
},
|
||||
set: function (newValue) {
|
||||
newTemplateSettings = this.templateSettings;
|
||||
newTemplateSettings.routing.domainStrategy = newValue;
|
||||
this.templateSettings = newTemplateSettings;
|
||||
}
|
||||
},
|
||||
blockedIPs: {
|
||||
get: function () {
|
||||
return this.templateRuleGetter({ outboundTag: "blocked", property: "ip" });
|
||||
},
|
||||
set: function (newValue) {
|
||||
this.templateRuleSetter({ outboundTag: "blocked", property: "ip", data: newValue });
|
||||
}
|
||||
},
|
||||
blockedDomains: {
|
||||
get: function () {
|
||||
return this.templateRuleGetter({ outboundTag: "blocked", property: "domain" });
|
||||
},
|
||||
set: function (newValue) {
|
||||
this.templateRuleSetter({ outboundTag: "blocked", property: "domain", data: newValue });
|
||||
}
|
||||
},
|
||||
blockedProtocols: {
|
||||
get: function () {
|
||||
return this.templateRuleGetter({ outboundTag: "blocked", property: "protocol" });
|
||||
},
|
||||
set: function (newValue) {
|
||||
this.templateRuleSetter({ outboundTag: "blocked", property: "protocol", data: newValue });
|
||||
}
|
||||
},
|
||||
directIPs: {
|
||||
get: function () {
|
||||
return this.templateRuleGetter({ outboundTag: "direct", property: "ip" });
|
||||
},
|
||||
set: function (newValue) {
|
||||
this.templateRuleSetter({ outboundTag: "direct", property: "ip", data: newValue });
|
||||
this.syncRulesWithOutbound("direct", this.directSettings);
|
||||
}
|
||||
},
|
||||
directDomains: {
|
||||
get: function () {
|
||||
return this.templateRuleGetter({ outboundTag: "direct", property: "domain" });
|
||||
},
|
||||
set: function (newValue) {
|
||||
this.templateRuleSetter({ outboundTag: "direct", property: "domain", data: newValue });
|
||||
this.syncRulesWithOutbound("direct", this.directSettings);
|
||||
}
|
||||
},
|
||||
ipv4Domains: {
|
||||
get: function () {
|
||||
return this.templateRuleGetter({ outboundTag: "IPv4", property: "domain" });
|
||||
},
|
||||
set: function (newValue) {
|
||||
this.templateRuleSetter({ outboundTag: "IPv4", property: "domain", data: newValue });
|
||||
this.syncRulesWithOutbound("IPv4", this.ipv4Settings);
|
||||
}
|
||||
},
|
||||
manualBlockedIPs: {
|
||||
get: function () { return JSON.stringify(this.blockedIPs, null, 2); },
|
||||
set: debounce(function (value) { this.blockedIPs = JSON.parse(value); }, 1000)
|
||||
},
|
||||
manualBlockedDomains: {
|
||||
get: function () { return JSON.stringify(this.blockedDomains, null, 2); },
|
||||
set: debounce(function (value) { this.blockedDomains = JSON.parse(value); }, 1000)
|
||||
},
|
||||
manualDirectIPs: {
|
||||
get: function () { return JSON.stringify(this.directIPs, null, 2); },
|
||||
set: debounce(function (value) { this.directIPs = JSON.parse(value); }, 1000)
|
||||
},
|
||||
manualDirectDomains: {
|
||||
get: function () { return JSON.stringify(this.directDomains, null, 2); },
|
||||
set: debounce(function (value) { this.directDomains = JSON.parse(value); }, 1000)
|
||||
},
|
||||
manualIPv4Domains: {
|
||||
get: function () { return JSON.stringify(this.ipv4Domains, null, 2); },
|
||||
set: debounce(function (value) { this.ipv4Domains = JSON.parse(value); }, 1000)
|
||||
},
|
||||
torrentSettings: {
|
||||
get: function () {
|
||||
return doAllItemsExist(this.settingsData.protocols.bittorrent, this.blockedProtocols);
|
||||
},
|
||||
set: function (newValue) {
|
||||
if (newValue) {
|
||||
this.blockedProtocols = [...this.blockedProtocols, ...this.settingsData.protocols.bittorrent];
|
||||
} else {
|
||||
this.blockedProtocols = this.blockedProtocols.filter(data => !this.settingsData.protocols.bittorrent.includes(data));
|
||||
}
|
||||
},
|
||||
},
|
||||
privateIpSettings: {
|
||||
get: function () {
|
||||
return doAllItemsExist(this.settingsData.ips.local, this.blockedIPs);
|
||||
},
|
||||
set: function (newValue) {
|
||||
if (newValue) {
|
||||
this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.local];
|
||||
} else {
|
||||
this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.local.includes(data));
|
||||
}
|
||||
},
|
||||
},
|
||||
AdsSettings: {
|
||||
get: function () {
|
||||
return doAllItemsExist(this.settingsData.domains.ads, this.blockedDomains);
|
||||
},
|
||||
set: function (newValue) {
|
||||
if (newValue) {
|
||||
this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.ads];
|
||||
} else {
|
||||
this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.ads.includes(data));
|
||||
}
|
||||
},
|
||||
},
|
||||
familyProtectSettings: {
|
||||
get: function () {
|
||||
if (!this.templateSettings || !this.templateSettings.dns || !this.templateSettings.dns.servers) return false;
|
||||
return doAllItemsExist(this.templateSettings.dns.servers, this.settingsData.familyProtectDNS.servers);
|
||||
},
|
||||
set: function (newValue) {
|
||||
newTemplateSettings = this.templateSettings;
|
||||
if (newValue) {
|
||||
newTemplateSettings.dns = this.settingsData.familyProtectDNS;
|
||||
} else {
|
||||
delete newTemplateSettings.dns;
|
||||
}
|
||||
this.templateSettings = newTemplateSettings;
|
||||
},
|
||||
},
|
||||
GoogleIPv4Settings: {
|
||||
get: function () {
|
||||
return doAllItemsExist(this.settingsData.domains.google, this.ipv4Domains);
|
||||
},
|
||||
set: function (newValue) {
|
||||
if (newValue) {
|
||||
this.ipv4Domains = [...this.ipv4Domains, ...this.settingsData.domains.google];
|
||||
} else {
|
||||
this.ipv4Domains = this.ipv4Domains.filter(data => !this.settingsData.domains.google.includes(data));
|
||||
}
|
||||
},
|
||||
},
|
||||
NetflixIPv4Settings: {
|
||||
get: function () {
|
||||
return doAllItemsExist(this.settingsData.domains.netflix, this.ipv4Domains);
|
||||
},
|
||||
set: function (newValue) {
|
||||
if (newValue) {
|
||||
this.ipv4Domains = [...this.ipv4Domains, ...this.settingsData.domains.netflix];
|
||||
} else {
|
||||
this.ipv4Domains = this.ipv4Domains.filter(data => !this.settingsData.domains.netflix.includes(data));
|
||||
}
|
||||
},
|
||||
},
|
||||
IRIpSettings: {
|
||||
get: function () {
|
||||
return doAllItemsExist(this.settingsData.ips.ir, this.blockedIPs);
|
||||
},
|
||||
set: function (newValue) {
|
||||
if (newValue) {
|
||||
this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.ir];
|
||||
} else {
|
||||
this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.ir.includes(data));
|
||||
}
|
||||
}
|
||||
},
|
||||
IRDomainSettings: {
|
||||
get: function () {
|
||||
return doAllItemsExist(this.settingsData.domains.ir, this.blockedDomains);
|
||||
},
|
||||
set: function (newValue) {
|
||||
if (newValue) {
|
||||
this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.ir];
|
||||
} else {
|
||||
this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.ir.includes(data));
|
||||
}
|
||||
}
|
||||
},
|
||||
ChinaIpSettings: {
|
||||
get: function () {
|
||||
return doAllItemsExist(this.settingsData.ips.cn, this.blockedIPs);
|
||||
},
|
||||
set: function (newValue) {
|
||||
if (newValue) {
|
||||
this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.cn];
|
||||
} else {
|
||||
this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.cn.includes(data));
|
||||
}
|
||||
}
|
||||
},
|
||||
ChinaDomainSettings: {
|
||||
get: function () {
|
||||
return doAllItemsExist(this.settingsData.domains.cn, this.blockedDomains);
|
||||
},
|
||||
set: function (newValue) {
|
||||
if (newValue) {
|
||||
this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.cn];
|
||||
} else {
|
||||
this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.cn.includes(data));
|
||||
}
|
||||
}
|
||||
},
|
||||
RussiaIpSettings: {
|
||||
get: function () {
|
||||
return doAllItemsExist(this.settingsData.ips.ru, this.blockedIPs);
|
||||
},
|
||||
set: function (newValue) {
|
||||
if (newValue) {
|
||||
this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.ru];
|
||||
} else {
|
||||
this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.ru.includes(data));
|
||||
}
|
||||
}
|
||||
},
|
||||
RussiaDomainSettings: {
|
||||
get: function () {
|
||||
return doAllItemsExist(this.settingsData.domains.ru, this.blockedDomains);
|
||||
},
|
||||
set: function (newValue) {
|
||||
if (newValue) {
|
||||
this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.ru];
|
||||
} else {
|
||||
this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.ru.includes(data));
|
||||
}
|
||||
}
|
||||
},
|
||||
IRIpDirectSettings: {
|
||||
get: function () {
|
||||
return doAllItemsExist(this.settingsData.ips.ir, this.directIPs);
|
||||
},
|
||||
set: function (newValue) {
|
||||
if (newValue) {
|
||||
this.directIPs = [...this.directIPs, ...this.settingsData.ips.ir];
|
||||
} else {
|
||||
this.directIPs = this.directIPs.filter(data => !this.settingsData.ips.ir.includes(data));
|
||||
}
|
||||
}
|
||||
},
|
||||
IRDomainDirectSettings: {
|
||||
get: function () {
|
||||
return doAllItemsExist(this.settingsData.domains.ir, this.directDomains);
|
||||
},
|
||||
set: function (newValue) {
|
||||
if (newValue) {
|
||||
this.directDomains = [...this.directDomains, ...this.settingsData.domains.ir];
|
||||
} else {
|
||||
this.directDomains = this.directDomains.filter(data => !this.settingsData.domains.ir.includes(data));
|
||||
}
|
||||
}
|
||||
},
|
||||
ChinaIpDirectSettings: {
|
||||
get: function () {
|
||||
return doAllItemsExist(this.settingsData.ips.cn, this.directIPs);
|
||||
},
|
||||
set: function (newValue) {
|
||||
if (newValue) {
|
||||
this.directIPs = [...this.directIPs, ...this.settingsData.ips.cn];
|
||||
} else {
|
||||
this.directIPs = this.directIPs.filter(data => !this.settingsData.ips.cn.includes(data));
|
||||
}
|
||||
}
|
||||
},
|
||||
ChinaDomainDirectSettings: {
|
||||
get: function () {
|
||||
return doAllItemsExist(this.settingsData.domains.cn, this.directDomains);
|
||||
},
|
||||
set: function (newValue) {
|
||||
if (newValue) {
|
||||
this.directDomains = [...this.directDomains, ...this.settingsData.domains.cn];
|
||||
} else {
|
||||
this.directDomains = this.directDomains.filter(data => !this.settingsData.domains.cn.includes(data));
|
||||
}
|
||||
}
|
||||
},
|
||||
RussiaIpDirectSettings: {
|
||||
get: function () {
|
||||
return doAllItemsExist(this.settingsData.ips.ru, this.directIPs);
|
||||
},
|
||||
set: function (newValue) {
|
||||
if (newValue) {
|
||||
this.directIPs = [...this.directIPs, ...this.settingsData.ips.ru];
|
||||
} else {
|
||||
this.directIPs = this.directIPs.filter(data => !this.settingsData.ips.ru.includes(data));
|
||||
}
|
||||
}
|
||||
},
|
||||
RussiaDomainDirectSettings: {
|
||||
get: function () {
|
||||
return doAllItemsExist(this.settingsData.domains.ru, this.directDomains);
|
||||
},
|
||||
set: function (newValue) {
|
||||
if (newValue) {
|
||||
this.directDomains = [...this.directDomains, ...this.settingsData.domains.ru];
|
||||
} else {
|
||||
this.directDomains = this.directDomains.filter(data => !this.settingsData.domains.ru.includes(data));
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,7 +1,7 @@
|
||||
package job
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
"x-ui/web/service"
|
||||
|
||||
@@ -24,7 +24,10 @@ func (j *CheckCpuJob) Run() {
|
||||
// get latest status of server
|
||||
percent, err := cpu.Percent(1*time.Second, false)
|
||||
if err == nil && percent[0] > float64(threshold) {
|
||||
msg := fmt.Sprintf("🔴 CPU usage %.2f%% is more than threshold %d%%", percent[0], threshold)
|
||||
msg := j.tgbotService.I18nBot("tgbot.messages.cpuThreshold",
|
||||
"Percent=="+strconv.FormatFloat(percent[0], 'f', 2, 64),
|
||||
"Threshold=="+strconv.Itoa(threshold))
|
||||
|
||||
j.tgbotService.SendMsgToTgbotAdmins(msg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,19 +15,21 @@ func NewCheckInboundJob() *CheckInboundJob {
|
||||
}
|
||||
|
||||
func (j *CheckInboundJob) Run() {
|
||||
count, err := j.inboundService.DisableInvalidClients()
|
||||
needRestart, count, err := j.inboundService.DisableInvalidClients()
|
||||
if err != nil {
|
||||
logger.Warning("disable invalid Client err:", err)
|
||||
logger.Warning("Error in disabling invalid clients:", err)
|
||||
} else if count > 0 {
|
||||
logger.Debugf("disabled %v Client", count)
|
||||
j.xrayService.SetToNeedRestart()
|
||||
logger.Debugf("%v clients disabled", count)
|
||||
if needRestart {
|
||||
j.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
|
||||
count, err = j.inboundService.DisableInvalidInbounds()
|
||||
if err != nil {
|
||||
logger.Warning("disable invalid inbounds err:", err)
|
||||
logger.Warning("Error in disabling invalid inbounds:", err)
|
||||
} else if count > 0 {
|
||||
logger.Debugf("disabled %v inbounds", count)
|
||||
logger.Debugf("%v inbounds disabled", count)
|
||||
j.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
|
||||
144
web/locale/locale.go
Normal file
144
web/locale/locale.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package locale
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"strings"
|
||||
"x-ui/logger"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
var i18nBundle *i18n.Bundle
|
||||
var LocalizerWeb *i18n.Localizer
|
||||
var LocalizerBot *i18n.Localizer
|
||||
|
||||
type I18nType string
|
||||
|
||||
const (
|
||||
Bot I18nType = "bot"
|
||||
Web I18nType = "web"
|
||||
)
|
||||
|
||||
type SettingService interface {
|
||||
GetTgLang() (string, error)
|
||||
}
|
||||
|
||||
func InitLocalizer(i18nFS embed.FS, settingService SettingService) error {
|
||||
// set default bundle to english
|
||||
i18nBundle = i18n.NewBundle(language.English)
|
||||
i18nBundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
|
||||
|
||||
// parse files
|
||||
if err := parseTranslationFiles(i18nFS, i18nBundle); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// setup bot locale
|
||||
if err := initTGBotLocalizer(settingService); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createTemplateData(params []string, seperator ...string) map[string]interface{} {
|
||||
var sep string = "=="
|
||||
if len(seperator) > 0 {
|
||||
sep = seperator[0]
|
||||
}
|
||||
|
||||
templateData := make(map[string]interface{})
|
||||
for _, param := range params {
|
||||
parts := strings.SplitN(param, sep, 2)
|
||||
templateData[parts[0]] = parts[1]
|
||||
}
|
||||
|
||||
return templateData
|
||||
}
|
||||
|
||||
func I18n(i18nType I18nType, key string, params ...string) string {
|
||||
var localizer *i18n.Localizer
|
||||
|
||||
switch i18nType {
|
||||
case "bot":
|
||||
localizer = LocalizerBot
|
||||
case "web":
|
||||
localizer = LocalizerWeb
|
||||
default:
|
||||
logger.Errorf("Invalid type for I18n: %s", i18nType)
|
||||
return ""
|
||||
}
|
||||
|
||||
templateData := createTemplateData(params)
|
||||
|
||||
msg, err := localizer.Localize(&i18n.LocalizeConfig{
|
||||
MessageID: key,
|
||||
TemplateData: templateData,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to localize message: %v", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
return msg
|
||||
}
|
||||
|
||||
func initTGBotLocalizer(settingService SettingService) error {
|
||||
botLang, err := settingService.GetTgLang()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
LocalizerBot = i18n.NewLocalizer(i18nBundle, botLang)
|
||||
return nil
|
||||
}
|
||||
|
||||
func LocalizerMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var lang string
|
||||
|
||||
if cookie, err := c.Request.Cookie("lang"); err == nil {
|
||||
lang = cookie.Value
|
||||
} else {
|
||||
lang = c.GetHeader("Accept-Language")
|
||||
}
|
||||
|
||||
LocalizerWeb = i18n.NewLocalizer(i18nBundle, lang)
|
||||
|
||||
c.Set("localizer", LocalizerWeb)
|
||||
c.Set("I18n", I18n)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func parseTranslationFiles(i18nFS embed.FS, i18nBundle *i18n.Bundle) error {
|
||||
err := fs.WalkDir(i18nFS, "translation",
|
||||
func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
data, err := i18nFS.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = i18nBundle.ParseMessageFileBytes(data, path)
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
21
web/middleware/domainValidator.go
Normal file
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()
|
||||
}
|
||||
}
|
||||
@@ -2,24 +2,19 @@
|
||||
"log": {
|
||||
"loglevel": "warning"
|
||||
},
|
||||
|
||||
"api": {
|
||||
"services": [
|
||||
"HandlerService",
|
||||
"LoggerService",
|
||||
"StatsService"
|
||||
],
|
||||
"tag": "api"
|
||||
"tag": "api",
|
||||
"services": ["HandlerService", "LoggerService", "StatsService"]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"tag": "api",
|
||||
"listen": "127.0.0.1",
|
||||
"port": 62789,
|
||||
"protocol": "dokodemo-door",
|
||||
"settings": {
|
||||
"address": "127.0.0.1"
|
||||
},
|
||||
"tag": "api"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
@@ -28,16 +23,16 @@
|
||||
"settings": {}
|
||||
},
|
||||
{
|
||||
"tag": "blocked",
|
||||
"protocol": "blackhole",
|
||||
"settings": {},
|
||||
"tag": "blocked"
|
||||
"settings": {}
|
||||
}
|
||||
],
|
||||
"policy": {
|
||||
"levels": {
|
||||
"0": {
|
||||
"statsUserUplink": true,
|
||||
"statsUserDownlink": true
|
||||
"statsUserDownlink": true,
|
||||
"statsUserUplink": true
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
@@ -46,27 +41,22 @@
|
||||
}
|
||||
},
|
||||
"routing": {
|
||||
"domainStrategy": "IPIfNonMatch",
|
||||
"rules": [
|
||||
{
|
||||
"inboundTag": [
|
||||
"api"
|
||||
],
|
||||
"outboundTag": "api",
|
||||
"type": "field"
|
||||
"type": "field",
|
||||
"inboundTag": ["api"],
|
||||
"outboundTag": "api"
|
||||
},
|
||||
{
|
||||
"ip": [
|
||||
"geoip:private"
|
||||
],
|
||||
"type": "field",
|
||||
"outboundTag": "blocked",
|
||||
"type": "field"
|
||||
"ip": ["geoip:private"]
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"outboundTag": "blocked",
|
||||
"protocol": [
|
||||
"bittorrent"
|
||||
],
|
||||
"type": "field"
|
||||
"protocol": ["bittorrent"]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,11 +7,17 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
"x-ui/config"
|
||||
"x-ui/database"
|
||||
"x-ui/logger"
|
||||
"x-ui/util/common"
|
||||
"x-ui/util/sys"
|
||||
"x-ui/xray"
|
||||
|
||||
@@ -70,7 +76,8 @@ type Release struct {
|
||||
}
|
||||
|
||||
type ServerService struct {
|
||||
xrayService XrayService
|
||||
xrayService XrayService
|
||||
inboundService InboundService
|
||||
}
|
||||
|
||||
func (s *ServerService) GetStatus(lastStatus *Status) *Status {
|
||||
@@ -191,33 +198,35 @@ 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.8.0" {
|
||||
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)
|
||||
return err
|
||||
}
|
||||
err := s.xrayService.StopXray()
|
||||
if err != nil {
|
||||
logger.Error("stop xray failed:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ServerService) RestartXrayService() (string error) {
|
||||
|
||||
s.xrayService.StopXray()
|
||||
defer func() {
|
||||
err := s.xrayService.RestartXray(true)
|
||||
if err != nil {
|
||||
logger.Error("start xray failed:", err)
|
||||
s.xrayService.StopXray()
|
||||
defer func() {
|
||||
err := s.xrayService.RestartXray(true)
|
||||
if err != nil {
|
||||
logger.Error("start xray failed:", err)
|
||||
}
|
||||
}()
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -324,3 +333,192 @@ func (s *ServerService) UpdateXray(version string) error {
|
||||
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}
|
||||
} else {
|
||||
return []string{"Unsupported operating system"}, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func (s *ServerService) GetConfigJson() (interface{}, error) {
|
||||
// Open the file for reading
|
||||
file, err := os.Open(xray.GetConfigPath())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Read the file contents
|
||||
fileContents, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var jsonData interface{}
|
||||
err = json.Unmarshal(fileContents, &jsonData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return jsonData, nil
|
||||
}
|
||||
|
||||
func (s *ServerService) GetDb() ([]byte, error) {
|
||||
// Open the file for reading
|
||||
file, err := os.Open(config.GetDBPath())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Read the file contents
|
||||
fileContents, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return fileContents, nil
|
||||
}
|
||||
|
||||
func (s *ServerService) ImportDB(file multipart.File) error {
|
||||
// Check if the file is a SQLite database
|
||||
isValidDb, err := database.IsSQLiteDB(file)
|
||||
if err != nil {
|
||||
return common.NewErrorf("Error checking db file format: %v", err)
|
||||
}
|
||||
if !isValidDb {
|
||||
return common.NewError("Invalid db file format")
|
||||
}
|
||||
|
||||
// Reset the file reader to the beginning
|
||||
_, err = file.Seek(0, 0)
|
||||
if err != nil {
|
||||
return common.NewErrorf("Error resetting file reader: %v", err)
|
||||
}
|
||||
|
||||
// Save the file as temporary file
|
||||
tempPath := fmt.Sprintf("%s.temp", config.GetDBPath())
|
||||
// Remove the existing fallback file (if any) before creating one
|
||||
_, err = os.Stat(tempPath)
|
||||
if err == nil {
|
||||
errRemove := os.Remove(tempPath)
|
||||
if errRemove != nil {
|
||||
return common.NewErrorf("Error removing existing temporary db file: %v", errRemove)
|
||||
}
|
||||
}
|
||||
// Create the temporary file
|
||||
tempFile, err := os.Create(tempPath)
|
||||
if err != nil {
|
||||
return common.NewErrorf("Error creating temporary db file: %v", err)
|
||||
}
|
||||
defer tempFile.Close()
|
||||
|
||||
// Remove temp file before returning
|
||||
defer os.Remove(tempPath)
|
||||
|
||||
// Save uploaded file to temporary file
|
||||
_, err = io.Copy(tempFile, file)
|
||||
if err != nil {
|
||||
return common.NewErrorf("Error saving db: %v", err)
|
||||
}
|
||||
|
||||
// Check if we can init db or not
|
||||
err = database.InitDB(tempPath)
|
||||
if err != nil {
|
||||
return common.NewErrorf("Error checking db: %v", err)
|
||||
}
|
||||
|
||||
// Stop Xray
|
||||
s.StopXrayService()
|
||||
|
||||
// Backup the current database for fallback
|
||||
fallbackPath := fmt.Sprintf("%s.backup", config.GetDBPath())
|
||||
// Remove the existing fallback file (if any)
|
||||
_, err = os.Stat(fallbackPath)
|
||||
if err == nil {
|
||||
errRemove := os.Remove(fallbackPath)
|
||||
if errRemove != nil {
|
||||
return common.NewErrorf("Error removing existing fallback db file: %v", errRemove)
|
||||
}
|
||||
}
|
||||
// Move the current database to the fallback location
|
||||
err = os.Rename(config.GetDBPath(), fallbackPath)
|
||||
if err != nil {
|
||||
return common.NewErrorf("Error backing up temporary db file: %v", err)
|
||||
}
|
||||
|
||||
// Remove the temporary file before returning
|
||||
defer os.Remove(fallbackPath)
|
||||
|
||||
// Move temp to DB path
|
||||
err = os.Rename(tempPath, config.GetDBPath())
|
||||
if err != nil {
|
||||
errRename := os.Rename(fallbackPath, config.GetDBPath())
|
||||
if errRename != nil {
|
||||
return common.NewErrorf("Error moving db file and restoring fallback: %v", errRename)
|
||||
}
|
||||
return common.NewErrorf("Error moving db file: %v", err)
|
||||
}
|
||||
|
||||
// Migrate DB
|
||||
err = database.InitDB(config.GetDBPath())
|
||||
if err != nil {
|
||||
errRename := os.Rename(fallbackPath, config.GetDBPath())
|
||||
if errRename != nil {
|
||||
return common.NewErrorf("Error migrating db and restoring fallback: %v", errRename)
|
||||
}
|
||||
return common.NewErrorf("Error migrating db: %v", err)
|
||||
}
|
||||
s.inboundService.MigrateDB()
|
||||
|
||||
// Start Xray
|
||||
err = s.RestartXrayService()
|
||||
if err != nil {
|
||||
return common.NewErrorf("Imported DB but Failed to start Xray: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ServerService) GetNewX25519Cert() (interface{}, error) {
|
||||
// Run the command
|
||||
cmd := exec.Command(xray.GetBinaryPath(), "x25519")
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lines := strings.Split(out.String(), "\n")
|
||||
|
||||
privateKeyLine := strings.Split(lines[0], ":")
|
||||
publicKeyLine := strings.Split(lines[1], ":")
|
||||
|
||||
privateKey := strings.TrimSpace(privateKeyLine[1])
|
||||
publicKey := strings.TrimSpace(publicKeyLine[1])
|
||||
|
||||
keyPair := map[string]interface{}{
|
||||
"privateKey": privateKey,
|
||||
"publicKey": publicKey,
|
||||
}
|
||||
|
||||
return keyPair, nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
@@ -23,20 +24,31 @@ var xrayTemplateConfig string
|
||||
var defaultValueMap = map[string]string{
|
||||
"xrayTemplateConfig": xrayTemplateConfig,
|
||||
"webListen": "",
|
||||
"webDomain": "",
|
||||
"webPort": "54321",
|
||||
"webCertFile": "",
|
||||
"webKeyFile": "",
|
||||
"secret": random.Seq(32),
|
||||
"webBasePath": "/",
|
||||
"sessionMaxAge": "0",
|
||||
"expireDiff": "0",
|
||||
"trafficDiff": "0",
|
||||
"timeLocation": "Asia/Tehran",
|
||||
"tgBotEnable": "false",
|
||||
"tgBotToken": "",
|
||||
"tgBotChatId": "",
|
||||
"tgRunTime": "@daily",
|
||||
"tgBotBackup": "false",
|
||||
"tgExpireDiff": "0",
|
||||
"tgTrafficDiff": "0",
|
||||
"tgCpu": "0",
|
||||
"tgLang": "en-US",
|
||||
"subEnable": "false",
|
||||
"subListen": "",
|
||||
"subPort": "2096",
|
||||
"subPath": "/sub/",
|
||||
"subDomain": "",
|
||||
"subCertFile": "",
|
||||
"subKeyFile": "",
|
||||
"subUpdates": "12",
|
||||
}
|
||||
|
||||
type SettingService struct {
|
||||
@@ -198,6 +210,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")
|
||||
}
|
||||
@@ -234,32 +250,12 @@ func (s *SettingService) GetTgBotBackup() (bool, error) {
|
||||
return s.getBool("tgBotBackup")
|
||||
}
|
||||
|
||||
func (s *SettingService) SetTgBotBackup(value bool) error {
|
||||
return s.setBool("tgBotBackup", value)
|
||||
}
|
||||
|
||||
func (s *SettingService) GetTgExpireDiff() (int, error) {
|
||||
return s.getInt("tgExpireDiff")
|
||||
}
|
||||
|
||||
func (s *SettingService) SetTgExpireDiff(value int) error {
|
||||
return s.setInt("tgExpireDiff", value)
|
||||
}
|
||||
|
||||
func (s *SettingService) GetTgTrafficDiff() (int, error) {
|
||||
return s.getInt("tgTrafficDiff")
|
||||
}
|
||||
|
||||
func (s *SettingService) SetTgTrafficDiff(value int) error {
|
||||
return s.setInt("tgTrafficDiff", value)
|
||||
}
|
||||
|
||||
func (s *SettingService) GetTgCpu() (int, error) {
|
||||
return s.getInt("tgCpu")
|
||||
}
|
||||
|
||||
func (s *SettingService) SetTgCpu(value int) error {
|
||||
return s.setInt("tgCpu", value)
|
||||
func (s *SettingService) GetTgLang() (string, error) {
|
||||
return s.getString("tgLang")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetPort() (int, error) {
|
||||
@@ -278,6 +274,18 @@ func (s *SettingService) GetKeyFile() (string, error) {
|
||||
return s.getString("webKeyFile")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetExpireDiff() (int, error) {
|
||||
return s.getInt("expireDiff")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetTrafficDiff() (int, error) {
|
||||
return s.getInt("trafficDiff")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSessionMaxAge() (int, error) {
|
||||
return s.getInt("sessionMaxAge")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSecret() ([]byte, error) {
|
||||
secret, err := s.getString("secret")
|
||||
if secret == defaultValueMap["secret"] {
|
||||
@@ -317,6 +325,48 @@ func (s *SettingService) GetTimeLocation() (*time.Location, error) {
|
||||
return location, nil
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubEnable() (bool, error) {
|
||||
return s.getBool("subEnable")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubListen() (string, error) {
|
||||
return s.getString("subListen")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubPort() (int, error) {
|
||||
return s.getInt("subPort")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubPath() (string, error) {
|
||||
subPath, err := s.getString("subPath")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !strings.HasPrefix(subPath, "/") {
|
||||
subPath = "/" + subPath
|
||||
}
|
||||
if !strings.HasSuffix(subPath, "/") {
|
||||
subPath += "/"
|
||||
}
|
||||
return subPath, nil
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubDomain() (string, error) {
|
||||
return s.getString("subDomain")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubCertFile() (string, error) {
|
||||
return s.getString("subCertFile")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubKeyFile() (string, error) {
|
||||
return s.getString("subKeyFile")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubUpdates() (int, error) {
|
||||
return s.getInt("subUpdates")
|
||||
}
|
||||
|
||||
func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error {
|
||||
if err := allSetting.CheckValid(); err != nil {
|
||||
return err
|
||||
@@ -337,3 +387,12 @@ func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error {
|
||||
}
|
||||
return common.Combine(errs...)
|
||||
}
|
||||
|
||||
func (s *SettingService) GetDefaultXrayConfig() (interface{}, error) {
|
||||
var jsonData interface{}
|
||||
err := json.Unmarshal([]byte(xrayTemplateConfig), &jsonData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return jsonData, nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
"x-ui/database/model"
|
||||
"x-ui/logger"
|
||||
"x-ui/util/common"
|
||||
"x-ui/web/locale"
|
||||
"x-ui/xray"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
@@ -19,6 +21,7 @@ import (
|
||||
var bot *tgbotapi.BotAPI
|
||||
var adminIds []int64
|
||||
var isRunning bool
|
||||
var hostname string
|
||||
|
||||
type LoginStatus byte
|
||||
|
||||
@@ -38,7 +41,17 @@ func (t *Tgbot) NewTgbot() *Tgbot {
|
||||
return new(Tgbot)
|
||||
}
|
||||
|
||||
func (t *Tgbot) Start() error {
|
||||
func (t *Tgbot) I18nBot(name string, params ...string) string {
|
||||
return locale.I18n(locale.Bot, name, params...)
|
||||
}
|
||||
|
||||
func (t *Tgbot) Start(i18nFS embed.FS) error {
|
||||
err := locale.InitLocalizer(i18nFS, &t.settingService)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.SetHostname()
|
||||
tgBottoken, err := t.settingService.GetTgBotToken()
|
||||
if err != nil || tgBottoken == "" {
|
||||
logger.Warning("Get TgBotToken failed:", err)
|
||||
@@ -51,13 +64,15 @@ func (t *Tgbot) Start() 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 = tgbotapi.NewBotAPI(tgBottoken)
|
||||
@@ -77,10 +92,20 @@ func (t *Tgbot) Start() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Tgbot) IsRunnging() bool {
|
||||
func (t *Tgbot) IsRunning() bool {
|
||||
return isRunning
|
||||
}
|
||||
|
||||
func (t *Tgbot) SetHostname() {
|
||||
host, err := os.Hostname()
|
||||
if err != nil {
|
||||
logger.Error("get hostname error:", err)
|
||||
hostname = ""
|
||||
return
|
||||
}
|
||||
hostname = host
|
||||
}
|
||||
|
||||
func (t *Tgbot) Stop() {
|
||||
bot.StopReceivingUpdates()
|
||||
logger.Info("Stop Telegram receiver ...")
|
||||
@@ -111,34 +136,52 @@ func (t *Tgbot) OnReceive() {
|
||||
}
|
||||
|
||||
func (t *Tgbot) answerCommand(message *tgbotapi.Message, chatId int64, isAdmin bool) {
|
||||
msg := ""
|
||||
msg, onlyMessage := "", false
|
||||
|
||||
command, commandArgs := message.Command(), message.CommandArguments()
|
||||
|
||||
// Extract the command from the Message.
|
||||
switch message.Command() {
|
||||
switch command {
|
||||
case "help":
|
||||
msg = "This bot is providing you some specefic data from the server.\n\n Please choose:"
|
||||
msg += t.I18nBot("tgbot.commands.help")
|
||||
msg += t.I18nBot("tgbot.commands.pleaseChoose")
|
||||
case "start":
|
||||
msg = "Hello <i>" + message.From.FirstName + "</i> 👋"
|
||||
msg += t.I18nBot("tgbot.commands.start", "Firstname=="+message.From.FirstName)
|
||||
if isAdmin {
|
||||
hostname, _ := os.Hostname()
|
||||
msg += "\nWelcome to <b>" + hostname + "</b> management bot"
|
||||
msg += t.I18nBot("tgbot.commands.welcome", "Hostname=="+hostname)
|
||||
}
|
||||
msg += "\n\nI can do some magics for you, please choose:"
|
||||
msg += "\n\n" + t.I18nBot("tgbot.commands.pleaseChoose")
|
||||
case "status":
|
||||
msg = "bot is ok ✅"
|
||||
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":
|
||||
if isAdmin {
|
||||
t.searchClient(chatId, message.CommandArguments())
|
||||
onlyMessage = true
|
||||
if len(commandArgs) > 1 {
|
||||
if isAdmin {
|
||||
t.searchClient(chatId, commandArgs)
|
||||
} else {
|
||||
t.searchForClient(chatId, commandArgs)
|
||||
}
|
||||
} else {
|
||||
t.searchForClient(chatId, message.CommandArguments())
|
||||
msg += t.I18nBot("tgbot.commands.usage")
|
||||
}
|
||||
case "inbound":
|
||||
onlyMessage = true
|
||||
if isAdmin {
|
||||
t.searchInbound(chatId, message.CommandArguments())
|
||||
t.searchInbound(chatId, commandArgs)
|
||||
} else {
|
||||
msg = "❗ Unknown command"
|
||||
msg += t.I18nBot("tgbot.commands.unknown")
|
||||
}
|
||||
default:
|
||||
msg = "❗ Unknown command"
|
||||
msg += t.I18nBot("tgbot.commands.unknown")
|
||||
}
|
||||
|
||||
if onlyMessage {
|
||||
t.SendMsgToTgbot(chatId, msg)
|
||||
return
|
||||
}
|
||||
t.SendAnswer(chatId, msg, isAdmin)
|
||||
}
|
||||
@@ -156,16 +199,16 @@ func (t *Tgbot) asnwerCallback(callbackQuery *tgbotapi.CallbackQuery, isAdmin bo
|
||||
t.SendMsgToTgbot(callbackQuery.From.ID, t.getServerUsage())
|
||||
case "inbounds":
|
||||
t.SendMsgToTgbot(callbackQuery.From.ID, t.getInboundUsages())
|
||||
case "exhausted_soon":
|
||||
case "deplete_soon":
|
||||
t.SendMsgToTgbot(callbackQuery.From.ID, t.getExhausted())
|
||||
case "get_backup":
|
||||
t.sendBackup(callbackQuery.From.ID)
|
||||
case "client_traffic":
|
||||
t.getClientUsage(callbackQuery.From.ID, callbackQuery.From.UserName)
|
||||
case "client_commands":
|
||||
t.SendMsgToTgbot(callbackQuery.From.ID, "To search for statistics, just use folowing command:\r\n \r\n<code>/usage [UID|Passowrd]</code>\r\n \r\nUse UID for vmess and vless and Password for Trojan.")
|
||||
t.SendMsgToTgbot(callbackQuery.From.ID, t.I18nBot("tgbot.commands.helpClientCommands"))
|
||||
case "commands":
|
||||
t.SendMsgToTgbot(callbackQuery.From.ID, "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>")
|
||||
t.SendMsgToTgbot(callbackQuery.From.ID, t.I18nBot("tgbot.commands.helpAdminCommands"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,45 +222,53 @@ func checkAdmin(tgId int64) bool {
|
||||
}
|
||||
|
||||
func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) {
|
||||
var numericKeyboard = tgbotapi.NewInlineKeyboardMarkup(
|
||||
numericKeyboard := tgbotapi.NewInlineKeyboardMarkup(
|
||||
tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("Server Usage", "get_usage"),
|
||||
tgbotapi.NewInlineKeyboardButtonData("Get DB Backup", "get_backup"),
|
||||
tgbotapi.NewInlineKeyboardButtonData(t.I18nBot("tgbot.buttons.serverUsage"), "get_usage"),
|
||||
tgbotapi.NewInlineKeyboardButtonData(t.I18nBot("tgbot.buttons.dbBackup"), "get_backup"),
|
||||
),
|
||||
tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("Get Inbounds", "inbounds"),
|
||||
tgbotapi.NewInlineKeyboardButtonData("Exhausted soon", "exhausted_soon"),
|
||||
tgbotapi.NewInlineKeyboardButtonData(t.I18nBot("tgbot.buttons.getInbounds"), "inbounds"),
|
||||
tgbotapi.NewInlineKeyboardButtonData(t.I18nBot("tgbot.buttons.depleteSoon"), "deplete_soon"),
|
||||
),
|
||||
tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("Commands", "commands"),
|
||||
tgbotapi.NewInlineKeyboardButtonData(t.I18nBot("tgbot.buttons.commands"), "commands"),
|
||||
),
|
||||
)
|
||||
var numericKeyboardClient = tgbotapi.NewInlineKeyboardMarkup(
|
||||
numericKeyboardClient := tgbotapi.NewInlineKeyboardMarkup(
|
||||
tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("Get Usage", "client_traffic"),
|
||||
tgbotapi.NewInlineKeyboardButtonData("Commands", "client_commands"),
|
||||
tgbotapi.NewInlineKeyboardButtonData(t.I18nBot("tgbot.buttons.clientUsage"), "client_traffic"),
|
||||
tgbotapi.NewInlineKeyboardButtonData(t.I18nBot("tgbot.buttons.commands"), "client_commands"),
|
||||
),
|
||||
)
|
||||
msgConfig := tgbotapi.NewMessage(chatId, msg)
|
||||
msgConfig.ParseMode = "HTML"
|
||||
|
||||
var keyboardMarkup tgbotapi.InlineKeyboardMarkup
|
||||
if isAdmin {
|
||||
msgConfig.ReplyMarkup = numericKeyboard
|
||||
keyboardMarkup = numericKeyboard
|
||||
} else {
|
||||
msgConfig.ReplyMarkup = numericKeyboardClient
|
||||
}
|
||||
_, err := bot.Send(msgConfig)
|
||||
if err != nil {
|
||||
logger.Warning("Error sending telegram message :", err)
|
||||
keyboardMarkup = numericKeyboardClient
|
||||
}
|
||||
t.SendMsgToTgbot(chatId, msg, keyboardMarkup)
|
||||
}
|
||||
|
||||
func (t *Tgbot) SendMsgToTgbot(tgid int64, msg string) {
|
||||
func (t *Tgbot) SendMsgToTgbot(tgid int64, msg string, replyMarkup ...tgbotapi.InlineKeyboardMarkup) {
|
||||
if !isRunning {
|
||||
return
|
||||
}
|
||||
|
||||
if msg == "" {
|
||||
logger.Info("[tgbot] message is empty!")
|
||||
return
|
||||
}
|
||||
|
||||
var allMessages []string
|
||||
limit := 2000
|
||||
|
||||
// paging message if it is big
|
||||
if len(msg) > limit {
|
||||
messages := strings.Split(msg, "\r\n \r\n")
|
||||
lastIndex := -1
|
||||
|
||||
for _, message := range messages {
|
||||
if (len(allMessages) == 0) || (len(allMessages[lastIndex])+len(message) > limit) {
|
||||
allMessages = append(allMessages, message)
|
||||
@@ -232,6 +283,9 @@ func (t *Tgbot) SendMsgToTgbot(tgid int64, msg string) {
|
||||
for _, message := range allMessages {
|
||||
info := tgbotapi.NewMessage(tgid, message)
|
||||
info.ParseMode = "HTML"
|
||||
if len(replyMarkup) > 0 {
|
||||
info.ReplyMarkup = replyMarkup[0]
|
||||
}
|
||||
_, err := bot.Send(info)
|
||||
if err != nil {
|
||||
logger.Warning("Error sending telegram message :", err)
|
||||
@@ -249,36 +303,44 @@ func (t *Tgbot) SendMsgToTgbotAdmins(msg string) {
|
||||
func (t *Tgbot) SendReport() {
|
||||
runTime, err := t.settingService.GetTgbotRuntime()
|
||||
if err == nil && len(runTime) > 0 {
|
||||
t.SendMsgToTgbotAdmins("🕰 Scheduled reports: " + runTime + "\r\nDate-Time: " + time.Now().Format("2006-01-02 15:04:05"))
|
||||
msg := ""
|
||||
msg += t.I18nBot("tgbot.messages.report", "RunTime=="+runTime)
|
||||
msg += t.I18nBot("tgbot.messages.datetime", "DateTime=="+time.Now().Format("2006-01-02 15:04:05"))
|
||||
t.SendMsgToTgbotAdmins(msg)
|
||||
}
|
||||
|
||||
info := t.getServerUsage()
|
||||
t.SendMsgToTgbotAdmins(info)
|
||||
|
||||
exhausted := t.getExhausted()
|
||||
t.SendMsgToTgbotAdmins(exhausted)
|
||||
|
||||
backupEnable, err := t.settingService.GetTgBotBackup()
|
||||
if err == nil && backupEnable {
|
||||
for _, adminId := range adminIds {
|
||||
t.sendBackup(int64(adminId))
|
||||
}
|
||||
t.SendBackupToAdmins()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tgbot) SendBackupToAdmins() {
|
||||
if !t.IsRunning() {
|
||||
return
|
||||
}
|
||||
for _, adminId := range adminIds {
|
||||
t.sendBackup(int64(adminId))
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tgbot) getServerUsage() string {
|
||||
var info string
|
||||
//get hostname
|
||||
name, err := os.Hostname()
|
||||
if err != nil {
|
||||
logger.Error("get hostname error:", err)
|
||||
name = ""
|
||||
}
|
||||
info = fmt.Sprintf("💻 Hostname: %s\r\n", name)
|
||||
//get ip address
|
||||
var ip string
|
||||
var ipv6 string
|
||||
info, ipv4, ipv6 := "", "", ""
|
||||
info += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname)
|
||||
info += t.I18nBot("tgbot.messages.version", "Version=="+config.GetVersion())
|
||||
|
||||
// get ip address
|
||||
netInterfaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
logger.Error("net.Interfaces failed, err:", err.Error())
|
||||
info += "🌐 IP: Unknown\r\n \r\n"
|
||||
logger.Error("net.Interfaces failed, err: ", err.Error())
|
||||
info += t.I18nBot("tgbot.messages.ip", "IP=="+t.I18nBot("tgbot.unknown"))
|
||||
info += " \r\n"
|
||||
} else {
|
||||
for i := 0; i < len(netInterfaces); i++ {
|
||||
if (netInterfaces[i].Flags & net.FlagUp) != 0 {
|
||||
@@ -287,7 +349,7 @@ func (t *Tgbot) getServerUsage() string {
|
||||
for _, address := range addrs {
|
||||
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
|
||||
if ipnet.IP.To4() != nil {
|
||||
ip += ipnet.IP.String() + " "
|
||||
ipv4 += ipnet.IP.String() + " "
|
||||
} else if ipnet.IP.To16() != nil && !ipnet.IP.IsLinkLocalUnicast() {
|
||||
ipv6 += ipnet.IP.String() + " "
|
||||
}
|
||||
@@ -295,42 +357,45 @@ func (t *Tgbot) getServerUsage() string {
|
||||
}
|
||||
}
|
||||
}
|
||||
info += fmt.Sprintf("🌐IP: %s\r\n🌐IPv6: %s\r\n", ip, ipv6)
|
||||
|
||||
info += t.I18nBot("tgbot.messages.ipv4", "IPv4=="+ipv4)
|
||||
info += t.I18nBot("tgbot.messages.ipv6", "IPv6=="+ipv6)
|
||||
}
|
||||
|
||||
// get latest status of server
|
||||
t.lastStatus = t.serverService.GetStatus(t.lastStatus)
|
||||
info += fmt.Sprintf("🔌Server Uptime: %d days\r\n", int(t.lastStatus.Uptime/86400))
|
||||
info += fmt.Sprintf("📈Server Load: %.1f, %.1f, %.1f\r\n", t.lastStatus.Loads[0], t.lastStatus.Loads[1], t.lastStatus.Loads[2])
|
||||
info += fmt.Sprintf("📋Server Memory: %s/%s\r\n", common.FormatTraffic(int64(t.lastStatus.Mem.Current)), common.FormatTraffic(int64(t.lastStatus.Mem.Total)))
|
||||
info += fmt.Sprintf("🔹TcpCount: %d\r\n", t.lastStatus.TcpCount)
|
||||
info += fmt.Sprintf("🔸UdpCount: %d\r\n", t.lastStatus.UdpCount)
|
||||
info += fmt.Sprintf("🚦Traffic: %s (↑%s,↓%s)\r\n", common.FormatTraffic(int64(t.lastStatus.NetTraffic.Sent+t.lastStatus.NetTraffic.Recv)), common.FormatTraffic(int64(t.lastStatus.NetTraffic.Sent)), common.FormatTraffic(int64(t.lastStatus.NetTraffic.Recv)))
|
||||
info += fmt.Sprintf("ℹXray status: %s", t.lastStatus.Xray.State)
|
||||
info += t.I18nBot("tgbot.messages.serverUpTime", "UpTime=="+strconv.FormatUint(t.lastStatus.Uptime/86400, 10), "Unit=="+t.I18nBot("tgbot.days"))
|
||||
info += t.I18nBot("tgbot.messages.serverLoad", "Load1=="+strconv.FormatFloat(t.lastStatus.Loads[0], 'f', 2, 64), "Load2=="+strconv.FormatFloat(t.lastStatus.Loads[1], 'f', 2, 64), "Load3=="+strconv.FormatFloat(t.lastStatus.Loads[2], 'f', 2, 64))
|
||||
info += t.I18nBot("tgbot.messages.serverMemory", "Current=="+common.FormatTraffic(int64(t.lastStatus.Mem.Current)), "Total=="+common.FormatTraffic(int64(t.lastStatus.Mem.Total)))
|
||||
info += t.I18nBot("tgbot.messages.tcpCount", "Count=="+strconv.Itoa(t.lastStatus.TcpCount))
|
||||
info += t.I18nBot("tgbot.messages.udpCount", "Count=="+strconv.Itoa(t.lastStatus.UdpCount))
|
||||
info += t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic(int64(t.lastStatus.NetTraffic.Sent+t.lastStatus.NetTraffic.Recv)), "Upload=="+common.FormatTraffic(int64(t.lastStatus.NetTraffic.Sent)), "Download=="+common.FormatTraffic(int64(t.lastStatus.NetTraffic.Recv)))
|
||||
info += t.I18nBot("tgbot.messages.xrayStatus", "State=="+fmt.Sprint(t.lastStatus.Xray.State))
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
func (t *Tgbot) UserLoginNotify(username string, ip string, time string, status LoginStatus) {
|
||||
if !t.IsRunning() {
|
||||
return
|
||||
}
|
||||
|
||||
if username == "" || ip == "" || time == "" {
|
||||
logger.Warning("UserLoginNotify failed,invalid info")
|
||||
return
|
||||
}
|
||||
var msg string
|
||||
// Get hostname
|
||||
name, err := os.Hostname()
|
||||
if err != nil {
|
||||
logger.Warning("get hostname error:", err)
|
||||
return
|
||||
}
|
||||
|
||||
msg := ""
|
||||
if status == LoginSuccess {
|
||||
msg = fmt.Sprintf("✅ Successfully logged-in to the panel\r\nHostname:%s\r\n", name)
|
||||
msg += t.I18nBot("tgbot.messages.loginSuccess")
|
||||
} else if status == LoginFail {
|
||||
msg = fmt.Sprintf("❗ Login to the panel was unsuccessful\r\nHostname:%s\r\n", name)
|
||||
msg += t.I18nBot("tgbot.messages.loginFailed")
|
||||
}
|
||||
msg += fmt.Sprintf("⏰ Time:%s\r\n", time)
|
||||
msg += fmt.Sprintf("🆔 Username:%s\r\n", username)
|
||||
msg += fmt.Sprintf("🌐 IP:%s\r\n", ip)
|
||||
msg += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname)
|
||||
msg += t.I18nBot("tgbot.messages.username", "Username=="+username)
|
||||
msg += t.I18nBot("tgbot.messages.ip", "IP=="+ip)
|
||||
msg += t.I18nBot("tgbot.messages.time", "Time=="+time)
|
||||
|
||||
t.SendMsgToTgbotAdmins(msg)
|
||||
}
|
||||
|
||||
@@ -340,17 +405,19 @@ func (t *Tgbot) getInboundUsages() string {
|
||||
inbouds, err := t.inboundService.GetAllInbounds()
|
||||
if err != nil {
|
||||
logger.Warning("GetAllInbounds run failed:", err)
|
||||
info += "❌ Failed to get inbounds"
|
||||
info += t.I18nBot("tgbot.answers.getInboundsFailed")
|
||||
} else {
|
||||
// NOTE:If there no any sessions here,need to notify here
|
||||
// TODO:Sub-node push, automatic conversion format
|
||||
for _, inbound := range inbouds {
|
||||
info += fmt.Sprintf("📍Inbound:%s\r\nPort:%d\r\n", inbound.Remark, inbound.Port)
|
||||
info += fmt.Sprintf("Traffic: %s (↑%s,↓%s)\r\n", common.FormatTraffic((inbound.Up + inbound.Down)), common.FormatTraffic(inbound.Up), common.FormatTraffic(inbound.Down))
|
||||
info += t.I18nBot("tgbot.messages.inbound", "Remark=="+inbound.Remark)
|
||||
info += t.I18nBot("tgbot.messages.port", "Port=="+strconv.Itoa(inbound.Port))
|
||||
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 += "Expire date: ♾ Unlimited\r\n \r\n"
|
||||
info += t.I18nBot("tgbot.messages.expire", "DateTime=="+t.I18nBot("tgbot.unlimited"))
|
||||
} else {
|
||||
info += fmt.Sprintf("Expire date:%s\r\n \r\n", time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
|
||||
info += t.I18nBot("tgbot.messages.expire", "DateTime=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -358,105 +425,149 @@ func (t *Tgbot) getInboundUsages() string {
|
||||
}
|
||||
|
||||
func (t *Tgbot) getClientUsage(chatId int64, tgUserName string) {
|
||||
if len(tgUserName) == 0 {
|
||||
msg := t.I18nBot("tgbot.answers.askToAddUser")
|
||||
t.SendMsgToTgbot(chatId, msg)
|
||||
return
|
||||
}
|
||||
|
||||
traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserName)
|
||||
if err != nil {
|
||||
logger.Warning(err)
|
||||
msg := "❌ Something went wrong!"
|
||||
msg := t.I18nBot("tgbot.wentWrong")
|
||||
t.SendMsgToTgbot(chatId, msg)
|
||||
return
|
||||
}
|
||||
if len(traffics) == 0 {
|
||||
msg := "Your configuration is not found!\nPlease ask your Admin to use your telegram username in your configuration(s).\n\nYour username: <b>@" + tgUserName + "</b>"
|
||||
msg := t.I18nBot("tgbot.answers.askToAddUserName", "TgUserName=="+tgUserName)
|
||||
t.SendMsgToTgbot(chatId, msg)
|
||||
return
|
||||
}
|
||||
|
||||
for _, traffic := range traffics {
|
||||
expiryTime := ""
|
||||
if traffic.ExpiryTime == 0 {
|
||||
expiryTime = "♾Unlimited"
|
||||
expiryTime = t.I18nBot("tgbot.unlimited")
|
||||
} else if traffic.ExpiryTime < 0 {
|
||||
expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
|
||||
} else {
|
||||
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
total := ""
|
||||
if traffic.Total == 0 {
|
||||
total = "♾Unlimited"
|
||||
total = t.I18nBot("tgbot.unlimited")
|
||||
} else {
|
||||
total = common.FormatTraffic((traffic.Total))
|
||||
}
|
||||
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
|
||||
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
|
||||
total, expiryTime)
|
||||
|
||||
output := ""
|
||||
output += t.I18nBot("tgbot.messages.active", "Enable=="+strconv.FormatBool(traffic.Enable))
|
||||
output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
|
||||
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)
|
||||
|
||||
t.SendMsgToTgbot(chatId, output)
|
||||
}
|
||||
t.SendAnswer(chatId, "Please choose:", false)
|
||||
t.SendAnswer(chatId, t.I18nBot("tgbot.commands.pleaseChoose"), false)
|
||||
}
|
||||
|
||||
func (t *Tgbot) searchClient(chatId int64, email string) {
|
||||
traffics, err := t.inboundService.GetClientTrafficByEmail(email)
|
||||
traffic, err := t.inboundService.GetClientTrafficByEmail(email)
|
||||
if err != nil {
|
||||
logger.Warning(err)
|
||||
msg := "❌ Something went wrong!"
|
||||
msg := t.I18nBot("tgbot.wentWrong")
|
||||
t.SendMsgToTgbot(chatId, msg)
|
||||
return
|
||||
}
|
||||
if len(traffics) == 0 {
|
||||
msg := "No result!"
|
||||
if traffic == nil {
|
||||
msg := t.I18nBot("tgbot.noResult")
|
||||
t.SendMsgToTgbot(chatId, msg)
|
||||
return
|
||||
}
|
||||
for _, traffic := range traffics {
|
||||
expiryTime := ""
|
||||
if traffic.ExpiryTime == 0 {
|
||||
expiryTime = "♾Unlimited"
|
||||
} else {
|
||||
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
|
||||
}
|
||||
total := ""
|
||||
if traffic.Total == 0 {
|
||||
total = "♾Unlimited"
|
||||
} else {
|
||||
total = common.FormatTraffic((traffic.Total))
|
||||
}
|
||||
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
|
||||
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
|
||||
total, expiryTime)
|
||||
t.SendMsgToTgbot(chatId, output)
|
||||
|
||||
expiryTime := ""
|
||||
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 {
|
||||
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
total := ""
|
||||
if traffic.Total == 0 {
|
||||
total = t.I18nBot("tgbot.unlimited")
|
||||
} else {
|
||||
total = common.FormatTraffic((traffic.Total))
|
||||
}
|
||||
|
||||
output := ""
|
||||
output += t.I18nBot("tgbot.messages.active", "Enable=="+strconv.FormatBool(traffic.Enable))
|
||||
output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
|
||||
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)
|
||||
|
||||
t.SendMsgToTgbot(chatId, output)
|
||||
}
|
||||
|
||||
func (t *Tgbot) searchInbound(chatId int64, remark string) {
|
||||
inbouds, err := t.inboundService.SearchInbounds(remark)
|
||||
if err != nil {
|
||||
logger.Warning(err)
|
||||
msg := "❌ Something went wrong!"
|
||||
msg := t.I18nBot("tgbot.wentWrong")
|
||||
t.SendMsgToTgbot(chatId, msg)
|
||||
return
|
||||
}
|
||||
|
||||
if len(inbouds) == 0 {
|
||||
msg := t.I18nBot("tgbot.noInbounds")
|
||||
t.SendMsgToTgbot(chatId, msg)
|
||||
return
|
||||
}
|
||||
|
||||
for _, inbound := range inbouds {
|
||||
info := ""
|
||||
info += fmt.Sprintf("📍Inbound:%s\r\nPort:%d\r\n", inbound.Remark, inbound.Port)
|
||||
info += fmt.Sprintf("Traffic: %s (↑%s,↓%s)\r\n", common.FormatTraffic((inbound.Up + inbound.Down)), common.FormatTraffic(inbound.Up), common.FormatTraffic(inbound.Down))
|
||||
info += t.I18nBot("tgbot.messages.inbound", "Remark=="+inbound.Remark)
|
||||
info += t.I18nBot("tgbot.messages.port", "Port=="+strconv.Itoa(inbound.Port))
|
||||
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 += "Expire date: ♾ Unlimited\r\n \r\n"
|
||||
info += t.I18nBot("tgbot.messages.expire", "DateTime=="+t.I18nBot("tgbot.unlimited"))
|
||||
} else {
|
||||
info += fmt.Sprintf("Expire date:%s\r\n \r\n", time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
|
||||
info += t.I18nBot("tgbot.messages.expire", "DateTime=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
t.SendMsgToTgbot(chatId, info)
|
||||
|
||||
for _, traffic := range inbound.ClientStats {
|
||||
expiryTime := ""
|
||||
if traffic.ExpiryTime == 0 {
|
||||
expiryTime = "♾Unlimited"
|
||||
expiryTime = t.I18nBot("tgbot.unlimited")
|
||||
} else if traffic.ExpiryTime < 0 {
|
||||
expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
|
||||
} else {
|
||||
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
total := ""
|
||||
if traffic.Total == 0 {
|
||||
total = "♾Unlimited"
|
||||
total = t.I18nBot("tgbot.unlimited")
|
||||
} else {
|
||||
total = common.FormatTraffic((traffic.Total))
|
||||
}
|
||||
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
|
||||
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
|
||||
total, expiryTime)
|
||||
|
||||
output := ""
|
||||
output += t.I18nBot("tgbot.messages.active", "Enable=="+strconv.FormatBool(traffic.Enable))
|
||||
output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
|
||||
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)
|
||||
|
||||
t.SendMsgToTgbot(chatId, output)
|
||||
}
|
||||
}
|
||||
@@ -466,30 +577,40 @@ func (t *Tgbot) searchForClient(chatId int64, query string) {
|
||||
traffic, err := t.inboundService.SearchClientTraffic(query)
|
||||
if err != nil {
|
||||
logger.Warning(err)
|
||||
msg := "❌ Something went wrong!"
|
||||
msg := t.I18nBot("tgbot.wentWrong")
|
||||
t.SendMsgToTgbot(chatId, msg)
|
||||
return
|
||||
}
|
||||
if traffic == nil {
|
||||
msg := "No result!"
|
||||
msg := t.I18nBot("tgbot.noResult")
|
||||
t.SendMsgToTgbot(chatId, msg)
|
||||
return
|
||||
}
|
||||
|
||||
expiryTime := ""
|
||||
if traffic.ExpiryTime == 0 {
|
||||
expiryTime = "♾Unlimited"
|
||||
expiryTime = t.I18nBot("tgbot.unlimited")
|
||||
} else if traffic.ExpiryTime < 0 {
|
||||
expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
|
||||
} else {
|
||||
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
total := ""
|
||||
if traffic.Total == 0 {
|
||||
total = "♾Unlimited"
|
||||
total = t.I18nBot("tgbot.unlimited")
|
||||
} else {
|
||||
total = common.FormatTraffic((traffic.Total))
|
||||
}
|
||||
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
|
||||
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
|
||||
total, expiryTime)
|
||||
|
||||
output := ""
|
||||
output += t.I18nBot("tgbot.messages.active", "Enable=="+strconv.FormatBool(traffic.Enable))
|
||||
output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
|
||||
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)
|
||||
|
||||
t.SendMsgToTgbot(chatId, output)
|
||||
}
|
||||
|
||||
@@ -501,30 +622,31 @@ func (t *Tgbot) getExhausted() string {
|
||||
var exhaustedClients []xray.ClientTraffic
|
||||
var disabledInbounds []model.Inbound
|
||||
var disabledClients []xray.ClientTraffic
|
||||
output := ""
|
||||
TrafficThreshold, err := t.settingService.GetTgTrafficDiff()
|
||||
|
||||
TrafficThreshold, err := t.settingService.GetTrafficDiff()
|
||||
if err == nil && TrafficThreshold > 0 {
|
||||
trDiff = int64(TrafficThreshold) * 1073741824
|
||||
}
|
||||
ExpireThreshold, err := t.settingService.GetTgExpireDiff()
|
||||
ExpireThreshold, err := t.settingService.GetExpireDiff()
|
||||
if err == nil && ExpireThreshold > 0 {
|
||||
exDiff = int64(ExpireThreshold) * 84600
|
||||
exDiff = int64(ExpireThreshold) * 86400000
|
||||
}
|
||||
inbounds, err := t.inboundService.GetAllInbounds()
|
||||
if err != nil {
|
||||
logger.Warning("Unable to load Inbounds", err)
|
||||
}
|
||||
|
||||
for _, inbound := range inbounds {
|
||||
if inbound.Enable {
|
||||
if (inbound.ExpiryTime > 0 && (now-inbound.ExpiryTime < exDiff)) ||
|
||||
(inbound.Total > 0 && (inbound.Total-inbound.Up+inbound.Down < trDiff)) {
|
||||
if (inbound.ExpiryTime > 0 && (inbound.ExpiryTime-now < exDiff)) ||
|
||||
(inbound.Total > 0 && (inbound.Total-(inbound.Up+inbound.Down) < trDiff)) {
|
||||
exhaustedInbounds = append(exhaustedInbounds, *inbound)
|
||||
}
|
||||
if len(inbound.ClientStats) > 0 {
|
||||
for _, client := range inbound.ClientStats {
|
||||
if client.Enable {
|
||||
if (client.ExpiryTime > 0 && (now-client.ExpiryTime < exDiff)) ||
|
||||
(client.Total > 0 && (client.Total-client.Up+client.Down < trDiff)) {
|
||||
if (client.ExpiryTime > 0 && (client.ExpiryTime-now < exDiff)) ||
|
||||
(client.Total > 0 && (client.Total-(client.Up+client.Down) < trDiff)) {
|
||||
exhaustedClients = append(exhaustedClients, client)
|
||||
}
|
||||
} else {
|
||||
@@ -536,37 +658,63 @@ func (t *Tgbot) getExhausted() string {
|
||||
disabledInbounds = append(disabledInbounds, *inbound)
|
||||
}
|
||||
}
|
||||
output += fmt.Sprintf("Exhausted Inbounds count:\r\n🛑 Disabled: %d\r\n🔜 Exhaust soon: %d\r\n \r\n", len(disabledInbounds), len(exhaustedInbounds))
|
||||
if len(disabledInbounds)+len(exhaustedInbounds) > 0 {
|
||||
output += "Exhausted Inbounds:\r\n"
|
||||
|
||||
// Inbounds
|
||||
output := ""
|
||||
output += t.I18nBot("tgbot.messages.exhaustedCount", "Type=="+t.I18nBot("tgbot.inbounds"))
|
||||
output += t.I18nBot("tgbot.messages.disabled", "Disabled=="+strconv.Itoa(len(disabledInbounds)))
|
||||
output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+strconv.Itoa(len(exhaustedInbounds)))
|
||||
output += "\r\n \r\n"
|
||||
|
||||
if len(exhaustedInbounds) > 0 {
|
||||
output += t.I18nBot("tgbot.messages.exhaustedMsg", "Type=="+t.I18nBot("tgbot.inbounds"))
|
||||
|
||||
for _, inbound := range exhaustedInbounds {
|
||||
output += fmt.Sprintf("📍Inbound:%s\r\nPort:%d\r\nTraffic: %s (↑%s,↓%s)\r\n", inbound.Remark, inbound.Port, common.FormatTraffic((inbound.Up + inbound.Down)), common.FormatTraffic(inbound.Up), common.FormatTraffic(inbound.Down))
|
||||
output += t.I18nBot("tgbot.messages.inbound", "Remark=="+inbound.Remark)
|
||||
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 += "Expire date: ♾Unlimited\r\n \r\n"
|
||||
output += t.I18nBot("tgbot.messages.expire", "DateTime=="+t.I18nBot("tgbot.unlimited"))
|
||||
} else {
|
||||
output += fmt.Sprintf("Expire date:%s\r\n \r\n", time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
|
||||
output += t.I18nBot("tgbot.messages.expire", "DateTime=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
output += "\r\n \r\n"
|
||||
}
|
||||
}
|
||||
output += fmt.Sprintf("Exhausted Clients count:\r\n🛑 Disabled: %d\r\n🔜 Exhaust soon: %d\r\n \r\n", len(disabledClients), len(exhaustedClients))
|
||||
if len(disabledClients)+len(exhaustedClients) > 0 {
|
||||
output += "Exhausted Clients:\r\n"
|
||||
|
||||
// Clients
|
||||
output += t.I18nBot("tgbot.messages.exhaustedCount", "Type=="+t.I18nBot("tgbot.clients"))
|
||||
output += t.I18nBot("tgbot.messages.disabled", "Disabled=="+strconv.Itoa(len(disabledClients)))
|
||||
output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+strconv.Itoa(len(exhaustedClients)))
|
||||
output += "\r\n \r\n"
|
||||
|
||||
if len(exhaustedClients) > 0 {
|
||||
output += t.I18nBot("tgbot.messages.exhaustedMsg", "Type=="+t.I18nBot("tgbot.clients"))
|
||||
|
||||
for _, traffic := range exhaustedClients {
|
||||
expiryTime := ""
|
||||
if traffic.ExpiryTime == 0 {
|
||||
expiryTime = "♾Unlimited"
|
||||
expiryTime = t.I18nBot("tgbot.unlimited")
|
||||
} else if traffic.ExpiryTime < 0 {
|
||||
expiryTime += fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
|
||||
} else {
|
||||
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
total := ""
|
||||
if traffic.Total == 0 {
|
||||
total = "♾Unlimited"
|
||||
total = t.I18nBot("tgbot.unlimited")
|
||||
} else {
|
||||
total = common.FormatTraffic((traffic.Total))
|
||||
}
|
||||
output += fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
|
||||
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
|
||||
total, expiryTime)
|
||||
|
||||
output += t.I18nBot("tgbot.messages.active", "Enable=="+strconv.FormatBool(traffic.Enable))
|
||||
output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
|
||||
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 += "\r\n \r\n"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -574,14 +722,20 @@ func (t *Tgbot) getExhausted() string {
|
||||
}
|
||||
|
||||
func (t *Tgbot) sendBackup(chatId int64) {
|
||||
sendingTime := time.Now().Format("2006-01-02 15:04:05")
|
||||
t.SendMsgToTgbot(chatId, "Backup time: "+sendingTime)
|
||||
if !t.IsRunning() {
|
||||
return
|
||||
}
|
||||
|
||||
output := t.I18nBot("tgbot.messages.backupTime", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
|
||||
t.SendMsgToTgbot(chatId, output)
|
||||
|
||||
file := tgbotapi.FilePath(config.GetDBPath())
|
||||
msg := tgbotapi.NewDocument(chatId, file)
|
||||
_, err := bot.Send(msg)
|
||||
if err != nil {
|
||||
logger.Warning("Error in uploading backup: ", err)
|
||||
}
|
||||
|
||||
file = tgbotapi.FilePath(xray.GetConfigPath())
|
||||
msg = tgbotapi.NewDocument(chatId, file)
|
||||
_, err = bot.Send(msg)
|
||||
|
||||
@@ -18,6 +18,7 @@ var result string
|
||||
type XrayService struct {
|
||||
inboundService InboundService
|
||||
settingService SettingService
|
||||
xrayAPI xray.XrayAPI
|
||||
}
|
||||
|
||||
func (s *XrayService) IsXrayRunning() bool {
|
||||
@@ -84,15 +85,16 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
||||
clients, ok := settings["clients"].([]interface{})
|
||||
if ok {
|
||||
// check users active or not
|
||||
|
||||
clientStats := inbound.ClientStats
|
||||
for _, clientTraffic := range clientStats {
|
||||
|
||||
indexDecrease := 0
|
||||
for index, client := range clients {
|
||||
c := client.(map[string]interface{})
|
||||
if c["email"] == clientTraffic.Email {
|
||||
if !clientTraffic.Enable {
|
||||
clients = RemoveIndex(clients, index)
|
||||
clients = RemoveIndex(clients, index-indexDecrease)
|
||||
indexDecrease++
|
||||
logger.Info("Remove Inbound User", c["email"], "due the expire or traffic limit")
|
||||
|
||||
}
|
||||
@@ -101,8 +103,31 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
||||
}
|
||||
|
||||
}
|
||||
settings["clients"] = clients
|
||||
modifiedSettings, err := json.Marshal(settings)
|
||||
|
||||
// clear client config for additional parameters
|
||||
var final_clients []interface{}
|
||||
for _, client := range clients {
|
||||
|
||||
c := client.(map[string]interface{})
|
||||
|
||||
if c["enable"] != nil {
|
||||
if enable, ok := c["enable"].(bool); ok && !enable {
|
||||
continue
|
||||
}
|
||||
}
|
||||
for key := range c {
|
||||
if key != "email" && key != "id" && key != "password" && key != "flow" && key != "alterId" {
|
||||
delete(c, key)
|
||||
}
|
||||
if c["flow"] == "xtls-rprx-vision-udp443" {
|
||||
c["flow"] = "xtls-rprx-vision"
|
||||
}
|
||||
}
|
||||
final_clients = append(final_clients, interface{}(c))
|
||||
}
|
||||
|
||||
settings["clients"] = final_clients
|
||||
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -119,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 {
|
||||
@@ -134,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()
|
||||
@@ -142,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 {
|
||||
|
||||
@@ -2,9 +2,10 @@ package session
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"x-ui/database/model"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
"x-ui/database/model"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -21,6 +22,15 @@ func SetLoginUser(c *gin.Context, user *model.User) error {
|
||||
return s.Save()
|
||||
}
|
||||
|
||||
func SetMaxAge(c *gin.Context, maxAge int) error {
|
||||
s := sessions.Default(c)
|
||||
s.Options(sessions.Options{
|
||||
Path: "/",
|
||||
MaxAge: maxAge,
|
||||
})
|
||||
return s.Save()
|
||||
}
|
||||
|
||||
func GetLoginUser(c *gin.Context) *model.User {
|
||||
s := sessions.Default(c)
|
||||
obj := s.Get(loginUser)
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"enable" = "Enable"
|
||||
"protocol" = "Protocol"
|
||||
"search" = "Search"
|
||||
"filter" = "Filter"
|
||||
|
||||
"loading" = "Loading"
|
||||
"second" = "Second"
|
||||
@@ -22,24 +23,27 @@
|
||||
"unlimited" = "Unlimited"
|
||||
"none" = "None"
|
||||
"qrCode" = "QR Code"
|
||||
"info" = "More information"
|
||||
"info" = "More Information"
|
||||
"edit" = "Edit"
|
||||
"delete" = "Delete"
|
||||
"reset" = "Reset"
|
||||
"copySuccess" = "Copy successfully"
|
||||
"copySuccess" = "Copied successfully"
|
||||
"sure" = "Sure"
|
||||
"encryption" = "Encryption"
|
||||
"transmission" = "Transmission"
|
||||
"host" = "Host"
|
||||
"path" = "Path"
|
||||
"camouflage" = "Camouflage"
|
||||
"status" = "Status"
|
||||
"enabled" = "Enabled"
|
||||
"disabled" = "Disabled"
|
||||
"depleted" = "Depleted"
|
||||
"depletingSoon" = "Depleting soon"
|
||||
"domainName" = "Domain name"
|
||||
"additional" = "Alter"
|
||||
"additional" = "Alter ID"
|
||||
"monitor" = "Listen IP"
|
||||
"certificate" = "Certificat"
|
||||
"fail" = "Fail"
|
||||
"certificate" = "Certificate"
|
||||
"fail" = " Fail"
|
||||
"success" = " Success"
|
||||
"getVersion" = "Get version"
|
||||
"install" = "Install"
|
||||
@@ -49,8 +53,8 @@
|
||||
[menu]
|
||||
"dashboard" = "System Status"
|
||||
"inbounds" = "Inbounds"
|
||||
"setting" = "Panel Setting"
|
||||
"logout" = "LogOut"
|
||||
"settings" = "Panel Settings"
|
||||
"logout" = "Logout"
|
||||
"link" = "Other"
|
||||
|
||||
[pages.login]
|
||||
@@ -58,90 +62,122 @@
|
||||
"loginAgain" = "The login time limit has expired, please log in again"
|
||||
|
||||
[pages.login.toasts]
|
||||
"invalidFormData" = "Input Data Format Is Invalid"
|
||||
"emptyUsername" = "Please Enter Username"
|
||||
"emptyPassword" = "Please Enter Password"
|
||||
"wrongUsernameOrPassword" = "Invalid username or password"
|
||||
"invalidFormData" = "Input data format is invalid."
|
||||
"emptyUsername" = "Please enter username."
|
||||
"emptyPassword" = "Please enter password."
|
||||
"wrongUsernameOrPassword" = "Invalid username or password."
|
||||
"successLogin" = "Login"
|
||||
|
||||
[pages.index]
|
||||
"title" = "System status"
|
||||
"title" = "System Status"
|
||||
"memory" = "Memory"
|
||||
"hard" = "Hard disk"
|
||||
"xrayStatus" = "xray Status"
|
||||
"stopXray" = "Stop xray"
|
||||
"restartXray" = "Restart xray"
|
||||
"hard" = "Hard Disk"
|
||||
"xrayStatus" = "Xray Status"
|
||||
"stopXray" = "Stop"
|
||||
"restartXray" = "Restart"
|
||||
"xraySwitch" = "Switch Version"
|
||||
"xraySwitchClick" = "Click on the version you want to switch"
|
||||
"xraySwitchClickDesk" = "Please choose carefully, older versions may have incompatible configurations"
|
||||
"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" = "The running time of the system since it was started"
|
||||
"operationHoursDesc" = "System uptime: time since startup."
|
||||
"systemLoad" = "System Load"
|
||||
"connectionCount" = "Connection Count"
|
||||
"connectionCountDesc" = "The total number of connections for 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"
|
||||
"totalReceive" = "Total download traffic of all network cards since system startup"
|
||||
"xraySwitchVersionDialog" = "Switch xray version"
|
||||
"xraySwitchVersionDialogDesc" = "Whether to switch the xray version to"
|
||||
"dontRefreshh" = "Installation is in progress, please do not refresh this page"
|
||||
"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."
|
||||
"totalReceive" = "Total download data across all network cards since system startup."
|
||||
"xraySwitchVersionDialog" = "Switch Xray Version"
|
||||
"xraySwitchVersionDialogDesc" = "Are you sure you want to switch the Xray version to"
|
||||
"dontRefresh" = "Installation is in progress, please do not refresh this page."
|
||||
"logs" = "Logs"
|
||||
"config" = "Config"
|
||||
"backup" = "Backup & Restore"
|
||||
"backupTitle" = "Backup & Restore Database"
|
||||
"backupDescription" = "Remember to backup before importing a new database."
|
||||
"exportDatabase" = "Download Database"
|
||||
"importDatabase" = "Upload Database"
|
||||
|
||||
[pages.inbounds]
|
||||
"title" = "Inbounds"
|
||||
"totalDownUp" = "Total uploads/downloads"
|
||||
"totalUsage" = "Total usage"
|
||||
"inboundCount" = "Number of inbound"
|
||||
"operate" = "Operate"
|
||||
"totalDownUp" = "Total Uploads/Downloads"
|
||||
"totalUsage" = "Total Usage"
|
||||
"inboundCount" = "Number of Inbounds"
|
||||
"operate" = "Menu"
|
||||
"enable" = "Enable"
|
||||
"remark" = "Remark"
|
||||
"protocol" = "Protocol"
|
||||
"port" = "Port"
|
||||
"traffic" = "Traffic"
|
||||
"details" = "Details"
|
||||
"transportConfig" = "Transport config"
|
||||
"expireDate" = "Expire date"
|
||||
"resetTraffic" = "Reset traffic"
|
||||
"transportConfig" = "Transport Config"
|
||||
"expireDate" = "Expire Date"
|
||||
"resetTraffic" = "Reset Traffic"
|
||||
"addInbound" = "Add Inbound"
|
||||
"addTo" = "Add To"
|
||||
"revise" = "Revise"
|
||||
"modifyInbound" = "Modify InBound"
|
||||
"generalActions" = "General Actions"
|
||||
"create" = "Create"
|
||||
"update" = "Update"
|
||||
"modifyInbound" = "Modify Inbound"
|
||||
"deleteInbound" = "Delete Inbound"
|
||||
"deleteInboundContent" = "Are you sure you want to delete inbound?"
|
||||
"resetTrafficContent" = "Are you sure you want to reset traffic?"
|
||||
"copyLink" = "Copy Link"
|
||||
"address" = "Address"
|
||||
"network" = "Network"
|
||||
"destinationPort" = "Destination port"
|
||||
"targetAddress" = "Target address"
|
||||
"disableInsecureEncryption" = "Disable insecure encryption"
|
||||
"destinationPort" = "Destination Port"
|
||||
"targetAddress" = "Target Address"
|
||||
"disableInsecureEncryption" = "Disable Insecure Encryption"
|
||||
"monitorDesc" = "Leave blank by default"
|
||||
"meansNoLimit" = "Means no limit"
|
||||
"totalFlow" = "Total flow"
|
||||
"meansNoLimit" = "Means No Limit"
|
||||
"totalFlow" = "Total Flow"
|
||||
"leaveBlankToNeverExpire" = "Leave blank to never expire"
|
||||
"noRecommendKeepDefault" = "There are no special requirements to keep the default"
|
||||
"certificatePath" = "Certificate file path"
|
||||
"certificateContent" = "Certificate file content"
|
||||
"publicKeyPath" = "Public key file path"
|
||||
"publicKeyContent" = "Public key content"
|
||||
"keyPath" = "Key file path"
|
||||
"keyContent" = "Key content"
|
||||
"noRecommendKeepDefault" = "No special requirements to keep the default"
|
||||
"certificatePath" = "Certificate File Path"
|
||||
"certificateContent" = "Certificate File Content"
|
||||
"publicKeyPath" = "Public Key Path"
|
||||
"publicKeyContent" = "Public Key Content"
|
||||
"keyPath" = "Private Key Path"
|
||||
"keyContent" = "Private Key Content"
|
||||
"clickOnQRcode" = "Click on QR Code to Copy"
|
||||
"client" = "Client"
|
||||
"export" = "Export links"
|
||||
"export" = "Export Links"
|
||||
"clone" = "Clone"
|
||||
"cloneInbound" = "Clone"
|
||||
"cloneInboundContent" = "All settings of this inbound, except for Port, Listening IP, and Clients, will be applied to the clone."
|
||||
"cloneInboundOk" = "Clone"
|
||||
"resetAllTraffic" = "Reset All Inbounds Traffic"
|
||||
"resetAllTrafficTitle" = "Reset all inbounds traffic"
|
||||
"resetAllTrafficContent" = "Are you sure you want to reset all inbounds traffic?"
|
||||
"resetInboundClientTraffics" = "Reset Clients Traffic"
|
||||
"resetInboundClientTrafficTitle" = "Reset all clients traffic"
|
||||
"resetInboundClientTrafficContent" = "Are you sure you want to reset all traffic for this inbound's clients?"
|
||||
"resetAllClientTraffics" = "Reset All Clients Traffic"
|
||||
"resetAllClientTrafficTitle" = "Reset all clients traffic"
|
||||
"resetAllClientTrafficContent" = "Are you sure you want to reset all traffics for all clients?"
|
||||
"delDepletedClients" = "Delete Depleted Clients"
|
||||
"delDepletedClientsTitle" = "Delete depleted clients"
|
||||
"delDepletedClientsContent" = "Are you sure you want to delete all depleted clients?"
|
||||
"email" = "Email"
|
||||
"emailDesc" = "Please provide a unique email address."
|
||||
"setDefaultCert" = "Set cert from panel"
|
||||
"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]
|
||||
"add" = "Add client"
|
||||
"edit" = "Edit client"
|
||||
"submitAdd" = "Add client"
|
||||
"add" = "Add Client"
|
||||
"edit" = "Edit Client"
|
||||
"submitAdd" = "Add Client"
|
||||
"submitEdit" = "Save changes"
|
||||
"clientCount" = "Number of clients"
|
||||
"bulk" = "Add bulk"
|
||||
"clientCount" = "Number of Clients"
|
||||
"bulk" = "Add Bulk"
|
||||
"method" = "Method"
|
||||
"first" = "First"
|
||||
"last" = "Last"
|
||||
"prefix" = "Prefix"
|
||||
"postfix" = "postfix"
|
||||
"postfix" = "Postfix"
|
||||
"delayedStart" = "Start after first use"
|
||||
"expireDays" = "Expire days"
|
||||
"days" = "day(s)"
|
||||
|
||||
[pages.inbounds.toasts]
|
||||
"obtain" = "Obtain"
|
||||
@@ -163,66 +199,217 @@
|
||||
[pages.inbounds.stream.quic]
|
||||
"encryption" = "Encryption"
|
||||
|
||||
[pages.setting]
|
||||
"title" = "Setting"
|
||||
[pages.settings]
|
||||
"title" = "Settings"
|
||||
"save" = "Save"
|
||||
"restartPanel" = "Restart Panel"
|
||||
"restartPanelDesc" = "Are you sure you want to restart the panel? Click OK to restart after 3 seconds. If you cannot access the panel after restarting, please go to the server to view the panel log information"
|
||||
"panelConfig" = "Panel Configuration"
|
||||
"userSetting" = "User Setting"
|
||||
"xrayConfiguration" = "xray Configuration"
|
||||
"TGReminder" = "TG Reminder Related Settings"
|
||||
"otherSetting" = "Other Setting"
|
||||
"panelListeningIP" = "Panel listening IP"
|
||||
"panelListeningIPDesc" = "Leave blank by default to monitor all IPs, restart the panel to take effect"
|
||||
"infoDesc" = "Every change made here needs to be saved. Please restart the panel for the changes to take effect."
|
||||
"restartPanel" = "Restart Panel "
|
||||
"restartPanelDesc" = "Are you sure you want to restart the panel? Click OK to restart after 3 seconds. If you cannot access the panel after restarting, please view the panel log information on the server."
|
||||
"resetDefaultConfig" = "Reset to default config"
|
||||
"panelConfig" = "Panel Configurations"
|
||||
"userSettings" = "User Settings"
|
||||
"xrayConfiguration" = "Xray Configurations"
|
||||
"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" = "Restart the panel to take effect"
|
||||
"publicKeyPath" = "Panel certificate public key file path"
|
||||
"publicKeyPathDesc" = "Fill in an absolute path starting with '/', restart the panel to take effect"
|
||||
"privateKeyPath" = "Panel certificate key file path"
|
||||
"privateKeyPathDesc" = "Fill in an absolute path starting with '/', restart the panel to take effect"
|
||||
"panelUrlPath" = "panel url root path"
|
||||
"panelUrlPathDesc" = "Must start with '/' and end with '/', restart the panel to take effect"
|
||||
"panelPortDesc" = "Port number for serving the panel."
|
||||
"publicKeyPath" = "Panel Certificate Public Key File Path"
|
||||
"publicKeyPathDesc" = "Fill in an absolute path starting with '/'"
|
||||
"privateKeyPath" = "Panel Certificate Private Key File Path"
|
||||
"privateKeyPathDesc" = "Fill in an absolute path starting with '/'"
|
||||
"panelUrlPath" = "Panel URL Root Path"
|
||||
"panelUrlPathDesc" = "Must start with '/' and end with '/'"
|
||||
"oldUsername" = "Current Username"
|
||||
"currentPassword" = "Current Password"
|
||||
"newUsername" = "New Username"
|
||||
"newPassword" = "New Password"
|
||||
"advancedTemplate" = "Advanced template parts"
|
||||
"completeTemplate" = "Complete template of Xray configuration"
|
||||
"xrayConfigTemplate" = "Xray Configuration Template"
|
||||
"xrayConfigTemplateDesc" = "Generate the final xray configuration file based on this template, restart the panel to take effect."
|
||||
"xrayConfigTorrent" = "Ban bittorrent usage"
|
||||
"xrayConfigTorrentDesc" = "Change the configuration temlate to avoid using bittorrent by users, restart the panel to take effect"
|
||||
"xrayConfigPrivateIp" = "Ban private ip range to connect"
|
||||
"xrayConfigPrivateIpDesc" = "Change the configuration temlate to avoid connecting with private IP ranges, restart the panel to take effect"
|
||||
"xrayConfigInbounds" = "Configuration of Inbounds"
|
||||
"xrayConfigInboundsDesc" = "Change the configuration temlate to accept special clients, restart the panel to take effect"
|
||||
"xrayConfigOutbounds" = "Configuration of Outbounds"
|
||||
"xrayConfigOutboundsDesc" = "Change the configuration temlate to define outgoing ways for this server, restart the panel to take effect"
|
||||
"xrayConfigRoutings" = "Configuration of Routing rules"
|
||||
"xrayConfigRoutingsDesc" = "Change the configuration temlate to define Routing rules for this server, restart the panel to take effect"
|
||||
"telegramBotEnable" = "Enable telegram bot"
|
||||
"telegramBotEnableDesc" = "Restart the panel to take effect"
|
||||
"telegramBotEnable" = "Enable Telegram bot"
|
||||
"telegramBotEnableDesc" = "Your telegram bot will interact with the panel"
|
||||
"telegramToken" = "Telegram Token"
|
||||
"telegramTokenDesc" = "Restart the panel to take effect"
|
||||
"telegramChatId" = "Telegram Admin ChatIds"
|
||||
"telegramChatIdDesc" = "Multi chatIDs separated by comma. Restart the panel to take effect"
|
||||
"telegramTokenDesc" = "The Token you have got from @BotFather"
|
||||
"telegramChatId" = "Telegram Admin ChatIDs"
|
||||
"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" = "Using Crontab timing format. Restart the panel to take effect"
|
||||
"tgNotifyBackup" = "Database backup"
|
||||
"tgNotifyBackupDesc" = "Sending database backup file with report notification. Restart the panel to take effect"
|
||||
"tgNotifyExpireTimeDiff" = "Remained time threshold"
|
||||
"tgNotifyExpireTimeDiffDesc" = "This telegram bot will send you a notification before expiration (unit:day)"
|
||||
"tgNotifyTrafficDiff" = "Remained traffic threshold"
|
||||
"tgNotifyTrafficDiffDesc" = "This telegram bot will send you a notification before finishing traffic (unit:GB)"
|
||||
"telegramNotifyTimeDesc" = "Use Crontab timing format."
|
||||
"tgNotifyBackup" = "Database Backup"
|
||||
"tgNotifyBackupDesc" = "Send database backup file with report notification"
|
||||
"sessionMaxAge" = "Session maximum age"
|
||||
"sessionMaxAgeDesc" = "The time that you can stay login (unit: minute)"
|
||||
"expireTimeDiff" = "Expiration threshold for notification"
|
||||
"expireTimeDiffDesc" = "Get notified about account expiration before the threshold (unit: day)"
|
||||
"trafficDiff" = "Traffic threshold for notification"
|
||||
"trafficDiffDesc" = "Get notified about traffic exhaustion before reaching the threshold (unit: GB)"
|
||||
"tgNotifyCpu" = "CPU percentage alert threshold"
|
||||
"tgNotifyCpuDesc" = "This telegram bot will send you a notification if CPU usage is more than this percentage (unit:%)"
|
||||
"timeZonee" = "Time Zone"
|
||||
"timeZoneDesc" = "The scheduled task runs according to the time in the time zone, and restarts the panel to take effect"
|
||||
"tgNotifyCpuDesc" = "Receive notification if CPU usage exceeds this threshold (unit: %)"
|
||||
"timeZone" = "Time Zone"
|
||||
"timeZoneDesc" = "Scheduled tasks run according to the time in this time zone."
|
||||
"subSettings" = "Subscription"
|
||||
"subEnable" = "Enable service"
|
||||
"subEnableDesc" = "Subscription feature with separate configuration"
|
||||
"subListen" = "Listening IP"
|
||||
"subListenDesc" = "Leave blank by default to monitor all IPs"
|
||||
"subPort" = "Subscription Port"
|
||||
"subPortDesc" = "Port number for serving the subscription service must be unused in server"
|
||||
"subCertPath" = "Subscription Certificate Public Key File Path"
|
||||
"subCertPathDesc" = "Fill in an absolute path starting with '/'"
|
||||
"subKeyPath" = "Subscription Certificate Private Key File Path"
|
||||
"subKeyPathDesc" = "Fill in an absolute path starting with '/'"
|
||||
"subPath" = "Subscription URL Root Path"
|
||||
"subPathDesc" = "Must start with '/' and end with '/'"
|
||||
"subDomain" = "Listening Domain"
|
||||
"subDomainDesc" = "Leave blank by default to monitor all domains and IPs"
|
||||
"subUpdates" = "Subscription update intervals"
|
||||
"subUpdatesDesc" = "Interval hours between updates in client application"
|
||||
|
||||
[pages.setting.toasts]
|
||||
"modifySetting" = "Modify setting"
|
||||
"getSetting" = "Get setting"
|
||||
"modifyUser" = "Modify user"
|
||||
"originalUserPassIncorrect" = "The original user name or original password is incorrect"
|
||||
"userPassMustBeNotEmpty" = "New username and new password cannot be empty"
|
||||
|
||||
[pages.settings.templates]
|
||||
"title" = "Templates"
|
||||
"basicTemplate" = "Basic Template"
|
||||
"advancedTemplate" = "Advanced Template"
|
||||
"completeTemplate" = "Complete Template"
|
||||
"generalConfigs" = "General Configs"
|
||||
"generalConfigsDesc" = "These options will provide general adjustments."
|
||||
"blockConfigs" = "Blocking Configs"
|
||||
"blockConfigsDesc" = "These options will prevent users from connecting to specific protocols and websites."
|
||||
"blockCountryConfigs" = "Block Country Configs"
|
||||
"blockCountryConfigsDesc" = "These options will prevent users from connecting to specific country domains."
|
||||
"directCountryConfigs" = "Direct Country Configs"
|
||||
"directCountryConfigsDesc" = "These options will connect users directly to specific country domains."
|
||||
"ipv4Configs" = "IPv4 Configs"
|
||||
"ipv4ConfigsDesc" = "These options will route to target domains only via IPv4."
|
||||
"xrayConfigTemplate" = "Xray Configuration Template"
|
||||
"xrayConfigTemplateDesc" = "Generate the final Xray configuration file based on this template."
|
||||
"xrayConfigFreedomStrategy" = "Configure Strategy for Freedom Protocol"
|
||||
"xrayConfigFreedomStrategyDesc" = "Set the output strategy of the network in the Freedom Protocol."
|
||||
"xrayConfigRoutingStrategy" = "Configure Domains Routing Strategy"
|
||||
"xrayConfigRoutingStrategyDesc" = "Set the overall routing strategy for DNS resolving."
|
||||
"xrayConfigTorrent" = "Ban BitTorrent Usage"
|
||||
"xrayConfigTorrentDesc" = "Change the configuration template to avoid using BitTorrent by users."
|
||||
"xrayConfigPrivateIp" = "Ban Private IP Ranges to Connect"
|
||||
"xrayConfigPrivateIpDesc" = "Change the configuration template to avoid connecting to private IP ranges."
|
||||
"xrayConfigAds" = "Block Ads"
|
||||
"xrayConfigAdsDesc" = "Change the configuration template to block ads"
|
||||
"xrayConfigFamily" = "Enable Family-Friendly Configuration"
|
||||
"xrayConfigFamilyDesc" = "Avoid connecting to unsafe websites for family protection."
|
||||
"xrayConfigIRIp" = "Disable connection to Iran IP ranges"
|
||||
"xrayConfigIRIpDesc" = "Change the configuration template to avoid connecting to Iran IP ranges."
|
||||
"xrayConfigIRDomain" = "Disable connection to Iran domains"
|
||||
"xrayConfigIRDomainDesc" = "Change the configuration template to avoid connecting to Iran domains."
|
||||
"xrayConfigChinaIp" = "Disable connection to China IP ranges"
|
||||
"xrayConfigChinaIpDesc" = "Change the configuration template to avoid connecting to China IP ranges."
|
||||
"xrayConfigChinaDomain" = "Disable connection to China domains"
|
||||
"xrayConfigChinaDomainDesc" = "Change the configuration template to avoid connecting to China domains."
|
||||
"xrayConfigRussiaIp" = "Disable connection to Russia IP ranges"
|
||||
"xrayConfigRussiaIpDesc" = "Change the configuration template to avoid connecting to Russia IP ranges."
|
||||
"xrayConfigRussiaDomain" = "Disable connection to Russia domains"
|
||||
"xrayConfigRussiaDomainDesc" = "Change the configuration template to avoid connecting to Russia domains."
|
||||
"xrayConfigDirectIRIp" = "Direct connection to Iran IP ranges"
|
||||
"xrayConfigDirectIRIpDesc" = "Change the configuration template for direct connecting to Iran IP ranges."
|
||||
"xrayConfigDirectIRDomain" = "Direct connection to Iran domains"
|
||||
"xrayConfigDirectIRDomainDesc" = "Change the configuration template for direct connecting to Iran domains."
|
||||
"xrayConfigDirectChinaIp" = "Direct connection to China IP ranges"
|
||||
"xrayConfigDirectChinaIpDesc" = "Change the configuration template for direct connecting to China IP ranges."
|
||||
"xrayConfigDirectChinaDomain" = "Direct connection to China domains"
|
||||
"xrayConfigDirectChinaDomainDesc" = "Change the configuration template for direct connecting to China domains."
|
||||
"xrayConfigDirectRussiaIp" = "Direct connection to Russia IP ranges"
|
||||
"xrayConfigDirectRussiaIpDesc" = "Change the configuration template for direct connecting to Russia IP ranges."
|
||||
"xrayConfigDirectRussiaDomain" = "Direct connection to Russia domains"
|
||||
"xrayConfigDirectRussiaDomainDesc" = "Change the configuration template for direct connecting to Russia domains."
|
||||
"xrayConfigGoogleIPv4" = "Use IPv4 for Google"
|
||||
"xrayConfigGoogleIPv4Desc" = "Add routing for Google to connect with IPv4."
|
||||
"xrayConfigNetflixIPv4" = "Use IPv4 for Netflix"
|
||||
"xrayConfigNetflixIPv4Desc" = "Add routing for Netflix to connect with IPv4."
|
||||
"xrayConfigInbounds" = "Configuration of Inbounds"
|
||||
"xrayConfigInboundsDesc" = "Change the configuration template to accept specific clients."
|
||||
"xrayConfigOutbounds" = "Configuration of Outbounds"
|
||||
"xrayConfigOutboundsDesc" = "Change the configuration template to define outgoing ways for this server."
|
||||
"xrayConfigRoutings" = "Configuration of routing rules"
|
||||
"xrayConfigRoutingsDesc" = "Change the configuration template to define routing rules for this server."
|
||||
"manualLists" = "Manual Lists"
|
||||
"manualListsDesc" = "Please use the JSON array format."
|
||||
"manualBlockedIPs" = "List of Blocked IPs"
|
||||
"manualBlockedDomains" = "List of Blocked Domains"
|
||||
"manualDirectIPs" = "List of Direct IPs"
|
||||
"manualDirectDomains" = "List of Direct Domains"
|
||||
"manualIPv4Domains" = "List of IPv4 Domains"
|
||||
|
||||
[pages.settings.toasts]
|
||||
"modifySettings" = "Modify Settings "
|
||||
"getSettings" = "Get Settings "
|
||||
"modifyUser" = "Modify User "
|
||||
"originalUserPassIncorrect" = "Incorrect original username or password"
|
||||
"userPassMustBeNotEmpty" = "New username and new password cannot be empty"
|
||||
|
||||
[tgbot]
|
||||
"noResult" = "❗ No result!"
|
||||
"wentWrong" = "❌ Something went wrong!"
|
||||
"noInbounds" = "❗ No inbound found!"
|
||||
"unlimited" = "♾ Unlimited"
|
||||
"day" = "Day"
|
||||
"days" = "Days"
|
||||
"unknown" = "Unknown"
|
||||
"inbounds" = "Inbounds"
|
||||
"clients" = "Clients"
|
||||
|
||||
[tgbot.commands]
|
||||
"unknown" = "❗ Unknown command"
|
||||
"pleaseChoose" = "👇 Please choose:\r\n"
|
||||
"help" = "🤖 Welcome to this bot! It's designed to offer you specific data from the server, and it allows you to make modifications as needed.\r\n\r\n"
|
||||
"start" = "👋 Hello <i>{{ .Firstname }}</i>.\r\n"
|
||||
"welcome" = "🤖 Welcome to <b>{{ .Hostname }}</b> management bot.\r\n"
|
||||
"status" = "✅ Bot is ok!"
|
||||
"usage" = "❗ Please provide a text to search!"
|
||||
"getID" = "🆔 Your ID: <code>{{ .ID }}</code>"
|
||||
"helpAdminCommands" = "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."
|
||||
|
||||
[tgbot.messages]
|
||||
"cpuThreshold" = "🔴 The CPU usage {{ .Percent }}% is more than threshold {{ .Threshold }}%"
|
||||
"loginSuccess" = "✅ Successfully logged-in to the panel.\r\n"
|
||||
"loginFailed" = "❗️ Login to the panel failed.\r\n"
|
||||
"report" = "🕰 Scheduled Reports: {{ .RunTime }}\r\n"
|
||||
"datetime" = "⏰ Date-Time: {{ .DateTime }}\r\n"
|
||||
"hostname" = "💻 Hostname: {{ .Hostname }}\r\n"
|
||||
"version" = "🚀 X-UI Version: {{ .Version }}\r\n"
|
||||
"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n"
|
||||
"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n"
|
||||
"ip" = "🌐 IP: {{ .IP }}\r\n"
|
||||
"serverUpTime" = "⏳ Server Uptime: {{ .UpTime }} {{ .Unit }}\r\n"
|
||||
"serverLoad" = "📈 Server Load: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
|
||||
"serverMemory" = "📋 Server Memory: {{ .Current }}/{{ .Total }}\r\n"
|
||||
"tcpCount" = "🔹 TcpCount: {{ .Count }}\r\n"
|
||||
"udpCount" = "🔸 UdpCount: {{ .Count }}\r\n"
|
||||
"traffic" = "🚦 Traffic: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
|
||||
"xrayStatus" = "ℹ️ Xray Status: {{ .State }}\r\n"
|
||||
"username" = "👤 Username: {{ .Username }}\r\n"
|
||||
"time" = "⏰ Time: {{ .Time }}\r\n"
|
||||
"inbound" = "📍 Inbound: {{ .Remark }}\r\n"
|
||||
"port" = "🔌 Port: {{ .Port }}\r\n"
|
||||
"expire" = "📅 Expire Date: {{ .DateTime }}\r\n \r\n"
|
||||
"expireIn" = "📅 Expire In: {{ .Time }}\r\n \r\n"
|
||||
"active" = "💡 Active: {{ .Enable }}\r\n"
|
||||
"email" = "📧 Email: {{ .Email }}\r\n"
|
||||
"upload" = "🔼 Upload↑: {{ .Upload }}\r\n"
|
||||
"download" = "🔽 Download↓: {{ .Download }}\r\n"
|
||||
"total" = "🔄 Total: {{ .UpDown }} / {{ .Total }}\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"
|
||||
|
||||
[tgbot.buttons]
|
||||
"dbBackup" = "Get DB Backup"
|
||||
"serverUsage" = "Server Usage"
|
||||
"getInbounds" = "Get Inbounds"
|
||||
"depleteSoon" = "Deplete soon"
|
||||
"clientUsage" = "Get Usage"
|
||||
"commands" = "Commands"
|
||||
|
||||
[tgbot.answers]
|
||||
"getInboundsFailed" = "❌ Failed to get inbounds"
|
||||
"askToAddUser" = "Your configuration is not found!\r\nYou should configure your telegram username and ask your Admin to add it to your configuration."
|
||||
"askToAddUserName" = "Your configuration is not found!\r\nPlease ask your Admin to use your telegram username in your configuration(s).\r\n\r\nYour username: <b>@{{ .TgUserName }}</b>"
|
||||
|
||||
@@ -11,8 +11,9 @@
|
||||
"enable" = "فعال"
|
||||
"protocol" = "پروتکل"
|
||||
"search" = "جستجو"
|
||||
"filter" = "فیلتر"
|
||||
|
||||
"loading" = "در حال بروزرسانی..."
|
||||
"loading" = "در حال بروزرسانی.."
|
||||
"second" = "ثانیه"
|
||||
"minute" = "دقیقه"
|
||||
"hour" = "ساعت"
|
||||
@@ -33,12 +34,15 @@
|
||||
"host" = "آدرس"
|
||||
"path" = "مسیر"
|
||||
"camouflage" = "استتار"
|
||||
"status" = "وضعیت"
|
||||
"enabled" = "فعال"
|
||||
"disabled" = "غیرفعال"
|
||||
"depleted" = "منقضی"
|
||||
"depletingSoon" = "در حال انقضا"
|
||||
"domainName" = "آدرس دامنه"
|
||||
"additional" = "آی دی جایگزین"
|
||||
"monitor" = "آی پی اتصال"
|
||||
"certificate" = "سرتیفیکیت"
|
||||
"certificate" = "گواهی دیجیتال"
|
||||
"fail" = "خطا"
|
||||
"success" = " موفق"
|
||||
"getVersion" = "دریافت ورژن"
|
||||
@@ -48,8 +52,8 @@
|
||||
|
||||
[menu]
|
||||
"dashboard" = "وضعیت سیستم"
|
||||
"inbounds" = "سرویس ها"
|
||||
"setting" = "تنظیمات پنل"
|
||||
"inbounds" = "سرویس ها"
|
||||
"settings" = "تنظیمات پنل"
|
||||
"logout" = "خروج"
|
||||
"link" = "دیگر"
|
||||
|
||||
@@ -69,13 +73,13 @@
|
||||
"memory" = "حافظه رم"
|
||||
"hard" = "حافظه دیسک"
|
||||
"xrayStatus" = "وضعیت Xray"
|
||||
"stopXray" = "توقف xray"
|
||||
"restartXray" = "شروع مجدد xray"
|
||||
"stopXray" = "توقف"
|
||||
"restartXray" = "شروع مجدد"
|
||||
"xraySwitch" = "تغییر ورژن"
|
||||
"xraySwitchClick" = "ورژن مورد نظر را انتخاب کنید"
|
||||
"xraySwitchClickDesk" = "لطفا با دقت انتخاب کنید ، در صورت انتخاب اشتباه امکان قطعی سیستم وجود دارد ."
|
||||
"operationHours" = "ساعت فعال"
|
||||
"operationHoursDesc" = "ساعت فعال بعد از شروع سیستم"
|
||||
"xraySwitchClickDesk" = "لطفا با دقت انتخاب کنید ، در صورت انتخاب اشتباه امکان قطعی سیستم وجود دارد "
|
||||
"operationHours" = "مدت فعالیت"
|
||||
"operationHoursDesc" = "مدت فعالیت سیستم بعد از روشن شدن"
|
||||
"systemLoad" = "بار روی سیستم"
|
||||
"connectionCount" = "تعداد کانکشن ها"
|
||||
"connectionCountDesc" = "تعداد کانکشن ها برای کل شبکه"
|
||||
@@ -85,7 +89,14 @@
|
||||
"totalReceive" = "جمع کل ترافیک دانلود مصرفی"
|
||||
"xraySwitchVersionDialog" = "تغییر ورژن Xray"
|
||||
"xraySwitchVersionDialogDesc" = "آیا از تغییر ورژن مطمئن هستین"
|
||||
"dontRefreshh" = "در حال نصب ، لطفا رفرش نکنید "
|
||||
"dontRefresh" = "در حال نصب ، لطفا رفرش نکنید "
|
||||
"logs" = "گزارش ها"
|
||||
"config" = "تنظیمات"
|
||||
"backup" = "پشتیبان گیری"
|
||||
"backupTitle" = "پشتیبان گیری دیتابیس"
|
||||
"backupDescription" = "به یاد داشته باشید که قبل از وارد کردن یک دیتابیس جدید، نسخه پشتیبان تهیه کنید"
|
||||
"exportDatabase" = "دانلود دیتابیس"
|
||||
"importDatabase" = "آپلود دیتابیس"
|
||||
|
||||
[pages.inbounds]
|
||||
"title" = "کاربران"
|
||||
@@ -103,8 +114,9 @@
|
||||
"expireDate" = "تاریخ انقضا"
|
||||
"resetTraffic" = "ریست ترافیک"
|
||||
"addInbound" = "اضافه کردن سرویس"
|
||||
"addTo" = "اضافه کردن"
|
||||
"revise" = "ویرایش"
|
||||
"generalActions" = "عملیات کلی"
|
||||
"create" = "اضافه کردن"
|
||||
"update" = "ویرایش"
|
||||
"modifyInbound" = "ویرایش سرویس"
|
||||
"deleteInbound" = "حذف سرویس"
|
||||
"deleteInboundContent" = "آیا مطمئن به حذف سرویس هستید ؟"
|
||||
@@ -114,7 +126,7 @@
|
||||
"network" = "شبکه"
|
||||
"destinationPort" = "پورت مقصد"
|
||||
"targetAddress" = "آدرس مقصد"
|
||||
"disableInsecureEncryption" = "رمزگذاری ناامن را غیرفعال کنید"
|
||||
"disableInsecureEncryption" = "غیرفعال سازی رمزگذاری ناامن"
|
||||
"monitorDesc" = "به طور پیش فرض خالی بگذارید"
|
||||
"meansNoLimit" = "یعنی بدون محدودیت"
|
||||
"totalFlow" = "کل ترافیک"
|
||||
@@ -122,13 +134,33 @@
|
||||
"noRecommendKeepDefault" = "توصیه می شود به عنوان پیش فرض حفظ شود"
|
||||
"certificatePath" = "مسیر فایل گواهی"
|
||||
"certificateContent" = "محتوای فایل گواهی"
|
||||
"publicKeyPath" = "مسیر فایل Certificate.crt"
|
||||
"publicKeyContent" = "محتوای Certificate.crt"
|
||||
"keyPath" = "مسیر فایل Private.key"
|
||||
"keyContent" = "محتوای Private.key"
|
||||
"publicKeyPath" = "مسیر کلید عمومی"
|
||||
"publicKeyContent" = "محتوای کلید عمومی"
|
||||
"keyPath" = "مسیر کلید خصوصی"
|
||||
"keyContent" = "محتوای کلید خصوصی"
|
||||
"clickOnQRcode" = "برای کپی بر روی کد تصویری کلیک کنید"
|
||||
"client" = "کاربر"
|
||||
"export" = "استخراج لینکها"
|
||||
"clone" = "شبیه سازی"
|
||||
"cloneInbound" = "ایجاد"
|
||||
"cloneInboundContent" = "همه موارد این ورودی بجز پورت ، ای پی و کلاینت ها شبیه سازی خواهند شد"
|
||||
"resetAllTraffic" = "ریست ترافیک کل سرویس ها"
|
||||
"resetAllTrafficTitle" = "ریست ترافیک کل سرویس ها"
|
||||
"resetAllTrafficContent" = "آیا مطمئن هستید که میخواهید تمام ترافیک سرویس ها را ریست کنید؟"
|
||||
"resetInboundClientTraffics" = "ریست ترافیک کاربران"
|
||||
"resetInboundClientTrafficTitle" = "ریست ترافیک کل کاربران"
|
||||
"resetInboundClientTrafficContent" = "آیا مطمئن هستید که میخواهید تمام ترافیک کاربران این سرویس را ریست کنید؟"
|
||||
"resetAllClientTraffics" = "ریست ترافیک کاربران"
|
||||
"resetAllClientTrafficTitle" = "ریست ترافیک کل کاربران"
|
||||
"resetAllClientTrafficContent" = "آیا مطمئن هستید که میخواهید تمام ترافیک کاربران را ریست کنید؟"
|
||||
"delDepletedClients" = "حذف کاربران منقضی"
|
||||
"delDepletedClientsTitle" = "حذف کاربران منقضی"
|
||||
"delDepletedClientsContent" = "آیا مطمئن هستید مه میخواهید تمامی کاربران منقضی شده را حذف کنید؟"
|
||||
"email" = "ایمیل"
|
||||
"emailDesc" = "ایمیل باید کاملا منحصر به فرد باشد"
|
||||
"setDefaultCert" = "استفاده از گواهی پنل"
|
||||
"telegramDesc" = "از آیدی تلگرام بدون @ یا آیدی چت استفاده کنید (می توانید آن را از اینجا دریافت کنید @userinfobot یا در ربات دستور '/id' را وارد کنید)"
|
||||
"subscriptionDesc" = "می توانید ساب لینک خود را در جزئیات پیدا کنید، همچنین می توانید از همین نام برای چندین کانفیگ استفاده کنید"
|
||||
|
||||
[pages.client]
|
||||
"add" = "کاربر جدید"
|
||||
@@ -142,6 +174,9 @@
|
||||
"last" = "تا"
|
||||
"prefix" = "پیشوند"
|
||||
"postfix" = "پسوند"
|
||||
"delayedStart" = "شروع بعد از اولین استفاده"
|
||||
"expireDays" = "روزهای اعتبار"
|
||||
"days" = "(روز)"
|
||||
|
||||
[pages.inbounds.toasts]
|
||||
"obtain" = "Obtain"
|
||||
@@ -163,66 +198,216 @@
|
||||
[pages.inbounds.stream.quic]
|
||||
"encryption" = "رمزنگاری"
|
||||
|
||||
[pages.setting]
|
||||
[pages.settings]
|
||||
"title" = "تنظیمات"
|
||||
"save" = "ذخیره"
|
||||
"infoDesc" = "برای اعمال تغییرات در این بخش باید پس از ذخیره کردن، پنل را ریستارت کنید"
|
||||
"restartPanel" = "ریستارت پنل"
|
||||
"restartPanelDesc" = "آیا مطمئن هستید که می خواهید پنل را دوباره راه اندازی کنید؟ برای راه اندازی مجدد روی OK کلیک کنید. اگر بعد از 3 ثانیه نمی توانید به پنل دسترسی پیدا کنید، لطفاً برای مشاهده اطلاعات گزارش پانل به سرور برگردید"
|
||||
"resetDefaultConfig" = "برگشت به تنظیمات پیشفرض"
|
||||
"panelConfig" = "تنظیمات پنل"
|
||||
"userSetting" = "تنظیمات مدیر"
|
||||
"userSettings" = "تنظیمات مدیر"
|
||||
"xrayConfiguration" = "تنظیمات Xray"
|
||||
"TGReminder" = "تنظیمات ربات تلگرام"
|
||||
"otherSetting" = "دیگر تنظیمات"
|
||||
"TGBotSettings" = "تنظیمات ربات تلگرام"
|
||||
"panelListeningIP" = "محدودیت آی پی پنل"
|
||||
"panelListeningIPDesc" = "برای استفاده از تمام IP ها به طور پیش فرض خالی بگذارید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"panelListeningIPDesc" = "برای استفاده از تمام آیپیها به طور پیش فرض خالی بگذارید"
|
||||
"panelListeningDomain" = "محدودیت دامین پنل"
|
||||
"panelListeningDomainDesc" = "برای استفاده از تمام دامنهها و آیپیها به طور پیش فرض خالی بگذارید"
|
||||
"panelPort" = "پورت پنل"
|
||||
"panelPortDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"publicKeyPath" = "مسیر فایل پنل Certificate.crt"
|
||||
"publicKeyPathDesc" = "باید یک مسیر مطلق باشد که با / شروع می شود . پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"privateKeyPath" = "مسیر فایل پنل private.key"
|
||||
"privateKeyPathDesc" = "باید یک مسیر مطلق باشد که با / شروع می شود . پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"panelPortDesc" = "پورت مورد استفاده برای نمایش این پنل"
|
||||
"publicKeyPath" = "مسیر فایل گواهی کلید عمومی پنل"
|
||||
"publicKeyPathDesc" = "باید یک مسیر مطلق باشد که با / شروع می شود "
|
||||
"privateKeyPath" = "مسیر فایل گواهی کلید خصوصی پنل"
|
||||
"privateKeyPathDesc" = "باید یک مسیر مطلق باشد که با / شروع می شود "
|
||||
"panelUrlPath" = "آدرس روت پنل"
|
||||
"panelUrlPathDesc" = "باید با '/' شروع شود و با '/' تمام شود. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"panelUrlPathDesc" = "باید با '/' شروع شود و با '/' تمام شود"
|
||||
"oldUsername" = "نام کاربری فعلی"
|
||||
"currentPassword" = "رمز عبور فعلی"
|
||||
"newUsername" = "نام کاربری جدید"
|
||||
"newPassword" = "رمز عبور جدید"
|
||||
"advancedTemplate" = "بخش های پیشرفته الگو"
|
||||
"completeTemplate" = "الگوی کامل تنظیمات ایکس ری"
|
||||
"xrayConfigTemplate" = "تنظیمات الگو ایکس ری"
|
||||
"xrayConfigTemplateDesc" = "فایل پیکربندی ایکس ری نهایی بر اساس این الگو ایجاد میشود. لطفاً این را تغییر ندهید مگر اینکه دقیقاً بدانید که چه کاری انجام می دهید! پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"xrayConfigTorrent" = "فیلتر کردن بیت تورنت"
|
||||
"xrayConfigTorrentDesc" = "الگوی تنظیمات را برای فیلتر کردن پروتکل بیت تورنت برای کاربران تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"xrayConfigPrivateIp" = "جلوگیری از اتصال آی پی های نامعتبر"
|
||||
"xrayConfigPrivateIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آی پی های نامعتبر و بسته های سرگردان تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"xrayConfigInbounds" = "تنظیمات ورودی"
|
||||
"xrayConfigInboundsDesc" = "میتوانید الگوی تنظیمات را برای ورودی های خاص تنظیم نمایید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"xrayConfigOutbounds" = "تنظیمات خروجی"
|
||||
"xrayConfigOutboundsDesc" = "میتوانید الگوی تنظیمات را برای خروجی اینترنت تنظیم نمایید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"xrayConfigRoutings" = "تنظیمات قوانین مسیریابی"
|
||||
"xrayConfigRoutingsDesc" = "میتوانید الگوی تنظیمات را برای مسیریابی تنظیم نمایید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"telegramBotEnable" = "فعالسازی ربات تلگرام"
|
||||
"telegramBotEnableDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"telegramBotEnableDesc" = "از طریق بات تلگرام به امکانات ابن پنل متصل شوید"
|
||||
"telegramToken" = "توکن تلگرام"
|
||||
"telegramTokenDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"telegramTokenDesc" = "توکن را باید از مدیر بات های تلگرام دریافت کنید @botfather"
|
||||
"telegramChatId" = "آی دی تلگرام مدیریت"
|
||||
"telegramChatIdDesc" = "با استفاده از کاما میتونید چند آی دی را از هم جدا کنید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"telegramChatIdDesc" = "از @userinfobot یا دستور '/id' در ربات برای دریافت شناسه های چت خود استفاده کنید. با استفاده از کاما میتونید چند آی دی را از هم جدا کنید. "
|
||||
"telegramNotifyTime" = "مدت زمان نوتیفیکیشن ربات تلگرام"
|
||||
"telegramNotifyTimeDesc" = "از فرمت زمان بندی Crontab استفاده کنید . پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"telegramNotifyTimeDesc" = "از فرمت زمان بندی لینوکس استفاده کنید "
|
||||
"tgNotifyBackup" = "پشتیبان گیری از پایگاه داده"
|
||||
"tgNotifyBackupDesc" = "ارسال کپی فایل پایگاه داده به همراه گزارش دوره ای"
|
||||
"tgNotifyExpireTimeDiff" = "آستانه زمان باقی مانده"
|
||||
"tgNotifyExpireTimeDiffDesc" = "این ربات تلگرام قبل از انقضا برای شما پیام ارسال می کند (واحد: روز)"
|
||||
"tgNotifyTrafficDiff" = "آستانه ترافیک باقی مانده"
|
||||
"tgNotifyTrafficDiffDesc" = "این ربات تلگرام قبل از اتمام ترافیک برای شما پیام ارسال می کند (واحد: گیگابایت)"
|
||||
"sessionMaxAge" = "بیشینه زمان جلسه وب"
|
||||
"sessionMaxAgeDesc" = "بیشینه زمانی که میتوانید لاگین بمانید (واحد: دقیقه)"
|
||||
"expireTimeDiff" = "آستانه زمان باقی مانده"
|
||||
"expireTimeDiffDesc" = "فاصله زمانی هشدار تا رسیدن به زمان انقضا (واحد: روز)"
|
||||
"trafficDiff" = "آستانه ترافیک باقی مانده"
|
||||
"trafficDiffDesc" = "فاصله زمانی هشدار تا رسیدن به اتمام ترافیک (واحد: گیگابایت)"
|
||||
"tgNotifyCpu" = "آستانه هشدار درصد پردازنده"
|
||||
"tgNotifyCpuDesc" = "این ربات تلگرام در صورت استفاده پردازنده بیشتر از این درصد برای شما پیام ارسال می کند.(واحد: درصد)"
|
||||
"timeZonee" = "منظقه زمانی"
|
||||
"timeZone" = "منظقه زمانی"
|
||||
"timeZoneDesc" = "وظایف برنامه ریزی شده بر اساس این منطقه زمانی اجرا می شوند. پنل را مجدداً راه اندازی می کند تا اعمال شود"
|
||||
"subSettings" = "سابسکریپشن"
|
||||
"subEnable" = "فعال کردن سرویس"
|
||||
"subEnableDesc" = "ویژگی سابسکریپشن با پیکربندی جداگانه"
|
||||
"subListen" = "محدودیت آیپی"
|
||||
"subListenDesc" = "برای استفاده از همه آیپی ها به طور پیش فرض خالی بگذارید"
|
||||
"subPort" = "پورت سرویس"
|
||||
"subPortDesc" = "شماره پورت برای ارائه خدمات سابسکریپشن باید خالی باشد"
|
||||
"subCertPath" = "مسیر فایل کلید عمومی گواهی سابسکریپشن"
|
||||
"subCertPathDesc" = "یک مسیر مطلق که با '/' شروع می شود را پر کنید."
|
||||
"subKeyPath" = "مسیر فایل کلید خصوصی گواهی سابسکریپشن"
|
||||
"subKeyPathDesc" = "یک مسیر مطلق که با '/' شروع می شود را پر کنید."
|
||||
"subPath" = "مسیر ریشه سابسکریپشن"
|
||||
"subPathDesc" = "باید با '/' شروع شود و با '/' ختم شود."
|
||||
"subDomain" = "دامنه مخصوص سابسکریپشن"
|
||||
"subDomainDesc" = "برای نظارت بر همه دامنه ها و آیپی ها به طور پیش فرض خالی بگذارید"
|
||||
"subUpdates" = "فاصله به روز رسانی های سابسکریپشن"
|
||||
"subUpdatesDesc" = "ساعت های فاصله بین به روز رسانی در برنامه کاربر"
|
||||
|
||||
[pages.setting.toasts]
|
||||
"modifySetting" = "ویرایش تنظیمات"
|
||||
"getSetting" = "دریافت تنظیمات"
|
||||
[pages.settings.templates]
|
||||
"title" = "الگوها"
|
||||
"basicTemplate" = "بخش الگو پایه"
|
||||
"advancedTemplate" = "بخش الگو پیشرفته"
|
||||
"completeTemplate" = "بخش الگو کامل"
|
||||
"generalConfigs" = "تنظیمات عمومی"
|
||||
"generalConfigsDesc" = "این تنظیمات میتواند ترافیک کلی سرویس را متاثر کند"
|
||||
"blockConfigs" = "مسدود سازی"
|
||||
"blockConfigsDesc" = "این گزینه ها از اتصال کاربران به پروتکل ها و وب سایت های خاص جلوگیری می کند"
|
||||
"blockCountryConfigs" = "تنظیمات برای مسدودسازی کشورها"
|
||||
"blockCountryConfigsDesc" = "این گزینه اتصال کاربران به دامنه های کشوری خاص را مسدود می کند"
|
||||
"directCountryConfigs" = "تنظیمات برای اتصال مستقیم کشورها"
|
||||
"directCountryConfigsDesc" = "این گزینه کاربران را به دامنه های کشوری خاص را به طور مستقیم، متصل می کند"
|
||||
"ipv4Configs" = "تنظیمات برای IPv4"
|
||||
"ipv4ConfigsDesc" = "این گزینه فقط از طریق آیپی ورژن ۴ به دامنه های هدف هدایت می شود"
|
||||
"xrayConfigTemplate" = "تنظیمات الگو ایکس ری"
|
||||
"xrayConfigTemplateDesc" = "فایل پیکربندی ایکس ری نهایی بر اساس این الگو ایجاد میشود. لطفاً این را تغییر ندهید مگر اینکه دقیقاً بدانید که چه کاری انجام می دهید!"
|
||||
"xrayConfigFreedomStrategy" = "روش استفاده از شبکه خروجی مستقیم"
|
||||
"xrayConfigFreedomStrategyDesc" = "تعیین روش استفاده از خروجی برای پرتکل مستقیم"
|
||||
"xrayConfigRoutingStrategy" = "پیکربندی استراتژی حل دامنه در مسیریابی"
|
||||
"xrayConfigRoutingStrategyDesc" = "تعیین استراتژی مسیریابی کلی برای پیدا کردن دامنه"
|
||||
"xrayConfigTorrent" = "فیلتر کردن بیت تورنت"
|
||||
"xrayConfigTorrentDesc" = "الگوی تنظیمات را برای فیلتر کردن پروتکل بیت تورنت برای کاربران تغییر میدهد"
|
||||
"xrayConfigPrivateIp" = "جلوگیری از اتصال آیپی های خصوصی یا محلی"
|
||||
"xrayConfigPrivateIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های خصوصی یا محلی و بسته های سرگردان تغییر میدهد"
|
||||
"xrayConfigAds" = "مسدود کردن تبلیغات"
|
||||
"xrayConfigAdsDesc" = "الگوی تنظیمات را برای مسدود کردن تبلیغات تغییر میدهد"
|
||||
"xrayConfigFamily" = "فعال کردن حالت خانواده"
|
||||
"xrayConfigFamilyDesc" = "برای جلوگیری از ارتباط با وبسایت های ناامن"
|
||||
"xrayConfigIRIp" = "جلوگیری از اتصال آیپی های ایران"
|
||||
"xrayConfigIRIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های ایران تغییر میدهد"
|
||||
"xrayConfigIRDomain" = "جلوگیری از اتصال دامنه های ایران"
|
||||
"xrayConfigIRDomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های ایران تغییر میدهد"
|
||||
"xrayConfigChinaIp" = "جلوگیری از اتصال آیپی های چین"
|
||||
"xrayConfigChinaIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های چین تغییر میدهد"
|
||||
"xrayConfigChinaDomain" = "جلوگیری از اتصال دامنه های چین"
|
||||
"xrayConfigChinaDomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های چین تغییر میدهد"
|
||||
"xrayConfigRussiaIp" = "جلوگیری از اتصال آیپی های روسیه"
|
||||
"xrayConfigRussiaIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های روسیه تغییر میدهد"
|
||||
"xrayConfigRussiaDomain" = "جلوگیری از اتصال دامنه های روسیه"
|
||||
"xrayConfigRussiaDomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های روسیه تغییر میدهد"
|
||||
"xrayConfigDirectIRIp" = "ارتباط مستقیم به آیپی های ایران"
|
||||
"xrayConfigDirectIRIpDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به آیپی های ایران تغییر میدهد"
|
||||
"xrayConfigDirectIRDomain" = "ارتباط مستقیم به دامنه های ایران"
|
||||
"xrayConfigDirectIRDomainDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به دامنه های ایران تغییر میدهد"
|
||||
"xrayConfigDirectChinaIp" = "ارتباط مستقیم به آیپی های چین"
|
||||
"xrayConfigDirectChinaIpDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به آیپی های چین تغییر میدهد"
|
||||
"xrayConfigDirectChinaDomain" = "ارتباط مستقیم به دامنه های چین"
|
||||
"xrayConfigDirectChinaDomainDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به دامنه های چین تغییر میدهد"
|
||||
"xrayConfigDirectRussiaIp" = "ارتباط مستقیم به آیپی های روسیه"
|
||||
"xrayConfigDirectRussiaIpDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به آیپی های روسیه تغییر میدهد"
|
||||
"xrayConfigDirectRussiaDomain" = "ارتباط مستقیم به دامنه های روسیه"
|
||||
"xrayConfigDirectRussiaDomainDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به دامنه های روسیه تغییر میدهد"
|
||||
"xrayConfigGoogleIPv4" = "استفاده از آیپی ورژن 4 برای اتصال به گوگل"
|
||||
"xrayConfigGoogleIPv4Desc" = "مسیردهی جدید برای اتصال به گوگل با آیپی ورژن 4 اضافه میکند"
|
||||
"xrayConfigNetflixIPv4" = "استفاده از آیپی ورژن 4 برای اتصال به نتفلیکس"
|
||||
"xrayConfigNetflixIPv4Desc" = "مسیردهی جدید برای اتصال به نتفلیکس با آیپی ورژن 4 اضافه میکند"
|
||||
"xrayConfigInbounds" = "تنظیمات ورودی"
|
||||
"xrayConfigInboundsDesc" = "میتوانید الگوی تنظیمات را برای ورودی های خاص تنظیم نمایید"
|
||||
"xrayConfigOutbounds" = "تنظیمات خروجی"
|
||||
"xrayConfigOutboundsDesc" = "میتوانید الگوی تنظیمات را برای خروجی اینترنت تنظیم نمایید"
|
||||
"xrayConfigRoutings" = "تنظیمات قوانین مسیریابی"
|
||||
"xrayConfigRoutingsDesc" = "میتوانید الگوی تنظیمات را برای مسیریابی تنظیم نمایید"
|
||||
"manualLists" = "لیست های دستی"
|
||||
"manualListsDesc" = "فرمت: JSON Array"
|
||||
"manualBlockedIPs" = "لیست آیپی های مسدود شده"
|
||||
"manualBlockedDomains" = "لیست دامنه های مسدود شده"
|
||||
"manualDirectIPs" = "لیست آیپی های مستقیم"
|
||||
"manualDirectDomains" = "لیست دامنه های مستقیم"
|
||||
"manualIPv4Domains" = "لیست دامنههای IPv4"
|
||||
|
||||
[pages.settings.toasts]
|
||||
"modifySettings" = "ویرایش تنظیمات"
|
||||
"getSettings" = "دریافت تنظیمات"
|
||||
"modifyUser" = "ویرایش کاربر"
|
||||
"originalUserPassIncorrect" = "نام کاربری و رمز عبور فعلی اشتباه می باشد ."
|
||||
"userPassMustBeNotEmpty" = "نام کاربری و رمز عبور جدید نمیتواند خالی باشد ."
|
||||
"originalUserPassIncorrect" = "نام کاربری و رمز عبور فعلی اشتباه می باشد "
|
||||
"userPassMustBeNotEmpty" = "نام کاربری و رمز عبور جدید نمیتواند خالی باشد "
|
||||
|
||||
[tgbot]
|
||||
"noResult" = "❗ نتیجهای یافت نشد!"
|
||||
"wentWrong" = "❌ مشکلی رخ داده است!"
|
||||
"noInbounds" = "❗ هیچ ورودی یافت نشد!"
|
||||
"unlimited" = "♾ نامحدود"
|
||||
"day" = "روز"
|
||||
"days" = "روزها"
|
||||
"unknown" = "نامشخص"
|
||||
"inbounds" = "ورودیها"
|
||||
"clients" = "کلاینتها"
|
||||
|
||||
[tgbot.commands]
|
||||
"unknown" = "❗ دستور ناشناخته"
|
||||
"pleaseChoose" = "👇 لطفاً انتخاب کنید:\r\n"
|
||||
"help" = "🤖 به این ربات خوش آمدید! این ربات برای ارائه دادههای خاص از سرور طراحی شده است و به شما امکان تغییرات لازم را میدهد.\r\n\r\n"
|
||||
"start" = "👋 سلام <i>{{ .Firstname }}</i>.\r\n"
|
||||
"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 استفاده کنید."
|
||||
|
||||
[tgbot.messages]
|
||||
"cpuThreshold" = "🔴 میزان استفاده از CPU {{ .Percent }}% بیشتر از آستانه {{ .Threshold }}% است."
|
||||
"loginSuccess" = "✅ با موفقیت به پنل وارد شدید.\r\n"
|
||||
"loginFailed" = "❗️ ورود به پنل ناموفق بود.\r\n"
|
||||
"report" = "🕰 گزارشات زمانبندی شده: {{ .RunTime }}\r\n"
|
||||
"datetime" = "⏰ تاریخ-زمان: {{ .DateTime }}\r\n"
|
||||
"hostname" = "💻 نام میزبان: {{ .Hostname }}\r\n"
|
||||
"version" = "🚀 نسخه X-UI: {{ .Version }}\r\n"
|
||||
"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n"
|
||||
"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n"
|
||||
"ip" = "🌐 آدرس IP: {{ .IP }}\r\n"
|
||||
"serverUpTime" = "⏳ زمان کارکرد سرور: {{ .UpTime }} {{ .Unit }}\r\n"
|
||||
"serverLoad" = "📈 بار سرور: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
|
||||
"serverMemory" = "📋 حافظه سرور: {{ .Current }}/{{ .Total }}\r\n"
|
||||
"tcpCount" = "🔹 تعداد ترافیک TCP: {{ .Count }}\r\n"
|
||||
"udpCount" = "🔸 تعداد ترافیک UDP: {{ .Count }}\r\n"
|
||||
"traffic" = "🚦 ترافیک: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
|
||||
"xrayStatus" = "ℹ️ وضعیت Xray: {{ .State }}\r\n"
|
||||
"username" = "👤 نام کاربری: {{ .Username }}\r\n"
|
||||
"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"
|
||||
"email" = "📧 ایمیل: {{ .Email }}\r\n"
|
||||
"upload" = "🔼 آپلود↑: {{ .Upload }}\r\n"
|
||||
"download" = "🔽 دانلود↓: {{ .Download }}\r\n"
|
||||
"total" = "🔄 کل: {{ .UpDown }} / {{ .Total }}\r\n"
|
||||
"exhaustedMsg" = "🚨 {{ .Type }} به اتمام رسیده است:\r\n"
|
||||
"exhaustedCount" = "🚨 تعداد {{ .Type }} به اتمام رسیده:\r\n"
|
||||
"disabled" = "🛑 غیرفعال: {{ .Disabled }}\r\n"
|
||||
"depleteSoon" = "🔜 به زودی به پایان خواهد رسید: {{ .Deplete }}\r\n \r\n"
|
||||
"backupTime" = "🗄 زمان پشتیبانگیری: {{ .Time }}\r\n"
|
||||
|
||||
[tgbot.buttons]
|
||||
"dbBackup" = "دریافت پشتیبان پایگاه داده"
|
||||
"serverUsage" = "استفاده از سرور"
|
||||
"getInbounds" = "دریافت ورودیها"
|
||||
"depleteSoon" = "به زودی به پایان خواهد رسید"
|
||||
"clientUsage" = "دریافت آمار کاربر"
|
||||
"commands" = "دستورات"
|
||||
|
||||
[tgbot.answers]
|
||||
"getInboundsFailed" = "❌ دریافت ورودیها با خطا مواجه شد."
|
||||
"askToAddUser" = "پیکربندی شما یافت نشد!\r\nشما باید نام کاربری تلگرام خود را پیکربندی کنید و از مدیر خود درخواست اضافه کردن آن به پیکربندی خود بکنید."
|
||||
"askToAddUserName" = "پیکربندی شما یافت نشد!\r\nلطفاً از مدیر خود درخواست استفاده از نام کاربری تلگرام خود در پیکربندی (ها) خود را بکنید.\r\n\r\nنام کاربری شما: <b>@{{ .TgUserName }}</b>"
|
||||
|
||||
414
web/translation/translate.ru_RU.toml
Normal file
414
web/translation/translate.ru_RU.toml
Normal file
@@ -0,0 +1,414 @@
|
||||
"username" = "имя пользователя"
|
||||
"password" = "пароль"
|
||||
"login" = "логин"
|
||||
"confirm" = "подтвердить"
|
||||
"cancel" = "отмена"
|
||||
"close" = "закрыть"
|
||||
"copy" = "копировать"
|
||||
"copied" = "скопировано"
|
||||
"download" = "скачать"
|
||||
"remark" = "примечание"
|
||||
"enable" = "включить"
|
||||
"protocol" = "протокол"
|
||||
"search" = "поиск"
|
||||
"filter" = "Фильтр"
|
||||
|
||||
"loading" = "загрузка"
|
||||
"second" = "секунда"
|
||||
"minute" = "минута"
|
||||
"hour" = "час"
|
||||
"day" = "день"
|
||||
"check" = "просмотр"
|
||||
"indefinite" = "бессрочно"
|
||||
"unlimited" = "безлимитно"
|
||||
"none" = "пусто"
|
||||
"qrCode" = "QR-код"
|
||||
"info" = "больше информации"
|
||||
"edit" = "изменить"
|
||||
"delete" = "удалить"
|
||||
"reset" = "обнулить"
|
||||
"copySuccess" = "скопировано"
|
||||
"sure" = "да"
|
||||
"encryption" = "Шифрование"
|
||||
"transmission" = "протокол передачи"
|
||||
"host" = "хост"
|
||||
"path" = "путь"
|
||||
"camouflage" = "маскировка"
|
||||
"status" = "статус"
|
||||
"enabled" = "включено"
|
||||
"disabled" = "отключено"
|
||||
"depleted" = "исчерпано"
|
||||
"depletingSoon" = "почти исчерпано"
|
||||
"domainName" = "домен"
|
||||
"additional" = "допольнительно"
|
||||
"monitor" = "порт IP"
|
||||
"certificate" = "сертификат"
|
||||
"fail" = "неудача"
|
||||
"success" = "успешно"
|
||||
"getVersion" = "узнать версию"
|
||||
"install" = "установка"
|
||||
"clients" = "клиенты"
|
||||
"usage" = "использование"
|
||||
|
||||
[menu]
|
||||
"dashboard" = "статус системы"
|
||||
"inbounds" = "пользователи"
|
||||
"settings" = "настройки"
|
||||
"logout" = "выход"
|
||||
"link" = "другое"
|
||||
|
||||
[pages.login]
|
||||
"title" = "логин"
|
||||
"loginAgain" = "Время пребывания в сети вышло. Пожалуйста, войдите в систему снова"
|
||||
|
||||
[pages.login.toasts]
|
||||
"invalidFormData" = "Недопустимый формат данных"
|
||||
"emptyUsername" = "Введите имя пользователя"
|
||||
"emptyPassword" = "Введите пароль"
|
||||
"wrongUsernameOrPassword" = "Неверное имя пользователя или пароль"
|
||||
"successLogin" = "успешный вход"
|
||||
|
||||
[pages.index]
|
||||
"title" = "статус системы"
|
||||
"memory" = "память"
|
||||
"hard" = "жесткий диск"
|
||||
"xrayStatus" = "статус Xray"
|
||||
"stopXray" = "стоп"
|
||||
"restartXray" = "рестарт Xray"
|
||||
"xraySwitch" = "переключить версию"
|
||||
"xraySwitchClick" = "Выберите желаемую версию"
|
||||
"xraySwitchClickDesk" = "Выбирайте внимательно, так как старые версии могут быть несовместимы с текущими конфигурациями"
|
||||
"operationHours" = "Часы работы"
|
||||
"operationHoursDesc" = "Аптайм системы: время системы в сети"
|
||||
"systemLoad" = "Системная нагрузка"
|
||||
"connectionCount" = "количество соединений"
|
||||
"connectionCountDesc" = "Всего подключений по всем сетям»"
|
||||
"upSpeed" = "Общая скорость upload"
|
||||
"downSpeed" = "Общая скорость download"
|
||||
"totalSent" = "Общий объем загруженных данных с момента запуска системы"
|
||||
"totalReceive" = "Общий объем полученных данных с момента запуска системы"
|
||||
"xraySwitchVersionDialog" = "переключить версию Xray"
|
||||
"xraySwitchVersionDialogDesc" = "Вы точно хотите сменить версию Xray?"
|
||||
"dontRefresh" = "Установка. Не обновляйте эту страницу"
|
||||
"logs" = "Логи"
|
||||
"config" = "Конфиг"
|
||||
"backup" = "Бекап и восстановление"
|
||||
"backupTitle" = "База данных бекапа и восстановления"
|
||||
"backupDescription" = "Не забудьте сделать резервную копию перед импортом новой базы данных"
|
||||
"exportDatabase" = "Экспорт базы данных"
|
||||
"importDatabase" = "Импорт базы данных"
|
||||
|
||||
[pages.inbounds]
|
||||
"title" = "пользователи"
|
||||
"totalDownUp" = "Всего входящих/исходящих"
|
||||
"totalUsage" = "Всего использовано"
|
||||
"inboundCount" = "Количество пользователей"
|
||||
"operate" = "Меню"
|
||||
"enable" = "Включить"
|
||||
"remark" = "Примечание"
|
||||
"protocol" = "Протокол"
|
||||
"port" = "Порт"
|
||||
"traffic" = "Траффик"
|
||||
"details" = "Подробнее"
|
||||
"transportConfig" = "Перенести"
|
||||
"expireDate" = "Дата окончания"
|
||||
"resetTraffic" = "Обнулить траффик"
|
||||
"addInbound" = "Добавить пользователя"
|
||||
"generalActions" = "Общие действия"
|
||||
"create" = "Создать"
|
||||
"update" = "Обновить"
|
||||
"modifyInbound" = "Изменить данные"
|
||||
"deleteInbound" = "Удалить пользователя"
|
||||
"deleteInboundContent" = "Подтвердите удаление пользователя?"
|
||||
"resetTrafficContent" = "Подтвердите обнуление траффика?"
|
||||
"copyLink" = "Копировать ключ"
|
||||
"address" = "Адрес"
|
||||
"network" = "Сеть"
|
||||
"destinationPort" = "Порт назначения"
|
||||
"targetAddress" = "Целевой адрес"
|
||||
"disableInsecureEncryption" = "Отключить небезопасное шифрование"
|
||||
"monitorDesc" = "Оставьте пустым по умолчанию"
|
||||
"meansNoLimit" = "Значит без ограничений"
|
||||
"totalFlow" = "Общий расход"
|
||||
"leaveBlankToNeverExpire" = "Оставьте пустым, чтобы никогда не истекать"
|
||||
"noRecommendKeepDefault" = "Нет требований для сохранения настроек по умолчанию"
|
||||
"certificatePath" = "Путь файла сертификата"
|
||||
"certificateContent" = "Содержимое файла сертификата"
|
||||
"publicKeyPath" = "Путь к публичному ключу"
|
||||
"publicKeyContent" = "Содержимое публичного ключа"
|
||||
"keyPath" = "Путь к приватному ключу"
|
||||
"keyContent" = "Содержимое приватного ключа"
|
||||
"clickOnQRcode" = "Нажмите на QR-код, чтобы скопировать"
|
||||
"client" = "Клиент"
|
||||
"export" = "Поделиться ключом"
|
||||
"clone" = "Клонировать"
|
||||
"cloneInbound" = "Клонировать пользователя"
|
||||
"cloneInboundContent" = "Все настройки этого пользователя, кроме порта, порт прослушки и клиентов, будут клонированы"
|
||||
"cloneInboundOk" = "Клонировать"
|
||||
"resetAllTraffic" = "Обнулить весь траффик"
|
||||
"resetAllTrafficTitle" = "Обнуление всего траффика"
|
||||
"resetAllTrafficContent" = "Подтверждаете обнуление всего траффика пользователей?"
|
||||
"resetInboundClientTraffics" = "Обнулить траффик пользователей"
|
||||
"resetInboundClientTrafficTitle" = "Обнуление траффика пользователей"
|
||||
"resetInboundClientTrafficContent" = "Вы уверены, что хотите обнулить весь трафик для этих пользователей?"
|
||||
"resetAllClientTraffics" = "Обнулить весь траффик пользователей"
|
||||
"resetAllClientTrafficTitle" = "Обнуление всего траффика пользователей"
|
||||
"resetAllClientTrafficContent" = "Подтверждаете обнуление всего траффика пользователей?"
|
||||
"delDepletedClients" = "Удалить отключенных пользователей"
|
||||
"delDepletedClientsTitle" = "Удаление отключенных пользователей"
|
||||
"delDepletedClientsContent" = "Подтверждаете удаление отключенных пользователей?"
|
||||
"email" = "Email"
|
||||
"emailDesc" = "Пожалуйста, укажите уникальный Email"
|
||||
"setDefaultCert" = "Установить сертификат с панели"
|
||||
"telegramDesc" = "Используйте идентификатор Telegram без символа @ или идентификатора чата (можно получить его здесь @userinfobot или использовать команду '/id' в боте)"
|
||||
"subscriptionDesc" = "вы можете найти свою ссылку подписки в разделе «Подробнее», также вы можете использовать одно и то же имя для нескольких конфигов"
|
||||
|
||||
[pages.client]
|
||||
"add" = "Добавить пользователя"
|
||||
"edit" = "Редактировать пользователя"
|
||||
"submitAdd" = "Добавить пользователя"
|
||||
"submitEdit" = "Сохранить изменения"
|
||||
"clientCount" = "Количество пользователей"
|
||||
"bulk" = "Добавить несколько"
|
||||
"method" = "Метод"
|
||||
"first" = "Первый"
|
||||
"last" = "Последний"
|
||||
"prefix" = "Префикс"
|
||||
"postfix" = "Постфикс"
|
||||
"delayedStart" = "Начать со времени первого подключения"
|
||||
"expireDays" = "Срок действия"
|
||||
"days" = "дней"
|
||||
|
||||
[pages.inbounds.toasts]
|
||||
"obtain" = "Получить"
|
||||
|
||||
[pages.inbounds.stream.general]
|
||||
"requestHeader" = "Требуется заголовок"
|
||||
"name" = "Имя"
|
||||
"value" = "Значение"
|
||||
|
||||
[pages.inbounds.stream.tcp]
|
||||
"requestVersion" = "Требуется версия"
|
||||
"requestMethod" = "Требуется метод"
|
||||
"requestPath" = "Требуется путь"
|
||||
"responseVersion" = "Указать версию"
|
||||
"responseStatus" = "Указать статус"
|
||||
"responseStatusDescription" = "Указать примечание статуса"
|
||||
"responseHeader" = "Указать заголовок"
|
||||
|
||||
[pages.inbounds.stream.quic]
|
||||
"encryption" = "Шифрование"
|
||||
|
||||
[pages.settings]
|
||||
"title" = "Настройки"
|
||||
"save" = "Сохранить"
|
||||
"infoDesc" = "Каждое изменение здесь необходимо сохранить и перезапустить панель, чтобы оно вступило в силу"
|
||||
"restartPanel" = "Рестарт панели"
|
||||
"restartPanelDesc" = "Подтвердите рестарт панели? ОК для рестарта панели через 3 сек. Если вы не можете пользоваться панелью после рестарта, пожалуйста, посмотрите лог панели на сервере"
|
||||
"resetDefaultConfig" = "Сбросить всё по-умолчанию"
|
||||
"panelConfig" = "Настройки панели"
|
||||
"userSettings" = "Настройки безопасности"
|
||||
"xrayConfiguration" = "Конфигурация Xray"
|
||||
"TGBotSettings" = "Настройки Телеграм-бота"
|
||||
"panelListeningIP" = "IP-порт панели"
|
||||
"panelListeningIPDesc" = "Оставьте пустым для работы с любого IP. Перезагрузите панель для применения настроек"
|
||||
"panelListeningDomain" = "Домен прослушивания панели"
|
||||
"panelListeningDomainDesc" = "По умолчанию оставьте пустым, чтобы отслеживать все домены и IP-адреса"
|
||||
"panelPort" = "Порт панели"
|
||||
"panelPortDesc" = "Перезагрузите панель для применения настроек"
|
||||
"publicKeyPath" = "Путь к файлу публичного ключа сертификата панели"
|
||||
"publicKeyPathDesc" = "Введите полный путь, начинающийся с «/». Перезагрузите панель для применения настроек"
|
||||
"privateKeyPath" = "Путь к файлу приватного ключа сертификата панели"
|
||||
"privateKeyPathDesc" = "Введите полный путь, начинающийся с «/». Перезагрузите панель для применения настроек"
|
||||
"panelUrlPath" = "Корневой путь URL-адреса панели"
|
||||
"panelUrlPathDesc" = "Должен начинаться с «/» и заканчиваться на «/». Перезагрузите панель для применения настроек"
|
||||
"oldUsername" = "Имя пользователя сейчас"
|
||||
"currentPassword" = "Пароль сейчас"
|
||||
"newUsername" = "Новое имя пользователя"
|
||||
"newPassword" = "Новый пароль"
|
||||
"telegramBotEnable" = "Включить Телеграм-бота"
|
||||
"telegramBotEnableDesc" = "Перезагрузите панель для применения настроек"
|
||||
"telegramToken" = "Токен Телеграм-бота"
|
||||
"telegramTokenDesc" = "Перезагрузите панель для применения настроек"
|
||||
"telegramChatId" = "Телеграм-ID админа бота"
|
||||
"telegramChatIdDesc" = "Множественные идентификаторы чата, разделенные запятыми. Чтобы получить свои идентификаторы чатов, используйте @userinfobot или команду '/id' в боте."
|
||||
"telegramNotifyTime" = "Частота уведомлений телеграм-бота"
|
||||
"telegramNotifyTimeDesc" = "Используйте формат Crontab. Перезагрузите панель для применения настроек"
|
||||
"tgNotifyBackup" = "Резервное копирование базы данных"
|
||||
"tgNotifyBackupDesc" = "Включать файл резервной копии базы данных с уведомлением об отчете. Перезагрузите панель для применения настроек"
|
||||
"sessionMaxAge" = "Продолжительность сессии"
|
||||
"sessionMaxAgeDesc" = "Продолжительность сессии в системе (значение: минута)"
|
||||
"expireTimeDiff" = "Порог истечения срока сессии для уведомления"
|
||||
"expireTimeDiffDesc" = "Получение уведомления об истечении срока действия сессии до достижения порогового значения (значение: день)"
|
||||
"trafficDiff" = "Порог траффика для уведомления"
|
||||
"trafficDiffDesc" = "Получение уведомления об исчерпании трафика до достижения порога (значение: ГБ)"
|
||||
"tgNotifyCpu" = "Порог нагрузки на ЦП для уведомления"
|
||||
"tgNotifyCpuDesc" = "Получение уведомления, если нагрузка на ЦП превышает этот порог (значение:%)"
|
||||
"timeZone" = "Временная зона"
|
||||
"timeZoneDesc" = "Запланированные задачи выполняются в соответствии со временем в этом часовом поясе. Перезагрузите панель для применения настроек"
|
||||
"subSettings" = "Подписка"
|
||||
"subEnable" = "Включить службу"
|
||||
"subEnableDesc" = "Функция подписки с отдельной конфигурацией"
|
||||
"subListen" = "Прослушивание IP"
|
||||
"subListenDesc" = "Оставьте пустым по умолчанию, чтобы отслеживать все IP-адреса"
|
||||
"subPort" = "Порт подписки"
|
||||
"subPortDesc" = "Номер порта для обслуживания службы подписки не должен использоваться на сервере"
|
||||
"subCertPath" = "Путь к файлу открытого ключа сертификата подписки"
|
||||
"subCertPathDesc" = "Введите абсолютный путь, начинающийся с '/'"
|
||||
"subKeyPath" = "Путь к файлу закрытого ключа сертификата подписки"
|
||||
"subKeyPathDesc" = "Введите абсолютный путь, начинающийся с '/'"
|
||||
"subPath" = "Корневой путь URL-адреса подписки"
|
||||
"subPathDesc" = "Должен начинаться с '/' и заканчиваться на '/'"
|
||||
"subDomain" = "Домен прослушивания"
|
||||
"subDomainDesc" = "Оставьте пустым по умолчанию, чтобы отслеживать все домены и IP-адреса"
|
||||
"subUpdates" = "Интервалы обновления подписки"
|
||||
"subUpdatesDesc" = "Часовой интервал между обновлениями в клиентском приложении"
|
||||
|
||||
[pages.settings.templates]
|
||||
"title" = "Шаблоны"
|
||||
"basicTemplate" = "Базовые шаблоны"
|
||||
"advancedTemplate" = "Расширенные шаблоны"
|
||||
"completeTemplate" = "Конфигурация шаблона"
|
||||
"generalConfigs" = "Основные настройки"
|
||||
"generalConfigsDesc" = "Эти параметры не позволят пользователям подключаться к определенным протоколам и веб-сайтам"
|
||||
"blockConfigs" = "Блокировка конфигураций"
|
||||
"blockConfigsDesc" = "Эти параметры не позволят пользователям подключаться к определенным протоколам и веб-сайтам."
|
||||
"blockCountryConfigs" = "Заблокировать конфигурации страны"
|
||||
"blockCountryConfigsDesc" = "Эти параметры не позволят пользователям подключаться к доменам определенной страны."
|
||||
"directCountryConfigs" = "Прямые настройки страны"
|
||||
"directCountryConfigsDesc" = "Эти параметры будут подключать пользователей напрямую к доменам определенной страны."
|
||||
"ipv4Configs" = "Настройки IPv4 "
|
||||
"ipv4ConfigsDesc" = "Эти параметры будут маршрутизироваться к целевым доменам только через IPv4"
|
||||
"xrayConfigTemplate" = "Шаблон конфигурации Xray"
|
||||
"xrayConfigTemplateDesc" = "Создание файла конфигурации Xray на основе этого шаблона. Перезагрузите панель для применения настроек"
|
||||
"xrayConfigFreedomStrategy" = "Настроить стратегию протокола Freedom"
|
||||
"xrayConfigFreedomStrategyDesc" = "Установить стратегию вывода сети в протоколе Freedom"
|
||||
"xrayConfigRoutingStrategy" = "Настроить доменную стратегию маршрутизации"
|
||||
"xrayConfigRoutingStrategyDesc" = "Установить общую стратегию маршрутизации разрешения DNS"
|
||||
"xrayConfigTorrent" = "Запретить использование BitTorrent"
|
||||
"xrayConfigTorrentDesc" = "Измените конфигурацию, чтобы пользователи не использовали BitTorrent. Перезагрузите панель для применения настроек"
|
||||
"xrayConfigPrivateIp" = "Запрет частных диапазонов IP-адресов для подключения"
|
||||
"xrayConfigPrivateIpDesc" = "Измените конфигурацию, чтобы избежать подключения к диапазонам частных IP-адресов. Перезагрузите панель для применения настроек"
|
||||
"xrayConfigAds" = "Бокировка рекламы"
|
||||
"xrayConfigAdsDesc" = "Измените конфигурацию, чтобы заблокировать рекламу. Перезагрузите панель для применения настроек"
|
||||
"xrayConfigFamily" = "Включить семейную конфигурацию"
|
||||
"xrayConfigFamilyDesc" = "Избегайте подключения к небезопасным веб-сайтам для всей семьи"
|
||||
"xrayConfigIRIp" = "Отключить подключение к диапазонам IP-адресов Ирана"
|
||||
"xrayConfigIRIpDesc" = "Измените конфигурацию, чтобы отключить подключение к диапазонам IP-адресов Ирана. Перезагрузите панель для применения настроек"
|
||||
"xrayConfigIRDomain" = "Отключить подключение к доменам Ирана"
|
||||
"xrayConfigIRDomainDesc" = "Измените конфигурацию, чтобы отключить подключение к доменам Ирана. Перезагрузите панель для применения настроек"
|
||||
"xrayConfigChinaIp" = "Отключить подключение к диапазонам IP-адресов Китая"
|
||||
"xrayConfigChinaIpDesc" = "Измените конфигурацию, чтобы отключить подключение к диапазонам IP-адресов Китая. Перезагрузите панель для применения настроек"
|
||||
"xrayConfigChinaDomain" = "Отключить подключение к доменам Китая"
|
||||
"xrayConfigChinaDomainDesc" = "Измените конфигурацию, чтобы отключить подключение к доменам Китая. Перезагрузите панель для применения настроек"
|
||||
"xrayConfigRussiaIp" = "Отключить подключение к диапазонам IP-адресов России"
|
||||
"xrayConfigRussiaIpDesc" = "Измените конфигурацию, чтобы отключить соединения с диапазонами IP-адресов России. Перезагрузите панель для применения настроек"
|
||||
"xrayConfigRussiaDomain" = "Отключить подключение к доменам России"
|
||||
"xrayConfigRussiaDomainDesc" = "Измените конфигурацию, чтобы избежать подключения к доменам России. Перезагрузите панель для применения настроек"
|
||||
"xrayConfigDirectIRIp" = "Прямое подключение к диапазонам IP-адресов Ирана"
|
||||
"xrayConfigDirectIRIpDesc" = "Измените шаблон конфигурации для прямого подключения к диапазонам IP-адресов Ирана"
|
||||
"xrayConfigDirectIRDomain" = "Прямое подключение к доменам Ирана"
|
||||
"xrayConfigDirectIRDomainDesc" = "Измените шаблон конфигурации для прямого подключения к доменам Ирана"
|
||||
"xrayConfigDirectChinaIp" = "Прямое подключение к диапазонам IP-адресов Китая"
|
||||
"xrayConfigDirectChinaIpDesc" = "Измените шаблон конфигурации для прямого подключения к диапазонам IP-адресов Китая"
|
||||
"xrayConfigDirectChinaDomain" = "Прямое подключение к доменам Китая"
|
||||
"xrayConfigDirectChinaDomainDesc" = "Измените шаблон конфигурации для прямого подключения к доменам Китая"
|
||||
"xrayConfigDirectRussiaIp" = "Прямое подключение к диапазонам IP-адресов России"
|
||||
"xrayConfigDirectRussiaIpDesc" = "Изменить шаблон конфигурации для прямого подключения к диапазонам IP-адресов России"
|
||||
"xrayConfigDirectRussiaDomain" = "Прямое подключение к доменам России"
|
||||
"xrayConfigDirectRussiaDomainDesc" = "Изменить шаблон конфигурации для прямого подключения к доменам России"
|
||||
"xrayConfigGoogleIPv4" = "Использовать IPv4 для Google"
|
||||
"xrayConfigGoogleIPv4Desc" = "Применить маршрутизацию Google для подключения к IPv4. Перезагрузите панель для применения настроек"
|
||||
"xrayConfigNetflixIPv4" = "Использовать IPv4 для Netflix"
|
||||
"xrayConfigNetflixIPv4Desc" = "Применить маршрутизацию Netflix для подключения к IPv4. Перезагрузите панель для применения настроек"
|
||||
"xrayConfigInbounds" = "Конфигурация подключений"
|
||||
"xrayConfigInboundsDesc" = "Изменение шаблона конфигурации, для подключения определенных пользователей. Перезагрузите панель для применения настроек"
|
||||
"xrayConfigOutbounds" = "Конфигурация исходящих"
|
||||
"xrayConfigOutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить исходящие пути для этого сервера. Перезагрузите панель для применения настроек"
|
||||
"xrayConfigRoutings" = "Настройка правил маршрутизации"
|
||||
"xrayConfigRoutingsDesc" = "Изменение шаблона конфигурации, для определения правил маршрутизации для этого сервера. Перезагрузите панель для применения настроек"
|
||||
"manualLists" = "ручные списки"
|
||||
"manualListsDesc" = "Пожалуйста, используйте формат массива JSON"
|
||||
"manualBlockedIPs" = "Список заблокированных IP-адресов"
|
||||
"manualBlockedDomains" = "Список заблокированных доменов"
|
||||
"manualDirectIPs" = "Список прямых IP-адресов"
|
||||
"manualDirectDomains" = "Список прямых доменов"
|
||||
"manualIPv4Domains" = "Список доменов IPv4"
|
||||
|
||||
[pages.settings.toasts]
|
||||
"modifySettings" = "Изменение настроек"
|
||||
"getSettings" = "Просмотр настроек"
|
||||
"modifyUser" = "Изменение пользователя "
|
||||
"originalUserPassIncorrect" = "Неверное имя пользователя или пароль"
|
||||
"userPassMustBeNotEmpty" = "Новое имя пользователя и новый пароль должны быть заполнены"
|
||||
|
||||
[tgbot]
|
||||
"noResult" = "❗ Нет результатов!"
|
||||
"wentWrong" = "❌ Что-то пошло не так!"
|
||||
"noInbounds" = "❗ Входящих соединений не найдено!"
|
||||
"unlimited" = "♾ Неограниченно"
|
||||
"day" = "День"
|
||||
"days" = "Дней"
|
||||
"unknown" = "Неизвестно"
|
||||
"inbounds" = "Входящие"
|
||||
"clients" = "Клиенты"
|
||||
|
||||
[tgbot.commands]
|
||||
"unknown" = "❗ Неизвестная команда"
|
||||
"pleaseChoose" = "👇 Пожалуйста, выберите:\r\n"
|
||||
"help" = "🤖 Добро пожаловать в этого бота! Он предназначен для предоставления вам конкретных данных с сервера и позволяет вносить необходимые изменения.\r\n\r\n"
|
||||
"start" = "👋 Привет, <i>{{ .Firstname }}</i>.\r\n"
|
||||
"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."
|
||||
|
||||
[tgbot.messages]
|
||||
"cpuThreshold" = "🔴 Загрузка процессора составляет {{ .Percent }}%, что превышает пороговое значение {{ .Threshold }}%"
|
||||
"loginSuccess" = "✅ Успешный вход в панель.\r\n"
|
||||
"loginFailed" = "❗️ Ошибка входа в панель.\r\n"
|
||||
"report" = "🕰 Запланированные отчеты: {{ .RunTime }}\r\n"
|
||||
"datetime" = "⏰ Дата и время: {{ .DateTime }}\r\n"
|
||||
"hostname" = "💻 Имя хоста: {{ .Hostname }}\r\n"
|
||||
"version" = "🚀 Версия X-UI: {{ .Version }}\r\n"
|
||||
"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n"
|
||||
"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n"
|
||||
"ip" = "🌐 IP: {{ .IP }}\r\n"
|
||||
"serverUpTime" = "⏳ Время работы сервера: {{ .UpTime }} {{ .Unit }}\r\n"
|
||||
"serverLoad" = "📈 Загрузка сервера: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
|
||||
"serverMemory" = "📋 Память сервера: {{ .Current }}/{{ .Total }}\r\n"
|
||||
"tcpCount" = "🔹 Количество TCP-соединений: {{ .Count }}\r\n"
|
||||
"udpCount" = "🔸 Количество UDP-соединений: {{ .Count }}\r\n"
|
||||
"traffic" = "🚦 Трафик: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
|
||||
"xrayStatus" = "ℹ️ Состояние Xray: {{ .State }}\r\n"
|
||||
"username" = "👤 Имя пользователя: {{ .Username }}\r\n"
|
||||
"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"
|
||||
"email" = "📧 Email: {{ .Email }}\r\n"
|
||||
"upload" = "🔼 Загрузка↑: {{ .Upload }}\r\n"
|
||||
"download" = "🔽 Скачивание↓: {{ .Download }}\r\n"
|
||||
"total" = "🔄 Всего: {{ .UpDown }} / {{ .Total }}\r\n"
|
||||
"exhaustedMsg" = "🚨 Исчерпаны {{ .Type }}:\r\n"
|
||||
"exhaustedCount" = "🚨 Количество исчерпанных {{ .Type }}:\r\n"
|
||||
"disabled" = "🛑 Отключено: {{ .Disabled }}\r\n"
|
||||
"depleteSoon" = "🔜 Скоро исчерпание: {{ .Deplete }}\r\n \r\n"
|
||||
"backupTime" = "🗄 Время резервного копирования: {{ .Time }}\r\n"
|
||||
|
||||
[tgbot.buttons]
|
||||
"dbBackup" = "Получить резервную копию DB"
|
||||
"serverUsage" = "Использование сервера"
|
||||
"getInbounds" = "Получить входящие потоки"
|
||||
"depleteSoon" = "Скоро исчерпание"
|
||||
"clientUsage" = "Получить использование"
|
||||
"commands" = "Команды"
|
||||
|
||||
[tgbot.answers]
|
||||
"getInboundsFailed" = "❌ Не удалось получить входящие потоки."
|
||||
"askToAddUser" = "Конфигурация не найдена!\r\nВы должны настроить свое телеграм-имя пользователя и попросить вашего администратора добавить его в вашу конфигурацию."
|
||||
"askToAddUserName" = "Конфигурация не найдена!\r\nПожалуйста, попросите вашего администратора использовать ваше телеграм-имя пользователя в вашей конфигурации(ях).\r\n\r\nВаше имя пользователя: <b>@{{ .TgUserName }}</b>"
|
||||
@@ -11,6 +11,7 @@
|
||||
"enable" = "启用"
|
||||
"protocol" = "协议"
|
||||
"search" = "搜尋"
|
||||
"filter" = "过滤器"
|
||||
|
||||
"loading" = "加载中"
|
||||
"second" = "秒"
|
||||
@@ -33,10 +34,13 @@
|
||||
"host" = "主持人"
|
||||
"path" = "小路"
|
||||
"camouflage" = "伪装"
|
||||
"status" = "状态"
|
||||
"enabled" = "开启"
|
||||
"disabled" = "关闭"
|
||||
"depleted" = "耗尽"
|
||||
"depletingSoon" = "即将耗尽"
|
||||
"domainName" = "域名"
|
||||
"additional" = "额外"
|
||||
"additional" = "额外 ID"
|
||||
"monitor" = "监听"
|
||||
"certificate" = "证书"
|
||||
"fail" = "失败"
|
||||
@@ -49,7 +53,7 @@
|
||||
[menu]
|
||||
"dashboard" = "系统状态"
|
||||
"inbounds" = "入站列表"
|
||||
"setting" = "面板设置"
|
||||
"settings" = "面板设置"
|
||||
"logout" = "退出登录"
|
||||
"link" = "其他"
|
||||
|
||||
@@ -68,9 +72,9 @@
|
||||
"title" = "系统状态"
|
||||
"memory" = "内存"
|
||||
"hard" = "硬盘"
|
||||
"xrayStatus" = "xray 状态"
|
||||
"stopXray" = "停止 Xray"
|
||||
"restartXray" = "重启 Xray"
|
||||
"xrayStatus" = "Xray 状态"
|
||||
"stopXray" = "停止"
|
||||
"restartXray" = "重启"
|
||||
"xraySwitch" = "切换版本"
|
||||
"xraySwitchClick" = "点击你想切换的版本"
|
||||
"xraySwitchClickDesk" = "请谨慎选择,旧版本可能配置不兼容"
|
||||
@@ -83,9 +87,16 @@
|
||||
"downSpeed" = "所有网卡的总下载速度"
|
||||
"totalSent" = "系统启动以来所有网卡的总上传流量"
|
||||
"totalReceive" = "系统启动以来所有网卡的总下载流量"
|
||||
"xraySwitchVersionDialog" = "切换 xray 版本"
|
||||
"xraySwitchVersionDialogDesc" = "是否切换 xray 版本至"
|
||||
"dontRefreshh" = "安装中,请不要刷新此页面"
|
||||
"xraySwitchVersionDialog" = "切换 Xray 版本"
|
||||
"xraySwitchVersionDialogDesc" = "是否切换 Xray 版本至"
|
||||
"dontRefresh" = "安装中,请不要刷新此页面"
|
||||
"logs" = "日志"
|
||||
"config" = "配置"
|
||||
"backup" = "备份"
|
||||
"backupTitle" = "备份数据库"
|
||||
"backupDescription" = "请记住在导入新数据库之前进行备份"
|
||||
"exportDatabase" = "下载数据库"
|
||||
"importDatabase" = "上传数据库"
|
||||
|
||||
[pages.inbounds]
|
||||
"title" = "入站列表"
|
||||
@@ -103,8 +114,9 @@
|
||||
"expireDate" = "到期时间"
|
||||
"resetTraffic" = "重置流量"
|
||||
"addInbound" = "添加入"
|
||||
"addTo" = "添加"
|
||||
"revise" = "修改"
|
||||
"generalActions" = "通用操作"
|
||||
"create" = "添加"
|
||||
"update" = "修改"
|
||||
"modifyInbound" = "修改入站"
|
||||
"deleteInbound" = "删除入站"
|
||||
"deleteInboundContent" = "确定要删除入站吗?"
|
||||
@@ -129,6 +141,27 @@
|
||||
"clickOnQRcode" = "点击二维码复制"
|
||||
"client" = "客户"
|
||||
"export" = "导出链接"
|
||||
"clone" = "克隆"
|
||||
"cloneInbound" = "创造"
|
||||
"cloneInboundContent" = "此入站的所有项目除 Port、Listening IP、Clients 将应用于克隆"
|
||||
"cloneInboundOk" = "创造"
|
||||
"resetAllTraffic" = "重置所有入站流量"
|
||||
"resetAllTrafficTitle" = "重置所有入站流量"
|
||||
"resetAllTrafficContent" = "您确定要重置所有入站流量吗?"
|
||||
"resetInboundClientTraffics" = "重置客户端流量"
|
||||
"resetInboundClientTrafficTitle" = "重置所有客户端流量"
|
||||
"resetInboundClientTrafficContent" = "您确定要重置此入站客户端的所有流量吗?"
|
||||
"resetAllClientTraffics" = "重置所有客户端流量"
|
||||
"resetAllClientTrafficTitle" = "重置所有客户端流量"
|
||||
"resetAllClientTrafficContent" = "你确定要重置所有客户端的所有流量吗?"
|
||||
"delDepletedClients" = "删除耗尽的客户端"
|
||||
"delDepletedClientsTitle" = "删除耗尽的客户"
|
||||
"delDepletedClientsContent" = "你确定要删除所有耗尽的客户端吗?"
|
||||
"email" = "电子邮件"
|
||||
"emailDesc" = "电子邮件必须完全唯"
|
||||
"setDefaultCert" = "从面板设置证书"
|
||||
"telegramDesc" = "使用 Telegram ID,不包含 @ 符号或聊天 ID(可以在 @userinfobot 处获取,或在机器人中使用'/id'命令)"
|
||||
"subscriptionDesc" = "您可以在详细信息上找到您的子链接,也可以对多个配置使用相同的名称"
|
||||
|
||||
[pages.client]
|
||||
"add" = "添加客户端"
|
||||
@@ -142,6 +175,9 @@
|
||||
"last" = "最后"
|
||||
"prefix" = "前缀"
|
||||
"postfix" = "后缀"
|
||||
"delayedStart" = "首次使用后开始"
|
||||
"expireDays" = "过期天数"
|
||||
"days" = "天"
|
||||
|
||||
[pages.inbounds.toasts]
|
||||
"obtain" = "获取"
|
||||
@@ -163,66 +199,216 @@
|
||||
[pages.inbounds.stream.quic]
|
||||
"encryption" = "加密"
|
||||
|
||||
[pages.setting]
|
||||
[pages.settings]
|
||||
"title" = "设置"
|
||||
"save" = "保存配置"
|
||||
"infoDesc" = "此处的所有更改都需要保存并重启面板才能生效"
|
||||
"restartPanel" = "重启面板"
|
||||
"restartPanelDesc" = "确定要重启面板吗?点击确定将于 3 秒后重启,若重启后无法访问面板,请前往服务器查看面板日志信息"
|
||||
"resetDefaultConfig" = "重置为默认配置"
|
||||
"panelConfig" = "面板配置"
|
||||
"userSetting" = "用户设置"
|
||||
"xrayConfiguration" = "xray 相关设置"
|
||||
"TGReminder" = "TG提醒相关设置"
|
||||
"otherSetting" = "其他设置"
|
||||
"userSettings" = "用户设置"
|
||||
"xrayConfiguration" = "Xray 相关设置"
|
||||
"TGBotSettings" = "TG提醒相关设置"
|
||||
"panelListeningIP" = "面板监听 IP"
|
||||
"panelListeningIPDesc" = "默认留空监听所有 IP,重启面板生效"
|
||||
"panelListeningIPDesc" = "默认留空监听所有 IP"
|
||||
"panelListeningDomain" = "面板监听域名"
|
||||
"panelListeningDomainDesc" = "默认情况下留空以监视所有域名和 IP 地址"
|
||||
"panelPort" = "面板监听端口"
|
||||
"panelPortDesc" = "重启面板生效"
|
||||
"publicKeyPath" = "面板证书公钥文件路径"
|
||||
"publicKeyPathDesc" = "填写一个 '/' 开头的绝对路径,重启面板生效"
|
||||
"publicKeyPathDesc" = "填写一个 '/' 开头的绝对路径"
|
||||
"privateKeyPath" = "面板证书密钥文件路径"
|
||||
"privateKeyPathDesc" = "填写一个 '/' 开头的绝对路径,重启面板生效"
|
||||
"privateKeyPathDesc" = "填写一个 '/' 开头的绝对路径"
|
||||
"panelUrlPath" = "面板 url 根路径"
|
||||
"panelUrlPathDesc" = "必须以 '/' 开头,以 '/' 结尾,重启面板生效"
|
||||
"panelUrlPathDesc" = "必须以 '/' 开头,以 '/' 结尾"
|
||||
"oldUsername" = "原用户名"
|
||||
"currentPassword" = "原密码"
|
||||
"newUsername" = "新用户名"
|
||||
"newPassword" = "新密码"
|
||||
"advancedTemplate" = "高级模板部件"
|
||||
"completeTemplate" = "Xray 配置的完整模板"
|
||||
"xrayConfigTemplate" = "xray 配置模板"
|
||||
"xrayConfigTemplateDesc" = "以该模型为基础生成最终的xray配置文件,重新启动面板生成效率"
|
||||
"xrayConfigTorrent" = "禁止使用 bittorrent"
|
||||
"xrayConfigTorrentDesc" = "更改配置模板避免用户使用bittorrent,重启面板生效"
|
||||
"xrayConfigPrivateIp" = "禁止私人 ip 范围连接"
|
||||
"xrayConfigPrivateIpDesc" = "更改配置模板以避免连接私有 IP 范围,重启面板生效"
|
||||
"xrayConfigInbounds" = "入站配置"
|
||||
"xrayConfigInboundsDesc" = "更改配置模板接受特殊客户端,重启面板生效"
|
||||
"xrayConfigOutbounds" = "出站配置"
|
||||
"xrayConfigOutboundsDesc" = "更改配置模板定义此服务器的传出方式,重启面板生效"
|
||||
"xrayConfigRoutings" = "路由规则配置"
|
||||
"xrayConfigRoutingsDesc" = "更改配置模板为该服务器定义路由规则,重启面板生效"
|
||||
"telegramBotEnable" = "启用电报机器人"
|
||||
"telegramBotEnableDesc" = "重启面板生效"
|
||||
"telegramToken" = "电报机器人TOKEN"
|
||||
"telegramTokenDesc" = "重启面板生效"
|
||||
"telegramChatId" = "以逗号分隔的多个 chatID 重启面板生效"
|
||||
"telegramChatIdDesc" = "重启面板生效"
|
||||
"telegramChatId" = "以逗号分隔的多个 chatID"
|
||||
"telegramChatIdDesc" = "多个聊天 ID 用逗号分隔。使用 @userinfobot 或在机器人中使用'/id'命令获取您的聊天 ID。"
|
||||
"telegramNotifyTime" = "电报机器人通知时间"
|
||||
"telegramNotifyTimeDesc" = "采用Crontab定时格式,重启面板生效"
|
||||
"telegramNotifyTimeDesc" = "采用Crontab定时格式"
|
||||
"tgNotifyBackup" = "数据库备份"
|
||||
"tgNotifyBackupDesc" = "正在发送数据库备份文件和报告通知。重启面板生效"
|
||||
"tgNotifyExpireTimeDiff" = "剩余时间阈值"
|
||||
"tgNotifyExpireTimeDiffDesc" = "这个 talegram bot 会在到期前给你发送通知(单位:天)"
|
||||
"tgNotifyTrafficDiff" = "剩余流量阈值"
|
||||
"tgNotifyTrafficDiffDesc" = "这个 talegram bot 会在流量结束前给你发送通知(单位:GB)"
|
||||
"tgNotifyBackupDesc" = "正在发送数据库备份文件和报告通知"
|
||||
"sessionMaxAge" = "会话最大年龄"
|
||||
"sessionMaxAgeDesc" = "您可以保持登录状态的时间(单位:分钟)"
|
||||
"expireTimeDiff" = "耗尽时间阈值"
|
||||
"expireTimeDiffDesc" = "到期前检测耗尽(单位:天)"
|
||||
"trafficDiff" = "耗尽流量阈值"
|
||||
"trafficDiffDesc" = "完成流量前检测耗尽(单位:GB)"
|
||||
"tgNotifyCpu" = "CPU 百分比警报阈值"
|
||||
"tgNotifyCpuDesc" = "如果 CPU 使用率超过此百分比(单位:%),此 talegram bot 将向您发送通知"
|
||||
"timeZonee" = "时区"
|
||||
"timeZoneDesc" = "定时任务按照该时区的时间运行,重启面板生效"
|
||||
"timeZone" = "时区"
|
||||
"timeZoneDesc" = "定时任务按照该时区的时间运行"
|
||||
"subSettings" = "订阅"
|
||||
"subEnable" = "启用服务"
|
||||
"subEnableDesc" = "具有单独配置的订阅功能"
|
||||
"subListen" = "监听IP"
|
||||
"subListenDesc" = "留空默认监听所有IP"
|
||||
"subPort" = "订阅端口"
|
||||
"subPortDesc" = "服务订阅服务的端口号必须在服务器中未使用"
|
||||
"subCertPath" = "订阅证书公钥文件路径"
|
||||
"subCertPathDesc" = "填写以'/'开头的绝对路径"
|
||||
"subKeyPath" = "订阅证书私钥文件路径"
|
||||
"subKeyPathDesc" = "填写以'/'开头的绝对路径"
|
||||
"subPath" = "订阅 URL 根路径"
|
||||
"subPathDesc" = "必须以'/'开始并以'/'结束"
|
||||
"subDomain" = "监听域"
|
||||
"subDomainDesc" = "留空默认监控所有域名和IP"
|
||||
"subUpdates" = "订阅更新间隔"
|
||||
"subUpdatesDesc" = "客户端应用程序更新之间的间隔时间"
|
||||
|
||||
[pages.setting.toasts]
|
||||
"modifySetting" = "修改设置"
|
||||
"getSetting" = "获取设置"
|
||||
[pages.settings.templates]
|
||||
"title" = "模板"
|
||||
"basicTemplate" = "基本模板"
|
||||
"advancedTemplate" = "高级模板部件"
|
||||
"completeTemplate" = "Xray 配置的完整模板"
|
||||
"generalConfigs" = "通用配置"
|
||||
"generalConfigsDesc" = "这些选项将提供一般调整"
|
||||
"blockConfigs" = "阻塞配置"
|
||||
"blockConfigsDesc" = "这些选项将阻止用户连接到特定协议和网站"
|
||||
"blockCountryConfigs" = "阻止国家配置"
|
||||
"blockCountryConfigsDesc" = "这些选项将阻止用户连接到特定国家/地区的域。"
|
||||
"directCountryConfigs" = "直接国家配置"
|
||||
"directCountryConfigsDesc" = "这些选项会将用户直接连接到特定国家/地区的域。"
|
||||
"ipv4Configs" = "IPv4 配置"
|
||||
"ipv4ConfigsDesc" = "此选项将仅通过 IPv4 路由到目标域"
|
||||
"xrayConfigTemplate" = "Xray 配置模板"
|
||||
"xrayConfigTemplateDesc" = "以该模型为基础生成最终的Xray配置文件,重新启动面板生成效率"
|
||||
"xrayConfigFreedomStrategy" = "配置自由协议的策略"
|
||||
"xrayConfigFreedomStrategyDesc" = "在自由协议中设置网络输出策略"
|
||||
"xrayConfigRoutingStrategy" = "配置路由域策略"
|
||||
"xrayConfigRoutingStrategyDesc" = "设置DNS解析的整体路由策略"
|
||||
"xrayConfigTorrent" = "禁止使用 bittorrent"
|
||||
"xrayConfigTorrentDesc" = "更改配置模板避免用户使用bittorrent"
|
||||
"xrayConfigPrivateIp" = "禁止私人 IP 范围连接"
|
||||
"xrayConfigPrivateIpDesc" = "更改配置模板以避免连接私有 IP 范围"
|
||||
"xrayConfigAds" = "屏蔽广告"
|
||||
"xrayConfigAdsDesc" = "修改配置模板屏蔽广告"
|
||||
"xrayConfigFamily" = "启用家庭友好配置"
|
||||
"xrayConfigFamilyDesc" = "避免为家人连接到不安全的网站"
|
||||
"xrayConfigIRIp" = "禁止伊朗 IP 范围连接"
|
||||
"xrayConfigIRIpDesc" = "修改配置模板避免连接伊朗IP段"
|
||||
"xrayConfigIRDomain" = "禁止伊朗域连接"
|
||||
"xrayConfigIRDomainDesc" = "更改配置模板避免连接伊朗域名"
|
||||
"xrayConfigChinaIp" = "禁止中国 IP 范围连接"
|
||||
"xrayConfigChinaIpDesc" = "修改配置模板避免连接中国IP段"
|
||||
"xrayConfigChinaDomain" = "禁止中国域名连接"
|
||||
"xrayConfigChinaDomainDesc" = "更改配置模板避免连接中国域"
|
||||
"xrayConfigRussiaIp" = "禁止俄罗斯 IP 范围连接"
|
||||
"xrayConfigRussiaIpDesc" = "修改配置模板避免连接俄罗斯IP范围"
|
||||
"xrayConfigRussiaDomain" = "禁止俄罗斯域连接"
|
||||
"xrayConfigRussiaDomainDesc" = "更改配置模板避免连接俄罗斯域"
|
||||
"xrayConfigDirectIRIp" = "直接连接到伊朗 IP 范围"
|
||||
"xrayConfigDirectIRIpDesc" = "更改直接连接到伊朗 IP 范围的配置模板"
|
||||
"xrayConfigDirectIRDomain" = "直接连接到伊朗域"
|
||||
"xrayConfigDirectIRDomainDesc" = "更改直接连接到伊朗域的配置模板"
|
||||
"xrayConfigDirectChinaIp" = "直连中国IP范围"
|
||||
"xrayConfigDirectChinaIpDesc" = "更改直连中国 IP 范围的配置模板"
|
||||
"xrayConfigDirectChinaDomain" = "直连中国域名"
|
||||
"xrayConfigDirectChinaDomainDesc" = "修改中国域名直连配置模板"
|
||||
"xrayConfigDirectRussiaIp" = "直接连接到俄罗斯 IP 范围"
|
||||
"xrayConfigDirectRussiaIpDesc" = "更改直接连接到俄罗斯 IP 范围的配置模板"
|
||||
"xrayConfigDirectRussiaDomain" = "直接连接到俄罗斯域"
|
||||
"xrayConfigDirectRussiaDomainDesc" = "更改直接连接到俄罗斯域的配置模板"
|
||||
"xrayConfigGoogleIPv4" = "为谷歌使用 IPv4"
|
||||
"xrayConfigGoogleIPv4Desc" = "添加谷歌连接IPv4的路由"
|
||||
"xrayConfigNetflixIPv4" = "为 Netflix 使用 IPv4"
|
||||
"xrayConfigNetflixIPv4Desc" = "添加Netflix连接IPv4的路由"
|
||||
"xrayConfigInbounds" = "入站配置"
|
||||
"xrayConfigInboundsDesc" = "更改配置模板接受特殊客户端"
|
||||
"xrayConfigOutbounds" = "出站配置"
|
||||
"xrayConfigOutboundsDesc" = "更改配置模板定义此服务器的传出方式"
|
||||
"xrayConfigRoutings" = "路由规则配置"
|
||||
"xrayConfigRoutingsDesc" = "更改配置模板为该服务器定义路由规则"
|
||||
"manualLists" = "手动列表"
|
||||
"manualListsDesc" = "请使用 JSON 数组格式"
|
||||
"manualBlockedIPs" = "被阻止的 IP 列表"
|
||||
"manualBlockedDomains" = "被阻止的域列表"
|
||||
"manualDirectIPs" = "直接 IP 列表"
|
||||
"manualDirectDomains" = "直接域列表"
|
||||
"manualIPv4Domains" = "IPv4 域名列表"
|
||||
|
||||
[pages.settings.toasts]
|
||||
"modifySettings" = "修改设置"
|
||||
"getSettings" = "获取设置"
|
||||
"modifyUser" = "修改用户"
|
||||
"originalUserPassIncorrect" = "原用户名或原密码错误"
|
||||
"userPassMustBeNotEmpty" = "新用户名和新密码不能为空"
|
||||
"userPassMustBeNotEmpty" = "新用户名和新密码不能为空"
|
||||
|
||||
[tgbot]
|
||||
"noResult" = "❗ 没有结果!"
|
||||
"wentWrong" = "❌ 出了点问题!"
|
||||
"noInbounds" = "❗ 没有找到入站连接!"
|
||||
"unlimited" = "♾ 无限制"
|
||||
"day" = "天"
|
||||
"days" = "天"
|
||||
"unknown" = "未知"
|
||||
"inbounds" = "入站连接"
|
||||
"clients" = "客户端"
|
||||
|
||||
[tgbot.commands]
|
||||
"unknown" = "❗ 未知命令"
|
||||
"pleaseChoose" = "👇 请选择:\r\n"
|
||||
"help" = "🤖 欢迎使用本机器人!它旨在为您提供来自服务器的特定数据,并允许您进行必要的修改。\r\n\r\n"
|
||||
"start" = "👋 你好,<i>{{ .Firstname }}</i>。\r\n"
|
||||
"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,请使用密码。"
|
||||
|
||||
[tgbot.messages]
|
||||
"cpuThreshold" = "🔴 CPU 使用率为 {{ .Percent }}%,超过阈值 {{ .Threshold }}%"
|
||||
"loginSuccess" = "✅ 成功登录到面板。\r\n"
|
||||
"loginFailed" = "❗️ 面板登录失败。\r\n"
|
||||
"report" = "🕰 定时报告:{{ .RunTime }}\r\n"
|
||||
"datetime" = "⏰ 日期时间:{{ .DateTime }}\r\n"
|
||||
"hostname" = "💻 主机名:{{ .Hostname }}\r\n"
|
||||
"version" = "🚀 X-UI 版本:{{ .Version }}\r\n"
|
||||
"ipv6" = "🌐 IPv6:{{ .IPv6 }}\r\n"
|
||||
"ipv4" = "🌐 IPv4:{{ .IPv4 }}\r\n"
|
||||
"ip" = "🌐 IP:{{ .IP }}\r\n"
|
||||
"serverUpTime" = "⏳ 服务器运行时间:{{ .UpTime }} {{ .Unit }}\r\n"
|
||||
"serverLoad" = "📈 服务器负载:{{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
|
||||
"serverMemory" = "📋 服务器内存:{{ .Current }}/{{ .Total }}\r\n"
|
||||
"tcpCount" = "🔹 TCP 连接数:{{ .Count }}\r\n"
|
||||
"udpCount" = "🔸 UDP 连接数:{{ .Count }}\r\n"
|
||||
"traffic" = "🚦 流量:{{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
|
||||
"xrayStatus" = "ℹ️ Xray 状态:{{ .State }}\r\n"
|
||||
"username" = "👤 用户名:{{ .Username }}\r\n"
|
||||
"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"
|
||||
"email" = "📧 邮箱:{{ .Email }}\r\n"
|
||||
"upload" = "🔼 上传↑:{{ .Upload }}\r\n"
|
||||
"download" = "🔽 下载↓:{{ .Download }}\r\n"
|
||||
"total" = "🔄 总计:{{ .UpDown }} / {{ .Total }}\r\n"
|
||||
"exhaustedMsg" = "🚨 耗尽的{{ .Type }}:\r\n"
|
||||
"exhaustedCount" = "🚨 耗尽的{{ .Type }}数量:\r\n"
|
||||
"disabled" = "🛑 禁用:{{ .Disabled }}\r\n"
|
||||
"depleteSoon" = "🔜 即将耗尽:{{ .Deplete }}\r\n \r\n"
|
||||
"backupTime" = "🗄 备份时间:{{ .Time }}\r\n"
|
||||
|
||||
[tgbot.buttons]
|
||||
"dbBackup" = "获取数据库备份"
|
||||
"serverUsage" = "服务器使用情况"
|
||||
"getInbounds" = "获取入站信息"
|
||||
"depleteSoon" = "即将耗尽"
|
||||
"clientUsage" = "获取使用情况"
|
||||
"commands" = "命令"
|
||||
|
||||
[tgbot.answers]
|
||||
"getInboundsFailed" = "❌ 获取入站信息失败。"
|
||||
"askToAddUser" = "找不到您的配置!\r\n您应该配置您的 Telegram 用户名,并要求管理员将其添加到您的配置中。"
|
||||
"askToAddUserName" = "找不到您的配置!\r\n请要求您的管理员在您的配置中使用您的 Telegram 用户名。\r\n\r\n您的用户名:<b>@{{ .TgUserName }}</b>"
|
||||
|
||||
122
web/web.go
122
web/web.go
@@ -18,16 +18,15 @@ import (
|
||||
"x-ui/util/common"
|
||||
"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"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/robfig/cron/v3"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
//go:embed assets/*
|
||||
@@ -157,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
|
||||
@@ -179,13 +187,23 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
c.Header("Cache-Control", "max-age=31536000")
|
||||
}
|
||||
})
|
||||
err = s.initI18n(engine)
|
||||
|
||||
// init i18n
|
||||
err = locale.InitLocalizer(i18nFS, &s.settingService)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Apply locale middleware for i18n
|
||||
i18nWebFunc := func(key string, params ...string) string {
|
||||
return locale.I18n(locale.Web, key, params...)
|
||||
}
|
||||
engine.FuncMap["i18n"] = i18nWebFunc
|
||||
engine.Use(locale.LocalizerMiddleware())
|
||||
|
||||
// set static files and template
|
||||
if config.IsDebug() {
|
||||
// for develop
|
||||
// for development
|
||||
files, err := s.getHtmlFiles()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -193,12 +211,12 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
engine.LoadHTMLFiles(files...)
|
||||
engine.StaticFS(basePath+"assets", http.FS(os.DirFS("web/assets")))
|
||||
} else {
|
||||
// for prod
|
||||
t, err := s.getHtmlTemplate(engine.FuncMap)
|
||||
// for production
|
||||
template, err := s.getHtmlTemplate(engine.FuncMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
engine.SetHTMLTemplate(t)
|
||||
engine.SetHTMLTemplate(template)
|
||||
engine.StaticFS(basePath+"assets", http.FS(&wrapAssetsFS{FS: assetsFS}))
|
||||
}
|
||||
|
||||
@@ -212,87 +230,6 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
return engine, nil
|
||||
}
|
||||
|
||||
func (s *Server) initI18n(engine *gin.Engine) error {
|
||||
bundle := i18n.NewBundle(language.SimplifiedChinese)
|
||||
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
|
||||
err := fs.WalkDir(i18nFS, "translation", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
data, err := i18nFS.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = bundle.ParseMessageFileBytes(data, path)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
findI18nParamNames := func(key string) []string {
|
||||
names := make([]string, 0)
|
||||
keyLen := len(key)
|
||||
for i := 0; i < keyLen-1; i++ {
|
||||
if key[i:i+2] == "{{" { // 判断开头 "{{"
|
||||
j := i + 2
|
||||
isFind := false
|
||||
for ; j < keyLen-1; j++ {
|
||||
if key[j:j+2] == "}}" { // 结尾 "}}"
|
||||
isFind = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isFind {
|
||||
names = append(names, key[i+3:j])
|
||||
}
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
var localizer *i18n.Localizer
|
||||
|
||||
I18n := func(key string, params ...string) (string, error) {
|
||||
names := findI18nParamNames(key)
|
||||
if len(names) != len(params) {
|
||||
return "", common.NewError("find names:", names, "---------- params:", params, "---------- num not equal")
|
||||
}
|
||||
templateData := map[string]interface{}{}
|
||||
for i := range names {
|
||||
templateData[names[i]] = params[i]
|
||||
}
|
||||
return localizer.Localize(&i18n.LocalizeConfig{
|
||||
MessageID: key,
|
||||
TemplateData: templateData,
|
||||
})
|
||||
}
|
||||
|
||||
engine.FuncMap["i18n"] = I18n
|
||||
|
||||
engine.Use(func(c *gin.Context) {
|
||||
//accept := c.GetHeader("Accept-Language")
|
||||
|
||||
var lang string
|
||||
|
||||
if cookie, err := c.Request.Cookie("lang"); err == nil {
|
||||
lang = cookie.Value
|
||||
} else {
|
||||
lang = c.GetHeader("Accept-Language")
|
||||
}
|
||||
|
||||
localizer = i18n.NewLocalizer(bundle, lang)
|
||||
c.Set("localizer", localizer)
|
||||
c.Set("I18n", I18n)
|
||||
c.Next()
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) startTask() {
|
||||
err := s.xrayService.RestartXray(true)
|
||||
if err != nil {
|
||||
@@ -316,7 +253,7 @@ func (s *Server) startTask() {
|
||||
if (err == nil) && (isTgbotenabled) {
|
||||
runtime, err := s.settingService.GetTgbotRuntime()
|
||||
if err != nil || runtime == "" {
|
||||
logger.Errorf("Add NewStatsNotifyJob error[%s],Runtime[%s] invalid,wil run default", err, runtime)
|
||||
logger.Errorf("Add NewStatsNotifyJob error[%s], Runtime[%s] invalid, will run default", err, runtime)
|
||||
runtime = "@daily"
|
||||
}
|
||||
logger.Infof("Tg notify enabled,run at %s", runtime)
|
||||
@@ -331,7 +268,6 @@ func (s *Server) startTask() {
|
||||
if (err == nil) && (cpuThreshold > 0) {
|
||||
s.cron.AddJob("@every 10s", job.NewCheckCpuJob())
|
||||
}
|
||||
|
||||
} else {
|
||||
s.cron.Remove(entry)
|
||||
}
|
||||
@@ -411,7 +347,7 @@ func (s *Server) Start() (err error) {
|
||||
isTgbotenabled, err := s.settingService.GetTgbotenabled()
|
||||
if (err == nil) && (isTgbotenabled) {
|
||||
tgBot := s.tgbotService.NewTgbot()
|
||||
tgBot.Start()
|
||||
tgBot.Start(i18nFS)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -423,7 +359,7 @@ func (s *Server) Stop() error {
|
||||
if s.cron != nil {
|
||||
s.cron.Stop()
|
||||
}
|
||||
if s.tgbotService.IsRunnging() {
|
||||
if s.tgbotService.IsRunning() {
|
||||
s.tgbotService.Stop()
|
||||
}
|
||||
var err1 error
|
||||
|
||||
16
x-ui.sh
16
x-ui.sh
@@ -39,22 +39,22 @@ os_version=""
|
||||
os_version=$(grep -i version_id /etc/os-release | cut -d \" -f2 | cut -d . -f1)
|
||||
|
||||
if [[ "${release}" == "centos" ]]; then
|
||||
if [[ ${os_version} -lt 7 ]]; then
|
||||
echo -e "${red} Please use CentOS 7 or higher ${plain}\n" && exit 1
|
||||
if [[ ${os_version} -lt 8 ]]; then
|
||||
echo -e "${red} Please use CentOS 8 or higher ${plain}\n" && exit 1
|
||||
fi
|
||||
elif [[ "${release}" == "ubuntu" ]]; then
|
||||
if [[ ${os_version} -lt 18 ]]; then
|
||||
echo -e "${red}please use Ubuntu 18 or higher version!${plain}\n" && exit 1
|
||||
if [[ ${os_version} -lt 20 ]]; then
|
||||
echo -e "${red}please use Ubuntu 20 or higher version! ${plain}\n" && exit 1
|
||||
fi
|
||||
|
||||
elif [[ "${release}" == "fedora" ]]; then
|
||||
if [[ ${os_version} -lt 36 ]]; then
|
||||
echo -e "${red}please use Fedora 36 or higher version!${plain}\n" && exit 1
|
||||
echo -e "${red}please use Fedora 36 or higher version! ${plain}\n" && exit 1
|
||||
fi
|
||||
|
||||
elif [[ "${release}" == "debian" ]]; then
|
||||
if [[ ${os_version} -lt 8 ]]; then
|
||||
echo -e "${red} Please use Debian 8 or higher ${plain}\n" && exit 1
|
||||
if [[ ${os_version} -lt 10 ]]; then
|
||||
echo -e "${red} Please use Debian 10 or higher ${plain}\n" && exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -520,7 +520,7 @@ show_menu() {
|
||||
${green}14.${plain} Cancel x-ui Autostart
|
||||
————————————————
|
||||
${green}15.${plain} 一A key installation bbr (latest kernel)
|
||||
${green}16.${plain} 一Apply for an SSL certificate with one click(acme script)
|
||||
${green}16.${plain} 一Apply for a SSL certificate with one click(acme script)
|
||||
"
|
||||
show_status
|
||||
echo && read -p "Please enter your selection [0-16]: " num
|
||||
|
||||
181
xray/api.go
Normal file
181
xray/api.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package xray
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"time"
|
||||
"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/proxy/shadowsocks"
|
||||
"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"
|
||||
)
|
||||
|
||||
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.WithInsecure())
|
||||
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) 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),
|
||||
AlterId: uint32(user["alterId"].(uint16)),
|
||||
})
|
||||
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":
|
||||
account = serial.ToTypedMessage(&shadowsocks.Account{
|
||||
Password: user["password"].(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
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
)
|
||||
|
||||
type InboundConfig struct {
|
||||
Listen json_util.RawMessage `json:"listen"` // listen 不能为空字符串
|
||||
Listen json_util.RawMessage `json:"listen"` // listen cannot be an empty string
|
||||
Port int `json:"port"`
|
||||
Protocol string `json:"protocol"`
|
||||
Settings json_util.RawMessage `json:"settings"`
|
||||
|
||||
@@ -3,45 +3,38 @@ package xray
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
"x-ui/config"
|
||||
"x-ui/util/common"
|
||||
|
||||
"github.com/Workiva/go-datastructures/queue"
|
||||
statsservice "github.com/xtls/xray-core/app/stats/command"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func GetBinaryPath() string {
|
||||
return "bin/" + GetBinaryName()
|
||||
return config.GetBinFolderPath() + "/" + GetBinaryName()
|
||||
}
|
||||
|
||||
func GetConfigPath() string {
|
||||
return "bin/config.json"
|
||||
return config.GetBinFolderPath() + "/config.json"
|
||||
}
|
||||
|
||||
func GetGeositePath() string {
|
||||
return "bin/geosite.dat"
|
||||
return config.GetBinFolderPath() + "/geosite.dat"
|
||||
}
|
||||
|
||||
func GetGeoipPath() string {
|
||||
return "bin/geoip.dat"
|
||||
return config.GetBinFolderPath() + "/geoip.dat"
|
||||
}
|
||||
|
||||
func stopProcess(p *Process) {
|
||||
@@ -229,85 +222,3 @@ func (p *process) Stop() error {
|
||||
}
|
||||
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.WithInsecure())
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user