Merge branch 'MHSanaei:main' into main

This commit is contained in:
Alireza Ahmand 2024-02-04 16:41:25 +03:30 committed by GitHub
commit e872edab4e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 512 additions and 581 deletions

View file

@ -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 }}

View file

@ -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 ../../

View file

@ -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" ]

View file

@ -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"
}, },
``` ```

View file

@ -1 +1 @@
2.1.1 2.1.3

View file

@ -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{})
} }

View file

@ -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
View file

@ -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
View file

@ -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=

View file

@ -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

View file

@ -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...))

View file

@ -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;

View file

@ -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;

View file

@ -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 {

View file

@ -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 {

View file

@ -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)
}

View file

@ -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) {

View file

@ -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,136 +25,32 @@
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) {
@ -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;
}, },

View file

@ -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">

View file

@ -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">

View file

@ -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];
}, },

View file

@ -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>
@ -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);
@ -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();
}, },
}); });

View file

@ -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 = '';

View file

@ -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,

View file

@ -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" });

View file

@ -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)

View file

@ -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()
} }

View file

@ -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": {

View file

@ -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
View 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
}

View file

@ -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 := ""
@ -1381,7 +1386,6 @@ func (t *Tgbot) getExhausted(chatId int64) {
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"))
var buttons []telego.InlineKeyboardButton var buttons []telego.InlineKeyboardButton
@ -1491,7 +1495,6 @@ func (t *Tgbot) onlineClients(chatId int64, messageID ...int) {
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
for _, online := range onlines { for _, online := range onlines {
@ -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)
} }
} }

View file

@ -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")
} }

View file

@ -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 {

View file

@ -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"

View file

@ -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"

View file

@ -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" = "ماه‌ها"

View file

@ -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" = "Первый"

View file

@ -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"

View file

@ -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
View file

@ -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
} }

View file

@ -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)

View file

@ -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:])

View file

@ -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
} }