mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-09-08 11:16:18 +00:00
Merge branch 'MHSanaei:main' into main
This commit is contained in:
commit
e872edab4e
43 changed files with 512 additions and 581 deletions
8
.github/workflows/docker.yml
vendored
8
.github/workflows/docker.yml
vendored
|
@ -18,7 +18,7 @@ jobs:
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3.0.0
|
uses: docker/setup-buildx-action@v3.0.0
|
||||||
|
|
||||||
- name: Log in to GitHub Container Registry
|
- name: Login to GHCR
|
||||||
uses: docker/login-action@v3.0.0
|
uses: docker/login-action@v3.0.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
|
@ -27,7 +27,7 @@ jobs:
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5.5.0
|
uses: docker/metadata-action@v5.5.1
|
||||||
with:
|
with:
|
||||||
images: ghcr.io/${{ github.repository }}
|
images: ghcr.io/${{ github.repository }}
|
||||||
|
|
||||||
|
@ -36,6 +36,6 @@ jobs:
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
platforms: linux/amd64, linux/arm64/v8, linux/arm/v7, linux/arm/v6, linux/386, linux/arm/v5
|
platforms: linux/amd64, linux/arm64/v8, linux/arm/v7, linux/arm/v6, linux/386
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
|
@ -1,5 +1,4 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
case $1 in
|
case $1 in
|
||||||
amd64)
|
amd64)
|
||||||
ARCH="64"
|
ARCH="64"
|
||||||
|
@ -21,28 +20,21 @@ case $1 in
|
||||||
ARCH="arm32-v6"
|
ARCH="arm32-v6"
|
||||||
FNAME="armv6"
|
FNAME="armv6"
|
||||||
;;
|
;;
|
||||||
armv5)
|
|
||||||
ARCH="arm32-v5"
|
|
||||||
FNAME="armv5"
|
|
||||||
;;
|
|
||||||
*)
|
*)
|
||||||
ARCH="64"
|
ARCH="64"
|
||||||
FNAME="amd64"
|
FNAME="amd64"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
|
||||||
mkdir -p build/bin
|
mkdir -p build/bin
|
||||||
cd build/bin
|
cd build/bin
|
||||||
|
|
||||||
wget "https://github.com/XTLS/Xray-core/releases/download/v1.8.7/Xray-linux-${ARCH}.zip"
|
wget "https://github.com/XTLS/Xray-core/releases/download/v1.8.7/Xray-linux-${ARCH}.zip"
|
||||||
unzip "Xray-linux-${ARCH}.zip"
|
unzip "Xray-linux-${ARCH}.zip"
|
||||||
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat
|
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat
|
||||||
mv xray "xray-linux-${FNAME}"
|
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/geoip.dat
|
||||||
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
|
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
|
||||||
wget -O geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
|
wget -O geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
|
||||||
wget -O geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
|
wget -O geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
|
||||||
wget -O geoip_VN.dat https://github.com/vuong2023/vn-v2ray-rules/releases/latest/download/geoip.dat
|
wget -O geoip_VN.dat https://github.com/vuong2023/vn-v2ray-rules/releases/latest/download/geoip.dat
|
||||||
wget -O geosite_VN.dat https://github.com/vuong2023/vn-v2ray-rules/releases/latest/download/geosite.dat
|
wget -O geosite_VN.dat https://github.com/vuong2023/vn-v2ray-rules/releases/latest/download/geosite.dat
|
||||||
|
cd ../../
|
17
Dockerfile
17
Dockerfile
|
@ -1,11 +1,9 @@
|
||||||
# ========================================================
|
# ========================================================
|
||||||
# Stage: Builder
|
# Stage: Builder
|
||||||
# ========================================================
|
# ========================================================
|
||||||
FROM --platform=$BUILDPLATFORM golang:1.21-alpine AS builder
|
FROM golang:1.21-alpine AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ENV CGO_ENABLED=1
|
|
||||||
ENV CGO_CFLAGS="-D_LARGEFILE64_SOURCE"
|
|
||||||
|
|
||||||
RUN apk --no-cache --update add \
|
RUN apk --no-cache --update add \
|
||||||
build-base \
|
build-base \
|
||||||
|
@ -15,6 +13,8 @@ RUN apk --no-cache --update add \
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
ENV CGO_ENABLED=1
|
||||||
|
ENV CGO_CFLAGS="-D_LARGEFILE64_SOURCE"
|
||||||
RUN go build -o build/x-ui main.go
|
RUN go build -o build/x-ui main.go
|
||||||
RUN ./DockerInit.sh "$TARGETARCH"
|
RUN ./DockerInit.sh "$TARGETARCH"
|
||||||
|
|
||||||
|
@ -28,11 +28,13 @@ WORKDIR /app
|
||||||
RUN apk add --no-cache --update \
|
RUN apk add --no-cache --update \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
tzdata \
|
tzdata \
|
||||||
fail2ban
|
fail2ban \
|
||||||
|
bash
|
||||||
|
|
||||||
|
COPY --from=builder /app/build/ /app/
|
||||||
|
COPY --from=builder /app/DockerEntrypoint.sh /app/
|
||||||
|
COPY --from=builder /app/x-ui.sh /usr/bin/x-ui
|
||||||
|
|
||||||
COPY --from=builder /app/build/ /app/
|
|
||||||
COPY --from=builder /app/DockerEntrypoint.sh /app/
|
|
||||||
COPY --from=builder /app/x-ui.sh /usr/bin/x-ui
|
|
||||||
|
|
||||||
# Configure fail2ban
|
# Configure fail2ban
|
||||||
RUN rm -f /etc/fail2ban/jail.d/alpine-ssh.conf \
|
RUN rm -f /etc/fail2ban/jail.d/alpine-ssh.conf \
|
||||||
|
@ -47,4 +49,5 @@ RUN chmod +x \
|
||||||
/usr/bin/x-ui
|
/usr/bin/x-ui
|
||||||
|
|
||||||
VOLUME [ "/etc/x-ui" ]
|
VOLUME [ "/etc/x-ui" ]
|
||||||
|
CMD [ "./x-ui" ]
|
||||||
ENTRYPOINT [ "/app/DockerEntrypoint.sh" ]
|
ENTRYPOINT [ "/app/DockerEntrypoint.sh" ]
|
||||||
|
|
83
README.md
83
README.md
|
@ -25,11 +25,39 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
|
||||||
|
|
||||||
## Install Custom Version
|
## Install Custom Version
|
||||||
|
|
||||||
To install your desired version, add the version to the end of the installation command. e.g., ver `v2.1.1`:
|
To install your desired version, add the version to the end of the installation command. e.g., ver `v2.1.3`:
|
||||||
|
|
||||||
```
|
```
|
||||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v2.1.1
|
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v2.1.3
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## SSL Certificate
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Click for SSL Certificate</summary>
|
||||||
|
|
||||||
|
### Cloudflare
|
||||||
|
|
||||||
|
The Management script has a built-in SSL certificate application for Cloudflare. To use this script to apply for a certificate, you need the following:
|
||||||
|
|
||||||
|
- Cloudflare registered email
|
||||||
|
- Cloudflare Global API Key
|
||||||
|
- The domain name has been resolved to the current server through cloudflare
|
||||||
|
|
||||||
|
**1:** Run the`x-ui`command on the terminal, then choose `Cloudflare SSL Certificate`.
|
||||||
|
|
||||||
|
|
||||||
|
### Certbot
|
||||||
|
```
|
||||||
|
apt-get install certbot -y
|
||||||
|
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com
|
||||||
|
certbot renew --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
***Tip:*** *Certbot is also built into the Management script. You can run the `x-ui` command, then choose `SSL Certificate Management`.*
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
## Manual Install & Upgrade
|
## Manual Install & Upgrade
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
|
@ -106,10 +134,19 @@ systemctl restart x-ui
|
||||||
update to latest version
|
update to latest version
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd 3x-ui
|
cd 3x-ui
|
||||||
docker compose down
|
docker compose down
|
||||||
docker compose pull 3x-ui
|
docker compose pull 3x-ui
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
remove 3x-ui from docker
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker stop 3x-ui
|
||||||
|
docker rm 3x-ui
|
||||||
|
cd --
|
||||||
|
rm -r 3x-ui
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
@ -192,34 +229,6 @@ Supports a variety of different architectures and devices. Here are some of the
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|
||||||
## SSL Certificate
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Click for SSL Certificate</summary>
|
|
||||||
|
|
||||||
### Cloudflare
|
|
||||||
|
|
||||||
The Management script has a built-in SSL certificate application for Cloudflare. To use this script to apply for a certificate, you need the following:
|
|
||||||
|
|
||||||
- Cloudflare registered email
|
|
||||||
- Cloudflare Global API Key
|
|
||||||
- The domain name has been resolved to the current server through cloudflare
|
|
||||||
|
|
||||||
**1:** Run the`x-ui`command on the terminal, then choose `Cloudflare SSL Certificate`.
|
|
||||||
|
|
||||||
|
|
||||||
### Certbot
|
|
||||||
```
|
|
||||||
apt-get install certbot -y
|
|
||||||
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com
|
|
||||||
certbot renew --dry-run
|
|
||||||
```
|
|
||||||
|
|
||||||
***Tip:*** *Certbot is also built into the Management script. You can run the `x-ui` command, then choose `SSL Certificate Management`.*
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## [WARP Configuration](https://gitlab.com/fscarmen/warp)
|
## [WARP Configuration](https://gitlab.com/fscarmen/warp)
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
|
@ -272,13 +281,13 @@ If you want to use routing to WARP before v2.1.0 follow steps as below:
|
||||||
2. Select `IP Limit Management`.
|
2. Select `IP Limit Management`.
|
||||||
3. Choose the appropriate options based on your needs.
|
3. Choose the appropriate options based on your needs.
|
||||||
|
|
||||||
- make sure you have access.log on your Xray Configuration
|
- make sure you have ./access.log on your Xray Configuration after v2.1.3 we have an option for it
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
"log": {
|
"log": {
|
||||||
"loglevel": "warning",
|
|
||||||
"access": "./access.log",
|
"access": "./access.log",
|
||||||
"error": "./error.log"
|
"dnsLog": false,
|
||||||
|
"loglevel": "warning"
|
||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
2.1.1
|
2.1.3
|
|
@ -21,6 +21,7 @@ var db *gorm.DB
|
||||||
var initializers = []func() error{
|
var initializers = []func() error{
|
||||||
initUser,
|
initUser,
|
||||||
initInbound,
|
initInbound,
|
||||||
|
initOutbound,
|
||||||
initSetting,
|
initSetting,
|
||||||
initInboundClientIps,
|
initInboundClientIps,
|
||||||
initClientTraffic,
|
initClientTraffic,
|
||||||
|
@ -51,6 +52,10 @@ func initInbound() error {
|
||||||
return db.AutoMigrate(&model.Inbound{})
|
return db.AutoMigrate(&model.Inbound{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func initOutbound() error {
|
||||||
|
return db.AutoMigrate(&model.OutboundTraffics{})
|
||||||
|
}
|
||||||
|
|
||||||
func initSetting() error {
|
func initSetting() error {
|
||||||
return db.AutoMigrate(&model.Setting{})
|
return db.AutoMigrate(&model.Setting{})
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,15 @@ type Inbound struct {
|
||||||
Tag string `json:"tag" form:"tag" gorm:"unique"`
|
Tag string `json:"tag" form:"tag" gorm:"unique"`
|
||||||
Sniffing string `json:"sniffing" form:"sniffing"`
|
Sniffing string `json:"sniffing" form:"sniffing"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OutboundTraffics struct {
|
||||||
|
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
||||||
|
Tag string `json:"tag" form:"tag" gorm:"unique"`
|
||||||
|
Up int64 `json:"up" form:"up" gorm:"default:0"`
|
||||||
|
Down int64 `json:"down" form:"down" gorm:"default:0"`
|
||||||
|
Total int64 `json:"total" form:"total" gorm:"default:0"`
|
||||||
|
}
|
||||||
|
|
||||||
type InboundClientIps struct {
|
type InboundClientIps struct {
|
||||||
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
|
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||||
ClientEmail string `json:"clientEmail" form:"clientEmail" gorm:"unique"`
|
ClientEmail string `json:"clientEmail" form:"clientEmail" gorm:"unique"`
|
||||||
|
|
6
go.mod
6
go.mod
|
@ -8,18 +8,18 @@ require (
|
||||||
github.com/gin-gonic/gin v1.9.1
|
github.com/gin-gonic/gin v1.9.1
|
||||||
github.com/goccy/go-json v0.10.2
|
github.com/goccy/go-json v0.10.2
|
||||||
github.com/mymmrac/telego v0.28.0
|
github.com/mymmrac/telego v0.28.0
|
||||||
github.com/nicksnyder/go-i18n/v2 v2.3.0
|
github.com/nicksnyder/go-i18n/v2 v2.4.0
|
||||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||||
github.com/pelletier/go-toml/v2 v2.1.1
|
github.com/pelletier/go-toml/v2 v2.1.1
|
||||||
github.com/robfig/cron/v3 v3.0.1
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
github.com/shirou/gopsutil/v3 v3.23.12
|
github.com/shirou/gopsutil/v3 v3.24.1
|
||||||
github.com/valyala/fasthttp v1.51.0
|
github.com/valyala/fasthttp v1.51.0
|
||||||
github.com/xtls/xray-core v1.8.7
|
github.com/xtls/xray-core v1.8.7
|
||||||
go.uber.org/atomic v1.11.0
|
go.uber.org/atomic v1.11.0
|
||||||
golang.org/x/text v0.14.0
|
golang.org/x/text v0.14.0
|
||||||
google.golang.org/grpc v1.61.0
|
google.golang.org/grpc v1.61.0
|
||||||
gorm.io/driver/sqlite v1.5.4
|
gorm.io/driver/sqlite v1.5.4
|
||||||
gorm.io/gorm v1.25.5
|
gorm.io/gorm v1.25.6
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|
13
go.sum
13
go.sum
|
@ -179,8 +179,8 @@ github.com/mymmrac/telego v0.28.0 h1:DNXaYISeZw1J9oB81vCNdskLow8gCRRUJxufqLuH3XE
|
||||||
github.com/mymmrac/telego v0.28.0/go.mod h1:oRperySNzJq8dRTl24+uBF1Uy7tlQGIjid/JQtHDsZg=
|
github.com/mymmrac/telego v0.28.0/go.mod h1:oRperySNzJq8dRTl24+uBF1Uy7tlQGIjid/JQtHDsZg=
|
||||||
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
|
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
|
||||||
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
|
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
|
||||||
github.com/nicksnyder/go-i18n/v2 v2.3.0 h1:2NPsCsNFCVd7i+Su0xYsBrIhS3bE2XMv5gNTft2O+PQ=
|
github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM=
|
||||||
github.com/nicksnyder/go-i18n/v2 v2.3.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4=
|
github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4=
|
||||||
github.com/onsi/ginkgo/v2 v2.13.2 h1:Bi2gGVkfn6gQcjNjZJVO8Gf0FHzMPf2phUei9tejVMs=
|
github.com/onsi/ginkgo/v2 v2.13.2 h1:Bi2gGVkfn6gQcjNjZJVO8Gf0FHzMPf2phUei9tejVMs=
|
||||||
github.com/onsi/ginkgo/v2 v2.13.2/go.mod h1:XStQ8QcGwLyF4HdfcZB8SFOS/MWCgDuXMSBe6zrvLgM=
|
github.com/onsi/ginkgo/v2 v2.13.2/go.mod h1:XStQ8QcGwLyF4HdfcZB8SFOS/MWCgDuXMSBe6zrvLgM=
|
||||||
github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=
|
github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=
|
||||||
|
@ -230,8 +230,8 @@ github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJ
|
||||||
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U=
|
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U=
|
||||||
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
|
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
|
||||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||||
github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4=
|
github.com/shirou/gopsutil/v3 v3.24.1 h1:R3t6ondCEvmARp3wxODhXMTLC/klMa87h2PHUw5m7QI=
|
||||||
github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM=
|
github.com/shirou/gopsutil/v3 v3.24.1/go.mod h1:UU7a2MSBQa+kW1uuDq8DeEBS8kmrnQwsv2b5O513rwU=
|
||||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
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/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 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
||||||
|
@ -369,7 +369,6 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
@ -437,8 +436,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gorm.io/driver/sqlite v1.5.4 h1:IqXwXi8M/ZlPzH/947tn5uik3aYQslP9BVveoax0nV0=
|
gorm.io/driver/sqlite v1.5.4 h1:IqXwXi8M/ZlPzH/947tn5uik3aYQslP9BVveoax0nV0=
|
||||||
gorm.io/driver/sqlite v1.5.4/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
|
gorm.io/driver/sqlite v1.5.4/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
|
||||||
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
|
gorm.io/gorm v1.25.6 h1:V92+vVda1wEISSOMtodHVRcUIOPYa2tgQtyF+DfFx+A=
|
||||||
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
gorm.io/gorm v1.25.6/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||||
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
|
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
|
||||||
gvisor.dev/gvisor v0.0.0-20231104011432-48a6d7d5bd0b h1:yqkg3pTifuKukuWanp8spDsL4irJkHF5WI0J47hU87o=
|
gvisor.dev/gvisor v0.0.0-20231104011432-48a6d7d5bd0b h1:yqkg3pTifuKukuWanp8spDsL4irJkHF5WI0J47hU87o=
|
||||||
gvisor.dev/gvisor v0.0.0-20231104011432-48a6d7d5bd0b/go.mod h1:10sU+Uh5KKNv1+2x2A0Gvzt8FjD3ASIhorV3YsauXhk=
|
gvisor.dev/gvisor v0.0.0-20231104011432-48a6d7d5bd0b/go.mod h1:10sU+Uh5KKNv1+2x2A0Gvzt8FjD3ASIhorV3YsauXhk=
|
||||||
|
|
|
@ -123,9 +123,9 @@ config_after_install() {
|
||||||
echo -e "${green}username:${usernameTemp}${plain}"
|
echo -e "${green}username:${usernameTemp}${plain}"
|
||||||
echo -e "${green}password:${passwordTemp}${plain}"
|
echo -e "${green}password:${passwordTemp}${plain}"
|
||||||
echo -e "###############################################"
|
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}"
|
echo -e "${red}if you forgot your login info,you can type x-ui and then type 8 to check after installation${plain}"
|
||||||
else
|
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}"
|
echo -e "${red} this is your upgrade,will keep old settings,if you forgot your login info,you can type x-ui and then type 8 to check${plain}"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
/usr/local/x-ui/x-ui migrate
|
/usr/local/x-ui/x-ui migrate
|
||||||
|
|
|
@ -65,6 +65,16 @@ func Infof(format string, args ...interface{}) {
|
||||||
addToBuffer("INFO", fmt.Sprintf(format, args...))
|
addToBuffer("INFO", fmt.Sprintf(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Notice(args ...interface{}) {
|
||||||
|
logger.Notice(args...)
|
||||||
|
addToBuffer("NOTICE", fmt.Sprint(args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Noticef(format string, args ...interface{}) {
|
||||||
|
logger.Noticef(format, args...)
|
||||||
|
addToBuffer("NOTICE", fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
func Warning(args ...interface{}) {
|
func Warning(args ...interface{}) {
|
||||||
logger.Warning(args...)
|
logger.Warning(args...)
|
||||||
addToBuffer("WARNING", fmt.Sprint(args...))
|
addToBuffer("WARNING", fmt.Sprint(args...))
|
||||||
|
|
|
@ -861,13 +861,13 @@ Outbound.SocksSettings = class extends CommonClass {
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromJson(json={}) {
|
static fromJson(json={}) {
|
||||||
servers = json.servers;
|
let servers = json.servers;
|
||||||
if(ObjectUtil.isArrEmpty(servers)) servers=[{users: [{}]}];
|
if(ObjectUtil.isArrEmpty(servers)) servers=[{users: [{}]}];
|
||||||
return new Outbound.SocksSettings(
|
return new Outbound.SocksSettings(
|
||||||
servers[0].address,
|
servers[0].address,
|
||||||
servers[0].port,
|
servers[0].port,
|
||||||
ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].user,
|
ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].user,
|
||||||
ObjectUtil.isArrEmpty(servers[0].pass) ? '' : servers[0].users[0].pass,
|
ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].pass,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -891,13 +891,13 @@ Outbound.HttpSettings = class extends CommonClass {
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromJson(json={}) {
|
static fromJson(json={}) {
|
||||||
servers = json.servers;
|
let servers = json.servers;
|
||||||
if(ObjectUtil.isArrEmpty(servers)) servers=[{users: [{}]}];
|
if(ObjectUtil.isArrEmpty(servers)) servers=[{users: [{}]}];
|
||||||
return new Outbound.HttpSettings(
|
return new Outbound.HttpSettings(
|
||||||
servers[0].address,
|
servers[0].address,
|
||||||
servers[0].port,
|
servers[0].port,
|
||||||
ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].user,
|
ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].user,
|
||||||
ObjectUtil.isArrEmpty(servers[0].pass) ? '' : servers[0].users[0].pass,
|
ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].pass,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -914,8 +914,8 @@ Outbound.HttpSettings = class extends CommonClass {
|
||||||
|
|
||||||
Outbound.WireguardSettings = class extends CommonClass {
|
Outbound.WireguardSettings = class extends CommonClass {
|
||||||
constructor(
|
constructor(
|
||||||
mtu=1420, secretKey=Wireguard.generateKeypair().privateKey,
|
mtu=1420, secretKey='',
|
||||||
address='', workers=2, domainStrategy='', reserved='',
|
address=[''], workers=2, domainStrategy='', reserved='',
|
||||||
peers=[new Outbound.WireguardSettings.Peer()], kernelMode=false) {
|
peers=[new Outbound.WireguardSettings.Peer()], kernelMode=false) {
|
||||||
super();
|
super();
|
||||||
this.mtu = mtu;
|
this.mtu = mtu;
|
||||||
|
|
|
@ -2297,7 +2297,7 @@ Inbound.WireguardSettings = class extends XrayCommonClass {
|
||||||
};
|
};
|
||||||
|
|
||||||
Inbound.WireguardSettings.Peer = class extends XrayCommonClass {
|
Inbound.WireguardSettings.Peer = class extends XrayCommonClass {
|
||||||
constructor(publicKey='', psk='', allowedIPs=['0.0.0.0/0','::/0'], keepAlive=0) {
|
constructor(publicKey=Wireguard.generateKeypair().publicKey, psk='', allowedIPs=['0.0.0.0/0','::/0'], keepAlive=0) {
|
||||||
super();
|
super();
|
||||||
this.publicKey = publicKey;
|
this.publicKey = publicKey;
|
||||||
this.psk = psk;
|
this.psk = psk;
|
||||||
|
|
|
@ -83,41 +83,6 @@ class HttpUtil {
|
||||||
}
|
}
|
||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async jsonPost(url, data) {
|
|
||||||
let msg;
|
|
||||||
try {
|
|
||||||
const requestOptions = {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
};
|
|
||||||
const resp = await fetch(url, requestOptions);
|
|
||||||
const response = await resp.json();
|
|
||||||
|
|
||||||
msg = this._respToMsg({data : response});
|
|
||||||
} catch (e) {
|
|
||||||
msg = new Msg(false, e.toString());
|
|
||||||
}
|
|
||||||
this._handleMsg(msg);
|
|
||||||
return msg;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async postWithModalJson(url, data, modal) {
|
|
||||||
if (modal) {
|
|
||||||
modal.loading(true);
|
|
||||||
}
|
|
||||||
const msg = await this.jsonPost(url, data);
|
|
||||||
if (modal) {
|
|
||||||
modal.loading(false);
|
|
||||||
if (msg instanceof Msg && msg.success) {
|
|
||||||
modal.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return msg;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class PromiseUtil {
|
class PromiseUtil {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -33,9 +32,7 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
|
||||||
g.POST("/clientIps/:email", a.getClientIps)
|
g.POST("/clientIps/:email", a.getClientIps)
|
||||||
g.POST("/clearClientIps/:email", a.clearClientIps)
|
g.POST("/clearClientIps/:email", a.clearClientIps)
|
||||||
g.POST("/addClient", a.addInboundClient)
|
g.POST("/addClient", a.addInboundClient)
|
||||||
g.POST("/addGroupClient", a.addGroupInboundClient)
|
|
||||||
g.POST("/:id/delClient/:clientId", a.delInboundClient)
|
g.POST("/:id/delClient/:clientId", a.delInboundClient)
|
||||||
g.POST("/updateClients", a.updateGroupInboundClient)
|
|
||||||
g.POST("/updateClient/:clientId", a.updateInboundClient)
|
g.POST("/updateClient/:clientId", a.updateInboundClient)
|
||||||
g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
|
g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
|
||||||
g.POST("/resetAllTraffics", a.resetAllTraffics)
|
g.POST("/resetAllTraffics", a.resetAllTraffics)
|
||||||
|
@ -163,51 +160,23 @@ func (a *InboundController) clearClientIps(c *gin.Context) {
|
||||||
|
|
||||||
func (a *InboundController) addInboundClient(c *gin.Context) {
|
func (a *InboundController) addInboundClient(c *gin.Context) {
|
||||||
data := &model.Inbound{}
|
data := &model.Inbound{}
|
||||||
err := c.ShouldBind(data)
|
err := c.ShouldBind(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
|
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
needRestart := true
|
needRestart := true
|
||||||
|
|
||||||
needRestart, err = a.inboundService.AddInboundClient(data)
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, "Something went wrong!", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
jsonMsg(c, "Client(s) added", nil)
|
|
||||||
if err == nil && needRestart {
|
|
||||||
a.xrayService.SetToNeedRestart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *InboundController) addGroupInboundClient(c *gin.Context) {
|
|
||||||
var requestData []model.Inbound
|
|
||||||
|
|
||||||
err := c.ShouldBindJSON(&requestData)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
needRestart := true
|
|
||||||
|
|
||||||
for _, data := range requestData {
|
|
||||||
|
|
||||||
needRestart, err = a.inboundService.AddInboundClient(&data)
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, "Something went wrong!", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonMsg(c, "Client(s) added", nil)
|
|
||||||
if err == nil && needRestart {
|
|
||||||
a.xrayService.SetToNeedRestart()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
needRestart, err = a.inboundService.AddInboundClient(data)
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, "Something went wrong!", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonMsg(c, "Client(s) added", nil)
|
||||||
|
if err == nil && needRestart {
|
||||||
|
a.xrayService.SetToNeedRestart()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *InboundController) delInboundClient(c *gin.Context) {
|
func (a *InboundController) delInboundClient(c *gin.Context) {
|
||||||
|
@ -254,56 +223,6 @@ func (a *InboundController) updateInboundClient(c *gin.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *InboundController) updateGroupInboundClient(c *gin.Context) {
|
|
||||||
var requestData []map[string]interface{}
|
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&requestData); err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
needRestart := false
|
|
||||||
|
|
||||||
for _, item := range requestData {
|
|
||||||
|
|
||||||
inboundMap, ok := item["inbound"].(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
jsonMsg(c, "Something went wrong!", errors.New("Failed to convert 'inbound' to map"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
clientId, ok := item["clientId"].(string)
|
|
||||||
if !ok {
|
|
||||||
jsonMsg(c, "Something went wrong!", errors.New("Failed to convert 'clientId' to string"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
inboundJSON, err := json.Marshal(inboundMap)
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, "Something went wrong!", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var inboundModel model.Inbound
|
|
||||||
if err := json.Unmarshal(inboundJSON, &inboundModel); err != nil {
|
|
||||||
jsonMsg(c, "Something went wrong!", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if restart, err := a.inboundService.UpdateInboundClient(&inboundModel, clientId); err != nil {
|
|
||||||
jsonMsg(c, "Something went wrong!", err)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
needRestart = needRestart || restart
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonMsg(c, "Client updated", nil)
|
|
||||||
if needRestart {
|
|
||||||
a.xrayService.SetToNeedRestart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *InboundController) resetClientTraffic(c *gin.Context) {
|
func (a *InboundController) resetClientTraffic(c *gin.Context) {
|
||||||
id, err := strconv.Atoi(c.Param("id"))
|
id, err := strconv.Atoi(c.Param("id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -10,6 +10,7 @@ type XraySettingController struct {
|
||||||
XraySettingService service.XraySettingService
|
XraySettingService service.XraySettingService
|
||||||
SettingService service.SettingService
|
SettingService service.SettingService
|
||||||
InboundService service.InboundService
|
InboundService service.InboundService
|
||||||
|
OutboundService service.OutboundService
|
||||||
XrayService service.XrayService
|
XrayService service.XrayService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,6 +28,7 @@ func (a *XraySettingController) initRouter(g *gin.RouterGroup) {
|
||||||
g.GET("/getXrayResult", a.getXrayResult)
|
g.GET("/getXrayResult", a.getXrayResult)
|
||||||
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
|
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
|
||||||
g.POST("/warp/:action", a.warp)
|
g.POST("/warp/:action", a.warp)
|
||||||
|
g.GET("/getOutboundsTraffic", a.getOutboundsTraffic)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *XraySettingController) getXraySetting(c *gin.Context) {
|
func (a *XraySettingController) getXraySetting(c *gin.Context) {
|
||||||
|
@ -84,3 +86,12 @@ func (a *XraySettingController) warp(c *gin.Context) {
|
||||||
|
|
||||||
jsonObj(c, resp, err)
|
jsonObj(c, resp, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *XraySettingController) getOutboundsTraffic(c *gin.Context) {
|
||||||
|
outboundsTraffic, err := a.OutboundService.GetOutboundsTraffic()
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, "Error getting traffics", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonObj(c, outboundsTraffic, nil)
|
||||||
|
}
|
||||||
|
|
|
@ -11,12 +11,10 @@
|
||||||
<a-divider>Subscription</a-divider>
|
<a-divider>Subscription</a-divider>
|
||||||
<div class="qr-bg"><canvas @click="copyToClipboard('qrCode-sub',genSubLink(qrModal.client.subId))" id="qrCode-sub" style="width: 100%; height: 100%;"></canvas></div>
|
<div class="qr-bg"><canvas @click="copyToClipboard('qrCode-sub',genSubLink(qrModal.client.subId))" id="qrCode-sub" style="width: 100%; height: 100%;"></canvas></div>
|
||||||
</template>
|
</template>
|
||||||
<a-divider v-if="!isJustSub">{{ i18n "pages.inbounds.client" }}</a-divider>
|
<a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
|
||||||
<template v-if="!isJustSub">
|
<template v-for="(row, index) in qrModal.qrcodes">
|
||||||
<template v-for="(row, index) in qrModal.qrcodes">
|
<a-tag color="green" style="margin: 10px 0; display: block; text-align: center;">[[ row.remark ]]</a-tag>
|
||||||
<a-tag color="green" style="margin: 10px 0; display: block; text-align: center;">[[ row.remark ]]</a-tag>
|
<div class="qr-bg"><canvas @click="copyToClipboard('qrCode-'+index, row.link)" :id="'qrCode-'+index" style="width: 100%; height: 100%;"></canvas></div>
|
||||||
<div class="qr-bg"><canvas @click="copyToClipboard('qrCode-'+index, row.link)" :id="'qrCode-'+index" style="width: 100%; height: 100%;"></canvas></div>
|
|
||||||
</template>
|
|
||||||
</template>
|
</template>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
|
|
||||||
|
@ -29,14 +27,12 @@
|
||||||
qrcodes: [],
|
qrcodes: [],
|
||||||
clipboard: null,
|
clipboard: null,
|
||||||
visible: false,
|
visible: false,
|
||||||
isJustSub: false,
|
|
||||||
subId: '',
|
subId: '',
|
||||||
show: function (title = '', dbInbound, client, isJustSub = false) {
|
show: function (title = '', dbInbound, client) {
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.dbInbound = dbInbound;
|
this.dbInbound = dbInbound;
|
||||||
this.inbound = dbInbound.toInbound();
|
this.inbound = dbInbound.toInbound();
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.isJustSub = isJustSub;
|
|
||||||
this.subId = '';
|
this.subId = '';
|
||||||
this.qrcodes = [];
|
this.qrcodes = [];
|
||||||
this.inbound.genAllLinks(this.dbInbound.remark, app.remarkModel, client).forEach(l => {
|
this.inbound.genAllLinks(this.dbInbound.remark, app.remarkModel, client).forEach(l => {
|
||||||
|
@ -57,9 +53,6 @@
|
||||||
el: '#qrcode-modal',
|
el: '#qrcode-modal',
|
||||||
data: {
|
data: {
|
||||||
qrModal: qrModal,
|
qrModal: qrModal,
|
||||||
get isJustSub(){
|
|
||||||
return qrModal.isJustSub
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
copyToClipboard(elmentId, content) {
|
copyToClipboard(elmentId, content) {
|
||||||
|
|
|
@ -15,16 +15,8 @@
|
||||||
confirmLoading: false,
|
confirmLoading: false,
|
||||||
title: '',
|
title: '',
|
||||||
okText: '',
|
okText: '',
|
||||||
group: {
|
isEdit: false,
|
||||||
canGroup: true,
|
|
||||||
isGroup: false,
|
|
||||||
currentClient: null,
|
|
||||||
inbounds: [],
|
|
||||||
clients: [],
|
|
||||||
editIds: []
|
|
||||||
},
|
|
||||||
dbInbound: new DBInbound(),
|
dbInbound: new DBInbound(),
|
||||||
dbInbounds: null,
|
|
||||||
inbound: new Inbound(),
|
inbound: new Inbound(),
|
||||||
clients: [],
|
clients: [],
|
||||||
clientStats: [],
|
clientStats: [],
|
||||||
|
@ -33,137 +25,33 @@
|
||||||
clientIps: null,
|
clientIps: null,
|
||||||
delayedStart: false,
|
delayedStart: false,
|
||||||
ok() {
|
ok() {
|
||||||
if (clientModal.group.isGroup && clientModal.group.canGroup) {
|
if (clientModal.isEdit) {
|
||||||
const currentClient = clientModal.group.currentClient;
|
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id, clientModal.oldClientId);
|
||||||
|
|
||||||
clientModal.group.clients.forEach((client, index) => {
|
|
||||||
const { email, limitIp, totalGB, expiryTime, reset, enable, subId, tgId, flow } = currentClient;
|
|
||||||
|
|
||||||
const match = email.match(/^(.*?)__/);
|
|
||||||
const new_email = match ? match[1] : email;
|
|
||||||
|
|
||||||
client.email = `${new_email}__${index + 1}`;
|
|
||||||
client.limitIp = limitIp;
|
|
||||||
client.totalGB = totalGB;
|
|
||||||
client.expiryTime = expiryTime;
|
|
||||||
client.reset = reset;
|
|
||||||
client.enable = enable;
|
|
||||||
|
|
||||||
if (subId) {
|
|
||||||
client.subId = subId;
|
|
||||||
}
|
|
||||||
if (tgId) {
|
|
||||||
client.tgId = tgId;
|
|
||||||
}
|
|
||||||
if (flow) {
|
|
||||||
client.flow = flow;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (clientModal.isEdit) {
|
|
||||||
ObjectUtil.execute(clientModal.confirm, clientModal.group.clients, clientModal.group.inbounds, clientModal.group.editIds);
|
|
||||||
}else{
|
|
||||||
ObjectUtil.execute(clientModal.confirm, clientModal.group.clients, clientModal.group.inbounds);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (clientModal.isEdit){
|
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id);
|
||||||
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, dbInbounds = null, confirm = () => { }, isEdit = false }) {
|
show({ title = '', okText = '{{ i18n "sure" }}', index = null, dbInbound = null, confirm = () => { }, isEdit = false }) {
|
||||||
this.group = {
|
|
||||||
canGroup: true,
|
|
||||||
isGroup: false,
|
|
||||||
currentClient: null,
|
|
||||||
inbounds: [],
|
|
||||||
clients: [],
|
|
||||||
editIds: []
|
|
||||||
}
|
|
||||||
this.dbInbounds = dbInbounds;
|
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.okText = okText;
|
this.okText = okText;
|
||||||
this.isEdit = isEdit;
|
this.isEdit = isEdit;
|
||||||
if (dbInbounds !== null && Array.isArray(dbInbounds)) {
|
|
||||||
if (isEdit) {
|
|
||||||
this.showProcess(dbInbound, index);
|
|
||||||
let processSingleEdit = true
|
|
||||||
if (this.group.canGroup){
|
|
||||||
this.group.currentClient = this.clients[this.index]
|
|
||||||
const response = this.getGroupInboundsClients(dbInbounds,this.group.currentClient)
|
|
||||||
if (response.clients.length > 1){
|
|
||||||
this.group.isGroup = true;
|
|
||||||
this.group.inbounds = response.inbounds
|
|
||||||
this.group.clients = response.clients
|
|
||||||
this.group.editIds = response.editIds
|
|
||||||
if (this.clients[index].expiryTime < 0) {
|
|
||||||
this.delayedStart = true;
|
|
||||||
}
|
|
||||||
processSingleEdit = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(processSingleEdit){
|
|
||||||
this.singleEditClientProcess(index)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.group.isGroup = true;
|
|
||||||
dbInbounds.forEach((dbInboundItem) => {
|
|
||||||
this.showProcess(dbInboundItem);
|
|
||||||
this.addClient(this.inbound.protocol, this.clients);
|
|
||||||
this.group.inbounds.push(dbInboundItem.id)
|
|
||||||
this.group.clients.push(this.clients[this.index])
|
|
||||||
})
|
|
||||||
this.group.currentClient = this.clients[this.index]
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.showProcess(dbInbound, index);
|
|
||||||
if (isEdit) {
|
|
||||||
this.singleEditClientProcess(index)
|
|
||||||
} else {
|
|
||||||
this.addClient(this.inbound.protocol, this.clients);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.clientStats = this.dbInbound.clientStats.find(row => row.email === this.clients[this.index].email);
|
|
||||||
this.confirm = confirm;
|
|
||||||
},
|
|
||||||
showProcess(dbInbound, index = null) {
|
|
||||||
this.dbInbound = new DBInbound(dbInbound);
|
this.dbInbound = new DBInbound(dbInbound);
|
||||||
this.inbound = dbInbound.toInbound();
|
this.inbound = dbInbound.toInbound();
|
||||||
this.clients = this.inbound.clients;
|
this.clients = this.inbound.clients;
|
||||||
this.index = index === null ? this.clients.length : index;
|
this.index = index === null ? this.clients.length : index;
|
||||||
this.delayedStart = false;
|
this.delayedStart = false;
|
||||||
},
|
if (isEdit) {
|
||||||
singleEditClientProcess(index) {
|
if (this.clients[index].expiryTime < 0) {
|
||||||
if (this.clients[index].expiryTime < 0) {
|
this.delayedStart = true;
|
||||||
this.delayedStart = true;
|
|
||||||
}
|
|
||||||
this.oldClientId = this.getClientId(this.dbInbound.protocol, this.clients[index]);
|
|
||||||
},
|
|
||||||
getGroupInboundsClients(dbInbounds, currentClient) {
|
|
||||||
const response = {
|
|
||||||
inbounds: [],
|
|
||||||
clients: [],
|
|
||||||
editIds: []
|
|
||||||
}
|
|
||||||
dbInbounds.forEach((dbInboundItem) => {
|
|
||||||
const dbInbound = new DBInbound(dbInboundItem);
|
|
||||||
const inbound = dbInbound.toInbound();
|
|
||||||
const clients = inbound.clients;
|
|
||||||
if (clients.length > 0){
|
|
||||||
clients.forEach((client) => {
|
|
||||||
if (client['subId'] === currentClient['subId']){
|
|
||||||
response.inbounds.push(dbInboundItem.id)
|
|
||||||
response.clients.push(client)
|
|
||||||
response.editIds.push(this.getClientId(dbInbound.protocol, client))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
this.oldClientId = this.getClientId(dbInbound.protocol, clients[index]);
|
||||||
return response;
|
} else {
|
||||||
},
|
this.addClient(this.inbound.protocol, this.clients);
|
||||||
|
}
|
||||||
|
this.clientStats = this.dbInbound.clientStats.find(row => row.email === this.clients[this.index].email);
|
||||||
|
this.confirm = confirm;
|
||||||
|
},
|
||||||
getClientId(protocol, client) {
|
getClientId(protocol, client) {
|
||||||
switch (protocol) {
|
switch (protocol) {
|
||||||
case Protocols.TROJAN: return client.password;
|
case Protocols.TROJAN: return client.password;
|
||||||
|
@ -206,18 +94,6 @@
|
||||||
get isEdit() {
|
get isEdit() {
|
||||||
return this.clientModal.isEdit;
|
return this.clientModal.isEdit;
|
||||||
},
|
},
|
||||||
get isGroup() {
|
|
||||||
return this.clientModal.group.isGroup;
|
|
||||||
},
|
|
||||||
get isGroupEdit() {
|
|
||||||
return this.clientModal.group.canGroup;
|
|
||||||
},
|
|
||||||
set isGroupEdit(value) {
|
|
||||||
this.clientModal.group.canGroup = value;
|
|
||||||
if (!value){
|
|
||||||
this.clientModal.singleEditClientProcess(this.clientModal.index)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
get datepicker() {
|
get datepicker() {
|
||||||
return app.datepicker;
|
return app.datepicker;
|
||||||
},
|
},
|
||||||
|
|
|
@ -3,18 +3,6 @@
|
||||||
<a-form-item label='{{ i18n "pages.inbounds.enable" }}'>
|
<a-form-item label='{{ i18n "pages.inbounds.enable" }}'>
|
||||||
<a-switch v-model="client.enable"></a-switch>
|
<a-switch v-model="client.enable"></a-switch>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item v-if="isEdit && app.subSettings.enable && isGroup">
|
|
||||||
<template slot="label">
|
|
||||||
<a-tooltip>
|
|
||||||
<template slot="title">
|
|
||||||
<span>{{ i18n "pages.inbounds.isGroupEditDesc" }}</span>
|
|
||||||
</template>
|
|
||||||
{{ i18n "pages.inbounds.isGroupEdit" }}
|
|
||||||
<a-icon type="question-circle"></a-icon>
|
|
||||||
</a-tooltip>
|
|
||||||
</template>
|
|
||||||
<a-switch v-model="isGroupEdit"></a-switch>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<template slot="label">
|
<template slot="label">
|
||||||
<a-tooltip>
|
<a-tooltip>
|
||||||
|
@ -27,7 +15,7 @@
|
||||||
</template>
|
</template>
|
||||||
<a-input v-model.trim="client.email"></a-input>
|
<a-input v-model.trim="client.email"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item v-if="(inbound.protocol === Protocols.TROJAN || inbound.protocol === Protocols.SHADOWSOCKS) && !isGroup">
|
<a-form-item v-if="inbound.protocol === Protocols.TROJAN || inbound.protocol === Protocols.SHADOWSOCKS">
|
||||||
<template slot="label">
|
<template slot="label">
|
||||||
<a-tooltip>
|
<a-tooltip>
|
||||||
<template slot="title">
|
<template slot="title">
|
||||||
|
@ -40,7 +28,7 @@
|
||||||
</template>
|
</template>
|
||||||
<a-input v-model.trim="client.password"></a-input>
|
<a-input v-model.trim="client.password"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item v-if="(inbound.protocol === Protocols.VMESS || inbound.protocol === Protocols.VLESS) && !isGroup">
|
<a-form-item v-if="inbound.protocol === Protocols.VMESS || inbound.protocol === Protocols.VLESS">
|
||||||
<template slot="label">
|
<template slot="label">
|
||||||
<a-tooltip>
|
<a-tooltip>
|
||||||
<template slot="title">
|
<template slot="title">
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<a-input v-model.trim="inbound.stream.ws.path"></a-input>
|
<a-input v-model.trim="inbound.stream.ws.path"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
|
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
|
||||||
<a-button size="small" @click="inbound.stream.ws.addHeader()">+</a-button>
|
<a-button size="small" @click="inbound.stream.ws.addHeader('host', '')">+</a-button>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item :wrapper-col="{span:24}">
|
<a-form-item :wrapper-col="{span:24}">
|
||||||
<a-input-group compact v-for="(header, index) in inbound.stream.ws.headers">
|
<a-input-group compact v-for="(header, index) in inbound.stream.ws.headers">
|
||||||
|
|
|
@ -13,7 +13,6 @@
|
||||||
confirmLoading: false,
|
confirmLoading: false,
|
||||||
okText: '{{ i18n "sure" }}',
|
okText: '{{ i18n "sure" }}',
|
||||||
isEdit: false,
|
isEdit: false,
|
||||||
isGroup: false,
|
|
||||||
confirm: null,
|
confirm: null,
|
||||||
inbound: new Inbound(),
|
inbound: new Inbound(),
|
||||||
dbInbound: new DBInbound(),
|
dbInbound: new DBInbound(),
|
||||||
|
@ -61,9 +60,6 @@
|
||||||
get isEdit() {
|
get isEdit() {
|
||||||
return inModal.isEdit;
|
return inModal.isEdit;
|
||||||
},
|
},
|
||||||
get isGroup() {
|
|
||||||
return inModal.isGroup;
|
|
||||||
},
|
|
||||||
get client() {
|
get client() {
|
||||||
return inModal.inbound.clients[0];
|
return inModal.inbound.clients[0];
|
||||||
},
|
},
|
||||||
|
|
|
@ -145,10 +145,6 @@
|
||||||
<a-icon type="rest"></a-icon>
|
<a-icon type="rest"></a-icon>
|
||||||
{{ i18n "pages.inbounds.delDepletedClients" }}
|
{{ i18n "pages.inbounds.delDepletedClients" }}
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
<a-menu-item v-if="subSettings.enable && dbInbounds.length > 0" key="addGroupClient">
|
|
||||||
<a-icon type="usergroup-add"></a-icon>
|
|
||||||
{{ i18n "pages.client.groupAdd"}}
|
|
||||||
</a-menu-item>
|
|
||||||
</a-menu>
|
</a-menu>
|
||||||
</a-dropdown>
|
</a-dropdown>
|
||||||
</a-col>
|
</a-col>
|
||||||
|
@ -289,7 +285,7 @@
|
||||||
<p v-for="clientEmail in clientCount[dbInbound.id].online">[[ clientEmail ]]</p>
|
<p v-for="clientEmail in clientCount[dbInbound.id].online">[[ clientEmail ]]</p>
|
||||||
</template>
|
</template>
|
||||||
<a-tag style="margin:0; padding: 0 2px;" color="blue" v-if="clientCount[dbInbound.id].online.length">[[ clientCount[dbInbound.id].online.length ]]</a-tag>
|
<a-tag style="margin:0; padding: 0 2px;" color="blue" v-if="clientCount[dbInbound.id].online.length">[[ clientCount[dbInbound.id].online.length ]]</a-tag>
|
||||||
</a-popover>
|
</a-popover>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<template slot="traffic" slot-scope="text, dbInbound">
|
<template slot="traffic" slot-scope="text, dbInbound">
|
||||||
|
@ -343,7 +339,7 @@
|
||||||
<a-tag style="margin:0;" color="blue">[[ dbInbound.toInbound().stream.network ]]</a-tag>
|
<a-tag style="margin:0;" color="blue">[[ dbInbound.toInbound().stream.network ]]</a-tag>
|
||||||
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isTls" color="green">tls</a-tag>
|
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isTls" color="green">tls</a-tag>
|
||||||
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isReality" color="green">reality</a-tag>
|
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isReality" color="green">reality</a-tag>
|
||||||
</template>
|
</template>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -377,7 +373,7 @@
|
||||||
<p v-for="clientEmail in clientCount[dbInbound.id].online">[[ clientEmail ]]</p>
|
<p v-for="clientEmail in clientCount[dbInbound.id].online">[[ clientEmail ]]</p>
|
||||||
</template>
|
</template>
|
||||||
<a-tag style="margin:0; padding: 0 2px;" color="green" v-if="clientCount[dbInbound.id].online.length">[[ clientCount[dbInbound.id].online.length ]]</a-tag>
|
<a-tag style="margin:0; padding: 0 2px;" color="green" v-if="clientCount[dbInbound.id].online.length">[[ clientCount[dbInbound.id].online.length ]]</a-tag>
|
||||||
</a-popover>
|
</a-popover>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -744,9 +740,6 @@
|
||||||
case "delDepletedClients":
|
case "delDepletedClients":
|
||||||
this.delDepletedClients(-1)
|
this.delDepletedClients(-1)
|
||||||
break;
|
break;
|
||||||
case "addGroupClient":
|
|
||||||
this.openGroupAddClient()
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
clickAction(action, dbInbound) {
|
clickAction(action, dbInbound) {
|
||||||
|
@ -890,20 +883,6 @@
|
||||||
|
|
||||||
await this.submit(`/panel/inbound/update/${dbInbound.id}`, data, inModal);
|
await this.submit(`/panel/inbound/update/${dbInbound.id}`, data, inModal);
|
||||||
},
|
},
|
||||||
openGroupAddClient() {
|
|
||||||
clientModal.show({
|
|
||||||
title: '{{ i18n "pages.client.groupAdd"}}',
|
|
||||||
okText: '{{ i18n "pages.client.submitAdd"}}',
|
|
||||||
dbInbounds: this.dbInbounds,
|
|
||||||
confirm: async (clients, dbInboundIds) => {
|
|
||||||
clientModal.loading();
|
|
||||||
await this.addGroupClient(clients, dbInboundIds);
|
|
||||||
clientModal.close();
|
|
||||||
await this.showQrcode(dbInboundIds[0],clients[0], true)
|
|
||||||
},
|
|
||||||
isEdit: false
|
|
||||||
});
|
|
||||||
},
|
|
||||||
openAddClient(dbInboundId) {
|
openAddClient(dbInboundId) {
|
||||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||||
clientModal.show({
|
clientModal.show({
|
||||||
|
@ -914,7 +893,6 @@
|
||||||
clientModal.loading();
|
clientModal.loading();
|
||||||
await this.addClient(clients, dbInboundId);
|
await this.addClient(clients, dbInboundId);
|
||||||
clientModal.close();
|
clientModal.close();
|
||||||
await this.showQrcode(dbInboundId,clients)
|
|
||||||
},
|
},
|
||||||
isEdit: false
|
isEdit: false
|
||||||
});
|
});
|
||||||
|
@ -939,7 +917,6 @@
|
||||||
clientModal.show({
|
clientModal.show({
|
||||||
title: '{{ i18n "pages.client.edit"}}',
|
title: '{{ i18n "pages.client.edit"}}',
|
||||||
okText: '{{ i18n "pages.client.submitEdit"}}',
|
okText: '{{ i18n "pages.client.submitEdit"}}',
|
||||||
dbInbounds: this.dbInbounds,
|
|
||||||
dbInbound: dbInbound,
|
dbInbound: dbInbound,
|
||||||
index: index,
|
index: index,
|
||||||
confirm: async (client, dbInboundId, clientId) => {
|
confirm: async (client, dbInboundId, clientId) => {
|
||||||
|
@ -963,41 +940,14 @@
|
||||||
id: dbInboundId,
|
id: dbInboundId,
|
||||||
settings: '{"clients": [' + clients.toString() + ']}',
|
settings: '{"clients": [' + clients.toString() + ']}',
|
||||||
};
|
};
|
||||||
|
await this.submit(`/panel/inbound/addClient`, data);
|
||||||
await this.submit(`/panel/inbound/addClient`, data)
|
|
||||||
},
|
|
||||||
|
|
||||||
async addGroupClient(clients, dbInboundIds) {
|
|
||||||
const data = []
|
|
||||||
dbInboundIds.forEach((dbInboundId, index) => {
|
|
||||||
data.push({
|
|
||||||
id: dbInboundId,
|
|
||||||
settings: '{"clients": [' + clients[index].toString() + ']}',
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
await this.submit(`/panel/inbound/addGroupClient`, data, true)
|
|
||||||
},
|
},
|
||||||
async updateClient(client, dbInboundId, clientId) {
|
async updateClient(client, dbInboundId, clientId) {
|
||||||
if (Array.isArray(client) && Array.isArray(dbInboundId) && Array.isArray(clientId)){
|
const data = {
|
||||||
const data = []
|
id: dbInboundId,
|
||||||
client.forEach((client, index) => {
|
settings: '{"clients": [' + client.toString() + ']}',
|
||||||
data.push({
|
};
|
||||||
clientId: clientId[index],
|
await this.submit(`/panel/inbound/updateClient/${clientId}`, data);
|
||||||
inbound: {
|
|
||||||
id: dbInboundId[index],
|
|
||||||
settings: '{"clients": [' + client.toString() + ']}',
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
await this.submit(`/panel/inbound/updateClients`, data, true);
|
|
||||||
}else{
|
|
||||||
const data = {
|
|
||||||
id: dbInboundId,
|
|
||||||
settings: '{"clients": [' + client.toString() + ']}',
|
|
||||||
};
|
|
||||||
await this.submit(`/panel/inbound/updateClient/${clientId}`, data);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
resetTraffic(dbInboundId) {
|
resetTraffic(dbInboundId) {
|
||||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||||
|
@ -1051,8 +1001,8 @@
|
||||||
checkFallback(dbInbound) {
|
checkFallback(dbInbound) {
|
||||||
newDbInbound = new DBInbound(dbInbound);
|
newDbInbound = new DBInbound(dbInbound);
|
||||||
if (dbInbound.listen.startsWith("@")){
|
if (dbInbound.listen.startsWith("@")){
|
||||||
rootInbound = this.inbounds.find((i) =>
|
rootInbound = this.inbounds.find((i) =>
|
||||||
i.isTcp &&
|
i.isTcp &&
|
||||||
['trojan','vless'].includes(i.protocol) &&
|
['trojan','vless'].includes(i.protocol) &&
|
||||||
i.settings.fallbacks.find(f => f.dest === dbInbound.listen)
|
i.settings.fallbacks.find(f => f.dest === dbInbound.listen)
|
||||||
);
|
);
|
||||||
|
@ -1068,10 +1018,10 @@
|
||||||
}
|
}
|
||||||
return newDbInbound;
|
return newDbInbound;
|
||||||
},
|
},
|
||||||
showQrcode(dbInboundId, client, isJustSub = false) {
|
showQrcode(dbInboundId, client) {
|
||||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||||
newDbInbound = this.checkFallback(dbInbound);
|
newDbInbound = this.checkFallback(dbInbound);
|
||||||
qrModal.show('{{ i18n "qrCode"}}', newDbInbound, client, isJustSub);
|
qrModal.show('{{ i18n "qrCode"}}', newDbInbound, client);
|
||||||
},
|
},
|
||||||
showInfo(dbInboundId, client) {
|
showInfo(dbInboundId, client) {
|
||||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||||
|
@ -1100,8 +1050,8 @@
|
||||||
await this.updateClient(clients[index], dbInboundId, clientId);
|
await this.updateClient(clients[index], dbInboundId, clientId);
|
||||||
this.loading(false);
|
this.loading(false);
|
||||||
},
|
},
|
||||||
async submit(url, data, isJson = false) {
|
async submit(url, data) {
|
||||||
const msg = isJson ? await HttpUtil.postWithModalJson(url, data) : await HttpUtil.postWithModal(url, data);
|
const msg = await HttpUtil.postWithModal(url, data);
|
||||||
if (msg.success) {
|
if (msg.success) {
|
||||||
await this.getDBInbounds();
|
await this.getDBInbounds();
|
||||||
}
|
}
|
||||||
|
@ -1243,7 +1193,7 @@
|
||||||
value: '',
|
value: '',
|
||||||
okText: '{{ i18n "pages.inbounds.import" }}',
|
okText: '{{ i18n "pages.inbounds.import" }}',
|
||||||
confirm: async (dbInboundText) => {
|
confirm: async (dbInboundText) => {
|
||||||
await this.submit('/panel/inbound/import', {data: dbInboundText});
|
await this.submit('/panel/inbound/import', {data: dbInboundText}, promptModal);
|
||||||
promptModal.close();
|
promptModal.close();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -299,12 +299,12 @@
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<a-button type="primary" style="margin-bottom: 10px;"
|
<a-button type="primary" style="margin-bottom: 10px;"
|
||||||
:href="'data:application/text;charset=utf-8,' + encodeURIComponent(logModal.logs)" download="x-ui.log">
|
:href="'data:application/text;charset=utf-8,' + encodeURIComponent(logModal.logs.join('\n'))" download="x-ui.log">
|
||||||
{{ i18n "download" }} x-ui.log
|
{{ i18n "download" }} x-ui.log
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
<div class="ant-input" style="height: auto; max-height: 500px; overflow: auto;" v-html="logModal.logs"></div>
|
<div class="ant-input" style="height: auto; max-height: 500px; overflow: auto;" v-html="logModal.formattedLogs"></div>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
|
|
||||||
<a-modal id="backup-modal" v-model="backupModal.visible" :title="backupModal.title"
|
<a-modal id="backup-modal" v-model="backupModal.visible" :title="backupModal.title"
|
||||||
|
@ -432,14 +432,16 @@
|
||||||
|
|
||||||
const logModal = {
|
const logModal = {
|
||||||
visible: false,
|
visible: false,
|
||||||
logs: '',
|
logs: [],
|
||||||
|
formattedLogs: '',
|
||||||
rows: 20,
|
rows: 20,
|
||||||
level: 'info',
|
level: 'info',
|
||||||
syslog: false,
|
syslog: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
show(logs) {
|
show(logs) {
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
this.logs = logs? this.formatLogs(logs) : "No Record...";
|
this.logs = logs;
|
||||||
|
this.formattedLogs = logs.length > 0 ? this.formatLogs(logs) : "No Record...";
|
||||||
},
|
},
|
||||||
formatLogs(logs) {
|
formatLogs(logs) {
|
||||||
let formattedLogs = '';
|
let formattedLogs = '';
|
||||||
|
|
|
@ -140,6 +140,7 @@
|
||||||
mtu: 1420,
|
mtu: 1420,
|
||||||
secretKey: warpModal.warpData.private_key,
|
secretKey: warpModal.warpData.private_key,
|
||||||
address: Object.values(config.interface.addresses),
|
address: Object.values(config.interface.addresses),
|
||||||
|
domainStrategy: 'ForceIP',
|
||||||
peers: [{
|
peers: [{
|
||||||
publicKey: peer.public_key,
|
publicKey: peer.public_key,
|
||||||
endpoint: peer.endpoint.host,
|
endpoint: peer.endpoint.host,
|
||||||
|
|
|
@ -147,6 +147,40 @@
|
||||||
</template>
|
</template>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
|
<a-row style="padding: 20px">
|
||||||
|
<a-col :lg="24" :xl="12">
|
||||||
|
<a-list-item-meta
|
||||||
|
title='{{ i18n "pages.xray.logLevel" }}'
|
||||||
|
description='{{ i18n "pages.xray.logLevelDesc" }}'/>
|
||||||
|
</a-col>
|
||||||
|
<a-col :lg="24" :xl="12">
|
||||||
|
<template>
|
||||||
|
<a-select
|
||||||
|
v-model="setLogLevel"
|
||||||
|
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||||
|
style="width: 100%">
|
||||||
|
<a-select-option v-for="s in logLevel" :value="s">[[ s ]]</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</template>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
<a-row style="padding: 20px">
|
||||||
|
<a-col :lg="24" :xl="12">
|
||||||
|
<a-list-item-meta
|
||||||
|
title='{{ i18n "pages.xray.accessLog" }}'
|
||||||
|
description='{{ i18n "pages.xray.accessLogDesc" }}'/>
|
||||||
|
</a-col>
|
||||||
|
<a-col :lg="24" :xl="12">
|
||||||
|
<template>
|
||||||
|
<a-select
|
||||||
|
v-model="setAccessLog"
|
||||||
|
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||||
|
style="width: 100%">
|
||||||
|
<a-select-option v-for="s in access" :value="s">[[ s ]]</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</template>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
</a-list-item>
|
</a-list-item>
|
||||||
</a-collapse-panel>
|
</a-collapse-panel>
|
||||||
<a-collapse-panel header='{{ i18n "pages.xray.blockConfigs"}}'>
|
<a-collapse-panel header='{{ i18n "pages.xray.blockConfigs"}}'>
|
||||||
|
@ -341,8 +375,15 @@
|
||||||
</a-table>
|
</a-table>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane key="tpl-3" tab='{{ i18n "pages.xray.Outbounds"}}' style="padding-top: 20px;" force-render="true">
|
<a-tab-pane key="tpl-3" tab='{{ i18n "pages.xray.Outbounds"}}' style="padding-top: 20px;" force-render="true">
|
||||||
<a-button type="primary" icon="plus" @click="addOutbound()" style="margin-bottom: 10px;">{{ i18n "pages.xray.outbound.addOutbound" }}</a-button>
|
<a-row>
|
||||||
<a-button type="primary" @click="showWarp()" style="margin-bottom: 10px;">WARP</a-button>
|
<a-col :xs="12" :sm="12" :lg="12">
|
||||||
|
<a-button type="primary" icon="plus" @click="addOutbound()" style="margin-bottom: 10px;">{{ i18n "pages.xray.outbound.addOutbound" }}</a-button>
|
||||||
|
<a-button type="primary" @click="showWarp()" style="margin-bottom: 10px;">WARP</a-button>
|
||||||
|
</a-col>
|
||||||
|
<a-col :xs="12" :sm="12" :lg="12" style="text-align: right;">
|
||||||
|
<a-icon type="sync" :spin="refreshing" @click="refreshOutboundTraffic()" style="margin: 0 5px;"/>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
<a-table :columns="outboundColumns" bordered
|
<a-table :columns="outboundColumns" bordered
|
||||||
:row-key="r => r.key"
|
:row-key="r => r.key"
|
||||||
:data-source="outboundData"
|
:data-source="outboundData"
|
||||||
|
@ -378,6 +419,9 @@
|
||||||
<a-tag style="margin:0;" v-if="outbound.streamSettings.security=='reality'" color="green">reality</a-tag>
|
<a-tag style="margin:0;" v-if="outbound.streamSettings.security=='reality'" color="green">reality</a-tag>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
<template slot="traffic" slot-scope="text, outbound, index">
|
||||||
|
<a-tag color="green">[[ findOutboundTraffic(outbound) ]]</a-tag>
|
||||||
|
</template>
|
||||||
</a-table>
|
</a-table>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane key="tpl-4" tab='{{ i18n "pages.xray.outbound.reverse"}}' style="padding-top: 20px;" force-render="true">
|
<a-tab-pane key="tpl-4" tab='{{ i18n "pages.xray.outbound.reverse"}}' style="padding-top: 20px;" force-render="true">
|
||||||
|
@ -463,6 +507,7 @@
|
||||||
{ title: '{{ i18n "pages.xray.outbound.tag"}}', dataIndex: 'tag', align: 'center', width: 50 },
|
{ title: '{{ i18n "pages.xray.outbound.tag"}}', dataIndex: 'tag', align: 'center', width: 50 },
|
||||||
{ title: '{{ i18n "protocol"}}', align: 'center', width: 50, scopedSlots: { customRender: 'protocol' } },
|
{ title: '{{ i18n "protocol"}}', align: 'center', width: 50, scopedSlots: { customRender: 'protocol' } },
|
||||||
{ title: '{{ i18n "pages.xray.outbound.address"}}', align: 'center', width: 50, scopedSlots: { customRender: 'address' } },
|
{ title: '{{ i18n "pages.xray.outbound.address"}}', align: 'center', width: 50, scopedSlots: { customRender: 'address' } },
|
||||||
|
{ title: '{{ i18n "pages.inbounds.traffic" }}', align: 'center', width: 50, scopedSlots: { customRender: 'traffic' } },
|
||||||
];
|
];
|
||||||
|
|
||||||
const reverseColumns = [
|
const reverseColumns = [
|
||||||
|
@ -483,7 +528,9 @@
|
||||||
oldXraySetting: '',
|
oldXraySetting: '',
|
||||||
xraySetting: '',
|
xraySetting: '',
|
||||||
inboundTags: [],
|
inboundTags: [],
|
||||||
|
outboundsTraffic: [],
|
||||||
saveBtnDisable: true,
|
saveBtnDisable: true,
|
||||||
|
refreshing: false,
|
||||||
restartResult: '',
|
restartResult: '',
|
||||||
isMobile: window.innerWidth <= 768,
|
isMobile: window.innerWidth <= 768,
|
||||||
advSettings: 'xraySetting',
|
advSettings: 'xraySetting',
|
||||||
|
@ -521,6 +568,8 @@
|
||||||
protocol: "freedom"
|
protocol: "freedom"
|
||||||
},
|
},
|
||||||
routingDomainStrategies: ["AsIs", "IPIfNonMatch", "IPOnDemand"],
|
routingDomainStrategies: ["AsIs", "IPIfNonMatch", "IPOnDemand"],
|
||||||
|
logLevel: ["none" , "debug" , "info" , "warning", "error"],
|
||||||
|
access: ["none" , "./access.log" ],
|
||||||
settingsData: {
|
settingsData: {
|
||||||
protocols: {
|
protocols: {
|
||||||
bittorrent: ["bittorrent"],
|
bittorrent: ["bittorrent"],
|
||||||
|
@ -569,9 +618,11 @@
|
||||||
familyProtectDNS: {
|
familyProtectDNS: {
|
||||||
"servers": [
|
"servers": [
|
||||||
"1.1.1.3", // https://developers.cloudflare.com/1.1.1.1/setup/
|
"1.1.1.3", // https://developers.cloudflare.com/1.1.1.1/setup/
|
||||||
"1.0.0.3"
|
"1.0.0.3",
|
||||||
|
"2606:4700:4700::1113",
|
||||||
|
"2606:4700:4700::1003"
|
||||||
],
|
],
|
||||||
"queryStrategy": "UseIPv4"
|
"queryStrategy": "UseIP"
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -579,6 +630,12 @@
|
||||||
loading(spinning = true) {
|
loading(spinning = true) {
|
||||||
this.spinning = spinning;
|
this.spinning = spinning;
|
||||||
},
|
},
|
||||||
|
async getOutboundsTraffic() {
|
||||||
|
const msg = await HttpUtil.get("/panel/xray/getOutboundsTraffic");
|
||||||
|
if (msg.success) {
|
||||||
|
this.outboundsTraffic = msg.obj;
|
||||||
|
}
|
||||||
|
},
|
||||||
async getXraySetting() {
|
async getXraySetting() {
|
||||||
this.loading(true);
|
this.loading(true);
|
||||||
const msg = await HttpUtil.post("/panel/xray/");
|
const msg = await HttpUtil.post("/panel/xray/");
|
||||||
|
@ -757,6 +814,14 @@
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
findOutboundTraffic(o) {
|
||||||
|
for (const otraffic of this.outboundsTraffic) {
|
||||||
|
if (otraffic.tag == o.tag) {
|
||||||
|
return sizeFormat(otraffic.up) + ' / ' + sizeFormat(otraffic.down);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sizeFormat(0) + ' / ' + sizeFormat(0);
|
||||||
|
},
|
||||||
findOutboundAddress(o) {
|
findOutboundAddress(o) {
|
||||||
serverObj = null;
|
serverObj = null;
|
||||||
switch(o.protocol){
|
switch(o.protocol){
|
||||||
|
@ -814,6 +879,22 @@
|
||||||
outbounds.splice(index,1);
|
outbounds.splice(index,1);
|
||||||
this.outboundSettings = JSON.stringify(outbounds);
|
this.outboundSettings = JSON.stringify(outbounds);
|
||||||
},
|
},
|
||||||
|
async refreshOutboundTraffic() {
|
||||||
|
if (!this.refreshing) {
|
||||||
|
this.refreshing = true;
|
||||||
|
await this.getOutboundsTraffic();
|
||||||
|
|
||||||
|
data = []
|
||||||
|
if (this.templateSettings != null) {
|
||||||
|
this.templateSettings.outbounds.forEach((o, index) => {
|
||||||
|
data.push({'key': index, ...o});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.outboundData = data;
|
||||||
|
this.refreshing = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
addReverse(){
|
addReverse(){
|
||||||
reverseModal.show({
|
reverseModal.show({
|
||||||
title: '{{ i18n "pages.xray.outbound.addReverse"}}',
|
title: '{{ i18n "pages.xray.outbound.addReverse"}}',
|
||||||
|
@ -947,6 +1028,7 @@
|
||||||
async mounted() {
|
async mounted() {
|
||||||
await this.getXraySetting();
|
await this.getXraySetting();
|
||||||
await this.getXrayResult();
|
await this.getXrayResult();
|
||||||
|
await this.getOutboundsTraffic();
|
||||||
while (true) {
|
while (true) {
|
||||||
await PromiseUtil.sleep(800);
|
await PromiseUtil.sleep(800);
|
||||||
this.saveBtnDisable = this.oldXraySetting === this.xraySetting;
|
this.saveBtnDisable = this.oldXraySetting === this.xraySetting;
|
||||||
|
@ -1063,6 +1145,28 @@
|
||||||
this.templateSettings = newTemplateSettings;
|
this.templateSettings = newTemplateSettings;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setLogLevel: {
|
||||||
|
get: function () {
|
||||||
|
if (!this.templateSettings || !this.templateSettings.log || !this.templateSettings.log.loglevel) return "warning";
|
||||||
|
return this.templateSettings.log.loglevel;
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
newTemplateSettings = this.templateSettings;
|
||||||
|
newTemplateSettings.log.loglevel = newValue;
|
||||||
|
this.templateSettings = newTemplateSettings;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setAccessLog: {
|
||||||
|
get: function () {
|
||||||
|
if (!this.templateSettings || !this.templateSettings.log || !this.templateSettings.log.access) return "none";
|
||||||
|
return this.templateSettings.log.access;
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
newTemplateSettings = this.templateSettings;
|
||||||
|
newTemplateSettings.log.access = newValue;
|
||||||
|
this.templateSettings = newTemplateSettings;
|
||||||
|
}
|
||||||
|
},
|
||||||
blockedIPs: {
|
blockedIPs: {
|
||||||
get: function () {
|
get: function () {
|
||||||
return this.templateRuleGetter({ outboundTag: "blocked", property: "ip" });
|
return this.templateRuleGetter({ outboundTag: "blocked", property: "ip" });
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
package job
|
package job
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
@ -92,24 +94,34 @@ func (j *CheckClientIpJob) checkFail2BanInstalled() {
|
||||||
|
|
||||||
func (j *CheckClientIpJob) processLogFile() {
|
func (j *CheckClientIpJob) processLogFile() {
|
||||||
accessLogPath := xray.GetAccessLogPath()
|
accessLogPath := xray.GetAccessLogPath()
|
||||||
if accessLogPath == "" {
|
|
||||||
logger.Warning("access.log doesn't exist in your config.json")
|
if accessLogPath == "none" {
|
||||||
|
logger.Warning("Access log is set to 'none' check your Xray Configs")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := os.ReadFile(accessLogPath)
|
if accessLogPath == "" {
|
||||||
InboundClientIps := make(map[string][]string)
|
logger.Warning("Access log doesn't exist in your Xray Configs")
|
||||||
j.checkError(err)
|
return
|
||||||
|
}
|
||||||
|
|
||||||
lines := strings.Split(string(data), "\n")
|
file, err := os.Open(accessLogPath)
|
||||||
for _, line := range lines {
|
j.checkError(err)
|
||||||
ipRegx, _ := regexp.Compile(`[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+`)
|
defer file.Close()
|
||||||
|
|
||||||
|
InboundClientIps := make(map[string][]string)
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
|
||||||
|
ipRegx, _ := regexp.Compile(`(\d+\.\d+\.\d+\.\d+).* accepted`)
|
||||||
emailRegx, _ := regexp.Compile(`email:.+`)
|
emailRegx, _ := regexp.Compile(`email:.+`)
|
||||||
|
|
||||||
matchesIp := ipRegx.FindString(line)
|
matches := ipRegx.FindStringSubmatch(line)
|
||||||
if len(matchesIp) > 0 {
|
if len(matches) > 1 {
|
||||||
ip := string(matchesIp)
|
ip := matches[1]
|
||||||
if ip == "127.0.0.1" || ip == "1.1.1.1" {
|
if ip == "127.0.0.1" || ip == "[::1]" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,13 +136,14 @@ func (j *CheckClientIpJob) processLogFile() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail], ip)
|
InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail], ip)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail], ip)
|
InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail], ip)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
j.checkError(scanner.Err())
|
||||||
|
|
||||||
shouldCleanLog := false
|
shouldCleanLog := false
|
||||||
|
|
||||||
for clientEmail, ips := range InboundClientIps {
|
for clientEmail, ips := range InboundClientIps {
|
||||||
|
@ -141,7 +154,6 @@ func (j *CheckClientIpJob) processLogFile() {
|
||||||
} else {
|
} else {
|
||||||
shouldCleanLog = j.updateInboundClientIps(inboundClientIps, clientEmail, ips)
|
shouldCleanLog = j.updateInboundClientIps(inboundClientIps, clientEmail, ips)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// added delay before cleaning logs to reduce chance of logging IP that already has been banned
|
// added delay before cleaning logs to reduce chance of logging IP that already has been banned
|
||||||
|
@ -151,13 +163,17 @@ func (j *CheckClientIpJob) processLogFile() {
|
||||||
// copy access log to persistent file
|
// copy access log to persistent file
|
||||||
logAccessP, err := os.OpenFile(xray.GetAccessPersistentLogPath(), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
|
logAccessP, err := os.OpenFile(xray.GetAccessPersistentLogPath(), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
|
||||||
j.checkError(err)
|
j.checkError(err)
|
||||||
input, err := os.ReadFile(accessLogPath)
|
|
||||||
j.checkError(err)
|
|
||||||
if _, err := logAccessP.Write(input); err != nil {
|
|
||||||
j.checkError(err)
|
|
||||||
}
|
|
||||||
defer logAccessP.Close()
|
defer logAccessP.Close()
|
||||||
|
|
||||||
|
// reopen the access log file for reading
|
||||||
|
file, err := os.Open(accessLogPath)
|
||||||
|
j.checkError(err)
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// copy access log content to persistent file
|
||||||
|
_, err = io.Copy(logAccessP, file)
|
||||||
|
j.checkError(err)
|
||||||
|
|
||||||
// clean access log
|
// clean access log
|
||||||
if err := os.Truncate(xray.GetAccessLogPath(), 0); err != nil {
|
if err := os.Truncate(xray.GetAccessLogPath(), 0); err != nil {
|
||||||
j.checkError(err)
|
j.checkError(err)
|
||||||
|
|
|
@ -6,8 +6,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type XrayTrafficJob struct {
|
type XrayTrafficJob struct {
|
||||||
xrayService service.XrayService
|
xrayService service.XrayService
|
||||||
inboundService service.InboundService
|
inboundService service.InboundService
|
||||||
|
outboundService service.OutboundService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewXrayTrafficJob() *XrayTrafficJob {
|
func NewXrayTrafficJob() *XrayTrafficJob {
|
||||||
|
@ -24,11 +25,15 @@ func (j *XrayTrafficJob) Run() {
|
||||||
logger.Warning("get xray traffic failed:", err)
|
logger.Warning("get xray traffic failed:", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err, needRestart := j.inboundService.AddTraffic(traffics, clientTraffics)
|
err, needRestart0 := j.inboundService.AddTraffic(traffics, clientTraffics)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warning("add traffic failed:", err)
|
logger.Warning("add inbound traffic failed:", err)
|
||||||
}
|
}
|
||||||
if needRestart {
|
err, needRestart1 := j.outboundService.AddTraffic(traffics, clientTraffics)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("add outbound traffic failed:", err)
|
||||||
|
}
|
||||||
|
if needRestart0 || needRestart1 {
|
||||||
j.xrayService.SetToNeedRestart()
|
j.xrayService.SetToNeedRestart()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
{
|
{
|
||||||
"log": {
|
"log": {
|
||||||
"loglevel": "warning",
|
"access": "none",
|
||||||
"error": "./error.log"
|
"dnsLog": false,
|
||||||
|
"loglevel": "warning"
|
||||||
},
|
},
|
||||||
"api": {
|
"api": {
|
||||||
"tag": "api",
|
"tag": "api",
|
||||||
|
@ -24,6 +25,7 @@
|
||||||
],
|
],
|
||||||
"outbounds": [
|
"outbounds": [
|
||||||
{
|
{
|
||||||
|
"tag": "direct",
|
||||||
"protocol": "freedom",
|
"protocol": "freedom",
|
||||||
"settings": {}
|
"settings": {}
|
||||||
},
|
},
|
||||||
|
@ -42,7 +44,9 @@
|
||||||
},
|
},
|
||||||
"system": {
|
"system": {
|
||||||
"statsInboundDownlink": true,
|
"statsInboundDownlink": true,
|
||||||
"statsInboundUplink": true
|
"statsInboundUplink": true,
|
||||||
|
"statsOutboundDownlink": true,
|
||||||
|
"statsOutboundUplink": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"routing": {
|
"routing": {
|
||||||
|
|
|
@ -682,7 +682,7 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
|
||||||
return needRestart, tx.Save(oldInbound).Error
|
return needRestart, tx.Save(oldInbound).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *InboundService) AddTraffic(inboundTraffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) {
|
func (s *InboundService) AddTraffic(traffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) {
|
||||||
var err error
|
var err error
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
tx := db.Begin()
|
tx := db.Begin()
|
||||||
|
@ -694,7 +694,7 @@ func (s *InboundService) AddTraffic(inboundTraffics []*xray.Traffic, clientTraff
|
||||||
tx.Commit()
|
tx.Commit()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
err = s.addInboundTraffic(tx, inboundTraffics)
|
err = s.addInboundTraffic(tx, traffics)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err, false
|
return err, false
|
||||||
}
|
}
|
||||||
|
|
80
web/service/outbound.go
Normal file
80
web/service/outbound.go
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"x-ui/database"
|
||||||
|
"x-ui/database/model"
|
||||||
|
"x-ui/logger"
|
||||||
|
"x-ui/xray"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OutboundService struct {
|
||||||
|
xrayApi xray.XrayAPI
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *OutboundService) AddTraffic(traffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) {
|
||||||
|
var err error
|
||||||
|
db := database.GetDB()
|
||||||
|
tx := db.Begin()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
} else {
|
||||||
|
tx.Commit()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = s.addOutboundTraffic(tx, traffics)
|
||||||
|
if err != nil {
|
||||||
|
return err, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *OutboundService) addOutboundTraffic(tx *gorm.DB, traffics []*xray.Traffic) error {
|
||||||
|
if len(traffics) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
for _, traffic := range traffics {
|
||||||
|
if traffic.IsOutbound {
|
||||||
|
|
||||||
|
var outbound model.OutboundTraffics
|
||||||
|
|
||||||
|
err = tx.Model(&model.OutboundTraffics{}).Where("tag = ?", traffic.Tag).
|
||||||
|
FirstOrCreate(&outbound).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
outbound.Tag = traffic.Tag
|
||||||
|
outbound.Up = outbound.Up + traffic.Up
|
||||||
|
outbound.Down = outbound.Down + traffic.Down
|
||||||
|
outbound.Total = outbound.Up + outbound.Down
|
||||||
|
|
||||||
|
err = tx.Save(&outbound).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *OutboundService) GetOutboundsTraffic() ([]*model.OutboundTraffics, error) {
|
||||||
|
db := database.GetDB()
|
||||||
|
var traffics []*model.OutboundTraffics
|
||||||
|
|
||||||
|
err := db.Model(model.OutboundTraffics{}).Find(&traffics).Error
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning(err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return traffics, nil
|
||||||
|
}
|
|
@ -6,9 +6,9 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"slices"
|
|
||||||
"time"
|
"time"
|
||||||
"x-ui/config"
|
"x-ui/config"
|
||||||
"x-ui/database"
|
"x-ui/database"
|
||||||
|
@ -115,14 +115,19 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tgbot) NewBot(token string, proxyUrl string) (*telego.Bot, error) {
|
func (t *Tgbot) NewBot(token string, proxyUrl string) (*telego.Bot, error) {
|
||||||
if proxyUrl == "" || !strings.HasPrefix(proxyUrl, "socks5://") {
|
if proxyUrl == "" {
|
||||||
logger.Warning("invalid socks5 url, start with default")
|
// No proxy URL provided, use default instance
|
||||||
|
return telego.NewBot(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(proxyUrl, "socks5://") {
|
||||||
|
logger.Warning("Invalid socks5 URL, starting with default")
|
||||||
return telego.NewBot(token)
|
return telego.NewBot(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := url.Parse(proxyUrl)
|
_, err := url.Parse(proxyUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warning("cant parse proxy url, use default instance for tgbot:", err)
|
logger.Warning("Can't parse proxy URL, using default instance for tgbot:", err)
|
||||||
return telego.NewBot(token)
|
return telego.NewBot(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -260,7 +265,7 @@ func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin boo
|
||||||
msg += t.I18nBot("tgbot.commands.unknown")
|
msg += t.I18nBot("tgbot.commands.unknown")
|
||||||
}
|
}
|
||||||
|
|
||||||
if msg != ""{
|
if msg != "" {
|
||||||
if onlyMessage {
|
if onlyMessage {
|
||||||
t.SendMsgToTgbot(chatId, msg)
|
t.SendMsgToTgbot(chatId, msg)
|
||||||
return
|
return
|
||||||
|
@ -346,7 +351,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||||
tu.InlineKeyboardButton("40 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 40")),
|
tu.InlineKeyboardButton("40 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 40")),
|
||||||
),
|
),
|
||||||
tu.InlineKeyboardRow(
|
tu.InlineKeyboardRow(
|
||||||
tu.InlineKeyboardButton("50 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 60")),
|
tu.InlineKeyboardButton("50 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 50")),
|
||||||
tu.InlineKeyboardButton("60 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 60")),
|
tu.InlineKeyboardButton("60 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 60")),
|
||||||
tu.InlineKeyboardButton("80 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 80")),
|
tu.InlineKeyboardButton("80 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 80")),
|
||||||
),
|
),
|
||||||
|
@ -1022,7 +1027,7 @@ func (t *Tgbot) getInboundUsages() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tgbot) clientInfoMsg(traffic *xray.ClientTraffic, printEnabled bool, printOnline bool, printActive bool,
|
func (t *Tgbot) clientInfoMsg(traffic *xray.ClientTraffic, printEnabled bool, printOnline bool, printActive bool,
|
||||||
printDate bool, printTraffic bool, printRefreshed bool) string {
|
printDate bool, printTraffic bool, printRefreshed bool) string {
|
||||||
|
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
expiryTime := ""
|
expiryTime := ""
|
||||||
|
@ -1380,7 +1385,6 @@ func (t *Tgbot) getExhausted(chatId int64) {
|
||||||
output += t.I18nBot("tgbot.messages.exhaustedCount", "Type=="+t.I18nBot("tgbot.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.disabled", "Disabled=="+strconv.Itoa(len(disabledClients)))
|
||||||
output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+strconv.Itoa(exhaustedCC))
|
output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+strconv.Itoa(exhaustedCC))
|
||||||
|
|
||||||
|
|
||||||
if exhaustedCC > 0 {
|
if exhaustedCC > 0 {
|
||||||
output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+t.I18nBot("tgbot.clients"))
|
output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+t.I18nBot("tgbot.clients"))
|
||||||
|
@ -1490,7 +1494,6 @@ func (t *Tgbot) onlineClients(chatId int64, messageID ...int) {
|
||||||
output := t.I18nBot("tgbot.messages.onlinesCount", "Count=="+fmt.Sprint(onlinesCount))
|
output := t.I18nBot("tgbot.messages.onlinesCount", "Count=="+fmt.Sprint(onlinesCount))
|
||||||
keyboard := tu.InlineKeyboard(tu.InlineKeyboardRow(
|
keyboard := tu.InlineKeyboard(tu.InlineKeyboardRow(
|
||||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.refresh")).WithCallbackData(t.encodeQuery("onlines_refresh"))))
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.refresh")).WithCallbackData(t.encodeQuery("onlines_refresh"))))
|
||||||
|
|
||||||
|
|
||||||
if onlinesCount > 0 {
|
if onlinesCount > 0 {
|
||||||
var buttons []telego.InlineKeyboardButton
|
var buttons []telego.InlineKeyboardButton
|
||||||
|
@ -1565,30 +1568,44 @@ func (t *Tgbot) sendBanLogs(chatId int64, dt bool) {
|
||||||
|
|
||||||
file, err := os.Open(xray.GetIPLimitBannedPrevLogPath())
|
file, err := os.Open(xray.GetIPLimitBannedPrevLogPath())
|
||||||
if err == nil {
|
if err == nil {
|
||||||
document := tu.Document(
|
// Check if the file is non-empty before attempting to upload
|
||||||
tu.ID(chatId),
|
fileInfo, _ := file.Stat()
|
||||||
tu.File(file),
|
if fileInfo.Size() > 0 {
|
||||||
)
|
document := tu.Document(
|
||||||
_, err = bot.SendDocument(document)
|
tu.ID(chatId),
|
||||||
if err != nil {
|
tu.File(file),
|
||||||
logger.Error("Error in uploading backup: ", err)
|
)
|
||||||
|
_, err = bot.SendDocument(document)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Error in uploading IPLimitBannedPrevLog: ", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.Warning("IPLimitBannedPrevLog file is empty, not uploading.")
|
||||||
}
|
}
|
||||||
|
file.Close()
|
||||||
} else {
|
} else {
|
||||||
logger.Error("Error in opening db file for backup: ", err)
|
logger.Error("Error in opening IPLimitBannedPrevLog file for backup: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err = os.Open(xray.GetIPLimitBannedLogPath())
|
file, err = os.Open(xray.GetIPLimitBannedLogPath())
|
||||||
if err == nil {
|
if err == nil {
|
||||||
document := tu.Document(
|
// Check if the file is non-empty before attempting to upload
|
||||||
tu.ID(chatId),
|
fileInfo, _ := file.Stat()
|
||||||
tu.File(file),
|
if fileInfo.Size() > 0 {
|
||||||
)
|
document := tu.Document(
|
||||||
_, err = bot.SendDocument(document)
|
tu.ID(chatId),
|
||||||
if err != nil {
|
tu.File(file),
|
||||||
logger.Error("Error in uploading config.json: ", err)
|
)
|
||||||
|
_, err = bot.SendDocument(document)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Error in uploading IPLimitBannedLog: ", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.Warning("IPLimitBannedLog file is empty, not uploading.")
|
||||||
}
|
}
|
||||||
|
file.Close()
|
||||||
} else {
|
} else {
|
||||||
logger.Error("Error in opening config.json file for backup: ", err)
|
logger.Error("Error in opening IPLimitBannedLog file for backup: ", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -95,7 +95,7 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
||||||
if !clientTraffic.Enable {
|
if !clientTraffic.Enable {
|
||||||
clients = RemoveIndex(clients, index-indexDecrease)
|
clients = RemoveIndex(clients, index-indexDecrease)
|
||||||
indexDecrease++
|
indexDecrease++
|
||||||
logger.Info("Remove Inbound User", c["email"], "due the expire or traffic limit")
|
logger.Info("Remove Inbound User ", c["email"], " due the expire or traffic limit")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -80,7 +80,7 @@ func (s *XraySettingService) RegWarp(secretKey string, publicKey string) (string
|
||||||
hostName, _ := os.Hostname()
|
hostName, _ := os.Hostname()
|
||||||
data := fmt.Sprintf(`{"key":"%s","tos":"%s","type": "PC","model": "x-ui", "name": "%s"}`, publicKey, tos, hostName)
|
data := fmt.Sprintf(`{"key":"%s","tos":"%s","type": "PC","model": "x-ui", "name": "%s"}`, publicKey, tos, hostName)
|
||||||
|
|
||||||
url := fmt.Sprintf("https://api.cloudflareclient.com/v0a2158/reg")
|
url := "https://api.cloudflareclient.com/v0a2158/reg"
|
||||||
|
|
||||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte(data)))
|
req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte(data)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -181,12 +181,9 @@
|
||||||
"exportInbound" = "Export Inbound"
|
"exportInbound" = "Export Inbound"
|
||||||
"import" = "Import"
|
"import" = "Import"
|
||||||
"importInbound" = "Import an Inbound"
|
"importInbound" = "Import an Inbound"
|
||||||
"isGroupEdit" = "Group editing"
|
|
||||||
"isGroupEditDesc" = "All clients with the same subscription are edited"
|
|
||||||
|
|
||||||
[pages.client]
|
[pages.client]
|
||||||
"add" = "Add Client"
|
"add" = "Add Client"
|
||||||
"groupAdd" = "Add subscription user"
|
|
||||||
"edit" = "Edit Client"
|
"edit" = "Edit Client"
|
||||||
"submitAdd" = "Add Client"
|
"submitAdd" = "Add Client"
|
||||||
"submitEdit" = "Save Changes"
|
"submitEdit" = "Save Changes"
|
||||||
|
@ -312,8 +309,8 @@
|
||||||
"restart" = "Restart Xray"
|
"restart" = "Restart Xray"
|
||||||
"basicTemplate" = "Basics"
|
"basicTemplate" = "Basics"
|
||||||
"advancedTemplate" = "Advanced"
|
"advancedTemplate" = "Advanced"
|
||||||
"generalConfigs" = "General Strategy"
|
"generalConfigs" = "General"
|
||||||
"generalConfigsDesc" = "These options will determine general strategy adjustments."
|
"generalConfigsDesc" = "These options will determine general adjustments."
|
||||||
"blockConfigs" = "Protection Shield"
|
"blockConfigs" = "Protection Shield"
|
||||||
"blockConfigsDesc" = "These options will block traffic based on specific requested protocols and websites."
|
"blockConfigsDesc" = "These options will block traffic based on specific requested protocols and websites."
|
||||||
"blockCountryConfigs" = "Block Country"
|
"blockCountryConfigs" = "Block Country"
|
||||||
|
@ -395,6 +392,10 @@
|
||||||
"Routings" = "Routing Rules"
|
"Routings" = "Routing Rules"
|
||||||
"RoutingsDesc" = "The priority of each rule is important!"
|
"RoutingsDesc" = "The priority of each rule is important!"
|
||||||
"completeTemplate" = "All"
|
"completeTemplate" = "All"
|
||||||
|
"logLevel" = "Log Level"
|
||||||
|
"logLevelDesc" = "The log level for error logs, indicating the information that needs to be recorded."
|
||||||
|
"accessLog" = "Access Log"
|
||||||
|
"accessLogDesc" = "The file path for the access log. The special value 'none' disabled access logs"
|
||||||
|
|
||||||
[pages.xray.rules]
|
[pages.xray.rules]
|
||||||
"first" = "First"
|
"first" = "First"
|
||||||
|
@ -455,7 +456,7 @@
|
||||||
"wentWrong" = "❌ Something went wrong!"
|
"wentWrong" = "❌ Something went wrong!"
|
||||||
"noIpRecord" = "❗ No IP Record!"
|
"noIpRecord" = "❗ No IP Record!"
|
||||||
"noInbounds" = "❗ No inbound found!"
|
"noInbounds" = "❗ No inbound found!"
|
||||||
"unlimited" = "♾ Unlimited"
|
"unlimited" = "♾ Unlimited(Reset)"
|
||||||
"add" = "Add"
|
"add" = "Add"
|
||||||
"month" = "Month"
|
"month" = "Month"
|
||||||
"months" = "Months"
|
"months" = "Months"
|
||||||
|
|
|
@ -57,7 +57,7 @@
|
||||||
"dashboard" = "Estado del Sistema"
|
"dashboard" = "Estado del Sistema"
|
||||||
"inbounds" = "Entradas"
|
"inbounds" = "Entradas"
|
||||||
"settings" = "Configuraciones"
|
"settings" = "Configuraciones"
|
||||||
"xray" = "Configuración Xray"
|
"xray" = "Ajustes Xray"
|
||||||
"logout" = "Cerrar Sesión"
|
"logout" = "Cerrar Sesión"
|
||||||
"link" = "Gestionar"
|
"link" = "Gestionar"
|
||||||
|
|
||||||
|
@ -181,12 +181,9 @@
|
||||||
"exportInbound" = "Exportación entrante"
|
"exportInbound" = "Exportación entrante"
|
||||||
"import" = "Importar"
|
"import" = "Importar"
|
||||||
"importInbound" = "Importar un entrante"
|
"importInbound" = "Importar un entrante"
|
||||||
"isGroupEdit" = "Edición de grupo"
|
|
||||||
"isGroupEditDesc" = "Se editan todos los usuarios con la misma suscripción"
|
|
||||||
|
|
||||||
[pages.client]
|
[pages.client]
|
||||||
"add" = "Agregar Cliente"
|
"add" = "Agregar Cliente"
|
||||||
"groupAdd" = "Agregar usuario de suscripción"
|
|
||||||
"edit" = "Editar Cliente"
|
"edit" = "Editar Cliente"
|
||||||
"submitAdd" = "Agregar Cliente"
|
"submitAdd" = "Agregar Cliente"
|
||||||
"submitEdit" = "Guardar Cambios"
|
"submitEdit" = "Guardar Cambios"
|
||||||
|
@ -395,6 +392,10 @@
|
||||||
"Routings" = "Reglas de enrutamiento"
|
"Routings" = "Reglas de enrutamiento"
|
||||||
"RoutingsDesc" = "¡La prioridad de cada regla es importante!"
|
"RoutingsDesc" = "¡La prioridad de cada regla es importante!"
|
||||||
"completeTemplate" = "Todos"
|
"completeTemplate" = "Todos"
|
||||||
|
"logLevel" = "Nivel de registro"
|
||||||
|
"logLevelDesc" = "El nivel de registro para registros de errores, que indica la información que debe registrarse."
|
||||||
|
"accessLog" = "Registro de acceso"
|
||||||
|
"accessLogDesc" = "La ruta del archivo para el registro de acceso. El valor especial 'ninguno' deshabilita los registros de acceso"
|
||||||
|
|
||||||
[pages.xray.rules]
|
[pages.xray.rules]
|
||||||
"first" = "Primero"
|
"first" = "Primero"
|
||||||
|
|
|
@ -181,12 +181,9 @@
|
||||||
"exportInbound" = "استخراج ورودی"
|
"exportInbound" = "استخراج ورودی"
|
||||||
"import" = "افزودن"
|
"import" = "افزودن"
|
||||||
"importInbound" = "افزودن یک ورودی"
|
"importInbound" = "افزودن یک ورودی"
|
||||||
"isGroupEdit" = "ویرایش گروهی"
|
|
||||||
"isGroupEditDesc" = "تمامی کاربران با سابسکریپشن یکسان ویرایش میشوند"
|
|
||||||
|
|
||||||
[pages.client]
|
[pages.client]
|
||||||
"add" = "کاربر جدید"
|
"add" = "کاربر جدید"
|
||||||
"groupAdd" = "کاربر جدید سابسکریپشن"
|
|
||||||
"edit" = "ویرایش کاربر"
|
"edit" = "ویرایش کاربر"
|
||||||
"submitAdd" = "اضافه کردن"
|
"submitAdd" = "اضافه کردن"
|
||||||
"submitEdit" = "ذخیره تغییرات"
|
"submitEdit" = "ذخیره تغییرات"
|
||||||
|
@ -395,6 +392,10 @@
|
||||||
"Routings" = "قوانین مسیریابی"
|
"Routings" = "قوانین مسیریابی"
|
||||||
"RoutingsDesc" = "اولویت هر قانون مهم است"
|
"RoutingsDesc" = "اولویت هر قانون مهم است"
|
||||||
"completeTemplate" = "کامل"
|
"completeTemplate" = "کامل"
|
||||||
|
"logLevel" = "سطح گزارش"
|
||||||
|
"logLevelDesc" = "سطح گزارش برای گزارش های خطا، نشان دهنده اطلاعاتی است که باید ثبت شوند."
|
||||||
|
"accessLog" = "مسیر گزارش"
|
||||||
|
"accessLogDesc" = "مسیر فایل برای گزارش دسترسی. مقدار ویژه «هیچ» گزارشهای دسترسی را غیرفعال میکند."
|
||||||
|
|
||||||
[pages.xray.rules]
|
[pages.xray.rules]
|
||||||
"first" = "اولین"
|
"first" = "اولین"
|
||||||
|
@ -455,7 +456,7 @@
|
||||||
"wentWrong" = "❌ مشکلی رخ داده است!"
|
"wentWrong" = "❌ مشکلی رخ داده است!"
|
||||||
"noIpRecord" = "❗ رکورد IP یافت نشد!"
|
"noIpRecord" = "❗ رکورد IP یافت نشد!"
|
||||||
"noInbounds" = "❗ هیچ ورودی یافت نشد!"
|
"noInbounds" = "❗ هیچ ورودی یافت نشد!"
|
||||||
"unlimited" = "♾ نامحدود"
|
"unlimited" = "♾ - نامحدود(ریست)"
|
||||||
"add" = "اضافه کردن"
|
"add" = "اضافه کردن"
|
||||||
"month" = "ماه"
|
"month" = "ماه"
|
||||||
"months" = "ماهها"
|
"months" = "ماهها"
|
||||||
|
|
|
@ -181,12 +181,9 @@
|
||||||
"exportInbound" = "Экспорт входящих"
|
"exportInbound" = "Экспорт входящих"
|
||||||
"import" = "Импортировать"
|
"import" = "Импортировать"
|
||||||
"importInbound" = "Импортировать входящее сообщение"
|
"importInbound" = "Импортировать входящее сообщение"
|
||||||
"isGroupEdit" = "Редактирование группы"
|
|
||||||
"isGroupEditDesc" = "Редактируются все пользователи с одной подпиской"
|
|
||||||
|
|
||||||
[pages.client]
|
[pages.client]
|
||||||
"add" = "Добавить пользователя"
|
"add" = "Добавить пользователя"
|
||||||
"groupAdd" = "Добавить пользователя подписки"
|
|
||||||
"edit" = "Редактировать пользователя"
|
"edit" = "Редактировать пользователя"
|
||||||
"submitAdd" = "Добавить пользователя"
|
"submitAdd" = "Добавить пользователя"
|
||||||
"submitEdit" = "Сохранить изменения"
|
"submitEdit" = "Сохранить изменения"
|
||||||
|
@ -395,6 +392,10 @@
|
||||||
"Routings" = "Правила маршрутизации"
|
"Routings" = "Правила маршрутизации"
|
||||||
"RoutingsDesc" = "Важен приоритет каждого правила!"
|
"RoutingsDesc" = "Важен приоритет каждого правила!"
|
||||||
"completeTemplate" = "Все"
|
"completeTemplate" = "Все"
|
||||||
|
"logLevel" = "Уровень журнала"
|
||||||
|
"logLevelDesc" = "Уровень журнала для журналов ошибок, указывающий информацию, которую необходимо записать."
|
||||||
|
"accessLog" = "Журнал доступа"
|
||||||
|
"accessLogDesc" = "Путь к файлу журнала доступа. Специальное значение «none» отключило журналы доступа."
|
||||||
|
|
||||||
[pages.xray.rules]
|
[pages.xray.rules]
|
||||||
"first" = "Первый"
|
"first" = "Первый"
|
||||||
|
|
|
@ -181,12 +181,9 @@
|
||||||
"exportInbound" = "Xuất nhập khẩu"
|
"exportInbound" = "Xuất nhập khẩu"
|
||||||
"import" = "Nhập"
|
"import" = "Nhập"
|
||||||
"importInbound" = "Nhập inbound"
|
"importInbound" = "Nhập inbound"
|
||||||
"isGroupEdit" = "Chỉnh sửa nhóm"
|
|
||||||
"isGroupEditDesc" = "Tất cả người dùng có cùng đăng ký đều được chỉnh sửa"
|
|
||||||
|
|
||||||
[pages.client]
|
[pages.client]
|
||||||
"add" = "Thêm người dùng"
|
"add" = "Thêm người dùng"
|
||||||
"groupAdd" = "Thêm người dùng đăng ký"
|
|
||||||
"edit" = "Chỉnh sửa người dùng"
|
"edit" = "Chỉnh sửa người dùng"
|
||||||
"submitAdd" = "Thêm"
|
"submitAdd" = "Thêm"
|
||||||
"submitEdit" = "Lưu thay đổi"
|
"submitEdit" = "Lưu thay đổi"
|
||||||
|
@ -395,6 +392,10 @@
|
||||||
"Routings" = "Quy tắc định tuyến"
|
"Routings" = "Quy tắc định tuyến"
|
||||||
"RoutingsDesc" = "Mức độ ưu tiên của mỗi quy tắc đều quan trọng!"
|
"RoutingsDesc" = "Mức độ ưu tiên của mỗi quy tắc đều quan trọng!"
|
||||||
"completeTemplate" = "All"
|
"completeTemplate" = "All"
|
||||||
|
"logLevel" = "Mức đăng nhập"
|
||||||
|
"logLevelDesc" = "Cấp độ nhật ký cho nhật ký lỗi, cho biết thông tin cần được ghi lại."
|
||||||
|
"accessLog" = "Nhật ký truy cập"
|
||||||
|
"accessLogDesc" = "Đường dẫn tệp cho nhật ký truy cập. Nhật ký truy cập bị vô hiệu hóa có giá trị đặc biệt 'không'"
|
||||||
|
|
||||||
[pages.xray.rules]
|
[pages.xray.rules]
|
||||||
"first" = "Đầu tiên"
|
"first" = "Đầu tiên"
|
||||||
|
|
|
@ -181,12 +181,9 @@
|
||||||
"exportInbound" = "出口 入境"
|
"exportInbound" = "出口 入境"
|
||||||
"import"="导入"
|
"import"="导入"
|
||||||
"importInbound" = "导入入站"
|
"importInbound" = "导入入站"
|
||||||
"isGroupEdit" = "分组编辑"
|
|
||||||
"isGroupEditDesc" = "编辑具有相同订阅的所有用户"
|
|
||||||
|
|
||||||
[pages.client]
|
[pages.client]
|
||||||
"add" = "添加客户端"
|
"add" = "添加客户端"
|
||||||
"groupAdd" = "添加订阅用户"
|
|
||||||
"edit" = "编辑客户端"
|
"edit" = "编辑客户端"
|
||||||
"submitAdd" = "添加客户端"
|
"submitAdd" = "添加客户端"
|
||||||
"submitEdit" = "保存修改"
|
"submitEdit" = "保存修改"
|
||||||
|
@ -395,6 +392,10 @@
|
||||||
"Routings" = "路由规则"
|
"Routings" = "路由规则"
|
||||||
"RoutingsDesc" = "每条规则的优先级都很重要"
|
"RoutingsDesc" = "每条规则的优先级都很重要"
|
||||||
"completeTemplate" = "全部"
|
"completeTemplate" = "全部"
|
||||||
|
"logLevel" = "日志级别"
|
||||||
|
"logLevelDesc" = "错误日志的日志级别,表示需要记录的信息。"
|
||||||
|
"accessLog" = "访问日志"
|
||||||
|
"accessLogDesc" = "访问日志的文件路径。 特殊值“none”禁用访问日志"
|
||||||
|
|
||||||
[pages.xray.rules]
|
[pages.xray.rules]
|
||||||
"first" = "第一个"
|
"first" = "第一个"
|
||||||
|
|
54
x-ui.sh
54
x-ui.sh
|
@ -806,34 +806,6 @@ warp_cloudflare() {
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
multi_protocol() {
|
|
||||||
echo "This script only supports Vless and Vmess. if you use another protocols, DON'T INSTALL or get backup first! "
|
|
||||||
echo -e "${green}\t1.${plain} Install Multi Protocol Script"
|
|
||||||
echo -e "${green}\t2.${plain} Uninstall"
|
|
||||||
echo -e "${green}\t3.${plain} Start Service"
|
|
||||||
echo -e "${green}\t4.${plain} Stop Service"
|
|
||||||
echo -e "${green}\t0.${plain} Back to Main Menu"
|
|
||||||
read -p "Choose an option: " choice
|
|
||||||
case "$choice" in
|
|
||||||
0)
|
|
||||||
show_menu
|
|
||||||
;;
|
|
||||||
1)
|
|
||||||
bash <(curl -Ls https://raw.githubusercontent.com/M4mmad/3xui-multi-protocol/master/install.sh --ipv4)
|
|
||||||
;;
|
|
||||||
2)
|
|
||||||
bash <(curl -Ls https://raw.githubusercontent.com/M4mmad/3xui-multi-protocol/master/unistall.sh --ipv4)
|
|
||||||
;;
|
|
||||||
3)
|
|
||||||
systemctl start 3xui-multi-protocol
|
|
||||||
;;
|
|
||||||
4)
|
|
||||||
systemctl stop 3xui-multi-protocol
|
|
||||||
;;
|
|
||||||
*) echo "Invalid choice" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
run_speedtest() {
|
run_speedtest() {
|
||||||
# Check if Speedtest is already installed
|
# Check if Speedtest is already installed
|
||||||
if ! command -v speedtest &>/dev/null; then
|
if ! command -v speedtest &>/dev/null; then
|
||||||
|
@ -1145,22 +1117,21 @@ show_menu() {
|
||||||
${green}12.${plain} Check Status
|
${green}12.${plain} Check Status
|
||||||
${green}13.${plain} Check Logs
|
${green}13.${plain} Check Logs
|
||||||
————————————————
|
————————————————
|
||||||
${green}14.${plain} Enable x-ui On System Startup
|
${green}14.${plain} Enable Autostart
|
||||||
${green}15.${plain} Disable x-ui On System Startup
|
${green}15.${plain} Disable Autostart
|
||||||
————————————————
|
————————————————
|
||||||
${green}16.${plain} SSL Certificate Management
|
${green}16.${plain} SSL Certificate Management
|
||||||
${green}17.${plain} Cloudflare SSL Certificate
|
${green}17.${plain} Cloudflare SSL Certificate
|
||||||
${green}18.${plain} IP Limit Management
|
${green}18.${plain} IP Limit Management
|
||||||
${green}19.${plain} WARP Management
|
${green}19.${plain} WARP Management
|
||||||
${green}20.${plain} Multi Protocol Management
|
|
||||||
————————————————
|
————————————————
|
||||||
${green}21.${plain} Enable BBR
|
${green}20.${plain} Enable BBR
|
||||||
${green}22.${plain} Update Geo Files
|
${green}21.${plain} Update Geo Files
|
||||||
${green}23.${plain} Active Firewall and open ports
|
${green}22.${plain} Active Firewall and open ports
|
||||||
${green}24.${plain} Speedtest by Ookla
|
${green}23.${plain} Speedtest by Ookla
|
||||||
"
|
"
|
||||||
show_status
|
show_status
|
||||||
echo && read -p "Please enter your selection [0-24]: " num
|
echo && read -p "Please enter your selection [0-23]: " num
|
||||||
|
|
||||||
case "${num}" in
|
case "${num}" in
|
||||||
0)
|
0)
|
||||||
|
@ -1224,22 +1195,19 @@ show_menu() {
|
||||||
warp_cloudflare
|
warp_cloudflare
|
||||||
;;
|
;;
|
||||||
20)
|
20)
|
||||||
multi_protocol
|
|
||||||
;;
|
|
||||||
21)
|
|
||||||
enable_bbr
|
enable_bbr
|
||||||
;;
|
;;
|
||||||
22)
|
21)
|
||||||
update_geo
|
update_geo
|
||||||
;;
|
;;
|
||||||
23)
|
22)
|
||||||
open_ports
|
open_ports
|
||||||
;;
|
;;
|
||||||
24)
|
23)
|
||||||
run_speedtest
|
run_speedtest
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
LOGE "Please enter the correct number [0-24]"
|
LOGE "Please enter the correct number [0-23]"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
|
@ -213,6 +213,7 @@ func (x *XrayAPI) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
isInbound := matchs[1] == "inbound"
|
isInbound := matchs[1] == "inbound"
|
||||||
|
isOutbound := matchs[1] == "outbound"
|
||||||
tag := matchs[2]
|
tag := matchs[2]
|
||||||
isDown := matchs[3] == "downlink"
|
isDown := matchs[3] == "downlink"
|
||||||
if tag == "api" {
|
if tag == "api" {
|
||||||
|
@ -221,8 +222,9 @@ func (x *XrayAPI) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) {
|
||||||
traffic, ok := tagTrafficMap[tag]
|
traffic, ok := tagTrafficMap[tag]
|
||||||
if !ok {
|
if !ok {
|
||||||
traffic = &Traffic{
|
traffic = &Traffic{
|
||||||
IsInbound: isInbound,
|
IsInbound: isInbound,
|
||||||
Tag: tag,
|
IsOutbound: isOutbound,
|
||||||
|
Tag: tag,
|
||||||
}
|
}
|
||||||
tagTrafficMap[tag] = traffic
|
tagTrafficMap[tag] = traffic
|
||||||
traffics = append(traffics, traffic)
|
traffics = append(traffics, traffic)
|
||||||
|
|
|
@ -31,7 +31,7 @@ func (lw *LogWriter) Write(m []byte) (n int, err error) {
|
||||||
// Find level in []
|
// Find level in []
|
||||||
startIndex := strings.Index(messageBody, "[")
|
startIndex := strings.Index(messageBody, "[")
|
||||||
endIndex := strings.Index(messageBody, "]")
|
endIndex := strings.Index(messageBody, "]")
|
||||||
if startIndex != -1 && endIndex != -1 {
|
if startIndex != -1 && endIndex != -1 && startIndex < endIndex {
|
||||||
level := strings.TrimSpace(messageBody[startIndex+1 : endIndex])
|
level := strings.TrimSpace(messageBody[startIndex+1 : endIndex])
|
||||||
msgBody := "XRAY: " + strings.TrimSpace(messageBody[endIndex+1:])
|
msgBody := "XRAY: " + strings.TrimSpace(messageBody[endIndex+1:])
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
package xray
|
package xray
|
||||||
|
|
||||||
type Traffic struct {
|
type Traffic struct {
|
||||||
IsInbound bool
|
IsInbound bool
|
||||||
Tag string
|
IsOutbound bool
|
||||||
Up int64
|
Tag string
|
||||||
Down int64
|
Up int64
|
||||||
|
Down int64
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue