Compare commits
89 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31339d6bf8 | ||
|
|
8865443438 | ||
|
|
835deb77f4 | ||
|
|
6a7c3716ac | ||
|
|
15211f81b1 | ||
|
|
b3f7a6572e | ||
|
|
6f28a3a2fe | ||
|
|
896cc5386c | ||
|
|
76f70ce1e9 | ||
|
|
6aa3c8d4a2 | ||
|
|
aff9d0ea15 | ||
|
|
526426e2dd | ||
|
|
2223a21cfc | ||
|
|
47ccc7b501 | ||
|
|
c38e1e0cfe | ||
|
|
f36034541e | ||
|
|
783fa856c3 | ||
|
|
6139effb8a | ||
|
|
8ae7f4a564 | ||
|
|
6f46f3e636 | ||
|
|
ba278a4269 | ||
|
|
3825e36ef7 | ||
|
|
f6e0e1b3cf | ||
|
|
769590d779 | ||
|
|
1fa9101b40 | ||
|
|
3f2e1aede9 | ||
|
|
235e6880c1 | ||
|
|
ffa23a43c6 | ||
|
|
66e3c505e3 | ||
|
|
0938519f49 | ||
|
|
5f489c3d08 | ||
|
|
f82d0051b2 | ||
|
|
76267b23a0 | ||
|
|
40a926a54a | ||
|
|
3a835fbeb8 | ||
|
|
f6461b8386 | ||
|
|
586663c840 | ||
|
|
82ead05093 | ||
|
|
d9b1b200ce | ||
|
|
786a3ac992 | ||
|
|
8c5648eb09 | ||
|
|
4dfe527f20 | ||
|
|
980ebd99ca | ||
|
|
9b7cddbec7 | ||
|
|
2bd706aa92 | ||
|
|
95e0d9a468 | ||
|
|
4865754b3d | ||
|
|
92eaff9608 | ||
|
|
0b7aa8a9e0 | ||
|
|
678962d4ca | ||
|
|
795835c54f | ||
|
|
91360a3f49 | ||
|
|
4831c2f1b2 | ||
|
|
3166d497f9 | ||
|
|
f50ccce9ec | ||
|
|
c7e300f14d | ||
|
|
419a1938ee | ||
|
|
a48745cb3e | ||
|
|
ac9408c37f | ||
|
|
3d71289075 | ||
|
|
fbd3772788 | ||
|
|
334b28cddc | ||
|
|
2fbfc88bc1 | ||
|
|
f781979d38 | ||
|
|
aba37be6eb | ||
|
|
30042bc047 | ||
|
|
49f60f7775 | ||
|
|
5f44c80cd5 | ||
|
|
85d42ce94f | ||
|
|
2f3c3d0ed2 | ||
|
|
7debf96610 | ||
|
|
d8b60c3cd5 | ||
|
|
f8eb548376 | ||
|
|
88fc4f81d4 | ||
|
|
911f2b0bb5 | ||
|
|
2d16eabc6e | ||
|
|
be50be75fe | ||
|
|
70e7987df5 | ||
|
|
8ceeb454ee | ||
|
|
f311bf1dbf | ||
|
|
837d7f77a1 | ||
|
|
d0213ce50b | ||
|
|
622e440366 | ||
|
|
1dc5452f1d | ||
|
|
a0daf2fae2 | ||
|
|
a21bdc9396 | ||
|
|
4cf7f75749 | ||
|
|
bcdac5aad6 | ||
|
|
3d5f851fce |
12
.github/workflows/docker.yml
vendored
12
.github/workflows/docker.yml
vendored
@@ -13,13 +13,13 @@ jobs:
|
||||
- name: Check out the code
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v2.1.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v2.5.0
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v2.1.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -27,15 +27,15 @@ jobs:
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
uses: docker/metadata-action@v4.4.0
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
platforms: linux/amd64, linux/arm64
|
||||
platforms: linux/amd64, linux/arm64/v8
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3.5.2
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4.0.0
|
||||
uses: actions/setup-go@v4.0.1
|
||||
with:
|
||||
go-version: "stable"
|
||||
- name: build linux amd64 version
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
- name: package
|
||||
run: tar -zcvf x-ui-linux-amd64.tar.gz x-ui
|
||||
- name: upload
|
||||
uses: svenstaro/upload-release-action@2.5.0
|
||||
uses: svenstaro/upload-release-action@2.6.0
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ github.ref }}
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3.5.2
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4.0.0
|
||||
uses: actions/setup-go@v4.0.1
|
||||
with:
|
||||
go-version: "stable"
|
||||
- name: build linux arm64 version
|
||||
@@ -80,7 +80,7 @@ jobs:
|
||||
- name: package
|
||||
run: tar -zcvf x-ui-linux-arm64.tar.gz x-ui
|
||||
- name: upload
|
||||
uses: svenstaro/upload-release-action@2.5.0
|
||||
uses: svenstaro/upload-release-action@2.6.0
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ github.ref }}
|
||||
|
||||
16
.gitignore
vendored
16
.gitignore
vendored
@@ -1,15 +1,15 @@
|
||||
.idea
|
||||
.vscode
|
||||
.cache
|
||||
.sync*
|
||||
*.tar.gz
|
||||
access.log
|
||||
error.log
|
||||
tmp
|
||||
main
|
||||
backup/
|
||||
bin/
|
||||
dist/
|
||||
x-ui-*.tar.gz
|
||||
/x-ui
|
||||
/release.sh
|
||||
.sync*
|
||||
main
|
||||
release/
|
||||
access.log
|
||||
error.log
|
||||
.cache
|
||||
/release.sh
|
||||
/x-ui
|
||||
|
||||
@@ -24,10 +24,10 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
|
||||
|
||||
## Install custom version
|
||||
|
||||
To install your desired version you can add the version to the end of install command. Example for ver `v1.4.6`:
|
||||
To install your desired version you can add the version to the end of install command. Example for ver `v1.6.0`:
|
||||
|
||||
```
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v1.4.6
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v1.6.0
|
||||
```
|
||||
|
||||
# SSL
|
||||
@@ -194,6 +194,7 @@ Reference syntax:
|
||||
| `GET` | `"/list"` | Get all inbounds |
|
||||
| `GET` | `"/get/:id"` | Get inbound with inbound.id |
|
||||
| `GET` | `"/getClientTraffics/:email"` | Get Client Traffics with email |
|
||||
| `GET` | `"/createbackup"` | Telegram bot sends backup to admins |
|
||||
| `POST` | `"/add"` | Add inbound |
|
||||
| `POST` | `"/del/:id"` | Delete Inbound |
|
||||
| `POST` | `"/update/:id"` | Update Inbound |
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.4.6
|
||||
1.6.1
|
||||
@@ -17,7 +17,16 @@ import (
|
||||
|
||||
var db *gorm.DB
|
||||
|
||||
var initializers = []func() error{
|
||||
initUser,
|
||||
initInbound,
|
||||
initSetting,
|
||||
initInboundClientIps,
|
||||
initClientTraffic,
|
||||
}
|
||||
|
||||
func initUser() error {
|
||||
|
||||
err := db.AutoMigrate(&model.User{})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -54,7 +63,7 @@ func initClientTraffic() error {
|
||||
|
||||
func InitDB(dbPath string) error {
|
||||
dir := path.Dir(dbPath)
|
||||
err := os.MkdirAll(dir, fs.ModeDir)
|
||||
err := os.MkdirAll(dir, fs.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -75,25 +84,10 @@ func InitDB(dbPath string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
err = initUser()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = initInbound()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = initSetting()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = initInboundClientIps()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = initClientTraffic()
|
||||
if err != nil {
|
||||
return err
|
||||
for _, initialize := range initializers {
|
||||
if err := initialize(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -107,10 +101,10 @@ func IsNotFound(err error) bool {
|
||||
return err == gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
func IsSQLiteDB(file io.Reader) (bool, error) {
|
||||
func IsSQLiteDB(file io.ReaderAt) (bool, error) {
|
||||
signature := []byte("SQLite format 3\x00")
|
||||
buf := make([]byte, len(signature))
|
||||
_, err := file.Read(buf)
|
||||
_, err := file.ReadAt(buf, 0)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
33
go.mod
33
go.mod
@@ -3,34 +3,37 @@ module x-ui
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/Workiva/go-datastructures v1.0.53
|
||||
github.com/Workiva/go-datastructures v1.1.0
|
||||
github.com/gin-contrib/sessions v0.0.4
|
||||
github.com/gin-gonic/gin v1.9.0
|
||||
github.com/go-cmd/cmd v1.4.1
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
|
||||
github.com/goccy/go-json v0.10.2
|
||||
github.com/mymmrac/telego v0.24.0
|
||||
github.com/nicksnyder/go-i18n/v2 v2.2.1
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||
github.com/pelletier/go-toml/v2 v2.0.7
|
||||
github.com/pelletier/go-toml/v2 v2.0.8
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/shirou/gopsutil/v3 v3.23.4
|
||||
github.com/xtls/xray-core v1.8.1
|
||||
go.uber.org/atomic v1.11.0
|
||||
golang.org/x/text v0.9.0
|
||||
google.golang.org/grpc v1.55.0
|
||||
gorm.io/driver/sqlite v1.5.0
|
||||
gorm.io/driver/sqlite v1.5.1
|
||||
gorm.io/gorm v1.25.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.2.1 // indirect
|
||||
github.com/bytedance/sonic v1.8.8 // indirect
|
||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||
github.com/bytedance/sonic v1.8.10 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/fasthttp/router v1.4.19 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.13.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/gorilla/context v1.1.1 // indirect
|
||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||
@@ -38,26 +41,30 @@ require (
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.16.5 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect
|
||||
github.com/mattn/go-isatty v0.0.18 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.16 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pires/go-proxyproto v0.7.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.5 // indirect
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.11 // indirect
|
||||
github.com/tklauser/numcpus v0.6.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.47.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.3 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.8.0 // indirect
|
||||
golang.org/x/net v0.9.0 // indirect
|
||||
golang.org/x/sys v0.7.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
|
||||
golang.org/x/crypto v0.9.0 // indirect
|
||||
golang.org/x/net v0.10.0 // indirect
|
||||
golang.org/x/sys v0.8.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230524185152-1884fd1fac28 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
68
go.sum
68
go.sum
@@ -1,16 +1,17 @@
|
||||
github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
|
||||
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/Workiva/go-datastructures v1.0.53 h1:J6Y/52yX10Xc5JjXmGtWoSSxs3mZnGSaq37xZZh7Yig=
|
||||
github.com/Workiva/go-datastructures v1.0.53/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A=
|
||||
github.com/Workiva/go-datastructures v1.1.0 h1:hu20UpgZneBhQ3ZvwiOGlqJSKIosin2Rd5wAKUHEO/k=
|
||||
github.com/Workiva/go-datastructures v1.1.0/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A=
|
||||
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/antonlindstrom/pgstore v0.0.0-20200229204646-b08ebf1105e0/go.mod h1:2Ti6VUHVxpC0VSmTZzEvpzysnaGAfGBOoMIz5ykPyyw=
|
||||
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
|
||||
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.8.8 h1:Kj4AYbZSeENfyXicsYppYKO0K2YWab+i2UTSY7Ukz9Q=
|
||||
github.com/bytedance/sonic v1.8.8/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/bytedance/sonic v1.8.10 h1:XFSQg4/rwpQnNWSybNDr8oz6QtQY9uRGfRKDVWVsvP8=
|
||||
github.com/bytedance/sonic v1.8.10/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
@@ -18,7 +19,11 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 h1:y7y0Oa6UawqTFPCDw9JG6pdKt4F9pAhHv0B7FMGaGD0=
|
||||
github.com/fasthttp/router v1.4.19 h1:RLE539IU/S4kfb4MP56zgP0TIBU9kEg0ID9GpWO0vqk=
|
||||
github.com/fasthttp/router v1.4.19/go.mod h1:+Fh3YOd8x1+he6ZS+d2iUDBH9MGGZ1xQFUor0DE9rKE=
|
||||
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/gaukas/godicttls v0.0.3 h1:YNDIf0d9adcxOijiLrEzpfZGAkNwLRzPaG6OjU7EITk=
|
||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
|
||||
github.com/gin-contrib/sessions v0.0.4 h1:gq4fNa1Zmp564iHP5G6EBuktilEos8VKhe2sza1KMgo=
|
||||
@@ -42,11 +47,9 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||
github.com/go-playground/validator/v10 v10.13.0 h1:cFRQdfaSMCOSfGCCLB20MHvuoHb/s5G8L5pu2ppK5AQ=
|
||||
github.com/go-playground/validator/v10 v10.13.0/go.mod h1:dwu7+CG8/CtBiJFZDz4e+5Upb6OLw04gtBYw0mcG/z4=
|
||||
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
|
||||
github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
@@ -81,6 +84,7 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw=
|
||||
github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
|
||||
github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
@@ -92,9 +96,8 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2
|
||||
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a h1:N9zuLhTvBSRt0gWSiJswwQ2HqDmtX/ZCDJURnKUt1Ik=
|
||||
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
|
||||
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc=
|
||||
@@ -105,14 +108,16 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mymmrac/telego v0.24.0 h1:0fd+v2/dToL6/DtsnWr+2saK7ZxIgLY+LI9kqJQbPEo=
|
||||
github.com/mymmrac/telego v0.24.0/go.mod h1:y557P/iMHSaOVDi5Nmy1gNelqrw+jaBMvP9guPaNJsQ=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.2.1 h1:aOzRCdwsJuoExfZhoiXHy4bjruwCMdt5otbYojM/PaA=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.2.1/go.mod h1:fF2++lPHlo+/kPaj3nB0uxtPwzlPm+BlgwGX7MkeGj0=
|
||||
github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||
github.com/pelletier/go-toml/v2 v2.0.7 h1:muncTPStnKRos5dpVKULv2FVd4bMOhNePj9CjgDb8Us=
|
||||
github.com/pelletier/go-toml/v2 v2.0.7/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
|
||||
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
|
||||
@@ -133,13 +138,16 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG
|
||||
github.com/sagernet/sing v0.2.3 h1:V50MvZ4c3Iij2lYFWPlzL1PyipwSzjGeN9x+Ox89vpk=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.1 h1:FvdLQOqpvxHBJUcUe4fvgiYP2XLLwH5i1DtXQviVEPw=
|
||||
github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c h1:vK2wyt9aWYHHvNLWniwijBu/n4pySypiKRhN32u/JGo=
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U=
|
||||
github.com/shirou/gopsutil/v3 v3.23.4 h1:hZwmDxZs7Ewt75DV81r4pFMqbq+di2cbt9FsQBqLD2o=
|
||||
github.com/shirou/gopsutil/v3 v3.23.4/go.mod h1:ZcGxyfzAMRevhUR2+cfhXDH6gQdFYE/t8j1nsU4mPI8=
|
||||
github.com/shoenig/go-m1cpu v0.1.5 h1:LF57Z/Fpb/WdGLjt2HZilNnmZOxg/q2bSKTQhgbrLrQ=
|
||||
github.com/shoenig/go-m1cpu v0.1.5/go.mod h1:Wwvst4LR89UxjeFtLRMrpgRiyY4xPsejnVZym39dbAQ=
|
||||
github.com/shoenig/test v0.6.3 h1:GVXWJFk9PiOjN0KoJ7VrJGH6uLPnqxR7/fe3HUPfE0c=
|
||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
||||
github.com/shoenig/test v0.6.3/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
|
||||
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
@@ -149,8 +157,9 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/tinylib/msgp v1.1.5/go.mod h1:eQsjooMTnV42mHu917E26IogZ2930nFyBQdofk10Udg=
|
||||
github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM=
|
||||
github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
|
||||
@@ -164,13 +173,18 @@ github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLY
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.47.0 h1:y7moDoxYzMooFpT5aHgNgVOQDrS3qlkfiP9mDtGGK9c=
|
||||
github.com/valyala/fasthttp v1.47.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
|
||||
github.com/xtls/reality v0.0.0-20230331223127-176a94313eda h1:psRJD2RrZbnI0OWyHvXfgYCPqlRM5q5SPDcjDoDBWhE=
|
||||
github.com/xtls/xray-core v1.8.1 h1:iSTTqXj82ZdwC1ah+eV331X4JTcnrDz+WuKuB/EB3P4=
|
||||
github.com/xtls/xray-core v1.8.1/go.mod h1:AXxSso0MZwUE4NhRocCfHCg73BtJ+T2dSpQVo1Cg9VM=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
|
||||
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
|
||||
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 h1:Ss6D3hLXTM0KobyBYEAygXzFfGcjnmfEJOBgSbemCtg=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
@@ -181,8 +195,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
@@ -192,8 +206,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -211,8 +225,9 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -232,8 +247,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
|
||||
google.golang.org/genproto v0.0.0-20230524185152-1884fd1fac28 h1:+55/MuGJORMxCrkAgo2595fMAnN/4rweCuwibbqrvpc=
|
||||
google.golang.org/genproto v0.0.0-20230524185152-1884fd1fac28/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
|
||||
google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag=
|
||||
google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
@@ -249,9 +264,8 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/sqlite v1.5.0 h1:zKYbzRCpBrT1bNijRnxLDJWPjVfImGEn0lSnUY5gZ+c=
|
||||
gorm.io/driver/sqlite v1.5.0/go.mod h1:kDMDfntV9u/vuMmz8APHtHF0b4nyBB7sfCieC6G8k8I=
|
||||
gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||
gorm.io/driver/sqlite v1.5.1 h1:hYyrLkAWE71bcarJDPdZNTLWtr8XrSjOWyjUYI6xdL4=
|
||||
gorm.io/driver/sqlite v1.5.1/go.mod h1:7MZZ2Z8bqyfSQA1gYEV6MagQWj3cpUkJj9Z+d1HEMEQ=
|
||||
gorm.io/gorm v1.25.1 h1:nsSALe5Pr+cM3V1qwwQ7rOkw+6UeLrX5O4v3llhHa64=
|
||||
gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||
gvisor.dev/gvisor v0.0.0-20220901235040-6ca97ef2ce1c h1:m5lcgWnL3OElQNVyp3qcncItJ2c0sQlSGjYK2+nJTA4=
|
||||
|
||||
@@ -2,17 +2,28 @@ package logger
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/op/go-logging"
|
||||
)
|
||||
|
||||
var logger *logging.Logger
|
||||
var (
|
||||
logger *logging.Logger
|
||||
mu sync.Mutex
|
||||
)
|
||||
|
||||
func init() {
|
||||
InitLogger(logging.INFO)
|
||||
}
|
||||
|
||||
func InitLogger(level logging.Level) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
if logger != nil {
|
||||
return
|
||||
}
|
||||
|
||||
format := logging.MustStringFormatter(
|
||||
`%{time:2006/01/02 15:04:05} %{level} - %{message}`,
|
||||
)
|
||||
@@ -21,39 +32,55 @@ func InitLogger(level logging.Level) {
|
||||
backendFormatter := logging.NewBackendFormatter(backend, format)
|
||||
backendLeveled := logging.AddModuleLevel(backendFormatter)
|
||||
backendLeveled.SetLevel(level, "")
|
||||
newLogger.SetBackend(backendLeveled)
|
||||
newLogger.SetBackend(logging.MultiLogger(backendLeveled))
|
||||
|
||||
logger = newLogger
|
||||
}
|
||||
|
||||
func Debug(args ...interface{}) {
|
||||
logger.Debug(args...)
|
||||
if logger != nil {
|
||||
logger.Debug(args...)
|
||||
}
|
||||
}
|
||||
|
||||
func Debugf(format string, args ...interface{}) {
|
||||
logger.Debugf(format, args...)
|
||||
if logger != nil {
|
||||
logger.Debugf(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func Info(args ...interface{}) {
|
||||
logger.Info(args...)
|
||||
if logger != nil {
|
||||
logger.Info(args...)
|
||||
}
|
||||
}
|
||||
|
||||
func Infof(format string, args ...interface{}) {
|
||||
logger.Infof(format, args...)
|
||||
if logger != nil {
|
||||
logger.Infof(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func Warning(args ...interface{}) {
|
||||
logger.Warning(args...)
|
||||
if logger != nil {
|
||||
logger.Warning(args...)
|
||||
}
|
||||
}
|
||||
|
||||
func Warningf(format string, args ...interface{}) {
|
||||
logger.Warningf(format, args...)
|
||||
if logger != nil {
|
||||
logger.Warningf(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func Error(args ...interface{}) {
|
||||
logger.Error(args...)
|
||||
if logger != nil {
|
||||
logger.Error(args...)
|
||||
}
|
||||
}
|
||||
|
||||
func Errorf(format string, args ...interface{}) {
|
||||
logger.Errorf(format, args...)
|
||||
if logger != nil {
|
||||
logger.Errorf(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
27
main.go
27
main.go
@@ -11,6 +11,7 @@ import (
|
||||
"x-ui/config"
|
||||
"x-ui/database"
|
||||
"x-ui/logger"
|
||||
"x-ui/sub"
|
||||
"x-ui/v2ui"
|
||||
"x-ui/web"
|
||||
"x-ui/web/global"
|
||||
@@ -50,6 +51,16 @@ func runWebServer() {
|
||||
return
|
||||
}
|
||||
|
||||
var subServer *sub.Server
|
||||
subServer = sub.NewServer()
|
||||
global.SetSubServer(subServer)
|
||||
|
||||
err = subServer.Start()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
// Trap shutdown signals
|
||||
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM)
|
||||
@@ -62,6 +73,11 @@ func runWebServer() {
|
||||
if err != nil {
|
||||
logger.Warning("stop server err:", err)
|
||||
}
|
||||
err = subServer.Stop()
|
||||
if err != nil {
|
||||
logger.Warning("stop server err:", err)
|
||||
}
|
||||
|
||||
server = web.NewServer()
|
||||
global.SetWebServer(server)
|
||||
err = server.Start()
|
||||
@@ -69,8 +85,18 @@ func runWebServer() {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
subServer = sub.NewServer()
|
||||
global.SetSubServer(subServer)
|
||||
|
||||
err = subServer.Start()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
default:
|
||||
server.Stop()
|
||||
subServer.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -133,7 +159,6 @@ func updateTgbotEnableSts(status bool) {
|
||||
logger.Infof("SetTgbotenabled[%v] success", status)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime string) {
|
||||
|
||||
@@ -6,7 +6,11 @@
|
||||
},
|
||||
"api": {
|
||||
"tag": "api",
|
||||
"services": ["HandlerService", "LoggerService", "StatsService"]
|
||||
"services": [
|
||||
"HandlerService",
|
||||
"LoggerService",
|
||||
"StatsService"
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
@@ -54,35 +58,41 @@
|
||||
"rules": [
|
||||
{
|
||||
"type": "field",
|
||||
"inboundTag": ["api"],
|
||||
"inboundTag": [
|
||||
"api"
|
||||
],
|
||||
"outboundTag": "api"
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"outboundTag": "blocked",
|
||||
"ip": ["geoip:private"]
|
||||
"ip": [
|
||||
"geoip:private"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"outboundTag": "blocked",
|
||||
"protocol": ["bittorrent"]
|
||||
"protocol": [
|
||||
"bittorrent"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"outboundTag": "blocked",
|
||||
"domain": [
|
||||
"geosite:category-ads-all",
|
||||
"geosite:category-ads",
|
||||
"geosite:google-ads",
|
||||
"geosite:spotify-ads"
|
||||
"ext:iran.dat:ads"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"outboundTag": "IPv4",
|
||||
"domain": ["geosite:google"]
|
||||
"domain": [
|
||||
"geosite:google"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"stats": {}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,11 @@
|
||||
},
|
||||
"api": {
|
||||
"tag": "api",
|
||||
"services": ["HandlerService", "LoggerService", "StatsService"]
|
||||
"services": [
|
||||
"HandlerService",
|
||||
"LoggerService",
|
||||
"StatsService"
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
@@ -59,40 +63,44 @@
|
||||
"rules": [
|
||||
{
|
||||
"type": "field",
|
||||
"inboundTag": ["api"],
|
||||
"inboundTag": [
|
||||
"api"
|
||||
],
|
||||
"outboundTag": "api"
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"outboundTag": "blocked",
|
||||
"ip": ["geoip:private"]
|
||||
"ip": [
|
||||
"geoip:private"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"outboundTag": "blocked",
|
||||
"protocol": ["bittorrent"]
|
||||
"protocol": [
|
||||
"bittorrent"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"outboundTag": "blocked",
|
||||
"domain": [
|
||||
"geosite:category-ads-all",
|
||||
"geosite:category-ads",
|
||||
"geosite:google-ads",
|
||||
"geosite:spotify-ads"
|
||||
"ext:iran.dat:ads"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"outboundTag": "WARP",
|
||||
"domain": [
|
||||
"geosite:google",
|
||||
"geosite:netflix",
|
||||
"geosite:spotify",
|
||||
"geosite:openai"
|
||||
"geosite:netflix",
|
||||
"geosite:openai",
|
||||
"geosite:google"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"stats": {}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,11 @@
|
||||
},
|
||||
"api": {
|
||||
"tag": "api",
|
||||
"services": ["HandlerService", "LoggerService", "StatsService"]
|
||||
"services": [
|
||||
"HandlerService",
|
||||
"LoggerService",
|
||||
"StatsService"
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
@@ -47,18 +51,24 @@
|
||||
"rules": [
|
||||
{
|
||||
"type": "field",
|
||||
"inboundTag": ["api"],
|
||||
"inboundTag": [
|
||||
"api"
|
||||
],
|
||||
"outboundTag": "api"
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"outboundTag": "blocked",
|
||||
"ip": ["geoip:private"]
|
||||
"ip": [
|
||||
"geoip:private"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"outboundTag": "blocked",
|
||||
"protocol": ["bittorrent"]
|
||||
"protocol": [
|
||||
"bittorrent"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
@@ -73,4 +83,4 @@
|
||||
]
|
||||
},
|
||||
"stats": {}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,11 @@
|
||||
},
|
||||
"api": {
|
||||
"tag": "api",
|
||||
"services": ["HandlerService", "LoggerService", "StatsService"]
|
||||
"services": [
|
||||
"HandlerService",
|
||||
"LoggerService",
|
||||
"StatsService"
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
@@ -47,30 +51,27 @@
|
||||
"rules": [
|
||||
{
|
||||
"type": "field",
|
||||
"inboundTag": ["api"],
|
||||
"inboundTag": [
|
||||
"api"
|
||||
],
|
||||
"outboundTag": "api"
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"outboundTag": "blocked",
|
||||
"ip": ["geoip:private"]
|
||||
"ip": [
|
||||
"geoip:private",
|
||||
"geoip:ir"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"outboundTag": "blocked",
|
||||
"protocol": ["bittorrent"]
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"outboundTag": "blocked",
|
||||
"ip": ["geoip:private"]
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"outboundTag": "blocked",
|
||||
"ip": ["geoip:ir"]
|
||||
"protocol": [
|
||||
"bittorrent"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"stats": {}
|
||||
}
|
||||
}
|
||||
171
sub/sub.go
Normal file
171
sub/sub.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package sub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"x-ui/config"
|
||||
"x-ui/logger"
|
||||
"x-ui/util/common"
|
||||
"x-ui/web/network"
|
||||
"x-ui/web/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
httpServer *http.Server
|
||||
listener net.Listener
|
||||
|
||||
sub *SUBController
|
||||
settingService service.SettingService
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func NewServer() *Server {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &Server{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
if config.IsDebug() {
|
||||
gin.SetMode(gin.DebugMode)
|
||||
} else {
|
||||
gin.DefaultWriter = io.Discard
|
||||
gin.DefaultErrorWriter = io.Discard
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
|
||||
engine := gin.Default()
|
||||
|
||||
subPath, err := s.settingService.GetSubPath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
subDomain, err := s.settingService.GetSubDomain()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if subDomain != "" {
|
||||
validateDomain := func(c *gin.Context) {
|
||||
host := strings.Split(c.Request.Host, ":")[0]
|
||||
|
||||
if host != subDomain {
|
||||
c.AbortWithStatus(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
|
||||
engine.Use(validateDomain)
|
||||
}
|
||||
|
||||
g := engine.Group(subPath)
|
||||
|
||||
s.sub = NewSUBController(g)
|
||||
|
||||
return engine, nil
|
||||
}
|
||||
|
||||
func (s *Server) Start() (err error) {
|
||||
//This is an anonymous function, no function name
|
||||
defer func() {
|
||||
if err != nil {
|
||||
s.Stop()
|
||||
}
|
||||
}()
|
||||
|
||||
subEnable, err := s.settingService.GetSubEnable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !subEnable {
|
||||
return nil
|
||||
}
|
||||
|
||||
engine, err := s.initRouter()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
certFile, err := s.settingService.GetSubCertFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
keyFile, err := s.settingService.GetSubKeyFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
listen, err := s.settingService.GetSubListen()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
port, err := s.settingService.GetSubPort()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
listenAddr := net.JoinHostPort(listen, strconv.Itoa(port))
|
||||
listener, err := net.Listen("tcp", listenAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if certFile != "" || keyFile != "" {
|
||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
listener.Close()
|
||||
return err
|
||||
}
|
||||
c := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
listener = network.NewAutoHttpsListener(listener)
|
||||
listener = tls.NewListener(listener, c)
|
||||
}
|
||||
|
||||
if certFile != "" || keyFile != "" {
|
||||
logger.Info("Sub server run https on", listener.Addr())
|
||||
} else {
|
||||
logger.Info("Sub server run http on", listener.Addr())
|
||||
}
|
||||
s.listener = listener
|
||||
|
||||
s.httpServer = &http.Server{
|
||||
Handler: engine,
|
||||
}
|
||||
|
||||
go func() {
|
||||
s.httpServer.Serve(listener)
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) Stop() error {
|
||||
s.cancel()
|
||||
|
||||
var err1 error
|
||||
var err2 error
|
||||
if s.httpServer != nil {
|
||||
err1 = s.httpServer.Shutdown(s.ctx)
|
||||
}
|
||||
if s.listener != nil {
|
||||
err2 = s.listener.Close()
|
||||
}
|
||||
return common.Combine(err1, err2)
|
||||
}
|
||||
|
||||
func (s *Server) GetCtx() context.Context {
|
||||
return s.ctx
|
||||
}
|
||||
@@ -1,17 +1,14 @@
|
||||
package controller
|
||||
package sub
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
"x-ui/web/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type SUBController struct {
|
||||
BaseController
|
||||
|
||||
subService service.SubService
|
||||
subService SubService
|
||||
}
|
||||
|
||||
func NewSUBController(g *gin.RouterGroup) *SUBController {
|
||||
@@ -21,7 +18,7 @@ func NewSUBController(g *gin.RouterGroup) *SUBController {
|
||||
}
|
||||
|
||||
func (a *SUBController) initRouter(g *gin.RouterGroup) {
|
||||
g = g.Group("/sub")
|
||||
g = g.Group("/")
|
||||
|
||||
g.GET("/:subid", a.subs)
|
||||
}
|
||||
@@ -29,7 +26,7 @@ func (a *SUBController) initRouter(g *gin.RouterGroup) {
|
||||
func (a *SUBController) subs(c *gin.Context) {
|
||||
subId := c.Param("subid")
|
||||
host := strings.Split(c.Request.Host, ":")[0]
|
||||
subs, header, err := a.subService.GetSubs(subId, host)
|
||||
subs, headers, err := a.subService.GetSubs(subId, host)
|
||||
if err != nil || len(subs) == 0 {
|
||||
c.String(400, "Error!")
|
||||
} else {
|
||||
@@ -38,8 +35,10 @@ func (a *SUBController) subs(c *gin.Context) {
|
||||
result += sub + "\n"
|
||||
}
|
||||
|
||||
// Add subscription-userinfo
|
||||
c.Writer.Header().Set("Subscription-Userinfo", header)
|
||||
// Add headers
|
||||
c.Writer.Header().Set("Subscription-Userinfo", headers[0])
|
||||
c.Writer.Header().Set("Profile-Update-Interval", headers[1])
|
||||
c.Writer.Header().Set("Profile-Title", headers[2])
|
||||
|
||||
c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package sub
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
@@ -8,35 +8,51 @@ import (
|
||||
"x-ui/database"
|
||||
"x-ui/database/model"
|
||||
"x-ui/logger"
|
||||
"x-ui/web/service"
|
||||
"x-ui/xray"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type SubService struct {
|
||||
address string
|
||||
inboundService InboundService
|
||||
inboundService service.InboundService
|
||||
settingServics service.SettingService
|
||||
}
|
||||
|
||||
func (s *SubService) GetSubs(subId string, host string) ([]string, string, error) {
|
||||
func (s *SubService) GetSubs(subId string, host string) ([]string, []string, error) {
|
||||
s.address = host
|
||||
var result []string
|
||||
var header string
|
||||
var headers []string
|
||||
var traffic xray.ClientTraffic
|
||||
var clientTraffics []xray.ClientTraffic
|
||||
inbounds, err := s.getInboundsBySubId(subId)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, inbound := range inbounds {
|
||||
clients, err := s.inboundService.getClients(inbound)
|
||||
clients, err := s.inboundService.GetClients(inbound)
|
||||
if err != nil {
|
||||
logger.Error("SubService - GetSub: Unable to get clients from inbound")
|
||||
}
|
||||
if clients == nil {
|
||||
continue
|
||||
}
|
||||
if len(inbound.Listen) > 0 && inbound.Listen[0] == '@' {
|
||||
fallbackMaster, err := s.getFallbackMaster(inbound.Listen)
|
||||
if err == nil {
|
||||
inbound.Listen = fallbackMaster.Listen
|
||||
inbound.Port = fallbackMaster.Port
|
||||
var stream map[string]interface{}
|
||||
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
|
||||
var masterStream map[string]interface{}
|
||||
json.Unmarshal([]byte(fallbackMaster.StreamSettings), &masterStream)
|
||||
stream["security"] = masterStream["security"]
|
||||
stream["tlsSettings"] = masterStream["tlsSettings"]
|
||||
modifiedStream, _ := json.MarshalIndent(stream, "", " ")
|
||||
inbound.StreamSettings = string(modifiedStream)
|
||||
}
|
||||
}
|
||||
for _, client := range clients {
|
||||
if client.Enable && client.SubID == subId {
|
||||
link := s.getLink(inbound, client.Email)
|
||||
@@ -66,15 +82,18 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, string, error
|
||||
}
|
||||
}
|
||||
}
|
||||
header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
|
||||
return result, header, nil
|
||||
headers = append(headers, fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000))
|
||||
updateInterval, _ := s.settingServics.GetSubUpdates()
|
||||
headers = append(headers, fmt.Sprintf("%d", updateInterval))
|
||||
headers = append(headers, subId)
|
||||
return result, headers, nil
|
||||
}
|
||||
|
||||
func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
|
||||
db := database.GetDB()
|
||||
var inbounds []*model.Inbound
|
||||
err := db.Model(model.Inbound{}).Preload("ClientStats").Where("settings like ? and enable = ?", fmt.Sprintf(`%%"subId": "%s"%%`, subId), true).Find(&inbounds).Error
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return inbounds, nil
|
||||
@@ -89,6 +108,19 @@ func (s *SubService) getClientTraffics(traffics []xray.ClientTraffic, email stri
|
||||
return xray.ClientTraffic{}
|
||||
}
|
||||
|
||||
func (s *SubService) getFallbackMaster(dest string) (*model.Inbound, error) {
|
||||
db := database.GetDB()
|
||||
var inbound *model.Inbound
|
||||
err := db.Model(model.Inbound{}).
|
||||
Where("JSON_TYPE(settings, '$.fallbacks') = 'array'").
|
||||
Where("EXISTS (SELECT * FROM json_each(settings, '$.fallbacks') WHERE json_extract(value, '$.dest') = ?)", dest).
|
||||
Find(&inbound).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return inbound, nil
|
||||
}
|
||||
|
||||
func (s *SubService) getLink(inbound *model.Inbound, email string) string {
|
||||
switch inbound.Protocol {
|
||||
case "vmess":
|
||||
@@ -162,6 +194,7 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
||||
}
|
||||
|
||||
security, _ := stream["security"].(string)
|
||||
var domains []interface{}
|
||||
obj["tls"] = security
|
||||
if security == "tls" {
|
||||
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
|
||||
@@ -184,6 +217,9 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
||||
if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
|
||||
obj["allowInsecure"], _ = insecure.(bool)
|
||||
}
|
||||
if domainSettings, ok := searchKey(tlsSettings, "domains"); ok {
|
||||
domains, _ = domainSettings.([]interface{})
|
||||
}
|
||||
}
|
||||
serverName, _ := tlsSetting["serverName"].(string)
|
||||
if serverName != "" {
|
||||
@@ -191,7 +227,7 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
||||
}
|
||||
}
|
||||
|
||||
clients, _ := s.inboundService.getClients(inbound)
|
||||
clients, _ := s.inboundService.GetClients(inbound)
|
||||
clientIndex := -1
|
||||
for i, client := range clients {
|
||||
if client.Email == email {
|
||||
@@ -202,6 +238,21 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
||||
obj["id"] = clients[clientIndex].ID
|
||||
obj["aid"] = clients[clientIndex].AlterIds
|
||||
|
||||
if len(domains) > 0 {
|
||||
links := ""
|
||||
for index, d := range domains {
|
||||
domain := d.(map[string]interface{})
|
||||
obj["ps"] = remark + "-" + domain["remark"].(string)
|
||||
obj["add"] = domain["domain"].(string)
|
||||
if index > 0 {
|
||||
links += "\n"
|
||||
}
|
||||
jsonStr, _ := json.MarshalIndent(obj, "", " ")
|
||||
links += "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
|
||||
}
|
||||
return links
|
||||
}
|
||||
|
||||
jsonStr, _ := json.MarshalIndent(obj, "", " ")
|
||||
return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
|
||||
}
|
||||
@@ -213,7 +264,7 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||
}
|
||||
var stream map[string]interface{}
|
||||
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
|
||||
clients, _ := s.inboundService.getClients(inbound)
|
||||
clients, _ := s.inboundService.GetClients(inbound)
|
||||
clientIndex := -1
|
||||
for i, client := range clients {
|
||||
if client.Email == email {
|
||||
@@ -269,6 +320,7 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||
}
|
||||
|
||||
security, _ := stream["security"].(string)
|
||||
var domains []interface{}
|
||||
if security == "tls" {
|
||||
params["security"] = "tls"
|
||||
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
|
||||
@@ -293,6 +345,9 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||
params["allowInsecure"] = "1"
|
||||
}
|
||||
}
|
||||
if domainSettings, ok := searchKey(tlsSettings, "domains"); ok {
|
||||
domains, _ = domainSettings.([]interface{})
|
||||
}
|
||||
}
|
||||
|
||||
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
|
||||
@@ -326,6 +381,11 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||
params["fp"] = fp
|
||||
}
|
||||
}
|
||||
if spxValue, ok := searchKey(realitySettings, "spiderX"); ok {
|
||||
if spx, ok := spxValue.(string); ok && len(spx) > 0 {
|
||||
params["spx"] = spx
|
||||
}
|
||||
}
|
||||
if serverName, ok := searchKey(realitySettings, "serverName"); ok {
|
||||
if sname, ok := serverName.(string); ok && len(sname) > 0 {
|
||||
address = sname
|
||||
@@ -387,6 +447,20 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||
url.RawQuery = q.Encode()
|
||||
|
||||
remark := fmt.Sprintf("%s-%s", inbound.Remark, email)
|
||||
|
||||
if len(domains) > 0 {
|
||||
links := ""
|
||||
for index, d := range domains {
|
||||
domain := d.(map[string]interface{})
|
||||
url.Fragment = remark + "-" + domain["remark"].(string)
|
||||
url.Host = fmt.Sprintf("%s:%d", domain["domain"].(string), port)
|
||||
if index > 0 {
|
||||
links += "\n"
|
||||
}
|
||||
links += url.String()
|
||||
}
|
||||
return links
|
||||
}
|
||||
url.Fragment = remark
|
||||
return url.String()
|
||||
}
|
||||
@@ -398,7 +472,7 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
|
||||
}
|
||||
var stream map[string]interface{}
|
||||
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
|
||||
clients, _ := s.inboundService.getClients(inbound)
|
||||
clients, _ := s.inboundService.GetClients(inbound)
|
||||
clientIndex := -1
|
||||
for i, client := range clients {
|
||||
if client.Email == email {
|
||||
@@ -454,6 +528,7 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
|
||||
}
|
||||
|
||||
security, _ := stream["security"].(string)
|
||||
var domains []interface{}
|
||||
if security == "tls" {
|
||||
params["security"] = "tls"
|
||||
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
|
||||
@@ -478,6 +553,9 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
|
||||
params["allowInsecure"] = "1"
|
||||
}
|
||||
}
|
||||
if domainSettings, ok := searchKey(tlsSettings, "domains"); ok {
|
||||
domains, _ = domainSettings.([]interface{})
|
||||
}
|
||||
}
|
||||
|
||||
serverName, _ := tlsSetting["serverName"].(string)
|
||||
@@ -507,6 +585,11 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
|
||||
params["fp"] = fp
|
||||
}
|
||||
}
|
||||
if spxValue, ok := searchKey(realitySettings, "spiderX"); ok {
|
||||
if spx, ok := spxValue.(string); ok && len(spx) > 0 {
|
||||
params["spx"] = spx
|
||||
}
|
||||
}
|
||||
if serverName, ok := searchKey(realitySettings, "serverName"); ok {
|
||||
if sname, ok := serverName.(string); ok && len(sname) > 0 {
|
||||
address = sname
|
||||
@@ -569,6 +652,21 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
|
||||
url.RawQuery = q.Encode()
|
||||
|
||||
remark := fmt.Sprintf("%s-%s", inbound.Remark, email)
|
||||
|
||||
if len(domains) > 0 {
|
||||
links := ""
|
||||
for index, d := range domains {
|
||||
domain := d.(map[string]interface{})
|
||||
url.Fragment = remark + "-" + domain["remark"].(string)
|
||||
url.Host = fmt.Sprintf("%s:%d", domain["domain"].(string), port)
|
||||
if index > 0 {
|
||||
links += "\n"
|
||||
}
|
||||
links += url.String()
|
||||
}
|
||||
return links
|
||||
}
|
||||
|
||||
url.Fragment = remark
|
||||
return url.String()
|
||||
}
|
||||
@@ -578,7 +676,7 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
|
||||
if inbound.Protocol != model.Shadowsocks {
|
||||
return ""
|
||||
}
|
||||
clients, _ := s.inboundService.getClients(inbound)
|
||||
clients, _ := s.inboundService.GetClients(inbound)
|
||||
|
||||
var settings map[string]interface{}
|
||||
json.Unmarshal([]byte(inbound.Settings), &settings)
|
||||
@@ -592,7 +690,8 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
|
||||
}
|
||||
}
|
||||
encPart := fmt.Sprintf("%s:%s:%s", method, inboundPassword, clients[clientIndex].Password)
|
||||
return fmt.Sprintf("ss://%s@%s:%d#%s", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port, clients[clientIndex].Email)
|
||||
remark := fmt.Sprintf("%s-%s", inbound.Remark, clients[clientIndex].Email)
|
||||
return fmt.Sprintf("ss://%s@%s:%d#%s", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port, remark)
|
||||
}
|
||||
|
||||
func searchKey(data interface{}, key string) (interface{}, bool) {
|
||||
@@ -24,8 +24,8 @@ func getLinesNum(filename string) (int, error) {
|
||||
|
||||
var buffPosition int
|
||||
for {
|
||||
i := bytes.IndexByte(buf[buffPosition:], '\n')
|
||||
if i < 0 || n == buffPosition {
|
||||
i := bytes.IndexByte(buf[buffPosition:n], '\n')
|
||||
if i < 0 {
|
||||
break
|
||||
}
|
||||
buffPosition += i + 1
|
||||
@@ -33,11 +33,12 @@ func getLinesNum(filename string) (int, error) {
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
return sum, nil
|
||||
break
|
||||
} else if err != nil {
|
||||
return sum, err
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return sum, nil
|
||||
}
|
||||
|
||||
func GetTCPCount() (int, error) {
|
||||
@@ -45,11 +46,11 @@ func GetTCPCount() (int, error) {
|
||||
|
||||
tcp4, err := getLinesNum(fmt.Sprintf("%v/net/tcp", root))
|
||||
if err != nil {
|
||||
return tcp4, err
|
||||
return 0, err
|
||||
}
|
||||
tcp6, err := getLinesNum(fmt.Sprintf("%v/net/tcp6", root))
|
||||
if err != nil {
|
||||
return tcp4 + tcp6, nil
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return tcp4 + tcp6, nil
|
||||
@@ -60,11 +61,11 @@ func GetUDPCount() (int, error) {
|
||||
|
||||
udp4, err := getLinesNum(fmt.Sprintf("%v/net/udp", root))
|
||||
if err != nil {
|
||||
return udp4, err
|
||||
return 0, err
|
||||
}
|
||||
udp6, err := getLinesNum(fmt.Sprintf("%v/net/udp6", root))
|
||||
if err != nil {
|
||||
return udp4 + udp6, nil
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return udp4 + udp6, nil
|
||||
|
||||
@@ -4,21 +4,27 @@
|
||||
package sys
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/shirou/gopsutil/v3/net"
|
||||
)
|
||||
|
||||
func GetTCPCount() (int, error) {
|
||||
stats, err := net.Connections("tcp")
|
||||
func GetConnectionCount(proto string) (int, error) {
|
||||
if proto != "tcp" && proto != "udp" {
|
||||
return 0, errors.New("invalid protocol")
|
||||
}
|
||||
|
||||
stats, err := net.Connections(proto)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(stats), nil
|
||||
}
|
||||
|
||||
func GetUDPCount() (int, error) {
|
||||
stats, err := net.Connections("udp")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(stats), nil
|
||||
func GetTCPCount() (int, error) {
|
||||
return GetConnectionCount("tcp")
|
||||
}
|
||||
|
||||
func GetUDPCount() (int, error) {
|
||||
return GetConnectionCount("udp")
|
||||
}
|
||||
|
||||
11
web/assets/ant-design-vue@1.7.2/antd.min.css
vendored
11
web/assets/ant-design-vue@1.7.2/antd.min.css
vendored
@@ -529,7 +529,7 @@ to{transform:scaleY(0);transform-origin:0 0;opacity:0}
|
||||
.ant-select-selection-selected-value{float:left;max-width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
|
||||
.ant-select-no-arrow .ant-select-selection-selected-value{padding-right:0}
|
||||
.ant-select-disabled{color:rgba(0,0,0,.25)}
|
||||
.ant-select-disabled .ant-select-selection{background:rgb(0 150 112);cursor:not-allowed}
|
||||
.ant-select-disabled .ant-select-selection{background:rgb(221, 221, 221);cursor:not-allowed}
|
||||
.ant-select-disabled .ant-select-selection:active,.ant-select-disabled .ant-select-selection:focus,.ant-select-disabled .ant-select-selection:hover{border-color:#d9d9d9;box-shadow:none}
|
||||
.ant-select-disabled .ant-select-selection__clear{display:none;visibility:hidden;pointer-events:none}
|
||||
.ant-select-disabled .ant-select-selection--multiple .ant-select-selection__choice{padding-right:10px;color:rgba(0,0,0,.33);background:#f5f5f5}
|
||||
@@ -996,7 +996,8 @@ to{transform:scale(0) translate(50%,-50%);opacity:0}
|
||||
.ant-menu-horizontal .ant-menu-item,.ant-menu-horizontal .ant-menu-submenu{margin-top:-1px}
|
||||
.ant-menu-horizontal>.ant-menu-item-active,.ant-menu-horizontal>.ant-menu-item:hover,.ant-menu-horizontal>.ant-menu-submenu .ant-menu-submenu-title:hover{background-color:transparent}
|
||||
.ant-menu-item-selected,.ant-menu-item-selected>a,.ant-menu-item-selected>a:hover{color:#1890ff}
|
||||
.ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected{background: linear-gradient(90deg,#009670 0,#026247 100%);color: #fff;border-radius: 0.5rem}
|
||||
.ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected{background-color: #0a7557;background-image: linear-gradient( 270deg, rgba(123, 199, 77, 0) 30%, #00ab80, rgba(123, 199, 77, 0) 100% );background-repeat: no-repeat;animation: ma-bg-move linear 6.6s infinite;/*background: linear-gradient(90deg,#009670 0,#026247 100%);*/color: #fff;border-radius: 0.5rem}
|
||||
@-webkit-keyframes ma-bg-move {0% {background-position: -500px 0;}100% {background-position: 1000px 0;}}@keyframes ma-bg-move {0% {background-position: -500px 0;}50% {background-position: 1000px 0;}100% {background-position: 1000px 0;}}
|
||||
.ant-menu-vertical-right{border-left:1px solid #e8e8e8}
|
||||
.ant-menu-vertical-left.ant-menu-sub,.ant-menu-vertical-right.ant-menu-sub,.ant-menu-vertical.ant-menu-sub{min-width:160px;padding:0;border-right:0;transform-origin:0 0}
|
||||
.ant-menu-vertical-left.ant-menu-sub .ant-menu-item,.ant-menu-vertical-right.ant-menu-sub .ant-menu-item,.ant-menu-vertical.ant-menu-sub .ant-menu-item{left:0;margin-left:0;border-right:0}
|
||||
@@ -1081,8 +1082,8 @@ to{transform:scale(0) translate(50%,-50%);opacity:0}
|
||||
.ant-menu-dark .ant-menu-item-selected{color:#fff;border-right:0}
|
||||
.ant-menu-dark .ant-menu-item-selected:after{border-right:0}
|
||||
.ant-menu-dark .ant-menu-item-selected .anticon,.ant-menu-dark .ant-menu-item-selected .anticon+span,.ant-menu-dark .ant-menu-item-selected>a,.ant-menu-dark .ant-menu-item-selected>a:hover{color:#ffffff}
|
||||
.ant-menu-submenu-popup.ant-menu-dark .ant-menu-item-selected,.ant-menu.ant-menu-dark .ant-menu-item-selected{background-color:#15223a}
|
||||
.ant-menu-submenu-popup.ant-menu-dark .ant-menu-item-active,.ant-menu.ant-menu-dark .ant-menu-item-active{background-color:#38383800}
|
||||
.ant-menu-submenu-popup.ant-menu-dark .ant-menu-item-selected,.ant-menu.ant-menu-dark .ant-menu-item-selected{background-color:#0a7557}
|
||||
/*.ant-menu-submenu-popup.ant-menu-dark .ant-menu-item-active,.ant-menu.ant-menu-dark .ant-menu-item-active{background-color:#0a7557}*/
|
||||
.ant-menu-dark .ant-menu-item-disabled,.ant-menu-dark .ant-menu-item-disabled>a,.ant-menu-dark .ant-menu-submenu-disabled,.ant-menu-dark .ant-menu-submenu-disabled>a{color:hsla(0,0%,100%,.35)!important;opacity:.8}
|
||||
.ant-menu-dark .ant-menu-item-disabled>.ant-menu-submenu-title,.ant-menu-dark .ant-menu-submenu-disabled>.ant-menu-submenu-title{color:hsla(0,0%,100%,.35)!important}
|
||||
.ant-menu-dark .ant-menu-item-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-item-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:before,.ant-menu-dark .ant-menu-submenu-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-submenu-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:before{background:hsla(0,0%,100%,.35)!important}
|
||||
@@ -3231,7 +3232,7 @@ textarea.ant-input-number{max-width:100%;height:auto;min-height:32px;line-height
|
||||
.ant-input-number-handler-down-inner:before,.ant-input-number-handler-up-inner:before{display:none}
|
||||
.ant-input-number-handler-down-inner .ant-input-number-handler-down-inner-icon,.ant-input-number-handler-down-inner .ant-input-number-handler-up-inner-icon,.ant-input-number-handler-up-inner .ant-input-number-handler-down-inner-icon,.ant-input-number-handler-up-inner .ant-input-number-handler-up-inner-icon{display:block}
|
||||
.ant-input-number-focused,.ant-input-number:hover{border-color:rgb(0, 150, 112) !important;border-right-width:1px!important}
|
||||
.ant-input-number-focused{outline:0;box-shadow:0 0 0 2px rgba(24,144,255,.2)}
|
||||
.ant-input-number-focused{outline:0;box-shadow:rgba(0, 150, 112, 0.2) 0px 0px 0px 2px}
|
||||
.ant-input-number-disabled{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}
|
||||
.ant-input-number-disabled:hover{border-color:#d9d9d9;border-right-width:1px!important}
|
||||
.ant-input-number-disabled .ant-input-number-input{cursor:not-allowed}
|
||||
|
||||
@@ -9,7 +9,6 @@ body {
|
||||
|
||||
#app {
|
||||
height: 100%;
|
||||
min-height: 100vh;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@@ -214,6 +213,7 @@ body {
|
||||
}
|
||||
|
||||
.ant-card-dark-box-nohover{
|
||||
margin-top: .5rem;
|
||||
padding: 0 20px 20px !important;
|
||||
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 0%) !important;
|
||||
}
|
||||
@@ -273,12 +273,12 @@ body {
|
||||
color: hsla(0,0%,100%,.65);
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-table-tbody>tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)>td,
|
||||
.ant-card-dark .ant-select-dropdown-menu-item:hover:not(.ant-select-dropdown-menu-item-disabled),
|
||||
.ant-card-dark .ant-select-dropdown-menu-item-active {
|
||||
.ant-card-dark .ant-table-tbody>tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)>td {
|
||||
background-color: #11314d;
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-select-dropdown-menu-item:hover:not(.ant-select-dropdown-menu-item-disabled),
|
||||
.ant-card-dark .ant-select-dropdown-menu-item-active,
|
||||
.ant-card-dark .ant-calendar-date:hover,
|
||||
.ant-card-dark li.ant-calendar-time-picker-select-option-selected {
|
||||
background-color: rgb(4, 119, 90);
|
||||
|
||||
@@ -153,9 +153,9 @@ class DBInbound {
|
||||
}
|
||||
}
|
||||
|
||||
genLink(clientIndex) {
|
||||
genLink(address=this.address, remark=this.remark, clientIndex=0) {
|
||||
const inbound = this.toInbound();
|
||||
return inbound.genLink(this.address, this.remark, clientIndex);
|
||||
return inbound.genLink(address, remark, clientIndex);
|
||||
}
|
||||
|
||||
get genInboundLinks() {
|
||||
@@ -181,8 +181,17 @@ class AllSetting {
|
||||
this.tgRunTime = "@daily";
|
||||
this.tgBotBackup = false;
|
||||
this.tgCpu = "";
|
||||
this.tgLang = "en-US";
|
||||
this.xrayTemplateConfig = "";
|
||||
this.secretEnable = false;
|
||||
this.subEnable = false;
|
||||
this.subListen = "";
|
||||
this.subPort = "2096";
|
||||
this.subPath = "sub/";
|
||||
this.subDomain = "";
|
||||
this.subCertFile = "";
|
||||
this.subKeyFile = "";
|
||||
this.subUpdates = 0;
|
||||
|
||||
this.timeLocation = "Asia/Tehran";
|
||||
|
||||
|
||||
@@ -482,8 +482,8 @@ class TlsStreamSettings extends XrayCommonClass {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
addCert(cert) {
|
||||
this.certs.push(cert);
|
||||
addCert() {
|
||||
this.certs.push(new TlsStreamSettings.Cert());
|
||||
}
|
||||
|
||||
removeCert(index) {
|
||||
@@ -498,8 +498,7 @@ class TlsStreamSettings extends XrayCommonClass {
|
||||
}
|
||||
|
||||
if (!ObjectUtil.isEmpty(json.settings)) {
|
||||
settings = new TlsStreamSettings.Settings(json.settings.allowInsecure , json.settings.fingerprint, json.settings.serverName);
|
||||
}
|
||||
settings = new TlsStreamSettings.Settings(json.settings.allowInsecure , json.settings.fingerprint, json.settings.serverName, json.settings.domains); }
|
||||
return new TlsStreamSettings(
|
||||
json.serverName,
|
||||
json.minVersion,
|
||||
@@ -566,17 +565,19 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
|
||||
};
|
||||
|
||||
TlsStreamSettings.Settings = class extends XrayCommonClass {
|
||||
constructor(allowInsecure = false, fingerprint = '', serverName = '') {
|
||||
constructor(allowInsecure = false, fingerprint = '', serverName = '', domains = []) {
|
||||
super();
|
||||
this.allowInsecure = allowInsecure;
|
||||
this.fingerprint = fingerprint;
|
||||
this.serverName = serverName;
|
||||
this.domains = domains;
|
||||
}
|
||||
static fromJson(json = {}) {
|
||||
return new TlsStreamSettings.Settings(
|
||||
json.allowInsecure,
|
||||
json.fingerprint,
|
||||
json.servername,
|
||||
json.serverName,
|
||||
json.domains,
|
||||
);
|
||||
}
|
||||
toJson() {
|
||||
@@ -584,6 +585,7 @@ TlsStreamSettings.Settings = class extends XrayCommonClass {
|
||||
allowInsecure: this.allowInsecure,
|
||||
fingerprint: this.fingerprint,
|
||||
serverName: this.serverName,
|
||||
domains: this.domains,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -599,8 +601,8 @@ class XtlsStreamSettings extends XrayCommonClass {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
addCert(cert) {
|
||||
this.certs.push(cert);
|
||||
addCert() {
|
||||
this.certs.push(new XtlsStreamSettings.Cert());
|
||||
}
|
||||
|
||||
removeCert(index) {
|
||||
@@ -706,7 +708,7 @@ class RealityStreamSettings extends XrayCommonClass {
|
||||
minClient = '',
|
||||
maxClient = '',
|
||||
maxTimediff = 0,
|
||||
shortIds = RandomUtil.randowShortId(),
|
||||
shortIds = RandomUtil.randomShortId(),
|
||||
settings= new RealityStreamSettings.Settings()
|
||||
){
|
||||
super();
|
||||
@@ -725,7 +727,7 @@ class RealityStreamSettings extends XrayCommonClass {
|
||||
static fromJson(json = {}) {
|
||||
let settings;
|
||||
if (!ObjectUtil.isEmpty(json.settings)) {
|
||||
settings = new RealityStreamSettings.Settings(json.settings.publicKey , json.settings.fingerprint, json.settings.serverName);
|
||||
settings = new RealityStreamSettings.Settings(json.settings.publicKey , json.settings.fingerprint, json.settings.serverName, json.settings.spiderX);
|
||||
}
|
||||
return new RealityStreamSettings(
|
||||
json.show,
|
||||
@@ -758,17 +760,19 @@ class RealityStreamSettings extends XrayCommonClass {
|
||||
}
|
||||
|
||||
RealityStreamSettings.Settings = class extends XrayCommonClass {
|
||||
constructor(publicKey = '', fingerprint = UTLS_FINGERPRINT.UTLS_FIREFOX, serverName = '') {
|
||||
constructor(publicKey = '', fingerprint = UTLS_FINGERPRINT.UTLS_FIREFOX, serverName = '', spiderX= '/') {
|
||||
super();
|
||||
this.publicKey = publicKey;
|
||||
this.fingerprint = fingerprint;
|
||||
this.serverName = serverName;
|
||||
this.spiderX = spiderX;
|
||||
}
|
||||
static fromJson(json = {}) {
|
||||
return new RealityStreamSettings.Settings(
|
||||
json.publicKey,
|
||||
json.fingerprint,
|
||||
json.serverName,
|
||||
json.spiderX,
|
||||
);
|
||||
}
|
||||
toJson() {
|
||||
@@ -776,6 +780,7 @@ RealityStreamSettings.Settings = class extends XrayCommonClass {
|
||||
publicKey: this.publicKey,
|
||||
fingerprint: this.fingerprint,
|
||||
serverName: this.serverName,
|
||||
spiderX: this.spiderX,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -1370,6 +1375,9 @@ class Inbound extends XrayCommonClass {
|
||||
if (!ObjectUtil.isEmpty(this.stream.reality.settings.serverName)) {
|
||||
address = this.stream.reality.settings.serverName;
|
||||
}
|
||||
if (!ObjectUtil.isEmpty(this.stream.reality.settings.spiderX)) {
|
||||
params.set("spx", this.stream.reality.settings.spiderX);
|
||||
}
|
||||
}
|
||||
|
||||
const link = `vless://${uuid}@${address}:${port}`;
|
||||
@@ -1470,6 +1478,9 @@ class Inbound extends XrayCommonClass {
|
||||
if (!ObjectUtil.isEmpty(this.stream.reality.settings.serverName)) {
|
||||
address = this.stream.reality.settings.serverName;
|
||||
}
|
||||
if (!ObjectUtil.isEmpty(this.stream.reality.settings.spiderX)) {
|
||||
params.set("spx", this.stream.reality.settings.spiderX);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.xtls) {
|
||||
@@ -1498,25 +1509,13 @@ class Inbound extends XrayCommonClass {
|
||||
|
||||
genLink(address='', remark='', clientIndex=0) {
|
||||
switch (this.protocol) {
|
||||
case Protocols.VMESS:
|
||||
if (this.settings.vmesses[clientIndex].email != ""){
|
||||
remark += '-' + this.settings.vmesses[clientIndex].email
|
||||
}
|
||||
case Protocols.VMESS:
|
||||
return this.genVmessLink(address, remark, clientIndex);
|
||||
case Protocols.VLESS:
|
||||
if (this.settings.vlesses[clientIndex].email != ""){
|
||||
remark += '-' + this.settings.vlesses[clientIndex].email
|
||||
}
|
||||
return this.genVLESSLink(address, remark, clientIndex);
|
||||
case Protocols.SHADOWSOCKS:
|
||||
if (this.settings.shadowsockses[clientIndex].email != ""){
|
||||
remark = this.settings.shadowsockses[clientIndex].email
|
||||
}
|
||||
return this.genSSLink(address, remark, clientIndex);
|
||||
case Protocols.TROJAN:
|
||||
if (this.settings.trojans[clientIndex].email != ""){
|
||||
remark += '-' + this.settings.trojans[clientIndex].email
|
||||
}
|
||||
return this.genTrojanLink(address, remark, clientIndex);
|
||||
default: return '';
|
||||
}
|
||||
@@ -1528,12 +1527,17 @@ class Inbound extends XrayCommonClass {
|
||||
case Protocols.VMESS:
|
||||
case Protocols.VLESS:
|
||||
case Protocols.TROJAN:
|
||||
JSON.parse(this.settings).clients.forEach((_,index) => {
|
||||
link += this.genLink(address, remark, index) + '\r\n';
|
||||
case Protocols.SHADOWSOCKS:
|
||||
JSON.parse(this.settings).clients.forEach((client,index) => {
|
||||
if(this.tls && !ObjectUtil.isArrEmpty(this.stream.tls.settings.domains)){
|
||||
this.stream.tls.settings.domains.forEach((domain) => {
|
||||
link += this.genLink(domain.domain, remark + '-' + client.email + '-' + domain.remark, index) + '\r\n';
|
||||
});
|
||||
} else {
|
||||
link += this.genLink(address, remark + '-' + client.email, index) + '\r\n';
|
||||
}
|
||||
});
|
||||
return link;
|
||||
case Protocols.SHADOWSOCKS:
|
||||
return (this.genSSLink(address, remark) + '\r\n');
|
||||
default: return '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ const ONE_TB = ONE_GB * 1024;
|
||||
const ONE_PB = ONE_TB * 1024;
|
||||
|
||||
function sizeFormat(size) {
|
||||
if (size < ONE_KB) {
|
||||
if (size < 0) {
|
||||
return "0 B";
|
||||
} else if (size < ONE_KB) {
|
||||
return size.toFixed(0) + " B";
|
||||
} else if (size < ONE_MB) {
|
||||
return (size / ONE_KB).toFixed(2) + " KB";
|
||||
@@ -20,6 +22,23 @@ function sizeFormat(size) {
|
||||
}
|
||||
}
|
||||
|
||||
function cpuSpeedFormat(speed) {
|
||||
if (speed > 1000) {
|
||||
const GHz = speed / 1000;
|
||||
return GHz.toFixed(2) + " GHz";
|
||||
} else {
|
||||
return speed.toFixed(2) + " MHz";
|
||||
}
|
||||
}
|
||||
|
||||
function cpuCoreFormat(cores) {
|
||||
if (cores === 1) {
|
||||
return "1 Core";
|
||||
} else {
|
||||
return cores + " Cores";
|
||||
}
|
||||
}
|
||||
|
||||
function base64(str) {
|
||||
return Base64.encode(str);
|
||||
}
|
||||
@@ -70,23 +89,49 @@ function debounce(fn, delay) {
|
||||
|
||||
function getCookie(cname) {
|
||||
let name = cname + '=';
|
||||
let decodedCookie = decodeURIComponent(document.cookie);
|
||||
let ca = decodedCookie.split(';');
|
||||
let ca = document.cookie.split(';');
|
||||
for (let i = 0; i < ca.length; i++) {
|
||||
let c = ca[i];
|
||||
while (c.charAt(0) == ' ') {
|
||||
c = c.substring(1);
|
||||
}
|
||||
if (c.indexOf(name) == 0) {
|
||||
return c.substring(name.length, c.length);
|
||||
// decode cookie value only
|
||||
return decodeURIComponent(c.substring(name.length, c.length));
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
function setCookie(cname, cvalue, exdays) {
|
||||
const d = new Date();
|
||||
d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000);
|
||||
let expires = 'expires=' + d.toUTCString();
|
||||
document.cookie = cname + '=' + cvalue + ';' + expires + ';path=/';
|
||||
// encode cookie value
|
||||
document.cookie = cname + '=' + encodeURIComponent(cvalue) + ';' + expires + ';path=/';
|
||||
}
|
||||
|
||||
function usageColor(data, threshold, total) {
|
||||
switch (true) {
|
||||
case data === null:
|
||||
return 'blue';
|
||||
case total <= 0:
|
||||
return 'blue';
|
||||
case data < total - threshold:
|
||||
return 'cyan';
|
||||
case data < total:
|
||||
return 'orange';
|
||||
default:
|
||||
return 'red';
|
||||
}
|
||||
}
|
||||
|
||||
function doAllItemsExist(array1, array2) {
|
||||
for (let i = 0; i < array1.length; i++) {
|
||||
if (!array2.includes(array1[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -75,26 +75,13 @@ class PromiseUtil {
|
||||
}
|
||||
}
|
||||
|
||||
const seq = [
|
||||
'a', 'b', 'c', 'd', 'e', 'f', 'g',
|
||||
'h', 'i', 'j', 'k', 'l', 'm', 'n',
|
||||
'o', 'p', 'q', 'r', 's', 't',
|
||||
'u', 'v', 'w', 'x', 'y', 'z',
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
||||
'A', 'B', 'C', 'D', 'E', 'F', 'G',
|
||||
'H', 'I', 'J', 'K', 'L', 'M', 'N',
|
||||
'O', 'P', 'Q', 'R', 'S', 'T',
|
||||
'U', 'V', 'W', 'X', 'Y', 'Z'
|
||||
];
|
||||
const seq = 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
|
||||
|
||||
const shortIdSeq = [
|
||||
'a', 'b', 'c', 'd', 'e', 'f',
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
||||
];
|
||||
const shortIdSeq = 'abcdef0123456789'.split('');
|
||||
|
||||
class RandomUtil {
|
||||
static randomIntRange(min, max) {
|
||||
return parseInt(Math.random() * (max - min) + min, 10);
|
||||
return Math.floor(Math.random() * (max - min) + min);
|
||||
}
|
||||
|
||||
static randomInt(n) {
|
||||
@@ -116,6 +103,10 @@ class RandomUtil {
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
static randomShortId() {
|
||||
return this.randomShortIdSeq(8);
|
||||
}
|
||||
|
||||
static randomLowerAndNum(count) {
|
||||
let str = '';
|
||||
@@ -156,12 +147,6 @@ class RandomUtil {
|
||||
return string;
|
||||
}
|
||||
|
||||
static randowShortId() {
|
||||
let str = '';
|
||||
str += this.randomShortIdSeq(8);
|
||||
return str;
|
||||
}
|
||||
|
||||
static randomShadowsocksPassword() {
|
||||
let array = new Uint8Array(32);
|
||||
window.crypto.getRandomValues(array);
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
package controller
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
import (
|
||||
"x-ui/web/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type APIController struct {
|
||||
BaseController
|
||||
inboundController *InboundController
|
||||
Tgbot service.Tgbot
|
||||
}
|
||||
|
||||
func NewAPIController(g *gin.RouterGroup) *APIController {
|
||||
@@ -32,7 +37,7 @@ func (a *APIController) initRouter(g *gin.RouterGroup) {
|
||||
g.POST("/resetAllTraffics", a.resetAllTraffics)
|
||||
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
|
||||
g.POST("/delDepletedClients/:id", a.delDepletedClients)
|
||||
|
||||
g.GET("/createbackup", a.createBackup)
|
||||
a.inboundController = NewInboundController(g)
|
||||
}
|
||||
|
||||
@@ -95,3 +100,7 @@ func (a *APIController) resetAllClientTraffics(c *gin.Context) {
|
||||
func (a *APIController) delDepletedClients(c *gin.Context) {
|
||||
a.inboundController.delDepletedClients(c)
|
||||
}
|
||||
|
||||
func (a *APIController) createBackup(c *gin.Context) {
|
||||
a.Tgbot.SendBackupToAdmins()
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"x-ui/logger"
|
||||
"x-ui/web/locale"
|
||||
"x-ui/web/session"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -13,7 +15,7 @@ type BaseController struct {
|
||||
func (a *BaseController) checkLogin(c *gin.Context) {
|
||||
if !session.IsLogin(c) {
|
||||
if isAjax(c) {
|
||||
pureJsonMsg(c, false, I18n(c, "pages.login.loginAgain"))
|
||||
pureJsonMsg(c, false, I18nWeb(c, "pages.login.loginAgain"))
|
||||
} else {
|
||||
c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
|
||||
}
|
||||
@@ -23,11 +25,13 @@ func (a *BaseController) checkLogin(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func I18n(c *gin.Context, name string) string {
|
||||
anyfunc, _ := c.Get("I18n")
|
||||
i18n, _ := anyfunc.(func(key string, params ...string) (string, error))
|
||||
|
||||
message, _ := i18n(name)
|
||||
|
||||
return message
|
||||
func I18nWeb(c *gin.Context, name string, params ...string) string {
|
||||
anyfunc, funcExists := c.Get("I18n")
|
||||
if !funcExists {
|
||||
logger.Warning("I18n function not exists in gin context!")
|
||||
return ""
|
||||
}
|
||||
i18nFunc, _ := anyfunc.(func(i18nType locale.I18nType, key string, keyParams ...string) string)
|
||||
msg := i18nFunc(locale.Web, name, params...)
|
||||
return msg
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ func (a *InboundController) getInbounds(c *gin.Context) {
|
||||
user := session.GetLoginUser(c)
|
||||
inbounds, err := a.inboundService.GetInbounds(user.Id)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.inbounds.toasts.obtain"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, inbounds, nil)
|
||||
@@ -68,12 +68,12 @@ func (a *InboundController) getInbounds(c *gin.Context) {
|
||||
func (a *InboundController) getInbound(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "get"), err)
|
||||
jsonMsg(c, I18nWeb(c, "get"), err)
|
||||
return
|
||||
}
|
||||
inbound, err := a.inboundService.GetInbound(id)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.inbounds.toasts.obtain"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, inbound, nil)
|
||||
@@ -93,7 +93,7 @@ func (a *InboundController) addInbound(c *gin.Context) {
|
||||
inbound := &model.Inbound{}
|
||||
err := c.ShouldBind(inbound)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.inbounds.create"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.create"), err)
|
||||
return
|
||||
}
|
||||
user := session.GetLoginUser(c)
|
||||
@@ -101,7 +101,7 @@ func (a *InboundController) addInbound(c *gin.Context) {
|
||||
inbound.Enable = true
|
||||
inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
|
||||
inbound, err = a.inboundService.AddInbound(inbound)
|
||||
jsonMsgObj(c, I18n(c, "pages.inbounds.create"), inbound, err)
|
||||
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.create"), inbound, err)
|
||||
if err == nil {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
@@ -110,11 +110,11 @@ func (a *InboundController) addInbound(c *gin.Context) {
|
||||
func (a *InboundController) delInbound(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "delete"), err)
|
||||
jsonMsg(c, I18nWeb(c, "delete"), err)
|
||||
return
|
||||
}
|
||||
err = a.inboundService.DelInbound(id)
|
||||
jsonMsgObj(c, I18n(c, "delete"), id, err)
|
||||
jsonMsgObj(c, I18nWeb(c, "delete"), id, err)
|
||||
if err == nil {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
@@ -123,7 +123,7 @@ func (a *InboundController) delInbound(c *gin.Context) {
|
||||
func (a *InboundController) updateInbound(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
|
||||
return
|
||||
}
|
||||
inbound := &model.Inbound{
|
||||
@@ -131,11 +131,11 @@ func (a *InboundController) updateInbound(c *gin.Context) {
|
||||
}
|
||||
err = c.ShouldBind(inbound)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
|
||||
return
|
||||
}
|
||||
inbound, err = a.inboundService.UpdateInbound(inbound)
|
||||
jsonMsgObj(c, I18n(c, "pages.inbounds.update"), inbound, err)
|
||||
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.update"), inbound, err)
|
||||
if err == nil {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
@@ -146,11 +146,18 @@ func (a *InboundController) getClientIps(c *gin.Context) {
|
||||
|
||||
ips, err := a.inboundService.GetInboundClientIps(email)
|
||||
if err != nil {
|
||||
jsonObj(c, "Failed to get client IPs", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if ips == "" {
|
||||
jsonObj(c, "No IP Record", nil)
|
||||
return
|
||||
}
|
||||
|
||||
jsonObj(c, ips, nil)
|
||||
}
|
||||
|
||||
func (a *InboundController) clearClientIps(c *gin.Context) {
|
||||
email := c.Param("email")
|
||||
|
||||
@@ -165,7 +172,7 @@ func (a *InboundController) addInboundClient(c *gin.Context) {
|
||||
data := &model.Inbound{}
|
||||
err := c.ShouldBind(data)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -183,7 +190,7 @@ func (a *InboundController) addInboundClient(c *gin.Context) {
|
||||
func (a *InboundController) delInboundClient(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
|
||||
return
|
||||
}
|
||||
clientId := c.Param("clientId")
|
||||
@@ -205,7 +212,7 @@ func (a *InboundController) updateInboundClient(c *gin.Context) {
|
||||
inbound := &model.Inbound{}
|
||||
err := c.ShouldBind(inbound)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -223,7 +230,7 @@ func (a *InboundController) updateInboundClient(c *gin.Context) {
|
||||
func (a *InboundController) resetClientTraffic(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
|
||||
return
|
||||
}
|
||||
email := c.Param("email")
|
||||
@@ -251,7 +258,7 @@ func (a *InboundController) resetAllTraffics(c *gin.Context) {
|
||||
func (a *InboundController) resetAllClientTraffics(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -266,7 +273,7 @@ func (a *InboundController) resetAllClientTraffics(c *gin.Context) {
|
||||
func (a *InboundController) delDepletedClients(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
|
||||
return
|
||||
}
|
||||
err = a.inboundService.DelDepletedClients(id)
|
||||
|
||||
@@ -49,26 +49,27 @@ func (a *IndexController) login(c *gin.Context) {
|
||||
var form LoginForm
|
||||
err := c.ShouldBind(&form)
|
||||
if err != nil {
|
||||
pureJsonMsg(c, false, I18n(c, "pages.login.toasts.invalidFormData"))
|
||||
pureJsonMsg(c, false, I18nWeb(c, "pages.login.toasts.invalidFormData"))
|
||||
return
|
||||
}
|
||||
if form.Username == "" {
|
||||
pureJsonMsg(c, false, I18n(c, "pages.login.toasts.emptyUsername"))
|
||||
pureJsonMsg(c, false, I18nWeb(c, "pages.login.toasts.emptyUsername"))
|
||||
return
|
||||
}
|
||||
if form.Password == "" {
|
||||
pureJsonMsg(c, false, I18n(c, "pages.login.toasts.emptyPassword"))
|
||||
pureJsonMsg(c, false, I18nWeb(c, "pages.login.toasts.emptyPassword"))
|
||||
return
|
||||
}
|
||||
|
||||
user := a.userService.CheckUser(form.Username, form.Password, form.LoginSecret)
|
||||
timeStr := time.Now().Format("2006-01-02 15:04:05")
|
||||
if user == nil {
|
||||
a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0)
|
||||
logger.Infof("wrong username or password: \"%s\" \"%s\"", form.Username, form.Password)
|
||||
pureJsonMsg(c, false, I18n(c, "pages.login.toasts.wrongUsernameOrPassword"))
|
||||
a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0)
|
||||
pureJsonMsg(c, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword"))
|
||||
return
|
||||
} else {
|
||||
logger.Infof("%s login success,Ip Address:%s\n", form.Username, getRemoteIp(c))
|
||||
logger.Infof("%s login success, Ip Address: %s\n", form.Username, getRemoteIp(c))
|
||||
a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 1)
|
||||
}
|
||||
|
||||
@@ -86,7 +87,7 @@ func (a *IndexController) login(c *gin.Context) {
|
||||
|
||||
err = session.SetLoginUser(c, user)
|
||||
logger.Info("user", user.Id, "login success")
|
||||
jsonMsg(c, I18n(c, "pages.login.toasts.successLogin"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.login.toasts.successLogin"), err)
|
||||
}
|
||||
|
||||
func (a *IndexController) logout(c *gin.Context) {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"time"
|
||||
"x-ui/web/global"
|
||||
"x-ui/web/service"
|
||||
@@ -8,6 +11,8 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var filenameRegex = regexp.MustCompile(`^[a-zA-Z0-9_\-.]+$`)
|
||||
|
||||
type ServerController struct {
|
||||
BaseController
|
||||
|
||||
@@ -76,7 +81,7 @@ func (a *ServerController) getXrayVersion(c *gin.Context) {
|
||||
|
||||
versions, err := a.serverService.GetXrayVersions()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "getVersion"), err)
|
||||
jsonMsg(c, I18nWeb(c, "getVersion"), err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -89,7 +94,7 @@ func (a *ServerController) getXrayVersion(c *gin.Context) {
|
||||
func (a *ServerController) installXray(c *gin.Context) {
|
||||
version := c.Param("version")
|
||||
err := a.serverService.UpdateXray(version)
|
||||
jsonMsg(c, I18n(c, "install")+" xray", err)
|
||||
jsonMsg(c, I18nWeb(c, "install")+" xray", err)
|
||||
}
|
||||
|
||||
func (a *ServerController) stopXrayService(c *gin.Context) {
|
||||
@@ -136,14 +141,27 @@ func (a *ServerController) getDb(c *gin.Context) {
|
||||
jsonMsg(c, "get Database", err)
|
||||
return
|
||||
}
|
||||
|
||||
filename := "x-ui.db"
|
||||
|
||||
if !isValidFilename(filename) {
|
||||
c.AbortWithError(http.StatusBadRequest, fmt.Errorf("invalid filename"))
|
||||
return
|
||||
}
|
||||
|
||||
// Set the headers for the response
|
||||
c.Header("Content-Type", "application/octet-stream")
|
||||
c.Header("Content-Disposition", "attachment; filename=x-ui.db")
|
||||
c.Header("Content-Disposition", "attachment; filename="+filename)
|
||||
|
||||
// Write the file contents to the response
|
||||
c.Writer.Write(db)
|
||||
}
|
||||
|
||||
func isValidFilename(filename string) bool {
|
||||
// Validate that the filename only contains allowed characters
|
||||
return filenameRegex.MatchString(filename)
|
||||
}
|
||||
|
||||
func (a *ServerController) importDB(c *gin.Context) {
|
||||
// Get the file from the request body
|
||||
file, _, err := c.Request.FormFile("db")
|
||||
|
||||
@@ -49,7 +49,7 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) {
|
||||
func (a *SettingController) getAllSetting(c *gin.Context) {
|
||||
allSetting, err := a.settingService.GetAllSetting()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, allSetting, nil)
|
||||
@@ -58,7 +58,7 @@ func (a *SettingController) getAllSetting(c *gin.Context) {
|
||||
func (a *SettingController) getDefaultJsonConfig(c *gin.Context) {
|
||||
defaultJsonConfig, err := a.settingService.GetDefaultJsonConfig()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, defaultJsonConfig, nil)
|
||||
@@ -67,29 +67,74 @@ func (a *SettingController) getDefaultJsonConfig(c *gin.Context) {
|
||||
func (a *SettingController) getDefaultSettings(c *gin.Context) {
|
||||
expireDiff, err := a.settingService.GetExpireDiff()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
trafficDiff, err := a.settingService.GetTrafficDiff()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
defaultCert, err := a.settingService.GetCertFile()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
defaultKey, err := a.settingService.GetKeyFile()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
tgBotEnable, err := a.settingService.GetTgbotenabled()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
subEnable, err := a.settingService.GetSubEnable()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
subPort, err := a.settingService.GetSubPort()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
subPath, err := a.settingService.GetSubPath()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
subDomain, err := a.settingService.GetSubDomain()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
subKeyFile, err := a.settingService.GetSubKeyFile()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
subCertFile, err := a.settingService.GetSubCertFile()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
subTLS := false
|
||||
if subKeyFile != "" || subCertFile != "" {
|
||||
subTLS = true
|
||||
}
|
||||
result := map[string]interface{}{
|
||||
"expireDiff": expireDiff,
|
||||
"trafficDiff": trafficDiff,
|
||||
"defaultCert": defaultCert,
|
||||
"defaultKey": defaultKey,
|
||||
"tgBotEnable": tgBotEnable,
|
||||
"subEnable": subEnable,
|
||||
"subPort": subPort,
|
||||
"subPath": subPath,
|
||||
"subDomain": subDomain,
|
||||
"subTLS": subTLS,
|
||||
}
|
||||
jsonObj(c, result, nil)
|
||||
}
|
||||
@@ -98,27 +143,27 @@ func (a *SettingController) updateSetting(c *gin.Context) {
|
||||
allSetting := &entity.AllSetting{}
|
||||
err := c.ShouldBind(allSetting)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.settings.toasts.modifySettings"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
|
||||
return
|
||||
}
|
||||
err = a.settingService.UpdateAllSetting(allSetting)
|
||||
jsonMsg(c, I18n(c, "pages.settings.toasts.modifySettings"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
|
||||
}
|
||||
|
||||
func (a *SettingController) updateUser(c *gin.Context) {
|
||||
form := &updateUserForm{}
|
||||
err := c.ShouldBind(form)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.settings.toasts.modifySettings"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
|
||||
return
|
||||
}
|
||||
user := session.GetLoginUser(c)
|
||||
if user.Username != form.OldUsername || user.Password != form.OldPassword {
|
||||
jsonMsg(c, I18n(c, "pages.settings.toasts.modifyUser"), errors.New(I18n(c, "pages.settings.toasts.originalUserPassIncorrect")))
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), errors.New(I18nWeb(c, "pages.settings.toasts.originalUserPassIncorrect")))
|
||||
return
|
||||
}
|
||||
if form.NewUsername == "" || form.NewPassword == "" {
|
||||
jsonMsg(c, I18n(c, "pages.settings.toasts.modifyUser"), errors.New(I18n(c, "pages.settings.toasts.userPassMustBeNotEmpty")))
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), errors.New(I18nWeb(c, "pages.settings.toasts.userPassMustBeNotEmpty")))
|
||||
return
|
||||
}
|
||||
err = a.userService.UpdateUser(user.Id, form.NewUsername, form.NewPassword)
|
||||
@@ -127,19 +172,19 @@ func (a *SettingController) updateUser(c *gin.Context) {
|
||||
user.Password = form.NewPassword
|
||||
session.SetLoginUser(c, user)
|
||||
}
|
||||
jsonMsg(c, I18n(c, "pages.settings.toasts.modifyUser"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err)
|
||||
}
|
||||
|
||||
func (a *SettingController) restartPanel(c *gin.Context) {
|
||||
err := a.panelService.RestartPanel(time.Second * 3)
|
||||
jsonMsg(c, I18n(c, "pages.settings.restartPanel"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.restartPanel"), err)
|
||||
}
|
||||
|
||||
func (a *SettingController) updateSecret(c *gin.Context) {
|
||||
form := &updateSecretForm{}
|
||||
err := c.ShouldBind(form)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.settings.toasts.modifySettings"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
|
||||
}
|
||||
user := session.GetLoginUser(c)
|
||||
err = a.userService.UpdateUserSecret(user.Id, form.LoginSecret)
|
||||
@@ -147,7 +192,7 @@ func (a *SettingController) updateSecret(c *gin.Context) {
|
||||
user.LoginSecret = form.LoginSecret
|
||||
session.SetLoginUser(c, user)
|
||||
}
|
||||
jsonMsg(c, I18n(c, "pages.settings.toasts.modifyUser"), err)
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err)
|
||||
}
|
||||
|
||||
func (a *SettingController) getUserSecret(c *gin.Context) {
|
||||
|
||||
@@ -38,12 +38,12 @@ func jsonMsgObj(c *gin.Context, msg string, obj interface{}, err error) {
|
||||
if err == nil {
|
||||
m.Success = true
|
||||
if msg != "" {
|
||||
m.Msg = msg + I18n(c, "success")
|
||||
m.Msg = msg + I18nWeb(c, "success")
|
||||
}
|
||||
} else {
|
||||
m.Success = false
|
||||
m.Msg = msg + I18n(c, "fail") + ": " + err.Error()
|
||||
logger.Warning(msg+I18n(c, "fail")+": ", err)
|
||||
m.Msg = msg + I18nWeb(c, "fail") + ": " + err.Error()
|
||||
logger.Warning(msg+I18nWeb(c, "fail")+": ", err)
|
||||
}
|
||||
c.JSON(http.StatusOK, m)
|
||||
}
|
||||
|
||||
@@ -41,9 +41,18 @@ type AllSetting struct {
|
||||
TgRunTime string `json:"tgRunTime" form:"tgRunTime"`
|
||||
TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"`
|
||||
TgCpu int `json:"tgCpu" form:"tgCpu"`
|
||||
TgLang string `json:"tgLang" form:"tgLang"`
|
||||
XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"`
|
||||
TimeLocation string `json:"timeLocation" form:"timeLocation"`
|
||||
SecretEnable bool `json:"secretEnable" form:"secretEnable"`
|
||||
SubEnable bool `json:"subEnable" form:"subEnable"`
|
||||
SubListen string `json:"subListen" form:"subListen"`
|
||||
SubPort int `json:"subPort" form:"subPort"`
|
||||
SubPath string `json:"subPath" form:"subPath"`
|
||||
SubDomain string `json:"subDomain" form:"subDomain"`
|
||||
SubCertFile string `json:"subCertFile" form:"subCertFile"`
|
||||
SubKeyFile string `json:"subKeyFile" form:"subKeyFile"`
|
||||
SubUpdates int `json:"subUpdates" form:"subUpdates"`
|
||||
}
|
||||
|
||||
func (s *AllSetting) CheckValid() error {
|
||||
@@ -54,10 +63,25 @@ func (s *AllSetting) CheckValid() error {
|
||||
}
|
||||
}
|
||||
|
||||
if s.SubListen != "" {
|
||||
ip := net.ParseIP(s.SubListen)
|
||||
if ip == nil {
|
||||
return common.NewError("Sub listen is not valid ip:", s.SubListen)
|
||||
}
|
||||
}
|
||||
|
||||
if s.WebPort <= 0 || s.WebPort > 65535 {
|
||||
return common.NewError("web port is not a valid port:", s.WebPort)
|
||||
}
|
||||
|
||||
if s.SubPort <= 0 || s.SubPort > 65535 {
|
||||
return common.NewError("Sub port is not a valid port:", s.SubPort)
|
||||
}
|
||||
|
||||
if s.SubPort == s.WebPort {
|
||||
return common.NewError("Sub and Web could not use same port:", s.SubPort)
|
||||
}
|
||||
|
||||
if s.WebCertFile != "" || s.WebKeyFile != "" {
|
||||
_, err := tls.LoadX509KeyPair(s.WebCertFile, s.WebKeyFile)
|
||||
if err != nil {
|
||||
@@ -65,6 +89,13 @@ func (s *AllSetting) CheckValid() error {
|
||||
}
|
||||
}
|
||||
|
||||
if s.SubCertFile != "" || s.SubKeyFile != "" {
|
||||
_, err := tls.LoadX509KeyPair(s.SubCertFile, s.SubKeyFile)
|
||||
if err != nil {
|
||||
return common.NewErrorf("cert file <%v> or key file <%v> invalid: %v", s.SubCertFile, s.SubKeyFile, err)
|
||||
}
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(s.WebBasePath, "/") {
|
||||
s.WebBasePath = "/" + s.WebBasePath
|
||||
}
|
||||
|
||||
@@ -8,12 +8,17 @@ import (
|
||||
)
|
||||
|
||||
var webServer WebServer
|
||||
var subServer SubServer
|
||||
|
||||
type WebServer interface {
|
||||
GetCron() *cron.Cron
|
||||
GetCtx() context.Context
|
||||
}
|
||||
|
||||
type SubServer interface {
|
||||
GetCtx() context.Context
|
||||
}
|
||||
|
||||
func SetWebServer(s WebServer) {
|
||||
webServer = s
|
||||
}
|
||||
@@ -21,3 +26,11 @@ func SetWebServer(s WebServer) {
|
||||
func GetWebServer() WebServer {
|
||||
return webServer
|
||||
}
|
||||
|
||||
func SetSubServer(s SubServer) {
|
||||
subServer = s
|
||||
}
|
||||
|
||||
func GetSubServer() SubServer {
|
||||
return subServer
|
||||
}
|
||||
|
||||
82
web/global/hashStorage.go
Normal file
82
web/global/hashStorage.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package global
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"regexp"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type HashEntry struct {
|
||||
Hash string
|
||||
Value string
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
type HashStorage struct {
|
||||
sync.RWMutex
|
||||
Data map[string]HashEntry
|
||||
Expiration time.Duration
|
||||
|
||||
}
|
||||
|
||||
func NewHashStorage(expiration time.Duration) *HashStorage {
|
||||
return &HashStorage{
|
||||
Data: make(map[string]HashEntry),
|
||||
Expiration: expiration,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HashStorage) SaveHash(query string) string {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
||||
md5Hash := md5.Sum([]byte(query))
|
||||
md5HashString := hex.EncodeToString(md5Hash[:])
|
||||
|
||||
entry := HashEntry{
|
||||
Hash: md5HashString,
|
||||
Value: query,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
h.Data[md5HashString] = entry
|
||||
|
||||
return md5HashString
|
||||
}
|
||||
|
||||
|
||||
func (h *HashStorage) GetValue(hash string) (string, bool) {
|
||||
h.RLock()
|
||||
defer h.RUnlock()
|
||||
|
||||
entry, exists := h.Data[hash]
|
||||
|
||||
return entry.Value, exists
|
||||
}
|
||||
|
||||
func (h *HashStorage) IsMD5(hash string) bool {
|
||||
match, _ := regexp.MatchString("^[a-f0-9]{32}$", hash)
|
||||
return match
|
||||
}
|
||||
|
||||
func (h *HashStorage) RemoveExpiredHashes() {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
|
||||
for hash, entry := range h.Data {
|
||||
if now.Sub(entry.Timestamp) > h.Expiration {
|
||||
delete(h.Data, hash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HashStorage) Reset() {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
||||
h.Data = make(map[string]HashEntry)
|
||||
}
|
||||
@@ -7,47 +7,54 @@
|
||||
<a-tag color="green" style="margin-bottom: 10px;display: block;text-align: center;">
|
||||
{{ i18n "pages.inbounds.clickOnQRcode" }}
|
||||
</a-tag>
|
||||
<a-tag v-if="qrModal.clientName" color="orange" style="margin-bottom: 10px;display: block;text-align: center;">
|
||||
{{ i18n "pages.inbounds.email" }}: "[[ qrModal.clientName ]]"
|
||||
</a-tag>
|
||||
<canvas @click="copyToClipboard()" id="qrCode" style="width: 100%; height: 100%; margin-top: 10px;"></canvas>
|
||||
<template v-if="app.subSettings.enable && qrModal.subId">
|
||||
<a-divider>Subscription</a-divider>
|
||||
<canvas @click="copyToClipboard('qrCode-sub',genSubLink(qrModal.client.subId))" id="qrCode-sub" style="width: 100%; height: 100%;"></canvas>
|
||||
</template>
|
||||
<a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
|
||||
<template v-for="(row, index) in qrModal.qrcodes">
|
||||
<a-tag color="orange" style="margin-top: 10px;display: block;text-align: center;">[[ row.remark ]]</a-tag>
|
||||
<canvas @click="copyToClipboard('qrCode-'+index, row.link)" :id="'qrCode-'+index" style="width: 100%; height: 100%;"></canvas>
|
||||
</template>
|
||||
</a-modal>
|
||||
|
||||
<script>
|
||||
|
||||
const qrModal = {
|
||||
title: '',
|
||||
content: '',
|
||||
clientIndex: 0,
|
||||
inbound: new Inbound(),
|
||||
dbInbound: new DBInbound(),
|
||||
copyText: '',
|
||||
clientName: null,
|
||||
qrcode: null,
|
||||
client: null,
|
||||
qrcodes: [],
|
||||
clipboard: null,
|
||||
visible: false,
|
||||
show: function (title = '', content = '', dbInbound = new DBInbound(), copyText = '', clientName = null) {
|
||||
subId: '',
|
||||
show: function (title = '', dbInbound = new DBInbound(), clientIndex = 0) {
|
||||
this.title = title;
|
||||
this.content = content;
|
||||
this.clientIndex = clientIndex;
|
||||
this.dbInbound = dbInbound;
|
||||
this.inbound = dbInbound.toInbound();
|
||||
this.clientName = clientName;
|
||||
if (ObjectUtil.isEmpty(copyText)) {
|
||||
this.copyText = content;
|
||||
settings = JSON.parse(this.inbound.settings);
|
||||
this.client = settings.clients[clientIndex];
|
||||
remark = this.dbInbound.remark + "-" + this.client.email;
|
||||
address = this.dbInbound.address;
|
||||
this.subId = '';
|
||||
this.qrcodes = [];
|
||||
if (this.inbound.tls && !ObjectUtil.isArrEmpty(this.inbound.stream.tls.settings.domains)) {
|
||||
this.inbound.stream.tls.settings.domains.forEach((domain) => {
|
||||
this.qrcodes.push({
|
||||
remark: remark + "-" + domain.remark,
|
||||
link: this.inbound.genLink(domain.domain, remark + "-" + domain.remark, clientIndex)
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.copyText = copyText;
|
||||
this.qrcodes.push({
|
||||
remark: remark,
|
||||
link: this.inbound.genLink(address, remark, clientIndex)
|
||||
});
|
||||
}
|
||||
this.visible = true;
|
||||
qrModalApp.$nextTick(() => {
|
||||
if (this.qrcode === null) {
|
||||
this.qrcode = new QRious({
|
||||
element: document.querySelector('#qrCode'),
|
||||
size: 260,
|
||||
value: content,
|
||||
});
|
||||
} else {
|
||||
this.qrcode.value = content;
|
||||
}
|
||||
});
|
||||
},
|
||||
close: function () {
|
||||
this.visible = false;
|
||||
@@ -61,16 +68,40 @@
|
||||
qrModal: qrModal,
|
||||
},
|
||||
methods: {
|
||||
copyToClipboard() {
|
||||
this.qrModal.clipboard = new ClipboardJS('#qrCode', {
|
||||
text: () => this.qrModal.copyText,
|
||||
copyToClipboard(elmentId,content) {
|
||||
this.qrModal.clipboard = new ClipboardJS('#'+elmentId, {
|
||||
text: () => content,
|
||||
});
|
||||
this.qrModal.clipboard.on('success', () => {
|
||||
app.$message.success('{{ i18n "copied" }}')
|
||||
this.qrModal.clipboard.destroy();
|
||||
});
|
||||
},
|
||||
setQrCode(elmentId,content) {
|
||||
new QRious({
|
||||
element: document.querySelector('#'+elmentId),
|
||||
size: 260,
|
||||
value: content,
|
||||
});
|
||||
},
|
||||
genSubLink(subID) {
|
||||
protocol = app.subSettings.tls ? "https://" : "http://";
|
||||
hostName = app.subSettings.domain === "" ? window.location.hostname : app.subSettings.domain;
|
||||
subPort = app.subSettings.port;
|
||||
port = (subPort === 443 && app.subSettings.tls) || (subPort === 80 && !app.subSettings.tls) ? "" : ":" + String(subPort);
|
||||
subPath = app.subSettings.path;
|
||||
return protocol + hostName + port + subPath + subID;
|
||||
}
|
||||
},
|
||||
updated() {
|
||||
if (qrModal.client.subId){
|
||||
qrModal.subId = qrModal.client.subId;
|
||||
this.setQrCode("qrCode-sub",this.genSubLink(qrModal.subId));
|
||||
}
|
||||
qrModal.qrcodes.forEach((element,index) => {
|
||||
this.setQrCode("qrCode-"+index, element.link);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<span slot="label">{{ i18n "pages.client.clientCount" }}</span>
|
||||
<a-input-number v-model="clientsBulkModal.quantity" :min="1" :max="100"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-form-item v-if="app.subSettings.enable">
|
||||
<span slot="label">
|
||||
Subscription
|
||||
<a-tooltip>
|
||||
@@ -45,7 +45,7 @@
|
||||
</span>
|
||||
<a-input v-model.trim="clientsBulkModal.subId"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-form-item v-if="app.tgBotEnable">
|
||||
<span slot="label">
|
||||
Telegram ID
|
||||
<a-tooltip>
|
||||
@@ -158,8 +158,8 @@
|
||||
newClient = clientsBulkModal.newClient(clientsBulkModal.dbInbound.protocol);
|
||||
if (method == 4) newClient.email = "";
|
||||
newClient.email += useNum ? prefix + i.toString() + postfix : prefix + postfix;
|
||||
newClient.subId = clientsBulkModal.subId;
|
||||
newClient.tgId = clientsBulkModal.tgId;
|
||||
if (clientsBulkModal.subId.length > 0) newClient.subId = clientsBulkModal.subId;
|
||||
if (clientsBulkModal.tgId.length > 0) newClient.tgId = clientsBulkModal.tgId;
|
||||
newClient.limitIp = clientsBulkModal.limitIp;
|
||||
newClient._totalGB = clientsBulkModal.totalGB;
|
||||
newClient._expiryTime = clientsBulkModal.expiryTime;
|
||||
@@ -204,6 +204,7 @@
|
||||
case Protocols.VMESS: return clientSettings.vmesses;
|
||||
case Protocols.VLESS: return clientSettings.vlesses;
|
||||
case Protocols.TROJAN: return clientSettings.trojans;
|
||||
case Protocols.SHADOWSOCKS: return clientSettings.shadowsockses;
|
||||
default: return null;
|
||||
}
|
||||
},
|
||||
@@ -212,6 +213,7 @@
|
||||
case Protocols.VMESS: return new Inbound.VmessSettings.Vmess();
|
||||
case Protocols.VLESS: return new Inbound.VLESSSettings.VLESS();
|
||||
case Protocols.TROJAN: return new Inbound.TrojanSettings.Trojan();
|
||||
case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings.Shadowsocks();
|
||||
default: return null;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
oldClientId: "",
|
||||
index: null,
|
||||
clientIps: null,
|
||||
isExpired: false,
|
||||
delayedStart: false,
|
||||
ok() {
|
||||
if (clientModal.isEdit) {
|
||||
@@ -38,7 +37,6 @@
|
||||
this.inbound = dbInbound.toInbound();
|
||||
this.clients = this.getClients(this.inbound.protocol, this.inbound.settings);
|
||||
this.index = index === null ? this.clients.length : index;
|
||||
this.isExpired = isEdit ? this.inbound.isExpiry(this.index) : false;
|
||||
this.delayedStart = false;
|
||||
if (isEdit) {
|
||||
if (this.clients[index].expiryTime < 0) {
|
||||
@@ -109,13 +107,10 @@
|
||||
return true
|
||||
},
|
||||
get isExpiry() {
|
||||
return this.clientModal.isExpired
|
||||
return this.clientModal.isEdit && this.client.expiryTime >0 ? (this.client.expiryTime < new Date().getTime()) : false;
|
||||
},
|
||||
get statsColor() {
|
||||
if (!clientStats) return 'blue'
|
||||
if (clientStats.total <= 0) return 'blue'
|
||||
else if (clientStats.total > 0 && (clientStats.down + clientStats.up) < clientStats.total) return 'cyan'
|
||||
else return 'red'
|
||||
return usageColor(clientStats.up + clientStats.down, app.trafficDiff, this.client.totalGB);
|
||||
},
|
||||
get delayedExpireDays() {
|
||||
return this.client && this.client.expiryTime < 0 ? this.client.expiryTime / -86400000 : 0;
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Password" v-if="inbound.protocol === Protocols.TROJAN || inbound.protocol === Protocols.SHADOWSOCKS">
|
||||
<a-icon v-if="inbound.protocol === Protocols.SHADOWSOCKS"
|
||||
@click="client.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
|
||||
<a-icon v-if="inbound.protocol === Protocols.SHADOWSOCKS" @click="client.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
|
||||
<a-icon v-if="inbound.protocol === Protocols.TROJAN" @click="client.password = RandomUtil.randomSeq(10)" type="sync"> </a-icon>
|
||||
<a-input v-model.trim="client.password" style="width: 300px;"></a-input>
|
||||
</a-form-item>
|
||||
<br>
|
||||
@@ -34,7 +34,7 @@
|
||||
<a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"> </a-icon>
|
||||
<a-input v-model.trim="client.id" style="width: 300px;"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="client.email">
|
||||
<a-form-item v-if="client.email && app.subSettings.enable">
|
||||
<span slot="label">
|
||||
Subscription
|
||||
<a-tooltip>
|
||||
@@ -47,7 +47,7 @@
|
||||
<a-icon @click="client.subId = RandomUtil.randomText()" type="sync"> </a-icon>
|
||||
<a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="client.email">
|
||||
<a-form-item v-if="client.email && app.tgBotEnable" >
|
||||
<span slot="label">
|
||||
Telegram ID
|
||||
<a-tooltip>
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
</span>
|
||||
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
|
||||
:dropdown-class-name="themeSwitcher.darkCardClass"
|
||||
v-model="dbInbound._expiryTime" style="width: 300px;"></a-date-picker>
|
||||
v-model="dbInbound._expiryTime" style="width: 250px;"></a-date-picker>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<a-icon @click="client.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
|
||||
<a-input v-model.trim="client.password" style="width: 250px;"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="client.email">
|
||||
<a-form-item v-if="client.email && app.subSettings.enable">
|
||||
<span slot="label">
|
||||
Subscription
|
||||
<a-tooltip>
|
||||
@@ -31,7 +31,7 @@
|
||||
<a-icon @click="client.subId = RandomUtil.randomText()" type="sync"> </a-icon>
|
||||
<a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="client.email">
|
||||
<a-form-item v-if="client.email && app.tgBotEnable">
|
||||
<span slot="label">
|
||||
Telegram ID
|
||||
<a-tooltip>
|
||||
|
||||
@@ -15,9 +15,10 @@
|
||||
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Password">
|
||||
<a-icon @click="client.password = RandomUtil.randomSeq(10)" type="sync"> </a-icon>
|
||||
<a-input v-model.trim="client.password" style="width: 150px;"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="client.email">
|
||||
<a-form-item v-if="client.email && app.subSettings.enable">
|
||||
<span slot="label">
|
||||
Subscription
|
||||
<a-tooltip>
|
||||
@@ -30,7 +31,7 @@
|
||||
<a-icon @click="client.subId = RandomUtil.randomText()" type="sync"> </a-icon>
|
||||
<a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="client.email">
|
||||
<a-form-item v-if="client.email && app.tgBotEnable">
|
||||
<span slot="label">
|
||||
Telegram ID
|
||||
<a-tooltip>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"> </a-icon>
|
||||
<a-input v-model.trim="client.id" style="width: 300px;"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="client.email">
|
||||
<a-form-item v-if="client.email && app.subSettings.enable">
|
||||
<span slot="label">
|
||||
Subscription
|
||||
<a-tooltip>
|
||||
@@ -31,7 +31,7 @@
|
||||
<a-icon @click="client.subId = RandomUtil.randomText()" type="sync"> </a-icon>
|
||||
<a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="client.email">
|
||||
<a-form-item v-if="client.email && app.tgBotEnable">
|
||||
<span slot="label">
|
||||
Telegram ID
|
||||
<a-tooltip>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"> </a-icon>
|
||||
<a-input v-model.trim="client.id" style="width: 300px;"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="client.email">
|
||||
<a-form-item v-if="client.email && app.subSettings.enable">
|
||||
<span slot="label">
|
||||
Subscription
|
||||
<a-tooltip>
|
||||
@@ -36,7 +36,7 @@
|
||||
<a-icon @click="client.subId = RandomUtil.randomText()" type="sync"> </a-icon>
|
||||
<a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="client.email">
|
||||
<a-form-item v-if="client.email && app.tgBotEnable">
|
||||
<span slot="label">
|
||||
Telegram ID
|
||||
<a-tooltip>
|
||||
|
||||
@@ -25,11 +25,11 @@
|
||||
<a-input v-model.trim="inbound.stream.tcp.request.path[index]"></a-input>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.general.requestHeader" }}'>
|
||||
<br>
|
||||
<a-form-item>
|
||||
<a-row>
|
||||
<a-button size="small" @click="inbound.stream.tcp.request.addHeader('Host', 'xxx.com')">
|
||||
+
|
||||
</a-button>
|
||||
<span>{{ i18n "pages.inbounds.stream.general.requestHeader" }}:</span>
|
||||
<a-button type="primary" size="small" style="margin-left: 10px" @click="inbound.stream.tcp.request.addHeader('Host', 'xxx.com')">+</a-button>
|
||||
</a-row>
|
||||
<a-input-group v-for="(header, index) in inbound.stream.tcp.request.headers">
|
||||
<a-input style="width: 50%" v-model.trim="header.name"
|
||||
@@ -37,13 +37,12 @@
|
||||
<a-input style="width: 50%" v-model.trim="header.value"
|
||||
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
|
||||
<template slot="addonAfter">
|
||||
<a-button size="small" @click="inbound.stream.tcp.request.removeHeader(index)">
|
||||
-
|
||||
</a-button>
|
||||
<a-button type="primary" size="small" style="margin-left: 10px" @click="inbound.stream.tcp.request.removeHeader(index)">-</a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
|
||||
</a-form>
|
||||
|
||||
<!-- tcp response -->
|
||||
@@ -57,11 +56,10 @@
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseStatusDescription" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tcp.response.reason"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseHeader" }}'>
|
||||
<a-form-item>
|
||||
<a-row>
|
||||
<a-button size="small" @click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')">
|
||||
+
|
||||
</a-button>
|
||||
<span>{{ i18n "pages.inbounds.stream.tcp.responseHeader" }}:</span>
|
||||
<a-button type="primary" size="small" style="margin-left: 10px" @click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')">+</a-button>
|
||||
</a-row>
|
||||
<a-input-group v-for="(header, index) in inbound.stream.tcp.response.headers">
|
||||
<a-input style="width: 50%" v-model.trim="header.name"
|
||||
@@ -69,9 +67,7 @@
|
||||
<a-input style="width: 50%" v-model.trim="header.value"
|
||||
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
|
||||
<template slot="addonAfter">
|
||||
<a-button size="small" @click="inbound.stream.tcp.response.removeHeader(index)">
|
||||
-
|
||||
</a-button>
|
||||
<a-button type="primary" size="small" style="margin-left: 10px" @click="inbound.stream.tcp.response.removeHeader(index)">-</a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
<a-input v-model.trim="inbound.stream.ws.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.general.requestHeader" }}'>
|
||||
<br>
|
||||
<a-form-item>
|
||||
<a-row>
|
||||
<a-button size="small" @click="inbound.stream.ws.addHeader('Host', '')">
|
||||
+
|
||||
</a-button>
|
||||
<span>{{ i18n "pages.inbounds.stream.general.requestHeader" }}:</span>
|
||||
<a-button type="primary" size="small" style="margin-left: 10px" @click="inbound.stream.ws.addHeader('Host', '')">+</a-button>
|
||||
</a-row>
|
||||
<a-input-group v-for="(header, index) in inbound.stream.ws.headers">
|
||||
<a-input style="width: 50%" v-model.trim="header.name"
|
||||
@@ -20,9 +20,7 @@
|
||||
<a-input style="width: 50%" v-model.trim="header.value"
|
||||
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
|
||||
<template slot="addonAfter">
|
||||
<a-button size="small" @click="inbound.stream.ws.removeHeader(index)">
|
||||
-
|
||||
</a-button>
|
||||
<a-button type="primary" size="small" style="margin-left: 10px" @click="inbound.stream.ws.removeHeader(index)">-</a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
Reality
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.realityDesc" }}</span>
|
||||
<span>{{ i18n "pages.inbounds.realityDesc" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
@@ -22,7 +22,7 @@
|
||||
XTLS
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.xtlsDesc" }}</span>
|
||||
<span>{{ i18n "pages.inbounds.xtlsDesc" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
@@ -33,7 +33,25 @@
|
||||
|
||||
<!-- tls settings -->
|
||||
<a-form v-if="inbound.tls" layout="inline">
|
||||
<a-form-item label='{{ i18n "domainName" }}'>
|
||||
<a-form-item label='Multi Domain'>
|
||||
<a-switch v-model="multiDomain"></a-switch>
|
||||
|
||||
</a-form-item>
|
||||
<a-form-item v-if="multiDomain">
|
||||
<a-row>
|
||||
<span>Domains:</span>
|
||||
<a-button v-if="multiDomain" type="primary" size="small" @click="inbound.stream.tls.settings.domains.push({remark: '', domain: ''})" style="margin-left: 10px">+</a-button>
|
||||
</a-row>
|
||||
<a-input-group v-for="(row, index) in inbound.stream.tls.settings.domains">
|
||||
<a-input style="width: 40%" v-model.trim="row.remark" addon-before='{{ i18n "remark" }}'></a-input>
|
||||
<a-input style="width: 60%" v-model.trim="row.domain" addon-before='{{ i18n "host" }}'>
|
||||
<template slot="addonAfter">
|
||||
<a-button type="primary" size="small" style="margin-left: 10px" @click="inbound.stream.tls.settings.domains.splice(index, 1)">-</a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
<a-form-item v-else label='{{ i18n "domainName" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tls.server" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="CipherSuites">
|
||||
@@ -70,33 +88,37 @@
|
||||
<a-form-item label="Allow insecure">
|
||||
<a-switch v-model="inbound.stream.tls.settings.allowInsecure"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "certificate" }}'>
|
||||
<a-radio-group v-model="inbound.stream.tls.certs[0].useFile" button-style="solid">
|
||||
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
|
||||
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<template v-if="inbound.stream.tls.certs[0].useFile">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.publicKeyPath" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tls.certs[0].certFile" style="width:300px;"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tls.certs[0].keyFile" style="width:300px;"></a-input>
|
||||
</a-form-item>
|
||||
<a-button type="primary" icon="import" @click="setDefaultCertData">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
|
||||
<a-input type="textarea" :rows="3" style="width:300px;" v-model="inbound.stream.tls.certs[0].cert"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.keyContent" }}'>
|
||||
<a-input type="textarea" :rows="3" style="width:300px;" v-model="inbound.stream.tls.certs[0].key"></a-input>
|
||||
<template v-for="cert,index in inbound.stream.tls.certs">
|
||||
<a-form-item label='{{ i18n "certificate" }}'>
|
||||
<a-radio-group v-model="cert.useFile" button-style="solid">
|
||||
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
|
||||
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
|
||||
</a-radio-group>
|
||||
<a-button v-if="index === 0" type="primary" size="small" @click="inbound.stream.tls.addCert()" style="margin-left: 10px">+</a-button>
|
||||
<a-button v-if="inbound.stream.tls.certs.length>1" type="primary" size="small" @click="inbound.stream.tls.removeCert(index)" style="margin-left: 10px">-</a-button>
|
||||
</a-form-item>
|
||||
<template v-if="cert.useFile">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.publicKeyPath" }}'>
|
||||
<a-input v-model.trim="cert.certFile" style="width:300px;"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
|
||||
<a-input v-model.trim="cert.keyFile" style="width:300px;"></a-input>
|
||||
</a-form-item>
|
||||
<a-button type="primary" icon="import" @click="setDefaultCertData(index)">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
|
||||
<a-input type="textarea" :rows="3" style="width:300px;" v-model="cert.cert"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.keyContent" }}'>
|
||||
<a-input type="textarea" :rows="3" style="width:300px;" v-model="cert.key"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
</a-form>
|
||||
|
||||
<!-- xtls settings -->
|
||||
<a-form v-if="inbound.xtls" layout="inline">
|
||||
<a-form v-else-if="inbound.xtls" layout="inline">
|
||||
<a-form-item label='{{ i18n "domainName" }}'>
|
||||
<a-input v-model.trim="inbound.stream.xtls.server"></a-input>
|
||||
</a-form-item>
|
||||
@@ -111,28 +133,32 @@
|
||||
<a-form-item label="Allow insecure">
|
||||
<a-switch v-model="inbound.stream.xtls.settings.allowInsecure"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "certificate" }}'>
|
||||
<a-radio-group v-model="inbound.stream.xtls.certs[0].useFile" button-style="solid">
|
||||
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
|
||||
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<template v-if="inbound.stream.xtls.certs[0].useFile">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.publicKeyPath" }}'>
|
||||
<a-input v-model.trim="inbound.stream.xtls.certs[0].certFile" style="width:300px;"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
|
||||
<a-input v-model.trim="inbound.stream.xtls.certs[0].keyFile" style="width:300px;"></a-input>
|
||||
</a-form-item>
|
||||
<a-button type="primary" icon="import" @click="setDefaultCertXtls">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
|
||||
<a-input type="textarea" :rows="3" style="width:300px;" v-model="inbound.stream.xtls.certs[0].cert"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.keyContent" }}'>
|
||||
<a-input type="textarea" :rows="3" style="width:300px;" v-model="inbound.stream.xtls.certs[0].key"></a-input>
|
||||
<template v-for="cert,index in inbound.stream.xtls.certs">
|
||||
<a-form-item label='{{ i18n "certificate" }}'>
|
||||
<a-radio-group v-model="cert.useFile" button-style="solid">
|
||||
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
|
||||
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
|
||||
</a-radio-group>
|
||||
<a-button v-if="index === 0" type="primary" size="small" @click="inbound.stream.xtls.addCert()" style="margin-left: 10px">+</a-button>
|
||||
<a-button v-if="inbound.stream.xtls.certs.length>1" type="primary" size="small" @click="inbound.stream.xtls.removeCert(index)" style="margin-left: 10px">-</a-button>
|
||||
</a-form-item>
|
||||
<template v-if="cert.useFile">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.publicKeyPath" }}'>
|
||||
<a-input v-model.trim="cert.certFile" style="width:300px;"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
|
||||
<a-input v-model.trim="cert.keyFile" style="width:300px;"></a-input>
|
||||
</a-form-item>
|
||||
<a-button type="primary" icon="import" @click="setDefaultCertXtls(index)">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
|
||||
<a-input type="textarea" :rows="3" style="width:300px;" v-model="cert.cert"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.keyContent" }}'>
|
||||
<a-input type="textarea" :rows="3" style="width:300px;" v-model="cert.key"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
</a-form>
|
||||
|
||||
@@ -154,14 +180,19 @@
|
||||
<a-form-item label='{{ i18n "domainName" }}'>
|
||||
<a-input v-model.trim="inbound.stream.reality.settings.serverName" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="dest">
|
||||
<a-form-item label="Dest">
|
||||
<a-input v-model.trim="inbound.stream.reality.dest" style="width: 300px"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Server Names">
|
||||
<a-input v-model.trim="inbound.stream.reality.serverNames" style="width: 300px"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="ShortIds">
|
||||
<a-input v-model.trim="inbound.stream.reality.shortIds"></a-input>
|
||||
<a-icon @click="inbound.stream.reality.shortIds = RandomUtil.randomShortId()" type="sync"> </a-icon>
|
||||
<a-input v-model.trim="inbound.stream.reality.shortIds" style="width: 150px;"></a-input>
|
||||
</a-form-item>
|
||||
<br>
|
||||
<a-form-item label="SpiderX">
|
||||
<a-input v-model.trim="inbound.stream.reality.settings.spiderX" style="width: 150px;"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Private Key">
|
||||
<a-input v-model.trim="inbound.stream.reality.privateKey" style="width: 300px"></a-input>
|
||||
|
||||
@@ -29,18 +29,15 @@
|
||||
<a-tag v-if="!isClientEnabled(record, client.email)" color="red">{{ i18n "depleted" }}</a-tag>
|
||||
</template>
|
||||
<template slot="traffic" slot-scope="text, client">
|
||||
<a-tag color="blue">
|
||||
[[ sizeFormat(getUpStats(record, client.email)) ]] / [[ sizeFormat(getDownStats(record, client.email)) ]]
|
||||
</a-tag>
|
||||
<a-tag :color="statsColor(record, client.email)">[[ sizeFormat(getUpStats(record, client.email)) ]] / [[ sizeFormat(getDownStats(record, client.email)) ]]</a-tag>
|
||||
<template v-if="client._totalGB > 0">
|
||||
<a-tag v-if="isTrafficExhausted(record, client.email)" color="red">[[client._totalGB]] GB</a-tag>
|
||||
<a-tag v-else color="cyan">[[client._totalGB]] GB</a-tag>
|
||||
<a-tag :color="statsColor(record, client.email)">[[client._totalGB]]GB</a-tag>
|
||||
</template>
|
||||
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
|
||||
</template>
|
||||
<template slot="expiryTime" slot-scope="text, client, index">
|
||||
<template v-if="client.expiryTime > 0">
|
||||
<a-tag :color="isExpiry(record, index)? 'red' : 'blue'">
|
||||
<a-tag :color="usageColor(new Date().getTime(), app.expireDiff, client.expiryTime)">
|
||||
[[ DateUtil.formatMillis(client._expiryTime) ]]
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
@@ -59,10 +59,9 @@
|
||||
</td>
|
||||
<td v-else-if="inbound.reality">
|
||||
reality: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
|
||||
reality {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
|
||||
reality Destination: <a-tag :color="inbound.stream.reality.dest ? 'green' : 'orange'">[[ inbound.stream.reality.dest ]]</a-tag>
|
||||
</td>
|
||||
<td v-else>
|
||||
tls: <a-tag color="red">{{ i18n "disabled" }}</a-tag>
|
||||
<td v-else>tls: <a-tag color="red">{{ i18n "disabled" }}</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -89,7 +88,7 @@
|
||||
<th>{{ i18n "pages.inbounds.expireDate" }}</th>
|
||||
<tr>
|
||||
<td>
|
||||
<a-tag v-if="infoModal.clientStats" :color="statsColor(infoModal.clientStats)">
|
||||
<a-tag v-if="infoModal.clientStats" color="green">
|
||||
[[ sizeFormat(infoModal.clientStats['up']) ]] /
|
||||
[[ sizeFormat(infoModal.clientStats['down']) ]]
|
||||
([[ sizeFormat(infoModal.clientStats['up'] + infoModal.clientStats['down']) ]])
|
||||
@@ -101,7 +100,7 @@
|
||||
</td>
|
||||
<td>
|
||||
<template v-if="infoModal.clientSettings.expiryTime > 0">
|
||||
<a-tag :color="infoModal.isExpired ? 'red' : 'blue'">
|
||||
<a-tag :color="usageColor(new Date().getTime(), app.expireDiff, infoModal.clientSettings.expiryTime)">
|
||||
[[ DateUtil.formatMillis(infoModal.clientSettings.expiryTime) ]]
|
||||
</a-tag>
|
||||
</template>
|
||||
@@ -110,17 +109,16 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table v-if="infoModal.clientSettings.subId + infoModal.clientSettings.tgId" style="margin-bottom: 10px;">
|
||||
<tr v-if="infoModal.clientSettings.subId">
|
||||
<td>Subscription link</td>
|
||||
<td><a :href="[[ subBase + infoModal.clientSettings.subId ]]" target="_blank">[[ subBase + infoModal.clientSettings.subId ]]</a></td>
|
||||
<td><a-icon id="copy-sub-link" type="snippets" @click="copyToClipboard('copy-sub-link', subBase + infoModal.clientSettings.subId)"></a-icon></td>
|
||||
</tr>
|
||||
<tr v-if="infoModal.clientSettings.tgId">
|
||||
<td>Telegram ID</td>
|
||||
<td><a :href="[[ tgBase + infoModal.clientSettings.tgId ]]" target="_blank">@[[ infoModal.clientSettings.tgId ]]</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
<template v-if="app.subSettings.enable && infoModal.clientSettings.subId">
|
||||
<a-divider>Subscription link</a-divider>
|
||||
<a :href="[[ infoModal.subLink ]]" target="_blank">[[ infoModal.subLink ]]</a>
|
||||
<a-icon id="copy-sub-link" type="snippets" @click="copyToClipboard('copy-sub-link', infoModal.subLink)"></a-icon>
|
||||
</template>
|
||||
<template v-if="app.tgBotEnable && infoModal.clientSettings.tgId">
|
||||
<a-divider>Telegram Username</a-divider>
|
||||
<a :href="[[ infoModal.tgLink ]]" target="_blank">@[[ infoModal.clientSettings.tgId ]]</a>
|
||||
<a-icon id="copy-tg-link" type="snippets" @click="copyToClipboard('copy-tg-link', '@' + infoModal.clientSettings.tgId)"></a-icon>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-divider></a-divider>
|
||||
@@ -190,8 +188,14 @@
|
||||
</template>
|
||||
<div v-if="dbInbound.hasLink()">
|
||||
<a-divider>URL</a-divider>
|
||||
<p>[[ infoModal.link ]]</p>
|
||||
<button class="ant-btn ant-btn-primary" id="copy-url-link" @click="copyToClipboard('copy-url-link', infoModal.link)"><a-icon type="snippets"></a-icon>{{ i18n "copy" }}</button>
|
||||
<a-row v-for="(link,index) in infoModal.links">
|
||||
<a-col :span="21"><a-tag color="cyan">[[ link.remark ]]</a-tag><br />[[ link.link ]]</a-col>
|
||||
<a-col :span="3" style="text-align: right;">
|
||||
<button class="ant-btn ant-btn-primary" :id="'copy-url-link-'+index" @click="copyToClipboard('copy-url-link-'+index, link.link)">
|
||||
<a-icon type="snippets"></a-icon>{{ i18n "copy" }}
|
||||
</button>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</a-modal>
|
||||
<script>
|
||||
@@ -206,23 +210,56 @@
|
||||
upStats: 0,
|
||||
downStats: 0,
|
||||
clipboard: null,
|
||||
link: null,
|
||||
links: [],
|
||||
index: null,
|
||||
isExpired: false,
|
||||
subLink: '',
|
||||
tgLink: '',
|
||||
show(dbInbound, index) {
|
||||
this.index = index;
|
||||
this.inbound = dbInbound.toInbound();
|
||||
this.dbInbound = new DBInbound(dbInbound);
|
||||
this.link = dbInbound.genLink(index);
|
||||
this.settings = JSON.parse(this.inbound.settings);
|
||||
this.clientSettings = this.settings.clients ? Object.values(this.settings.clients)[index] : null;
|
||||
this.isExpired = this.inbound.isExpiry(index);
|
||||
this.clientStats = this.settings.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : [];
|
||||
remark = this.dbInbound.remark + "-" + this.clientSettings.email;
|
||||
address = this.dbInbound.address;
|
||||
this.links = [];
|
||||
if (this.inbound.tls && !ObjectUtil.isArrEmpty(this.inbound.stream.tls.settings.domains)) {
|
||||
this.inbound.stream.tls.settings.domains.forEach((domain) => {
|
||||
this.links.push({
|
||||
remark: remark + "-" + domain.remark,
|
||||
link: this.inbound.genLink(domain.domain, remark + "-" + domain.remark, index)
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.links.push({
|
||||
remark: remark,
|
||||
link: this.inbound.genLink(address, remark, index)
|
||||
});
|
||||
}
|
||||
if (this.clientSettings) {
|
||||
if (this.clientSettings.subId) {
|
||||
this.subLink = this.genSubLink(this.clientSettings.subId);
|
||||
}
|
||||
if (this.clientSettings.tgId) {
|
||||
this.tgLink = "https://t.me/" + this.clientSettings.tgId;
|
||||
}
|
||||
}
|
||||
this.visible = true;
|
||||
},
|
||||
close() {
|
||||
infoModal.visible = false;
|
||||
},
|
||||
genSubLink(subID) {
|
||||
protocol = app.subSettings.tls ? "https://" : "http://";
|
||||
hostName = app.subSettings.domain === "" ? window.location.hostname : app.subSettings.domain;
|
||||
subPort = app.subSettings.port;
|
||||
port = (subPort === 443 && app.subSettings.tls) || (subPort === 80 && !app.subSettings.tls) ? "" : ":" + String(subPort);
|
||||
subPath = app.subSettings.path;
|
||||
return protocol + hostName + port + subPath + subID;
|
||||
}
|
||||
};
|
||||
|
||||
const infoModalApp = new Vue({
|
||||
@@ -248,12 +285,6 @@
|
||||
}
|
||||
return infoModal.dbInbound.isEnable;
|
||||
},
|
||||
get subBase() {
|
||||
return window.location.protocol + "//" + window.location.hostname + (window.location.port ? ":" + window.location.port : "") + basePath + "sub/";
|
||||
},
|
||||
get tgBase() {
|
||||
return "https://t.me/"
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
copyToClipboard(elmentId, content) {
|
||||
@@ -266,10 +297,7 @@
|
||||
});
|
||||
},
|
||||
statsColor(stats) {
|
||||
if (!stats) return 'blue'
|
||||
if (stats['total'] === 0) return 'blue'
|
||||
else if (stats['total'] > 0 && (stats['down'] + stats['up']) < stats['total']) return 'cyan'
|
||||
else return 'red'
|
||||
return usageColor(stats.up + stats.down, app.trafficDiff, stats.total);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -90,6 +90,18 @@
|
||||
set delayedExpireDays(days) {
|
||||
this.client.expiryTime = -86400000 * days;
|
||||
},
|
||||
get multiDomain() {
|
||||
return this.inbound.stream.tls.settings.domains.length > 0;
|
||||
},
|
||||
set multiDomain(value) {
|
||||
if (value) {
|
||||
inModal.inbound.stream.tls.server = "";
|
||||
inModal.inbound.stream.tls.settings.domains = [{remark: "", domain: window.location.host.split(":")[0]}];
|
||||
} else {
|
||||
inModal.inbound.stream.tls.server = "";
|
||||
inModal.inbound.stream.tls.settings.domains = [];
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
streamNetworkChange() {
|
||||
@@ -100,13 +112,13 @@
|
||||
this.inModal.inbound.reality = false;
|
||||
}
|
||||
},
|
||||
setDefaultCertData() {
|
||||
inModal.inbound.stream.tls.certs[0].certFile = app.defaultCert;
|
||||
inModal.inbound.stream.tls.certs[0].keyFile = app.defaultKey;
|
||||
setDefaultCertData(index) {
|
||||
inModal.inbound.stream.tls.certs[index].certFile = app.defaultCert;
|
||||
inModal.inbound.stream.tls.certs[index].keyFile = app.defaultKey;
|
||||
},
|
||||
setDefaultCertXtls() {
|
||||
inModal.inbound.stream.xtls.certs[0].certFile = app.defaultCert;
|
||||
inModal.inbound.stream.xtls.certs[0].keyFile = app.defaultKey;
|
||||
setDefaultCertXtls(index) {
|
||||
inModal.inbound.stream.xtls.certs[index].certFile = app.defaultCert;
|
||||
inModal.inbound.stream.xtls.certs[index].keyFile = app.defaultKey;
|
||||
},
|
||||
async getNewX25519Cert() {
|
||||
inModal.loading(true);
|
||||
|
||||
@@ -105,7 +105,17 @@
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
<a-input v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus style="max-width: 300px"></a-input>
|
||||
<a-switch v-model="enableFilter"
|
||||
checked-children='{{ i18n "search" }}' un-checked-children='{{ i18n "filter" }}'
|
||||
@change="toggleFilter" style="margin-right: 10px;">
|
||||
</a-switch>
|
||||
<a-input v-if="!enableFilter" v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus style="max-width: 300px"></a-input>
|
||||
<a-radio-group v-if="enableFilter" v-model="filterBy" @change="filterInbounds" button-style="solid">
|
||||
<a-radio-button value="">{{ i18n "none" }}</a-radio-button>
|
||||
<a-radio-button value="deactive">{{ i18n "disabled" }}</a-radio-button>
|
||||
<a-radio-button value="depleted">{{ i18n "depleted" }}</a-radio-button>
|
||||
<a-radio-button value="expiring">{{ i18n "depletingSoon" }}</a-radio-button>
|
||||
</a-radio-group>
|
||||
<a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
|
||||
:data-source="searchedInbounds"
|
||||
:loading="spinning" :scroll="{ x: 1300 }"
|
||||
@@ -165,7 +175,7 @@
|
||||
</template>
|
||||
<template slot="protocol" slot-scope="text, dbInbound">
|
||||
<a-tag style="margin:0;" color="blue">[[ dbInbound.protocol ]]</a-tag>
|
||||
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan">
|
||||
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
|
||||
<a-tag style="margin:0;" color="green">[[ dbInbound.toInbound().stream.network ]]</a-tag>
|
||||
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isTls" color="cyan">TLS</a-tag>
|
||||
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isXtls" color="cyan">XTLS</a-tag>
|
||||
@@ -323,6 +333,8 @@
|
||||
inbounds: [],
|
||||
dbInbounds: [],
|
||||
searchKey: '',
|
||||
enableFilter: false,
|
||||
filterBy: '',
|
||||
searchedInbounds: [],
|
||||
expireDiff: 0,
|
||||
trafficDiff: 0,
|
||||
@@ -332,6 +344,14 @@
|
||||
isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false,
|
||||
refreshing: false,
|
||||
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
|
||||
subSettings: {
|
||||
enable : false,
|
||||
port: 0,
|
||||
path: '',
|
||||
domain: '',
|
||||
tls: false
|
||||
},
|
||||
tgBotEnable: false
|
||||
},
|
||||
methods: {
|
||||
loading(spinning = true) {
|
||||
@@ -341,6 +361,7 @@
|
||||
this.refreshing = true;
|
||||
const msg = await HttpUtil.post('/panel/inbound/list');
|
||||
if (!msg.success) {
|
||||
this.refreshing = false;
|
||||
return;
|
||||
}
|
||||
this.setInbounds(msg.obj);
|
||||
@@ -353,10 +374,20 @@
|
||||
if (!msg.success) {
|
||||
return;
|
||||
}
|
||||
this.expireDiff = msg.obj.expireDiff * 86400000;
|
||||
this.trafficDiff = msg.obj.trafficDiff * 1073741824;
|
||||
this.defaultCert = msg.obj.defaultCert;
|
||||
this.defaultKey = msg.obj.defaultKey;
|
||||
with(msg.obj){
|
||||
this.expireDiff = expireDiff * 86400000;
|
||||
this.trafficDiff = trafficDiff * 1073741824;
|
||||
this.defaultCert = defaultCert;
|
||||
this.defaultKey = defaultKey;
|
||||
this.tgBotEnable = tgBotEnable;
|
||||
this.subSettings = {
|
||||
enable : subEnable,
|
||||
port: subPort,
|
||||
path: subPath,
|
||||
domain: subDomain,
|
||||
tls: subTLS
|
||||
};
|
||||
}
|
||||
},
|
||||
setInbounds(dbInbounds) {
|
||||
this.inbounds.splice(0);
|
||||
@@ -366,11 +397,15 @@
|
||||
to_inbound = dbInbound.toInbound()
|
||||
this.inbounds.push(to_inbound);
|
||||
this.dbInbounds.push(dbInbound);
|
||||
if ([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN].includes(inbound.protocol)) {
|
||||
if ([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(inbound.protocol)) {
|
||||
this.clientCount[inbound.id] = this.getClientCounts(inbound, to_inbound);
|
||||
}
|
||||
}
|
||||
this.searchInbounds(this.searchKey);
|
||||
if(this.enableFilter){
|
||||
this.filterInbounds();
|
||||
} else {
|
||||
this.searchInbounds(this.searchKey);
|
||||
}
|
||||
},
|
||||
getClientCounts(dbInbound, inbound) {
|
||||
let clientCount = 0, active = [], deactive = [], depleted = [], expiring = [];
|
||||
@@ -428,6 +463,38 @@
|
||||
});
|
||||
}
|
||||
},
|
||||
filterInbounds() {
|
||||
if (ObjectUtil.isEmpty(this.filterBy)) {
|
||||
this.searchedInbounds = this.dbInbounds.slice();
|
||||
} else {
|
||||
this.searchedInbounds.splice(0, this.searchedInbounds.length);
|
||||
this.dbInbounds.forEach(inbound => {
|
||||
const newInbound = new DBInbound(inbound);
|
||||
const inboundSettings = JSON.parse(inbound.settings);
|
||||
if (this.clientCount[inbound.id] && this.clientCount[inbound.id].hasOwnProperty(this.filterBy)){
|
||||
const list = this.clientCount[inbound.id][this.filterBy];
|
||||
if (list.length > 0) {
|
||||
const filteredSettings = { "clients": [] };
|
||||
inboundSettings.clients.forEach(client => {
|
||||
if (list.includes(client.email)) {
|
||||
filteredSettings.clients.push(client);
|
||||
}
|
||||
});
|
||||
newInbound.settings = Inbound.Settings.fromJson(inbound.protocol, filteredSettings);
|
||||
this.searchedInbounds.push(newInbound);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
toggleFilter(){
|
||||
if(this.enableFilter) {
|
||||
this.searchKey = '';
|
||||
} else {
|
||||
this.filterBy = '';
|
||||
this.searchedInbounds = this.dbInbounds.slice();
|
||||
}
|
||||
},
|
||||
generalActions(action) {
|
||||
switch (action.key) {
|
||||
case "export":
|
||||
@@ -697,13 +764,32 @@
|
||||
default: return client.id;
|
||||
}
|
||||
},
|
||||
checkFallback(dbInbound) {
|
||||
newDbInbound = new DBInbound(dbInbound);
|
||||
if (dbInbound.listen.startsWith("@")){
|
||||
rootInbound = this.inbounds.find((i) =>
|
||||
i.tls &&
|
||||
['trojan','vless'].includes(i.protocol) &&
|
||||
i.settings.fallbacks.find(f => f.dest === dbInbound.listen)
|
||||
);
|
||||
if (rootInbound) {
|
||||
newDbInbound.listen = rootInbound.listen;
|
||||
newDbInbound.port = rootInbound.port;
|
||||
newInbound = newDbInbound.toInbound();
|
||||
newInbound.stream.security = 'tls';
|
||||
newInbound.stream.tls = rootInbound.stream.tls;
|
||||
newDbInbound.streamSettings = newInbound.stream.toString();
|
||||
}
|
||||
}
|
||||
return newDbInbound;
|
||||
},
|
||||
showQrcode(dbInbound, clientIndex) {
|
||||
const clientName = JSON.parse(dbInbound.settings).clients[clientIndex].email;
|
||||
const link = dbInbound.genLink(clientIndex);
|
||||
qrModal.show('{{ i18n "qrCode"}}', link, dbInbound, '', clientName);
|
||||
newDbInbound = this.checkFallback(dbInbound);
|
||||
qrModal.show('{{ i18n "qrCode"}}', newDbInbound, clientIndex);
|
||||
},
|
||||
showInfo(dbInbound, index) {
|
||||
infoModal.show(dbInbound, index);
|
||||
newDbInbound = this.checkFallback(dbInbound);
|
||||
infoModal.show(newDbInbound, index);
|
||||
},
|
||||
switchEnable(dbInboundId) {
|
||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
@@ -790,10 +876,10 @@
|
||||
clientStats = dbInbound.clientStats.find(stats => stats.email === email)
|
||||
return clientStats ? clientStats.down : 0
|
||||
},
|
||||
isTrafficExhausted(dbInbound, email) {
|
||||
if (email.length == 0) return false
|
||||
clientStats = dbInbound.clientStats.find(stats => stats.email === email)
|
||||
return clientStats ? clientStats.down + clientStats.up > clientStats.total : false
|
||||
statsColor(dbInbound, email) {
|
||||
if(email.length == 0) return 'blue';
|
||||
clientStats = dbInbound.clientStats.find(stats => stats.email === email);
|
||||
return usageColor(clientStats.down + clientStats.up, this.trafficDiff, clientStats.total);
|
||||
},
|
||||
isClientEnabled(dbInbound, email) {
|
||||
clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null
|
||||
@@ -804,7 +890,8 @@
|
||||
},
|
||||
inboundLinks(dbInboundId) {
|
||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
txtModal.show('{{ i18n "pages.inbounds.export"}}', dbInbound.genInboundLinks, dbInbound.remark);
|
||||
newDbInbound = this.checkFallback(dbInbound);
|
||||
txtModal.show('{{ i18n "pages.inbounds.export"}}', newDbInbound.genInboundLinks, newDbInbound.remark);
|
||||
},
|
||||
exportAllLinks() {
|
||||
let copyText = '';
|
||||
|
||||
@@ -34,7 +34,8 @@
|
||||
:stroke-color="status.cpu.color"
|
||||
:class="themeSwitcher.darkCardClass"
|
||||
:percent="status.cpu.percent"></a-progress>
|
||||
<div>CPU</div>
|
||||
<div>CPU: [[ cpuCoreFormat(status.cpuCores) ]]</div>
|
||||
<div>Speed: [[ cpuSpeedFormat(status.cpuSpeedMhz) ]]</div>
|
||||
</a-col>
|
||||
<a-col :span="12" style="text-align: center">
|
||||
<a-progress type="dashboard" status="normal"
|
||||
@@ -84,14 +85,10 @@
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-card hoverable :class="themeSwitcher.darkCardClass">
|
||||
{{ i18n "pages.index.operationHours" }}:
|
||||
<a-tag color="green">[[ formatSecond(status.uptime) ]]</a-tag>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.operationHoursDesc" }}
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
{{ i18n "menu.link" }}:
|
||||
<a-tag color="blue" style="cursor: pointer;" @click="openLogs(20)">{{ i18n "pages.index.logs" }}</a-tag>
|
||||
<a-tag color="blue" style="cursor: pointer;" @click="openConfig">{{ i18n "pages.index.config" }}</a-tag>
|
||||
<a-tag color="blue" style="cursor: pointer;" @click="openBackup">{{ i18n "pages.index.backup" }}</a-tag>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
@@ -111,26 +108,69 @@
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-card hoverable :class="themeSwitcher.darkCardClass">
|
||||
{{ i18n "menu.link" }}:
|
||||
<a-tag color="blue" style="cursor: pointer;" @click="openLogs(20)">{{ i18n "pages.index.logs" }}</a-tag>
|
||||
<a-tag color="blue" style="cursor: pointer;" @click="openConfig">{{ i18n "pages.index.config" }}</a-tag>
|
||||
<a-tag color="blue" style="cursor: pointer;" @click="openBackup">{{ i18n "pages.index.backup" }}</a-tag>
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
{{ i18n "pages.index.systemLoad" }}: [[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.systemLoadDesc" }}
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
{{ i18n "pages.index.operationHours" }}:
|
||||
<a-tag color="green">[[ formatSecond(status.uptime) ]]</a-tag>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-card hoverable :class="themeSwitcher.darkCardClass">
|
||||
{{ i18n "pages.index.systemLoad" }}: [[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
IPv4:
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
[[ status.publicIP.ipv4 ]]
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
IPv6:
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
[[ status.publicIP.ipv6 ]]
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-card hoverable :class="themeSwitcher.darkCardClass">
|
||||
TCP / UDP {{ i18n "pages.index.connectionCount" }}: [[ status.tcpCount ]] / [[ status.udpCount ]]
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.connectionCountDesc" }}
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
TCP: [[ status.tcpCount ]]
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.connectionTcpCountDesc" }}
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
UDP: [[ status.udpCount ]]
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.connectionUdpCountDesc" }}
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
@@ -138,7 +178,7 @@
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
<a-icon type="arrow-up"></a-icon>
|
||||
[[ sizeFormat(status.netIO.up) ]] / S
|
||||
[[ sizeFormat(status.netIO.up) ]]/S
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.upSpeed" }}
|
||||
@@ -148,7 +188,7 @@
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-icon type="arrow-down"></a-icon>
|
||||
[[ sizeFormat(status.netIO.down) ]] / S
|
||||
[[ sizeFormat(status.netIO.down) ]]/S
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.downSpeed" }}
|
||||
@@ -294,11 +334,14 @@
|
||||
class Status {
|
||||
constructor(data) {
|
||||
this.cpu = new CurTotal(0, 0);
|
||||
this.cpuCores = 0;
|
||||
this.cpuSpeedMhz = 0;
|
||||
this.disk = new CurTotal(0, 0);
|
||||
this.loads = [0, 0, 0];
|
||||
this.mem = new CurTotal(0, 0);
|
||||
this.netIO = { up: 0, down: 0 };
|
||||
this.netTraffic = { sent: 0, recv: 0 };
|
||||
this.publicIP = { ipv4: 0, ipv6: 0 };
|
||||
this.swap = new CurTotal(0, 0);
|
||||
this.tcpCount = 0;
|
||||
this.udpCount = 0;
|
||||
@@ -309,11 +352,14 @@
|
||||
return;
|
||||
}
|
||||
this.cpu = new CurTotal(data.cpu, 100);
|
||||
this.cpuCores = data.cpuCores;
|
||||
this.cpuSpeedMhz = data.cpuSpeedMhz;
|
||||
this.disk = new CurTotal(data.disk.current, data.disk.total);
|
||||
this.loads = data.loads.map(load => toFixed(load, 2));
|
||||
this.mem = new CurTotal(data.mem.current, data.mem.total);
|
||||
this.netIO = data.netIO;
|
||||
this.netTraffic = data.netTraffic;
|
||||
this.publicIP = data.publicIP;
|
||||
this.swap = new CurTotal(data.swap.current, data.swap.total);
|
||||
this.tcpCount = data.tcpCount;
|
||||
this.udpCount = data.udpCount;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"regexp"
|
||||
"sync"
|
||||
"x-ui/database"
|
||||
"x-ui/database/model"
|
||||
"x-ui/logger"
|
||||
@@ -20,31 +21,41 @@ import (
|
||||
|
||||
type CheckClientIpJob struct {
|
||||
xrayService service.XrayService
|
||||
AllowedIps []string
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
var job *CheckClientIpJob
|
||||
var disAllowedIps []string
|
||||
var AllowedIps []string
|
||||
var ipRegx *regexp.Regexp = regexp.MustCompile(`[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+`)
|
||||
var emailRegx *regexp.Regexp = regexp.MustCompile(`email:.+`)
|
||||
|
||||
func NewCheckClientIpJob() *CheckClientIpJob {
|
||||
job = new(CheckClientIpJob)
|
||||
job := &CheckClientIpJob{}
|
||||
return job
|
||||
}
|
||||
|
||||
func (j *CheckClientIpJob) Run() {
|
||||
logger.Debug("Check Client IP Job...")
|
||||
processLogFile()
|
||||
j.processLogFile()
|
||||
|
||||
// disAllowedIps = []string{"192.168.1.183","192.168.1.197"}
|
||||
blockedIps := []byte(strings.Join(disAllowedIps, ","))
|
||||
err := os.WriteFile(xray.GetBlockedIPsPath(), blockedIps, 0755)
|
||||
// AllowedIps = []string{"192.168.1.183","192.168.1.197"}
|
||||
allowedIps := []byte(strings.Join(j.getAllowedIps(), ","))
|
||||
|
||||
// check if file exists, if not create one
|
||||
_, err := os.Stat(xray.GetAllowedIPsPath())
|
||||
if os.IsNotExist(err) {
|
||||
_, err = os.OpenFile(xray.GetAllowedIPsPath(), os.O_RDWR|os.O_CREATE, 0755)
|
||||
checkError(err)
|
||||
}
|
||||
err = os.WriteFile(xray.GetAllowedIPsPath(), allowedIps, 0755)
|
||||
checkError(err)
|
||||
|
||||
}
|
||||
|
||||
func processLogFile() {
|
||||
func (j *CheckClientIpJob) processLogFile() {
|
||||
accessLogPath := GetAccessLogPath()
|
||||
if accessLogPath == "" {
|
||||
logger.Warning("xray log not init in config.json")
|
||||
logger.Warning("access.log doesn't exist in your config.json")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -59,8 +70,6 @@ func processLogFile() {
|
||||
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for _, line := range lines {
|
||||
ipRegx, _ := regexp.Compile(`[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+`)
|
||||
emailRegx, _ := regexp.Compile(`email:.+`)
|
||||
|
||||
matchesIp := ipRegx.FindString(line)
|
||||
if len(matchesIp) > 0 {
|
||||
@@ -73,30 +82,26 @@ func processLogFile() {
|
||||
if matchesEmail == "" {
|
||||
continue
|
||||
}
|
||||
matchesEmail = strings.Split(matchesEmail, "email: ")[1]
|
||||
matchesEmail = strings.TrimSpace(strings.Split(matchesEmail, "email: ")[1])
|
||||
|
||||
if InboundClientIps[matchesEmail] != nil {
|
||||
if contains(InboundClientIps[matchesEmail], ip) {
|
||||
continue
|
||||
}
|
||||
InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail], ip)
|
||||
|
||||
} else {
|
||||
if !contains(InboundClientIps[matchesEmail], ip) {
|
||||
InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail], ip)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
disAllowedIps = []string{}
|
||||
j.setAllowedIps([]string{})
|
||||
|
||||
for clientEmail, ips := range InboundClientIps {
|
||||
inboundClientIps, err := GetInboundClientIps(clientEmail)
|
||||
sort.Strings(ips)
|
||||
if err != nil {
|
||||
addInboundClientIps(clientEmail, ips)
|
||||
|
||||
} else {
|
||||
updateInboundClientIps(inboundClientIps, clientEmail, ips)
|
||||
j.updateInboundClientIps(inboundClientIps, clientEmail, ips)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// check if inbound connection is more than limited ip and drop connection
|
||||
@@ -107,6 +112,7 @@ func processLogFile() {
|
||||
stop <- true
|
||||
|
||||
}
|
||||
|
||||
func GetAccessLogPath() string {
|
||||
|
||||
config, err := os.ReadFile(xray.GetConfigPath())
|
||||
@@ -127,20 +133,22 @@ func GetAccessLogPath() string {
|
||||
return ""
|
||||
|
||||
}
|
||||
|
||||
func checkError(e error) {
|
||||
if e != nil {
|
||||
logger.Warning("client ip job err:", e)
|
||||
}
|
||||
}
|
||||
|
||||
func contains(s []string, str string) bool {
|
||||
for _, v := range s {
|
||||
if v == str {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func GetInboundClientIps(clientEmail string) (*model.InboundClientIps, error) {
|
||||
db := database.GetDB()
|
||||
InboundClientIps := &model.InboundClientIps{}
|
||||
@@ -150,14 +158,12 @@ func GetInboundClientIps(clientEmail string) (*model.InboundClientIps, error) {
|
||||
}
|
||||
return InboundClientIps, nil
|
||||
}
|
||||
|
||||
func addInboundClientIps(clientEmail string, ips []string) error {
|
||||
inboundClientIps := &model.InboundClientIps{}
|
||||
jsonIps, err := json.Marshal(ips)
|
||||
checkError(err)
|
||||
|
||||
// Trim any leading/trailing whitespace from clientEmail
|
||||
clientEmail = strings.TrimSpace(clientEmail)
|
||||
|
||||
inboundClientIps.ClientEmail = clientEmail
|
||||
inboundClientIps.Ips = string(jsonIps)
|
||||
|
||||
@@ -165,10 +171,10 @@ func addInboundClientIps(clientEmail string, ips []string) error {
|
||||
tx := db.Begin()
|
||||
|
||||
defer func() {
|
||||
if err == nil {
|
||||
tx.Commit()
|
||||
} else {
|
||||
if r := recover(); r != nil {
|
||||
tx.Rollback()
|
||||
} else {
|
||||
tx.Commit()
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -178,13 +184,7 @@ func addInboundClientIps(clientEmail string, ips []string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmail string, ips []string) error {
|
||||
|
||||
jsonIps, err := json.Marshal(ips)
|
||||
checkError(err)
|
||||
|
||||
inboundClientIps.ClientEmail = clientEmail
|
||||
inboundClientIps.Ips = string(jsonIps)
|
||||
func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmail string, ips []string) error {
|
||||
|
||||
// check inbound limitation
|
||||
inbound, err := GetInboundByEmail(clientEmail)
|
||||
@@ -199,21 +199,24 @@ func updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmai
|
||||
json.Unmarshal([]byte(inbound.Settings), &settings)
|
||||
clients := settings["clients"]
|
||||
|
||||
var disAllowedIps []string // initialize the slice
|
||||
|
||||
for _, client := range clients {
|
||||
if client.Email == clientEmail {
|
||||
|
||||
limitIp := client.LimitIP
|
||||
|
||||
if limitIp < len(ips) && limitIp != 0 && inbound.Enable {
|
||||
|
||||
disAllowedIps = append(disAllowedIps, ips[limitIp:]...)
|
||||
for _, ip := range ips[:limitIp] {
|
||||
j.addAllowedIp(ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.Debug("disAllowedIps ", disAllowedIps)
|
||||
sort.Strings(disAllowedIps)
|
||||
|
||||
jsonIps, err := json.Marshal(ips) // marshal the possibly truncated list of IPs
|
||||
checkError(err)
|
||||
|
||||
inboundClientIps.ClientEmail = clientEmail
|
||||
inboundClientIps.Ips = string(jsonIps)
|
||||
|
||||
logger.Debug("Allowed IPs: ", ips)
|
||||
|
||||
db := database.GetDB()
|
||||
err = db.Save(inboundClientIps).Error
|
||||
@@ -223,6 +226,24 @@ func updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmai
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *CheckClientIpJob) setAllowedIps(ips []string) {
|
||||
j.mutex.Lock()
|
||||
defer j.mutex.Unlock()
|
||||
j.AllowedIps = ips
|
||||
}
|
||||
|
||||
func (j *CheckClientIpJob) addAllowedIp(ip string) {
|
||||
j.mutex.Lock()
|
||||
defer j.mutex.Unlock()
|
||||
j.AllowedIps = append(j.AllowedIps, ip)
|
||||
}
|
||||
|
||||
func (j *CheckClientIpJob) getAllowedIps() []string {
|
||||
j.mutex.Lock()
|
||||
defer j.mutex.Unlock()
|
||||
return j.AllowedIps
|
||||
}
|
||||
|
||||
func DisableInbound(id int) error {
|
||||
db := database.GetDB()
|
||||
result := db.Model(model.Inbound{}).
|
||||
@@ -249,7 +270,6 @@ func GetInboundByEmail(clientEmail string) (*model.Inbound, error) {
|
||||
}
|
||||
|
||||
func LimitDevice() {
|
||||
var destIp, destPort, srcIp, srcPort string
|
||||
|
||||
localIp, err := LocalIP()
|
||||
checkError(err)
|
||||
@@ -265,19 +285,19 @@ func LimitDevice() {
|
||||
|
||||
data := strings.Split(row, " ")
|
||||
|
||||
if len(data) < 2 {
|
||||
continue // Skip this row if it doesn't have at least two elements
|
||||
}
|
||||
destIp, destPort, srcIp, srcPort := "", "", "", ""
|
||||
|
||||
destIp = string(ipRegx.FindString(data[0]))
|
||||
|
||||
destPort = portRegx.FindString(data[0])
|
||||
destPort = strings.Replace(destPort, ":", "", -1)
|
||||
|
||||
srcIp = string(ipRegx.FindString(data[1]))
|
||||
|
||||
srcPort = portRegx.FindString(data[1])
|
||||
srcPort = strings.Replace(srcPort, ":", "", -1)
|
||||
|
||||
if contains(disAllowedIps, srcIp) {
|
||||
if contains(AllowedIps, srcIp) {
|
||||
dropCmd := cmd.NewCmd("bash", "-c", "ss -K dport = "+srcPort)
|
||||
dropCmd.Start()
|
||||
|
||||
@@ -285,6 +305,7 @@ func LimitDevice() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func LocalIP() ([]string, error) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package job
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
"x-ui/web/service"
|
||||
|
||||
@@ -24,7 +24,10 @@ func (j *CheckCpuJob) Run() {
|
||||
// get latest status of server
|
||||
percent, err := cpu.Percent(1*time.Second, false)
|
||||
if err == nil && percent[0] > float64(threshold) {
|
||||
msg := fmt.Sprintf("🔴 CPU usage %.2f%% is more than threshold %d%%", percent[0], threshold)
|
||||
msg := j.tgbotService.I18nBot("tgbot.messages.cpuThreshold",
|
||||
"Percent=="+strconv.FormatFloat(percent[0], 'f', 2, 64),
|
||||
"Threshold=="+strconv.Itoa(threshold))
|
||||
|
||||
j.tgbotService.SendMsgToTgbotAdmins(msg)
|
||||
}
|
||||
}
|
||||
|
||||
19
web/job/check_hash_storage.go
Normal file
19
web/job/check_hash_storage.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package job
|
||||
|
||||
import (
|
||||
"x-ui/web/service"
|
||||
)
|
||||
|
||||
type CheckHashStorageJob struct {
|
||||
tgbotService service.Tgbot
|
||||
}
|
||||
|
||||
func NewCheckHashStorageJob() *CheckHashStorageJob {
|
||||
return new(CheckHashStorageJob)
|
||||
}
|
||||
|
||||
// Here Run is an interface method of the Job interface
|
||||
func (j *CheckHashStorageJob) Run() {
|
||||
// Remove expired hashes from storage
|
||||
j.tgbotService.GetHashStorage().RemoveExpiredHashes()
|
||||
}
|
||||
144
web/locale/locale.go
Normal file
144
web/locale/locale.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package locale
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"strings"
|
||||
"x-ui/logger"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
var i18nBundle *i18n.Bundle
|
||||
var LocalizerWeb *i18n.Localizer
|
||||
var LocalizerBot *i18n.Localizer
|
||||
|
||||
type I18nType string
|
||||
|
||||
const (
|
||||
Bot I18nType = "bot"
|
||||
Web I18nType = "web"
|
||||
)
|
||||
|
||||
type SettingService interface {
|
||||
GetTgLang() (string, error)
|
||||
}
|
||||
|
||||
func InitLocalizer(i18nFS embed.FS, settingService SettingService) error {
|
||||
// set default bundle to english
|
||||
i18nBundle = i18n.NewBundle(language.English)
|
||||
i18nBundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
|
||||
|
||||
// parse files
|
||||
if err := parseTranslationFiles(i18nFS, i18nBundle); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// setup bot locale
|
||||
if err := initTGBotLocalizer(settingService); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createTemplateData(params []string, seperator ...string) map[string]interface{} {
|
||||
var sep string = "=="
|
||||
if len(seperator) > 0 {
|
||||
sep = seperator[0]
|
||||
}
|
||||
|
||||
templateData := make(map[string]interface{})
|
||||
for _, param := range params {
|
||||
parts := strings.SplitN(param, sep, 2)
|
||||
templateData[parts[0]] = parts[1]
|
||||
}
|
||||
|
||||
return templateData
|
||||
}
|
||||
|
||||
func I18n(i18nType I18nType, key string, params ...string) string {
|
||||
var localizer *i18n.Localizer
|
||||
|
||||
switch i18nType {
|
||||
case "bot":
|
||||
localizer = LocalizerBot
|
||||
case "web":
|
||||
localizer = LocalizerWeb
|
||||
default:
|
||||
logger.Errorf("Invalid type for I18n: %s", i18nType)
|
||||
return ""
|
||||
}
|
||||
|
||||
templateData := createTemplateData(params)
|
||||
|
||||
msg, err := localizer.Localize(&i18n.LocalizeConfig{
|
||||
MessageID: key,
|
||||
TemplateData: templateData,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to localize message: %v", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
return msg
|
||||
}
|
||||
|
||||
func initTGBotLocalizer(settingService SettingService) error {
|
||||
botLang, err := settingService.GetTgLang()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
LocalizerBot = i18n.NewLocalizer(i18nBundle, botLang)
|
||||
return nil
|
||||
}
|
||||
|
||||
func LocalizerMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var lang string
|
||||
|
||||
if cookie, err := c.Request.Cookie("lang"); err == nil {
|
||||
lang = cookie.Value
|
||||
} else {
|
||||
lang = c.GetHeader("Accept-Language")
|
||||
}
|
||||
|
||||
LocalizerWeb = i18n.NewLocalizer(i18nBundle, lang)
|
||||
|
||||
c.Set("localizer", LocalizerWeb)
|
||||
c.Set("I18n", I18n)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func parseTranslationFiles(i18nFS embed.FS, i18nBundle *i18n.Bundle) error {
|
||||
err := fs.WalkDir(i18nFS, "translation",
|
||||
func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
data, err := i18nFS.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = i18nBundle.ParseMessageFileBytes(data, path)
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -6,7 +6,11 @@
|
||||
},
|
||||
"api": {
|
||||
"tag": "api",
|
||||
"services": ["HandlerService", "LoggerService", "StatsService"]
|
||||
"services": [
|
||||
"HandlerService",
|
||||
"LoggerService",
|
||||
"StatsService"
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
@@ -47,20 +51,26 @@
|
||||
"rules": [
|
||||
{
|
||||
"type": "field",
|
||||
"inboundTag": ["api"],
|
||||
"inboundTag": [
|
||||
"api"
|
||||
],
|
||||
"outboundTag": "api"
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"outboundTag": "blocked",
|
||||
"ip": ["geoip:private"]
|
||||
"ip": [
|
||||
"geoip:private"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"outboundTag": "blocked",
|
||||
"protocol": ["bittorrent"]
|
||||
"protocol": [
|
||||
"bittorrent"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"stats": {}
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ func (s *InboundService) checkPortExist(port int, ignoreId int) (bool, error) {
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (s *InboundService) getClients(inbound *model.Inbound) ([]model.Client, error) {
|
||||
func (s *InboundService) GetClients(inbound *model.Inbound) ([]model.Client, error) {
|
||||
settings := map[string][]model.Client{}
|
||||
json.Unmarshal([]byte(inbound.Settings), &settings)
|
||||
if settings == nil {
|
||||
@@ -110,7 +110,7 @@ func (s *InboundService) checkEmailsExistForClients(clients []model.Client) (str
|
||||
}
|
||||
|
||||
func (s *InboundService) checkEmailExistForInbound(inbound *model.Inbound) (string, error) {
|
||||
clients, err := s.getClients(inbound)
|
||||
clients, err := s.GetClients(inbound)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -150,7 +150,7 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, err
|
||||
return inbound, common.NewError("Duplicate email:", existEmail)
|
||||
}
|
||||
|
||||
clients, err := s.getClients(inbound)
|
||||
clients, err := s.GetClients(inbound)
|
||||
if err != nil {
|
||||
return inbound, err
|
||||
}
|
||||
@@ -208,7 +208,7 @@ func (s *InboundService) DelInbound(id int) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
clients, err := s.getClients(inbound)
|
||||
clients, err := s.GetClients(inbound)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -263,7 +263,7 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
|
||||
}
|
||||
|
||||
func (s *InboundService) AddInboundClient(data *model.Inbound) error {
|
||||
clients, err := s.getClients(data)
|
||||
clients, err := s.GetClients(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -372,7 +372,7 @@ func (s *InboundService) DelInboundClient(inboundId int, clientId string) error
|
||||
}
|
||||
|
||||
func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId string) error {
|
||||
clients, err := s.getClients(data)
|
||||
clients, err := s.GetClients(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -390,7 +390,7 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
|
||||
return err
|
||||
}
|
||||
|
||||
oldClients, err := s.getClients(oldInbound)
|
||||
oldClients, err := s.GetClients(oldInbound)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -673,6 +673,21 @@ func (s *InboundService) DelClientIPs(tx *gorm.DB, email string) error {
|
||||
return tx.Where("client_email = ?", email).Delete(model.InboundClientIps{}).Error
|
||||
}
|
||||
|
||||
func (s *InboundService) GetClientInboundByTrafficID(trafficId int) (traffic *xray.ClientTraffic, inbound *model.Inbound, err error) {
|
||||
db := database.GetDB()
|
||||
var traffics []*xray.ClientTraffic
|
||||
err = db.Model(xray.ClientTraffic{}).Where("id = ?", trafficId).Find(&traffics).Error
|
||||
if err != nil {
|
||||
logger.Warning(err)
|
||||
return nil, nil, err
|
||||
}
|
||||
if len(traffics) > 0 {
|
||||
inbound, err = s.GetInbound(traffics[0].InboundId)
|
||||
return traffics[0], inbound, err
|
||||
}
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
func (s *InboundService) GetClientInboundByEmail(email string) (traffic *xray.ClientTraffic, inbound *model.Inbound, err error) {
|
||||
db := database.GetDB()
|
||||
var traffics []*xray.ClientTraffic
|
||||
@@ -688,6 +703,85 @@ func (s *InboundService) GetClientInboundByEmail(email string) (traffic *xray.Cl
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
func (s *InboundService) GetClientByEmail(clientEmail string) (*xray.ClientTraffic, *model.Client, error) {
|
||||
traffic, inbound, err := s.GetClientInboundByEmail(clientEmail)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if inbound == nil {
|
||||
return nil, nil, common.NewError("Inbound Not Found For Email:", clientEmail)
|
||||
}
|
||||
|
||||
clients, err := s.GetClients(inbound)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
for _, client := range clients {
|
||||
if client.Email == clientEmail {
|
||||
return traffic, &client, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil, common.NewError("Client Not Found In Inbound For Email:", clientEmail)
|
||||
}
|
||||
|
||||
func (s *InboundService) SetClientTelegramUserID(trafficId int, tgId string) error {
|
||||
traffic, inbound, err := s.GetClientInboundByTrafficID(trafficId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if inbound == nil {
|
||||
return common.NewError("Inbound Not Found For Traffic ID:", trafficId)
|
||||
}
|
||||
|
||||
clientEmail := traffic.Email
|
||||
|
||||
oldClients, err := s.GetClients(inbound)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
clientId := ""
|
||||
|
||||
for _, oldClient := range oldClients {
|
||||
if oldClient.Email == clientEmail {
|
||||
if inbound.Protocol == "trojan" {
|
||||
clientId = oldClient.Password
|
||||
} else {
|
||||
clientId = oldClient.ID
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(clientId) == 0 {
|
||||
return common.NewError("Client Not Found For Email:", clientEmail)
|
||||
}
|
||||
|
||||
var settings map[string]interface{}
|
||||
err = json.Unmarshal([]byte(inbound.Settings), &settings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
clients := settings["clients"].([]interface{})
|
||||
var newClients []interface{}
|
||||
for client_index := range clients {
|
||||
c := clients[client_index].(map[string]interface{})
|
||||
if c["email"] == clientEmail {
|
||||
c["tgId"] = tgId
|
||||
newClients = append(newClients, interface{}(c))
|
||||
}
|
||||
}
|
||||
settings["clients"] = newClients
|
||||
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
inbound.Settings = string(modifiedSettings)
|
||||
return s.UpdateInboundClient(inbound, clientId)
|
||||
}
|
||||
|
||||
func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, error) {
|
||||
_, inbound, err := s.GetClientInboundByEmail(clientEmail)
|
||||
if err != nil {
|
||||
@@ -697,7 +791,7 @@ func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, er
|
||||
return false, common.NewError("Inbound Not Found For Email:", clientEmail)
|
||||
}
|
||||
|
||||
oldClients, err := s.getClients(inbound)
|
||||
oldClients, err := s.GetClients(inbound)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -753,7 +847,7 @@ func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int
|
||||
return common.NewError("Inbound Not Found For Email:", clientEmail)
|
||||
}
|
||||
|
||||
oldClients, err := s.getClients(inbound)
|
||||
oldClients, err := s.GetClients(inbound)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -807,7 +901,7 @@ func (s *InboundService) ResetClientExpiryTimeByEmail(clientEmail string, expiry
|
||||
return common.NewError("Inbound Not Found For Email:", clientEmail)
|
||||
}
|
||||
|
||||
oldClients, err := s.getClients(inbound)
|
||||
oldClients, err := s.GetClients(inbound)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1006,7 +1100,7 @@ func (s *InboundService) GetClientTrafficTgBot(tguname string) ([]*xray.ClientTr
|
||||
}
|
||||
var emails []string
|
||||
for _, inbound := range inbounds {
|
||||
clients, err := s.getClients(inbound)
|
||||
clients, err := s.GetClients(inbound)
|
||||
if err != nil {
|
||||
logger.Error("Unable to get clients from inbound")
|
||||
}
|
||||
@@ -1156,7 +1250,7 @@ func (s *InboundService) MigrationRequirements() {
|
||||
inbounds[inbound_index].Settings = string(modifiedSettings)
|
||||
}
|
||||
// Add client traffic row for all clients which has email
|
||||
modelClients, err := s.getClients(inbounds[inbound_index])
|
||||
modelClients, err := s.GetClients(inbounds[inbound_index])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -38,9 +38,11 @@ const (
|
||||
)
|
||||
|
||||
type Status struct {
|
||||
T time.Time `json:"-"`
|
||||
Cpu float64 `json:"cpu"`
|
||||
Mem struct {
|
||||
T time.Time `json:"-"`
|
||||
Cpu float64 `json:"cpu"`
|
||||
CpuCores int `json:"cpuCores"`
|
||||
CpuSpeedMhz float64 `json:"cpuSpeedMhz"`
|
||||
Mem struct {
|
||||
Current uint64 `json:"current"`
|
||||
Total uint64 `json:"total"`
|
||||
} `json:"mem"`
|
||||
@@ -69,6 +71,10 @@ type Status struct {
|
||||
Sent uint64 `json:"sent"`
|
||||
Recv uint64 `json:"recv"`
|
||||
} `json:"netTraffic"`
|
||||
PublicIP struct {
|
||||
IPv4 string `json:"ipv4"`
|
||||
IPv6 string `json:"ipv6"`
|
||||
} `json:"publicIP"`
|
||||
}
|
||||
|
||||
type Release struct {
|
||||
@@ -80,6 +86,33 @@ type ServerService struct {
|
||||
inboundService InboundService
|
||||
}
|
||||
|
||||
const DebugMode = false // Set to true during development
|
||||
|
||||
func getPublicIP(url string) string {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
if DebugMode {
|
||||
logger.Warning("get public IP failed:", err)
|
||||
}
|
||||
return "N/A"
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
ip, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
if DebugMode {
|
||||
logger.Warning("read public IP failed:", err)
|
||||
}
|
||||
return "N/A"
|
||||
}
|
||||
|
||||
if string(ip) == "" {
|
||||
return "N/A" // default value
|
||||
}
|
||||
|
||||
return string(ip)
|
||||
}
|
||||
|
||||
func (s *ServerService) GetStatus(lastStatus *Status) *Status {
|
||||
now := time.Now()
|
||||
status := &Status{
|
||||
@@ -93,6 +126,21 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
|
||||
status.Cpu = percents[0]
|
||||
}
|
||||
|
||||
status.CpuCores, err = cpu.Counts(false)
|
||||
if err != nil {
|
||||
logger.Warning("get cpu cores count failed:", err)
|
||||
}
|
||||
|
||||
cpuInfos, err := cpu.Info()
|
||||
if err != nil {
|
||||
logger.Warning("get cpu info failed:", err)
|
||||
} else if len(cpuInfos) > 0 {
|
||||
cpuInfo := cpuInfos[0]
|
||||
status.CpuSpeedMhz = cpuInfo.Mhz // setting CPU speed in MHz
|
||||
} else {
|
||||
logger.Warning("could not find cpu info")
|
||||
}
|
||||
|
||||
upTime, err := host.Uptime()
|
||||
if err != nil {
|
||||
logger.Warning("get uptime failed:", err)
|
||||
@@ -161,6 +209,9 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
|
||||
logger.Warning("get udp connections failed:", err)
|
||||
}
|
||||
|
||||
status.PublicIP.IPv4 = getPublicIP("https://api.ipify.org")
|
||||
status.PublicIP.IPv6 = getPublicIP("https://api6.ipify.org")
|
||||
|
||||
if s.xrayService.IsXrayRunning() {
|
||||
status.Xray.State = Running
|
||||
status.Xray.ErrorMsg = ""
|
||||
|
||||
@@ -39,7 +39,16 @@ var defaultValueMap = map[string]string{
|
||||
"tgRunTime": "@daily",
|
||||
"tgBotBackup": "false",
|
||||
"tgCpu": "0",
|
||||
"tgLang": "en-US",
|
||||
"secretEnable": "false",
|
||||
"subEnable": "false",
|
||||
"subListen": "",
|
||||
"subPort": "2096",
|
||||
"subPath": "sub/",
|
||||
"subDomain": "",
|
||||
"subCertFile": "",
|
||||
"subKeyFile": "",
|
||||
"subUpdates": "12",
|
||||
}
|
||||
|
||||
type SettingService struct {
|
||||
@@ -256,6 +265,10 @@ func (s *SettingService) GetTgCpu() (int, error) {
|
||||
return s.getInt("tgCpu")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetTgLang() (string, error) {
|
||||
return s.getString("tgLang")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetPort() (int, error) {
|
||||
return s.getInt("webPort")
|
||||
}
|
||||
@@ -331,6 +344,48 @@ func (s *SettingService) GetTimeLocation() (*time.Location, error) {
|
||||
return location, nil
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubEnable() (bool, error) {
|
||||
return s.getBool("subEnable")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubListen() (string, error) {
|
||||
return s.getString("subListen")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubPort() (int, error) {
|
||||
return s.getInt("subPort")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubPath() (string, error) {
|
||||
subPath, err := s.getString("subPath")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !strings.HasPrefix(subPath, "/") {
|
||||
subPath = "/" + subPath
|
||||
}
|
||||
if !strings.HasSuffix(subPath, "/") {
|
||||
subPath += "/"
|
||||
}
|
||||
return subPath, nil
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubDomain() (string, error) {
|
||||
return s.getString("subDomain")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubCertFile() (string, error) {
|
||||
return s.getString("subCertFile")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubKeyFile() (string, error) {
|
||||
return s.getString("subKeyFile")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubUpdates() (int, error) {
|
||||
return s.getInt("subUpdates")
|
||||
}
|
||||
|
||||
func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error {
|
||||
if err := allSetting.CheckValid(); err != nil {
|
||||
return err
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@
|
||||
"enable" = "Enable"
|
||||
"protocol" = "Protocol"
|
||||
"search" = "Search"
|
||||
|
||||
"filter" = "Filter"
|
||||
"loading" = "Loading"
|
||||
"second" = "Second"
|
||||
"minute" = "Minute"
|
||||
@@ -75,14 +75,15 @@
|
||||
"xrayStatus" = "Xray Status"
|
||||
"stopXray" = "Stop"
|
||||
"restartXray" = "Restart"
|
||||
"xraySwitch" = "Switch Version"
|
||||
"xraySwitch" = "SwitchV"
|
||||
"xraySwitchClick" = "Choose the version you want to switch to."
|
||||
"xraySwitchClickDesk" = "Choose wisely, as older versions may not be compatible with current configurations."
|
||||
"operationHours" = "Operation Hours"
|
||||
"operationHoursDesc" = "System uptime: time since startup."
|
||||
"operationHours" = "Uptime"
|
||||
"systemLoad" = "System Load"
|
||||
"systemLoadDesc" = "system load average for the past 1, 5, and 15 minutes"
|
||||
"connectionTcpCountDesc" = "Total TCP connections across all network cards."
|
||||
"connectionUdpCountDesc" = "Total UDP connections across all network cards."
|
||||
"connectionCount" = "Number of Connections"
|
||||
"connectionCountDesc" = "Total connections across all network cards."
|
||||
"upSpeed" = "Total upload speed for all network cards."
|
||||
"downSpeed" = "Total download speed for all network cards."
|
||||
"totalSent" = "Total upload traffic of all network cards since system startup."
|
||||
@@ -148,8 +149,6 @@
|
||||
"resetAllTraffic" = "Reset All Inbounds Traffic"
|
||||
"resetAllTrafficTitle" = "Reset all inbounds traffic"
|
||||
"resetAllTrafficContent" = "Are you sure you want to reset all inbounds traffic?"
|
||||
"resetAllTrafficOkText" = "Confirm"
|
||||
"resetAllTrafficCancelText" = "Cancel"
|
||||
"resetInboundClientTraffics" = "Reset Clients Traffic"
|
||||
"resetInboundClientTrafficTitle" = "Reset all client traffic"
|
||||
"resetInboundClientTrafficContent" = "Are you sure you want to reset all traffic for this inbound's clients?"
|
||||
@@ -211,6 +210,7 @@
|
||||
[pages.settings]
|
||||
"title" = "Settings"
|
||||
"save" = "Save"
|
||||
"infoDesc" = "Every change made here needs to be saved. Please restart the panel to apply changes."
|
||||
"restartPanel" = "Restart Panel "
|
||||
"restartPanelDesc" = "Are you sure you want to restart the panel? Click OK to restart after 3 seconds. If you cannot access the panel after restarting, please view the panel log information on the server."
|
||||
"actions" = "Actions"
|
||||
@@ -220,29 +220,29 @@
|
||||
"xrayConfiguration" = "Xray Configuration"
|
||||
"TGBotSettings" = "Telegram Bot Settings"
|
||||
"panelListeningIP" = "Panel Listening IP"
|
||||
"panelListeningIPDesc" = "Leave blank by default to monitor all IPs. Restart the panel to apply changes."
|
||||
"panelListeningIPDesc" = "Leave blank by default to monitor all IPs."
|
||||
"panelPort" = "Panel Port"
|
||||
"panelPortDesc" = "Restart the panel to apply changes."
|
||||
"panelPortDesc" = "The port used to display this panel"
|
||||
"publicKeyPath" = "Panel Certificate Public Key File Path"
|
||||
"publicKeyPathDesc" = "Fill in an absolute path starting with '/'. Restart the panel to apply changes."
|
||||
"publicKeyPathDesc" = "Fill in an absolute path starting with."
|
||||
"privateKeyPath" = "Panel Certificate Private Key File Path"
|
||||
"privateKeyPathDesc" = "Fill in an absolute path starting with '/'. Restart the panel to apply changes."
|
||||
"privateKeyPathDesc" = "Fill in an absolute path starting with."
|
||||
"panelUrlPath" = "Panel URL Root Path"
|
||||
"panelUrlPathDesc" = "Must start with '/' and end with '/'. Restart the panel to apply changes."
|
||||
"panelUrlPathDesc" = "Must start with '/' and end with."
|
||||
"oldUsername" = "Current Username"
|
||||
"currentPassword" = "Current Password"
|
||||
"newUsername" = "New Username"
|
||||
"newPassword" = "New Password"
|
||||
"telegramBotEnable" = "Enable Telegram bot"
|
||||
"telegramBotEnableDesc" = "Restart the panel to take effect."
|
||||
"telegramBotEnableDesc" = "Connect to the features of this panel through the Telegram bot"
|
||||
"telegramToken" = "Telegram Token"
|
||||
"telegramTokenDesc" = "Restart the panel to take effect."
|
||||
"telegramTokenDesc" = "You must get the token from the manager of Telegram bots @botfather"
|
||||
"telegramChatId" = "Telegram Admin Chat IDs"
|
||||
"telegramChatIdDesc" = "Multiple Chat IDs separated by comma. use @userinfobot to get your Chat IDs. Restart the panel to apply changes."
|
||||
"telegramChatIdDesc" = "Multiple Chat IDs separated by comma. use @userinfobot to get your Chat IDs."
|
||||
"telegramNotifyTime" = "Telegram bot notification time"
|
||||
"telegramNotifyTimeDesc" = "Use Crontab timing format. Restart the panel to apply changes."
|
||||
"telegramNotifyTimeDesc" = "Use Crontab timing format."
|
||||
"tgNotifyBackup" = "Database Backup"
|
||||
"tgNotifyBackupDesc" = "Include database backup file with report notification. Restart the panel to apply changes."
|
||||
"tgNotifyBackupDesc" = "Include database backup file with report notification."
|
||||
"sessionMaxAge" = "Session maximum age"
|
||||
"sessionMaxAgeDesc" = "The duration of a login session (unit: minute)"
|
||||
"expireTimeDiff" = "Expiration threshold for notification"
|
||||
@@ -252,7 +252,24 @@
|
||||
"tgNotifyCpu" = "CPU percentage alert threshold"
|
||||
"tgNotifyCpuDesc" = "Receive notification if CPU usage exceeds this threshold (unit: %)"
|
||||
"timeZone" = "Time zone"
|
||||
"timeZoneDesc" = "Scheduled tasks run according to the time in this time zone. Restart the panel to apply changes."
|
||||
"timeZoneDesc" = "Scheduled tasks run according to the time in this time zone."
|
||||
"subSettings" = "Subscription"
|
||||
"subEnable" = "Enable service"
|
||||
"subEnableDesc" = "Subscription feature with separate configuration"
|
||||
"subListen" = "Listening IP"
|
||||
"subListenDesc" = "Leave blank by default to monitor all IPs"
|
||||
"subPort" = "Subscription Port"
|
||||
"subPortDesc" = "Port number for serving the subscription service must be unused in server"
|
||||
"subCertPath" = "Subscription Certificate Public Key File Path"
|
||||
"subCertPathDesc" = "Fill in an absolute path starting with '/'"
|
||||
"subKeyPath" = "Subscription Certificate Private Key File Path"
|
||||
"subKeyPathDesc" = "Fill in an absolute path starting with '/'"
|
||||
"subPath" = "Subscription URL Root Path"
|
||||
"subPathDesc" = "Must start with '/' and end with '/'"
|
||||
"subDomain" = "Listening Domain"
|
||||
"subDomainDesc" = "Leave blank by default to monitor all domains and IPs"
|
||||
"subUpdates" = "Subscription update intervals"
|
||||
"subUpdatesDesc" = "Interval hours between updates in client application"
|
||||
|
||||
[pages.settings.templates]
|
||||
"title" = "Templates"
|
||||
@@ -260,57 +277,85 @@
|
||||
"advancedTemplate" = "Advanced Template"
|
||||
"completeTemplate" = "Complete Template"
|
||||
"generalConfigs" = "General Configs"
|
||||
"generalConfigsDesc" = "These options will prevent users from connecting to specific protocols and websites."
|
||||
"countryConfigs" = "Country Configs"
|
||||
"countryConfigsDesc" = "These options will prevent users from connecting to specific country domains."
|
||||
"generalConfigsDesc" = "These options will provide general adjustments."
|
||||
"blockConfigs" = "Blocking Configs"
|
||||
"blockConfigsDesc" = "These options will prevent users from connecting to specific protocols and websites."
|
||||
"blockCountryConfigs" = "Block Country Configs"
|
||||
"blockCountryConfigsDesc" = "These options will prevent users from connecting to specific country domains."
|
||||
"directCountryConfigs" = "Direct Country Configs"
|
||||
"directCountryConfigsDesc" = "These options will connect users directly to specific country domains."
|
||||
"ipv4Configs" = "IPv4 Configs"
|
||||
"ipv4ConfigsDesc" = "These options will route to target domains only via IPv4."
|
||||
"warpConfigs" = "WARP Configs"
|
||||
"warpConfigsDesc" = "Caution: Before using these options, install WARP in socks5 proxy mode on your server by following the steps on the panel's GitHub. WARP will route traffic to websites through Cloudflare servers."
|
||||
"xrayConfigTemplate" = "Xray Configuration Template"
|
||||
"xrayConfigTemplateDesc" = "Generate the final Xray configuration file based on this template. Restart the panel to apply changes."
|
||||
"xrayConfigTemplateDesc" = "Generate the final Xray configuration file based on this template."
|
||||
"xrayConfigFreedomStrategy" = "Configure Strategy for Freedom Protocol"
|
||||
"xrayConfigFreedomStrategyDesc" = "Set the output strategy of the network in the Freedom Protocol."
|
||||
"xrayConfigRoutingStrategy" = "Configure Domains Routing Strategy"
|
||||
"xrayConfigRoutingStrategyDesc" = "Set the overall routing strategy for DNS resolving."
|
||||
"xrayConfigTorrent" = "Ban BitTorrent Usage"
|
||||
"xrayConfigTorrentDesc" = "Change the configuration template to avoid using BitTorrent by users. Restart the panel to apply changes."
|
||||
"xrayConfigTorrentDesc" = "Change the configuration template to avoid using BitTorrent by users."
|
||||
"xrayConfigPrivateIp" = "Ban Private IP Ranges to Connect"
|
||||
"xrayConfigPrivateIpDesc" = "Change the configuration template to avoid connecting with private IP ranges. Restart the panel to apply changes."
|
||||
"xrayConfigPrivateIpDesc" = "Change the configuration template to avoid connecting to private IP ranges."
|
||||
"xrayConfigAds" = "Block Ads"
|
||||
"xrayConfigAdsDesc" = "Change the configuration template to block ads. Restart the panel to apply changes."
|
||||
"xrayConfigPorn" = "Block Porn Websites"
|
||||
"xrayConfigPornDesc" = "Change the configuration template to avoid connecting to porn websites. Restart the panel to apply changes."
|
||||
"xrayConfigAdsDesc" = "Change the configuration template to block ads."
|
||||
"xrayConfigFamily" = "Block Malware and Adult Content"
|
||||
"xrayConfigFamilyDesc" = "DNS resolvers to block malware and adult content for family protection."
|
||||
"xrayConfigSpeedtest" = "Block Speedtest Websites"
|
||||
"xrayConfigSpeedtestDesc" = "Change the configuration template to avoid connecting to speedtest websites. Restart the panel to apply changes."
|
||||
"xrayConfigSpeedtestDesc" = "Change the configuration template to avoid connecting to speedtest websites."
|
||||
"xrayConfigIRIp" = "Disable connection to Iran IP ranges"
|
||||
"xrayConfigIRIpDesc" = "Change the configuration template to avoid connecting with Iran IP ranges. Restart the panel to apply changes."
|
||||
"xrayConfigIRIpDesc" = "Change the configuration template to avoid connecting to Iran IP ranges."
|
||||
"xrayConfigIRDomain" = "Disable connection to Iran domains"
|
||||
"xrayConfigIRDomainDesc" = "Change the configuration template to avoid connecting with Iran domains. Restart the panel to apply changes."
|
||||
"xrayConfigIRDomainDesc" = "Change the configuration template to avoid connecting to Iran domains."
|
||||
"xrayConfigChinaIp" = "Disable connection to China IP ranges"
|
||||
"xrayConfigChinaIpDesc" = "Change the configuration template to avoid connecting with China IP ranges. Restart the panel to apply changes."
|
||||
"xrayConfigChinaIpDesc" = "Change the configuration template to avoid connecting to China IP ranges."
|
||||
"xrayConfigChinaDomain" = "Disable connection to China domains"
|
||||
"xrayConfigChinaDomainDesc" = "Change the configuration template to avoid connecting with China domains. Restart the panel to apply changes."
|
||||
"xrayConfigChinaDomainDesc" = "Change the configuration template to avoid connecting to China domains."
|
||||
"xrayConfigRussiaIp" = "Disable connection to Russia IP ranges"
|
||||
"xrayConfigRussiaIpDesc" = "Change the configuration template to avoid connecting with Russia IP ranges. Restart the panel to apply changes."
|
||||
"xrayConfigRussiaIpDesc" = "Change the configuration template to avoid connecting to Russia IP ranges."
|
||||
"xrayConfigRussiaDomain" = "Disable connection to Russia domains"
|
||||
"xrayConfigRussiaDomainDesc" = "Change the configuration template to avoid connecting with Russia domains. Restart the panel to apply changes."
|
||||
"xrayConfigRussiaDomainDesc" = "Change the configuration template to avoid connecting to Russia domains."
|
||||
"xrayConfigDirectIRIp" = "Direct connection to Iran IP ranges"
|
||||
"xrayConfigDirectIRIpDesc" = "Change the configuration template for direct connecting to Iran IP ranges."
|
||||
"xrayConfigDirectIRDomain" = "Direct connection to Iran domains"
|
||||
"xrayConfigDirectIRDomainDesc" = "Change the configuration template for direct connecting to Iran domains."
|
||||
"xrayConfigDirectChinaIp" = "Direct connection to China IP ranges"
|
||||
"xrayConfigDirectChinaIpDesc" = "Change the configuration template for direct connecting to China IP ranges."
|
||||
"xrayConfigDirectChinaDomain" = "Direct connection to China domains"
|
||||
"xrayConfigDirectChinaDomainDesc" = "Change the configuration template for direct connecting to China domains."
|
||||
"xrayConfigDirectRussiaIp" = "Direct connection to Russia IP ranges"
|
||||
"xrayConfigDirectRussiaIpDesc" = "Change the configuration template for direct connecting to Russia IP ranges."
|
||||
"xrayConfigDirectRussiaDomain" = "Direct connection to Russia domains"
|
||||
"xrayConfigDirectRussiaDomainDesc" = "Change the configuration template for direct connecting to Russia domains."
|
||||
"xrayConfigGoogleIPv4" = "Use IPv4 for Google"
|
||||
"xrayConfigGoogleIPv4Desc" = "Add routing for Google to connect with IPv4. Restart the panel to apply changes."
|
||||
"xrayConfigGoogleIPv4Desc" = "Add routing for Google to connect with IPv4."
|
||||
"xrayConfigNetflixIPv4" = "Use IPv4 for Netflix"
|
||||
"xrayConfigNetflixIPv4Desc" = "Add routing for Netflix to connect with IPv4. Restart the panel to apply changes."
|
||||
"xrayConfigNetflixIPv4Desc" = "Add routing for Netflix to connect with IPv4."
|
||||
"xrayConfigGoogleWARP" = "Route Google through WARP."
|
||||
"xrayConfigGoogleWARPDesc" = "Add routing for Google via WARP. Restart the panel to apply changes."
|
||||
"xrayConfigGoogleWARPDesc" = "Add routing for Google via WARP."
|
||||
"xrayConfigOpenAIWARP" = "Route OpenAI (ChatGPT) through WARP."
|
||||
"xrayConfigOpenAIWARPDesc" = "Add routing for OpenAI (ChatGPT) via WARP. Restart the panel to apply changes."
|
||||
"xrayConfigOpenAIWARPDesc" = "Add routing for OpenAI (ChatGPT) via WARP."
|
||||
"xrayConfigNetflixWARP" = "Route Netflix through WARP."
|
||||
"xrayConfigNetflixWARPDesc" = "Add routing for Netflix via WARP. Restart the panel to apply changes."
|
||||
"xrayConfigNetflixWARPDesc" = "Add routing for Netflix via WARP."
|
||||
"xrayConfigSpotifyWARP" = "Route Spotify through WARP."
|
||||
"xrayConfigSpotifyWARPDesc" = "Add routing for Spotify via WARP. Restart the panel to apply changes."
|
||||
"xrayConfigSpotifyWARPDesc" = "Add routing for Spotify via WARP."
|
||||
"xrayConfigIRWARP" = "Route Iran domains through WARP."
|
||||
"xrayConfigIRWARPDesc" = "Add routing for Iran domains via WARP. Restart the panel to apply changes."
|
||||
"xrayConfigIRWARPDesc" = "Add routing for Iran domains via WARP."
|
||||
"xrayConfigInbounds" = "Configuration of Inbounds"
|
||||
"xrayConfigInboundsDesc" = "Change the configuration template to accept specific clients. Restart the panel to apply changes."
|
||||
"xrayConfigInboundsDesc" = "Change the configuration template to accept specific clients."
|
||||
"xrayConfigOutbounds" = "Configuration of Outbounds"
|
||||
"xrayConfigOutboundsDesc" = "Change the configuration template to define outgoing ways for this server. Restart the panel to apply changes."
|
||||
"xrayConfigOutboundsDesc" = "Change the configuration template to define outgoing ways for this server."
|
||||
"xrayConfigRoutings" = "Configuration of routing rules."
|
||||
"xrayConfigRoutingsDesc" = "Change the configuration template to define routing rules for this server. Restart the panel to apply changes."
|
||||
"xrayConfigRoutingsDesc" = "Change the configuration template to define routing rules for this server."
|
||||
"manualLists" = "Manual Lists"
|
||||
"manualListsDesc" = "Please use the JSON array format."
|
||||
"manualBlockedIPs" = "List of Blocked IPs"
|
||||
"manualBlockedDomains" = "List of Blocked Domains"
|
||||
"manualDirectIPs" = "List of Direct IPs"
|
||||
"manualDirectDomains" = "List of Direct Domains"
|
||||
"manualIPv4Domains" = "List of IPv4 Domains"
|
||||
"manualWARPDomains" = "List of WARP Domains"
|
||||
|
||||
[pages.settings.security]
|
||||
"admin" = "Admin"
|
||||
@@ -326,3 +371,115 @@
|
||||
"modifyUser" = "Modify User "
|
||||
"originalUserPassIncorrect" = "Incorrect original username or password"
|
||||
"userPassMustBeNotEmpty" = "New username and new password cannot be empty"
|
||||
|
||||
[tgbot]
|
||||
"keyboardClosed" = "❌ Custom keyboard closed!"
|
||||
"noResult" = "❗ No result!"
|
||||
"noQuery" = "❌ Query not found! Please use the command again!"
|
||||
"wentWrong" = "❌ Something went wrong!"
|
||||
"noIpRecord" = "❗ No IP Record!"
|
||||
"noInbounds" = "❗ No inbound found!"
|
||||
"unlimited" = "♾ Unlimited"
|
||||
"month" = "Month"
|
||||
"months" = "Months"
|
||||
"day" = "Day"
|
||||
"days" = "Days"
|
||||
"unknown" = "Unknown"
|
||||
"inbounds" = "Inbounds"
|
||||
"clients" = "Clients"
|
||||
|
||||
[tgbot.commands]
|
||||
"unknown" = "❗ Unknown command"
|
||||
"pleaseChoose" = "👇 Please choose:\r\n"
|
||||
"help" = "🤖 Welcome to this bot! It's designed to offer you specific data from the server, and it allows you to make modifications as needed.\r\n\r\n"
|
||||
"start" = "👋 Hello <i>{{ .Firstname }}</i>.\r\n"
|
||||
"welcome" = "🤖 Welcome to <b>{{ .Hostname }}</b> management bot.\r\n"
|
||||
"status" = "✅ Bot is ok!"
|
||||
"usage" = "❗ Please provide a text to search!"
|
||||
"helpAdminCommands" = "Search for a client email:\r\n<code>/usage [Email]</code>\r\n \r\nSearch for inbounds (with client stats):\r\n<code>/inbound [Remark]</code>"
|
||||
"helpClientCommands" = "To search for statistics, just use folowing command:\r\n \r\n<code>/usage [UUID|Password]</code>\r\n \r\nUse UUID for vmess/vless and Password for Trojan."
|
||||
|
||||
[tgbot.messages]
|
||||
"cpuThreshold" = "🔴 The CPU usage {{ .Percent }}% is more than threshold {{ .Threshold }}%"
|
||||
"selectUserFailed" = "❌ Error in user selection!"
|
||||
"userSaved" = "✅ Telegram User saved."
|
||||
"loginSuccess" = "✅ Successfully logged-in to the panel.\r\n"
|
||||
"loginFailed" = "❗️ Login to the panel failed.\r\n"
|
||||
"report" = "🕰 Scheduled Reports: {{ .RunTime }}\r\n"
|
||||
"datetime" = "⏰ Date-Time: {{ .DateTime }}\r\n"
|
||||
"hostname" = "💻 Hostname: {{ .Hostname }}\r\n"
|
||||
"version" = "🚀 X-UI Version: {{ .Version }}\r\n"
|
||||
"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n"
|
||||
"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n"
|
||||
"ip" = "🌐 IP: {{ .IP }}\r\n"
|
||||
"ips" = "🔢 IPs: \r\n{{ .IPs }}\r\n"
|
||||
"serverUpTime" = "⏳ Server Uptime: {{ .UpTime }} {{ .Unit }}\r\n"
|
||||
"serverLoad" = "📈 Server Load: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
|
||||
"serverMemory" = "📋 Server Memory: {{ .Current }}/{{ .Total }}\r\n"
|
||||
"tcpCount" = "🔹 TcpCount: {{ .Count }}\r\n"
|
||||
"udpCount" = "🔸 UdpCount: {{ .Count }}\r\n"
|
||||
"traffic" = "🚦 Traffic: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
|
||||
"xrayStatus" = "ℹ️ Xray Status: {{ .State }}\r\n"
|
||||
"username" = "👤 Username: {{ .Username }}\r\n"
|
||||
"time" = "⏰ Time: {{ .Time }}\r\n"
|
||||
"inbound" = "📍 Inbound: {{ .Remark }}\r\n"
|
||||
"port" = "🔌 Port: {{ .Port }}\r\n"
|
||||
"expire" = "📅 Expire Date: {{ .DateTime }}\r\n \r\n"
|
||||
"expireIn" = "📅 Expire In: {{ .Time }}\r\n \r\n"
|
||||
"active" = "💡 Active: {{ .Enable }}\r\n"
|
||||
"email" = "📧 Email: {{ .Email }}\r\n"
|
||||
"upload" = "🔼 Upload↑: {{ .Upload }}\r\n"
|
||||
"download" = "🔽 Download↓: {{ .Download }}\r\n"
|
||||
"total" = "🔄 Total: {{ .UpDown }} / {{ .Total }}\r\n"
|
||||
"TGUser" = "👤 Telegram User: {{ .TelegramID }}\r\n"
|
||||
"exhaustedMsg" = "🚨 Exhausted {{ .Type }}:\r\n"
|
||||
"exhaustedCount" = "🚨 Exhausted {{ .Type }} count:\r\n"
|
||||
"disabled" = "🛑 Disabled: {{ .Disabled }}\r\n"
|
||||
"depleteSoon" = "🔜 Deplete soon: {{ .Deplete }}\r\n \r\n"
|
||||
"backupTime" = "🗄 Backup Time: {{ .Time }}\r\n"
|
||||
"refreshedOn" = "🔄🕒 Refreshed On: {{ .Time }}\r\n"
|
||||
|
||||
[tgbot.buttons]
|
||||
"closeKeyboard" = "❌ Close Keyboard"
|
||||
"cancel" = "❌ Cancel"
|
||||
"cancelReset" = "❌ Cancel Reset"
|
||||
"cancelIpLimit" = "❌ Cancel IP Limit"
|
||||
"confirmResetTraffic" = "✅ Confirm Reset Traffic?"
|
||||
"confirmClearIps" = "✅ Confirm Clear IPs?"
|
||||
"confirmRemoveTGUser" = "✅ Confirm Remove Telegram User?"
|
||||
"dbBackup" = "Get DB Backup"
|
||||
"serverUsage" = "Server Usage"
|
||||
"getInbounds" = "Get Inbounds"
|
||||
"depleteSoon" = "Deplete soon"
|
||||
"clientUsage" = "Get Usage"
|
||||
"commands" = "Commands"
|
||||
"refresh" = "🔄 Refresh"
|
||||
"clearIPs" = "❌ Clear IPs"
|
||||
"removeTGUser" = "❌ Remove Telegram User"
|
||||
"selectTGUser" = "👤 Select Telegram User"
|
||||
"selectOneTGUser" = "👤 Select a telegram user:"
|
||||
"resetTraffic" = "📈 Reset Traffic"
|
||||
"resetExpire" = "📅 Reset Expire Days"
|
||||
"ipLog" = "🔢 IP Log"
|
||||
"ipLimit" = "🔢 IP Limit"
|
||||
"setTGUser" = "👤 Set Telegram User"
|
||||
"toggle" = "🔘 Enable / Disable"
|
||||
|
||||
[tgbot.answers]
|
||||
"errorOperation" = "❗ Error in Operation."
|
||||
"getInboundsFailed" = "❌ Failed to get inbounds"
|
||||
"canceled" = "❌ {{ .Email }} : Operation canceled."
|
||||
"clientRefreshSuccess" = "✅ {{ .Email }} : Client refreshed successfully."
|
||||
"IpRefreshSuccess" = "✅ {{ .Email }} : IPs refreshed successfully."
|
||||
"TGIdRefreshSuccess" = "✅ {{ .Email }} : Client's Telegram User refreshed successfully."
|
||||
"resetTrafficSuccess" = "✅ {{ .Email }} : Traffic reset successfully."
|
||||
"expireResetSuccess" = "✅ {{ .Email }} : Expire days reset successfully."
|
||||
"resetIpSuccess" = "✅ {{ .Email }} : IP limit {{ .Count }} saved successfully."
|
||||
"clearIpSuccess" = "✅ {{ .Email }} : IPs cleared successfully."
|
||||
"getIpLog" = "✅ {{ .Email }} : Get IP Log."
|
||||
"getUserInfo" = "✅ {{ .Email }} : Get Telegram User Info."
|
||||
"removedTGUserSuccess" = "✅ {{ .Email }} : Telegram User removed successfully."
|
||||
"enableSuccess" = "✅ {{ .Email }} : Enabled successfully."
|
||||
"disableSuccess" = "✅ {{ .Email }} : Disabled successfully."
|
||||
"askToAddUserId" = "Your configuration is not found!\r\nPlease ask your Admin to use your telegram user id in your configuration(s).\r\n\r\nYour user id: <b>{{ .TgUserID }}</b>"
|
||||
"askToAddUserName" = "Your configuration is not found!\r\nPlease ask your Admin to use your telegram username or user id in your configuration(s).\r\n\r\nYour username: <b>@{{ .TgUserName }}</b>\r\n\r\nYour user id: <b>{{ .TgUserID }}</b>"
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"enable" = "فعال"
|
||||
"protocol" = "پروتکل"
|
||||
"search" = "جستجو"
|
||||
|
||||
"filter" = "فیلتر"
|
||||
"loading" = "در حال بروزرسانی..."
|
||||
"second" = "ثانیه"
|
||||
"minute" = "دقیقه"
|
||||
@@ -77,12 +77,13 @@
|
||||
"restartXray" = "شروع مجدد"
|
||||
"xraySwitch" = "تغییر ورژن"
|
||||
"xraySwitchClick" = "ورژن مورد نظر را انتخاب کنید"
|
||||
"xraySwitchClickDesk" = "لطفا با دقت انتخاب کنید ، در صورت انتخاب اشتباه امکان قطعی سیستم وجود دارد ."
|
||||
"operationHours" = "مدت فعالیت"
|
||||
"operationHoursDesc" = "مدت فعالیت سیستم بعد از روشن شدن"
|
||||
"systemLoad" = "بار روی سیستم"
|
||||
"xraySwitchClickDesk" = "لطفا با دقت انتخاب کنید ، در صورت انتخاب اشتباه امکان قطعی سیستم وجود دارد "
|
||||
"operationHours" = "آپ تایم سیستم"
|
||||
"systemLoad" = "بار سیستم"
|
||||
"systemLoadDesc" = "میانگین بار سیستم برای 1، 5 و 15 دقیقه گذشته"
|
||||
"connectionTcpCountDesc" = "مجموع اتصالات TCP در تمام کارت های شبکه"
|
||||
"connectionUdpCountDesc" = "مجموع اتصالات UDP در تمام کارت های شبکه"
|
||||
"connectionCount" = "تعداد کانکشن ها"
|
||||
"connectionCountDesc" = "تعداد کانکشن ها برای کل شبکه"
|
||||
"upSpeed" = "سرعت آپلود در حال حاضر سیستم"
|
||||
"downSpeed" = "سرعت دانلود در حال حاضر سیستم"
|
||||
"totalSent" = "جمع کل ترافیک آپلود مصرفی"
|
||||
@@ -94,7 +95,7 @@
|
||||
"config" = "تنظیمات"
|
||||
"backup" = "پشتیبان گیری و بازیابی"
|
||||
"backupTitle" = "پشتیبان گیری و بازیابی دیتابیس"
|
||||
"backupDescription" = "به یاد داشته باشید که قبل از وارد کردن یک دیتابیس جدید، نسخه پشتیبان تهیه کنید."
|
||||
"backupDescription" = "به یاد داشته باشید که قبل از وارد کردن یک دیتابیس جدید، نسخه پشتیبان تهیه کنید"
|
||||
"exportDatabase" = "دانلود دیتابیس"
|
||||
"importDatabase" = "آپلود دیتابیس"
|
||||
|
||||
@@ -209,6 +210,7 @@
|
||||
[pages.settings]
|
||||
"title" = "تنظیمات"
|
||||
"save" = "ذخیره"
|
||||
"infoDesc" = "برای اعمال تغییرات در این بخش باید پس از ذخیره کردن، پنل را ریستارت کنید"
|
||||
"restartPanel" = "ریستارت پنل"
|
||||
"restartPanelDesc" = "آیا مطمئن هستید که می خواهید پنل را دوباره راه اندازی کنید؟ برای راه اندازی مجدد روی OK کلیک کنید. اگر بعد از 3 ثانیه نمی توانید به پنل دسترسی پیدا کنید، لطفاً برای مشاهده اطلاعات گزارش پانل به سرور برگردید"
|
||||
"actions" = "عملیات ها"
|
||||
@@ -218,27 +220,27 @@
|
||||
"xrayConfiguration" = "تنظیمات Xray"
|
||||
"TGBotSettings" = "تنظیمات ربات تلگرام"
|
||||
"panelListeningIP" = "محدودیت آی پی پنل"
|
||||
"panelListeningIPDesc" = "برای استفاده از تمام IP ها به طور پیش فرض خالی بگذارید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"panelListeningIPDesc" = "برای استفاده از تمام آیپیها به طور پیش فرض خالی بگذارید"
|
||||
"panelPort" = "پورت پنل"
|
||||
"panelPortDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"panelPortDesc" = "پورت مورد استفاده برای نمایش این پنل"
|
||||
"publicKeyPath" = "مسیر فایل گواهی کلید عمومی پنل"
|
||||
"publicKeyPathDesc" = "باید یک مسیر مطلق باشد که با / شروع می شود . پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"publicKeyPathDesc" = "باید یک مسیر مطلق باشد که با / شروع می شود "
|
||||
"privateKeyPath" = "مسیر فایل گواهی کلید خصوصی پنل"
|
||||
"privateKeyPathDesc" = "باید یک مسیر مطلق باشد که با / شروع می شود . پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"privateKeyPathDesc" = "باید یک مسیر مطلق باشد که با / شروع می شود "
|
||||
"panelUrlPath" = "آدرس روت پنل"
|
||||
"panelUrlPathDesc" = "باید با '/' شروع شود و با '/' تمام شود. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"panelUrlPathDesc" = "باید با '/' شروع شود و با '/' تمام شود"
|
||||
"oldUsername" = "نام کاربری فعلی"
|
||||
"currentPassword" = "رمز عبور فعلی"
|
||||
"newUsername" = "نام کاربری جدید"
|
||||
"newPassword" = "رمز عبور جدید"
|
||||
"telegramBotEnable" = "فعالسازی ربات تلگرام"
|
||||
"telegramBotEnableDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"telegramBotEnableDesc" = "از طریق بات تلگرام به امکانات این پنل متصل شوید"
|
||||
"telegramToken" = "توکن تلگرام"
|
||||
"telegramTokenDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"telegramTokenDesc" = "توکن را باید از مدیر بات های تلگرام دریافت کنید @botfather"
|
||||
"telegramChatId" = "آی دی تلگرام مدیریت"
|
||||
"telegramChatIdDesc" = "از @userinfobot برای دریافت شناسه های چت خود استفاده کنید. با استفاده از کاما میتونید چند آی دی را از هم جدا کنید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"telegramChatIdDesc" = "از @userinfobot برای دریافت شناسه های چت خود استفاده کنید. با استفاده از کاما میتونید چند آی دی را از هم جدا کنید. "
|
||||
"telegramNotifyTime" = "مدت زمان نوتیفیکیشن ربات تلگرام"
|
||||
"telegramNotifyTimeDesc" = "از فرمت زمان بندی لینوکس استفاده کنید . پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"telegramNotifyTimeDesc" = "از فرمت زمان بندی لینوکس استفاده کنید "
|
||||
"tgNotifyBackup" = "پشتیبان گیری از پایگاه داده"
|
||||
"tgNotifyBackupDesc" = "ارسال کپی فایل پایگاه داده به همراه گزارش دوره ای"
|
||||
"sessionMaxAge" = "بیشینه زمان جلسه وب"
|
||||
@@ -251,6 +253,23 @@
|
||||
"tgNotifyCpuDesc" = "این ربات تلگرام در صورت استفاده پردازنده بیشتر از این درصد برای شما پیام ارسال می کند.(واحد: درصد)"
|
||||
"timeZone" = "منظقه زمانی"
|
||||
"timeZoneDesc" = "وظایف برنامه ریزی شده بر اساس این منطقه زمانی اجرا می شوند. پنل را مجدداً راه اندازی می کند تا اعمال شود"
|
||||
"subSettings" = "سابسکریپشن"
|
||||
"subEnable" = "فعال کردن سرویس"
|
||||
"subEnableDesc" = "ویژگی سابسکریپشن با پیکربندی جداگانه"
|
||||
"subListen" = "محدودیت آیپی"
|
||||
"subListenDesc" = "برای استفاده از همه آیپی ها به طور پیش فرض خالی بگذارید"
|
||||
"subPort" = "پورت سرویس"
|
||||
"subPortDesc" = "شماره پورت برای ارائه خدمات سابسکریپشن باید خالی باشد"
|
||||
"subCertPath" = "مسیر فایل کلید عمومی گواهی سابسکریپشن"
|
||||
"subCertPathDesc" = "یک مسیر مطلق که با '/' شروع می شود را پر کنید."
|
||||
"subKeyPath" = "مسیر فایل کلید خصوصی گواهی سابسکریپشن"
|
||||
"subKeyPathDesc" = "یک مسیر مطلق که با '/' شروع می شود را پر کنید."
|
||||
"subPath" = "مسیر ریشه سابسکریپشن"
|
||||
"subPathDesc" = "باید با '/' شروع شود و با '/' ختم شود."
|
||||
"subDomain" = "دامنه مخصوص سابسکریپشن"
|
||||
"subDomainDesc" = "برای نظارت بر همه دامنه ها و آیپی ها به طور پیش فرض خالی بگذارید"
|
||||
"subUpdates" = "فاصله به روز رسانی های سابسکریپشن"
|
||||
"subUpdatesDesc" = "ساعت های فاصله بین به روز رسانی در برنامه کاربر"
|
||||
|
||||
[pages.settings.templates]
|
||||
"title" = "الگوها"
|
||||
@@ -258,57 +277,85 @@
|
||||
"advancedTemplate" = "بخش الگو پیشرفته"
|
||||
"completeTemplate" = "بخش الگو کامل"
|
||||
"generalConfigs" = "تنظیمات عمومی"
|
||||
"generalConfigsDesc" = "این گزینه ها از اتصال کاربران به پروتکل ها و وب سایت های خاص جلوگیری می کند."
|
||||
"countryConfigs" = "تنظیمات برای کشورها"
|
||||
"countryConfigsDesc" = "این گزینه از اتصال کاربران به دامنه های کشوری خاص جلوگیری می کند."
|
||||
"generalConfigsDesc" = "این تنظیمات میتواند ترافیک کلی سرویس را متاثر کند"
|
||||
"blockConfigs" = "مسدود سازی"
|
||||
"blockConfigsDesc" = "این گزینه ها از اتصال کاربران به پروتکل ها و وب سایت های خاص جلوگیری می کند"
|
||||
"blockCountryConfigs" = "تنظیمات برای مسدودسازی کشورها"
|
||||
"blockCountryConfigsDesc" = "این گزینه اتصال کاربران به دامنه های کشوری خاص را مسدود می کند"
|
||||
"directCountryConfigs" = "تنظیمات برای اتصال مستقیم کشورها"
|
||||
"directCountryConfigsDesc" = "این گزینه کاربران را به دامنه های کشوری خاص را به طور مستقیم، متصل می کند"
|
||||
"ipv4Configs" = "تنظیمات برای IPv4"
|
||||
"ipv4ConfigsDesc" = "این گزینه فقط از طریق آیپی ورژن 4 به دامنه های هدف هدایت می شود."
|
||||
"ipv4ConfigsDesc" = "این گزینه فقط از طریق آیپی ورژن ۴ به دامنه های هدف هدایت می شود"
|
||||
"warpConfigs" = "تنظیمات برای WARP"
|
||||
"warpConfigsDesc" = "هشدار: قبل از استفاده از این گزینه، WARP را در حالت پراکسی socks5 با دنبال کردن مراحل در GitHub پنل روی سرور خود نصب کنید. WARP ترافیک را از طریق سرورهای Cloudflare به وب سایت ها هدایت می کند."
|
||||
"warpConfigsDesc" = "هشدار: قبل از استفاده از این گزینه، WARP را در حالت پراکسی socks5 با دنبال کردن مراحل در GitHub پنل روی سرور خود نصب کنید. WARP ترافیک را از طریق سرورهای Cloudflare به وب سایت ها هدایت می کند"
|
||||
"xrayConfigTemplate" = "تنظیمات الگو ایکس ری"
|
||||
"xrayConfigTemplateDesc" = "فایل پیکربندی ایکس ری نهایی بر اساس این الگو ایجاد میشود. لطفاً این را تغییر ندهید مگر اینکه دقیقاً بدانید که چه کاری انجام می دهید! پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"xrayConfigTemplateDesc" = "فایل پیکربندی ایکس ری نهایی بر اساس این الگو ایجاد میشود. لطفاً این را تغییر ندهید مگر اینکه دقیقاً بدانید که چه کاری انجام می دهید!"
|
||||
"xrayConfigFreedomStrategy" = "روش استفاده از شبکه خروجی مستقیم"
|
||||
"xrayConfigFreedomStrategyDesc" = "تعیین روش استفاده از خروجی برای پرتکل مستقیم"
|
||||
"xrayConfigRoutingStrategy" = "پیکربندی استراتژی حل دامنه در مسیریابی"
|
||||
"xrayConfigRoutingStrategyDesc" = "تعیین استراتژی مسیریابی کلی برای پیدا کردن دامنه"
|
||||
"xrayConfigTorrent" = "فیلتر کردن بیت تورنت"
|
||||
"xrayConfigTorrentDesc" = "الگوی تنظیمات را برای فیلتر کردن پروتکل بیت تورنت برای کاربران تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"xrayConfigTorrentDesc" = "الگوی تنظیمات را برای فیلتر کردن پروتکل بیت تورنت برای کاربران تغییر میدهد"
|
||||
"xrayConfigPrivateIp" = "جلوگیری از اتصال آیپی های خصوصی یا محلی"
|
||||
"xrayConfigPrivateIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های خصوصی یا محلی و بسته های سرگردان تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"xrayConfigPrivateIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های خصوصی یا محلی و بسته های سرگردان تغییر میدهد"
|
||||
"xrayConfigAds" = "مسدود کردن تبلیغات"
|
||||
"xrayConfigAdsDesc" = "الگوی تنظیمات را برای مسدود کردن تبلیغات تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"xrayConfigPorn" = "جلوگیری از اتصال به سایت های پورن"
|
||||
"xrayConfigPornDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال به سایت های پورن تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"xrayConfigAdsDesc" = "الگوی تنظیمات را برای مسدود کردن تبلیغات تغییر میدهد"
|
||||
"xrayConfigFamily" = "فعال کردن حالت خانواده"
|
||||
"xrayConfigFamilyDesc" = "برای جلوگیری از ارتباط با وبسایت های ناامن"
|
||||
"xrayConfigSpeedtest" = "جلوگیری از اتصال به سایت های تست سرعت"
|
||||
"xrayConfigSpeedtestDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال به سایت های تست سرعت تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"xrayConfigSpeedtestDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال به سایت های تست سرعت تغییر میدهد"
|
||||
"xrayConfigIRIp" = "جلوگیری از اتصال آیپی های ایران"
|
||||
"xrayConfigIRIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های ایران تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"xrayConfigIRIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های ایران تغییر میدهد"
|
||||
"xrayConfigIRDomain" = "جلوگیری از اتصال دامنه های ایران"
|
||||
"xrayConfigIRDomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های ایران تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"xrayConfigIRDomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های ایران تغییر میدهد"
|
||||
"xrayConfigChinaIp" = "جلوگیری از اتصال آیپی های چین"
|
||||
"xrayConfigChinaIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های چین تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"xrayConfigChinaIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های چین تغییر میدهد"
|
||||
"xrayConfigChinaDomain" = "جلوگیری از اتصال دامنه های چین"
|
||||
"xrayConfigChinaDomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های چین تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"xrayConfigChinaDomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های چین تغییر میدهد"
|
||||
"xrayConfigRussiaIp" = "جلوگیری از اتصال آیپی های روسیه"
|
||||
"xrayConfigRussiaIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های روسیه تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"xrayConfigRussiaIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های روسیه تغییر میدهد"
|
||||
"xrayConfigRussiaDomain" = "جلوگیری از اتصال دامنه های روسیه"
|
||||
"xrayConfigRussiaDomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های روسیه تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"xrayConfigRussiaDomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های روسیه تغییر میدهد"
|
||||
"xrayConfigDirectIRIp" = "ارتباط مستقیم به آیپی های ایران"
|
||||
"xrayConfigDirectIRIpDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به آیپی های ایران تغییر میدهد"
|
||||
"xrayConfigDirectIRDomain" = "ارتباط مستقیم به دامنه های ایران"
|
||||
"xrayConfigDirectIRDomainDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به دامنه های ایران تغییر میدهد"
|
||||
"xrayConfigDirectChinaIp" = "ارتباط مستقیم به آیپی های چین"
|
||||
"xrayConfigDirectChinaIpDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به آیپی های چین تغییر میدهد"
|
||||
"xrayConfigDirectChinaDomain" = "ارتباط مستقیم به دامنه های چین"
|
||||
"xrayConfigDirectChinaDomainDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به دامنه های چین تغییر میدهد"
|
||||
"xrayConfigDirectRussiaIp" = "ارتباط مستقیم به آیپی های روسیه"
|
||||
"xrayConfigDirectRussiaIpDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به آیپی های روسیه تغییر میدهد"
|
||||
"xrayConfigDirectRussiaDomain" = "ارتباط مستقیم به دامنه های روسیه"
|
||||
"xrayConfigDirectRussiaDomainDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به دامنه های روسیه تغییر میدهد"
|
||||
"xrayConfigGoogleIPv4" = "استفاده از آیپی ورژن 4 برای اتصال به گوگل"
|
||||
"xrayConfigGoogleIPv4Desc" = "مسیردهی جدید برای اتصال به گوگل با آیپی ورژن 4 اضافه میکند. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"xrayConfigGoogleIPv4Desc" = "مسیردهی جدید برای اتصال به گوگل با آیپی ورژن 4 اضافه میکند"
|
||||
"xrayConfigNetflixIPv4" = "استفاده از آیپی ورژن 4 برای اتصال به نتفلیکس"
|
||||
"xrayConfigNetflixIPv4Desc" = "مسیردهی جدید برای اتصال به نتفلیکس با آیپی ورژن 4 اضافه میکند. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"xrayConfigNetflixIPv4Desc" = "مسیردهی جدید برای اتصال به نتفلیکس با آیپی ورژن 4 اضافه میکند"
|
||||
"xrayConfigGoogleWARP" = "مسیردهی گوگل به WARP"
|
||||
"xrayConfigGoogleWARPDesc" = "مسیردهی جدید برای اتصال به گوگل به WARP اضافه میکند. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"xrayConfigGoogleWARPDesc" = "مسیردهی جدید برای اتصال به گوگل به WARP اضافه میکند"
|
||||
"xrayConfigOpenAIWARP" = "مسیردهی OpenAI (ChatGPT) به WARP"
|
||||
"xrayConfigOpenAIWARPDesc" = "مسیردهی جدید برای اتصال به OpenAI (ChatGPT) به WARP اضافه میکند. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"xrayConfigOpenAIWARPDesc" = "مسیردهی جدید برای اتصال به OpenAI (ChatGPT) به WARP اضافه میکند"
|
||||
"xrayConfigNetflixWARP" = "مسیردهی نتفلیکس به WARP"
|
||||
"xrayConfigNetflixWARPDesc" = "مسیردهی جدید برای اتصال به نتفلیکس به WARP اضافه میکند. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"xrayConfigNetflixWARPDesc" = "مسیردهی جدید برای اتصال به نتفلیکس به WARP اضافه میکند"
|
||||
"xrayConfigSpotifyWARP" = "مسیردهی اسپاتیفای به WARP"
|
||||
"xrayConfigSpotifyWARPDesc" = "مسیردهی جدید برای اتصال به اسپاتیفای به WARP اضافه میکند. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"xrayConfigSpotifyWARPDesc" = "مسیردهی جدید برای اتصال به اسپاتیفای به WARP اضافه میکند"
|
||||
"xrayConfigIRWARP" = "مسیردهی دامنه های ایران به WARP"
|
||||
"xrayConfigIRWARPDesc" = "مسیردهی جدید برای اتصال به دامنه های ایران به WARP اضافه میکند. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"xrayConfigIRWARPDesc" = "مسیردهی جدید برای اتصال به دامنه های ایران به WARP اضافه میکند"
|
||||
"xrayConfigInbounds" = "تنظیمات ورودی"
|
||||
"xrayConfigInboundsDesc" = "میتوانید الگوی تنظیمات را برای ورودی های خاص تنظیم نمایید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"xrayConfigInboundsDesc" = "میتوانید الگوی تنظیمات را برای ورودی های خاص تنظیم نمایید"
|
||||
"xrayConfigOutbounds" = "تنظیمات خروجی"
|
||||
"xrayConfigOutboundsDesc" = "میتوانید الگوی تنظیمات را برای خروجی اینترنت تنظیم نمایید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"xrayConfigOutboundsDesc" = "میتوانید الگوی تنظیمات را برای خروجی اینترنت تنظیم نمایید"
|
||||
"xrayConfigRoutings" = "تنظیمات قوانین مسیریابی"
|
||||
"xrayConfigRoutingsDesc" = "میتوانید الگوی تنظیمات را برای مسیریابی تنظیم نمایید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"xrayConfigRoutingsDesc" = "میتوانید الگوی تنظیمات را برای مسیریابی تنظیم نمایید"
|
||||
"manualLists" = "لیست های دستی"
|
||||
"manualListsDesc" = "فرمت: JSON Array"
|
||||
"manualBlockedIPs" = "لیست آیپی های مسدود شده"
|
||||
"manualBlockedDomains" = "لیست دامنه های مسدود شده"
|
||||
"manualDirectIPs" = "لیست آیپی های مستقیم"
|
||||
"manualDirectDomains" = "لیست دامنه های مستقیم"
|
||||
"manualIPv4Domains" = "لیست دامنههای IPv4"
|
||||
"manualWARPDomains" = "لیست دامنه های WARP"
|
||||
|
||||
[pages.settings.security]
|
||||
"admin" = "مدیر"
|
||||
@@ -322,5 +369,117 @@
|
||||
"modifySettings" = "ویرایش تنظیمات"
|
||||
"getSettings" = "دریافت تنظیمات"
|
||||
"modifyUser" = "ویرایش کاربر"
|
||||
"originalUserPassIncorrect" = "نام کاربری و رمز عبور فعلی اشتباه می باشد ."
|
||||
"userPassMustBeNotEmpty" = "نام کاربری و رمز عبور جدید نمیتواند خالی باشد ."
|
||||
"originalUserPassIncorrect" = "نام کاربری و رمز عبور فعلی اشتباه می باشد "
|
||||
"userPassMustBeNotEmpty" = "نام کاربری و رمز عبور جدید نمیتواند خالی باشد "
|
||||
|
||||
[tgbot]
|
||||
"keyboardClosed" = "❌ کیبورد سفارشی بسته شد!"
|
||||
"noResult" = "❗ نتیجهای یافت نشد!"
|
||||
"noQuery" = "❌ کوئری یافت نشد! لطفاً دستور را مجدداً استفاده کنید!"
|
||||
"wentWrong" = "❌ مشکلی رخ داده است!"
|
||||
"noIpRecord" = "❗ رکورد IP یافت نشد!"
|
||||
"noInbounds" = "❗ هیچ ورودی یافت نشد!"
|
||||
"unlimited" = "♾ نامحدود"
|
||||
"month" = "ماه"
|
||||
"months" = "ماهها"
|
||||
"day" = "روز"
|
||||
"days" = "روزها"
|
||||
"unknown" = "نامشخص"
|
||||
"inbounds" = "ورودیها"
|
||||
"clients" = "کلاینتها"
|
||||
|
||||
[tgbot.commands]
|
||||
"unknown" = "❗ دستور ناشناخته"
|
||||
"pleaseChoose" = "👇 لطفاً انتخاب کنید:\r\n"
|
||||
"help" = "🤖 به این ربات خوش آمدید! این ربات برای ارائه دادههای خاص از سرور طراحی شده است و به شما امکان تغییرات لازم را میدهد.\r\n\r\n"
|
||||
"start" = "👋 سلام <i>{{ .Firstname }}</i>.\r\n"
|
||||
"welcome" = "🤖 به ربات مدیریت <b>{{ .Hostname }}</b> خوش آمدید.\r\n"
|
||||
"status" = "✅ ربات در حالت عادی است!"
|
||||
"usage" = "❗ لطفاً یک متن برای جستجو وارد کنید!"
|
||||
"helpAdminCommands" = "برای جستجوی ایمیل مشتری:\r\n<code>/usage [ایمیل]</code>\r\n \r\nبرای جستجوی ورودیها (با آمار مشتری):\r\n<code>/inbound [توضیح]</code>"
|
||||
"helpClientCommands" = "برای جستجوی آمار، فقط از دستور زیر استفاده کنید:\r\n \r\n<code>/usage [UUID|رمز عبور]</code>\r\n \r\nاز UUID برای vmess/vless و از رمز عبور برای Trojan استفاده کنید."
|
||||
|
||||
[tgbot.messages]
|
||||
"cpuThreshold" = "🔴 میزان استفاده از CPU {{ .Percent }}% بیشتر از آستانه {{ .Threshold }}% است."
|
||||
"selectUserFailed" = "❌ خطا در انتخاب کاربر!"
|
||||
"userSaved" = "✅ کاربر تلگرام ذخیره شد."
|
||||
"loginSuccess" = "✅ با موفقیت به پنل وارد شدید.\r\n"
|
||||
"loginFailed" = "❗️ ورود به پنل ناموفق بود.\r\n"
|
||||
"report" = "🕰 گزارشات زمانبندی شده: {{ .RunTime }}\r\n"
|
||||
"datetime" = "⏰ تاریخ-زمان: {{ .DateTime }}\r\n"
|
||||
"hostname" = "💻 نام میزبان: {{ .Hostname }}\r\n"
|
||||
"version" = "🚀 نسخه X-UI: {{ .Version }}\r\n"
|
||||
"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n"
|
||||
"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n"
|
||||
"ip" = "🌐 آدرس IP: {{ .IP }}\r\n"
|
||||
"ips" = "🔢 آدرسهای IP: \r\n{{ .IPs }}\r\n"
|
||||
"serverUpTime" = "⏳ زمان کارکرد سرور: {{ .UpTime }} {{ .Unit }}\r\n"
|
||||
"serverLoad" = "📈 بار سرور: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
|
||||
"serverMemory" = "📋 حافظه سرور: {{ .Current }}/{{ .Total }}\r\n"
|
||||
"tcpCount" = "🔹 تعداد ترافیک TCP: {{ .Count }}\r\n"
|
||||
"udpCount" = "🔸 تعداد ترافیک UDP: {{ .Count }}\r\n"
|
||||
"traffic" = "🚦 ترافیک: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
|
||||
"xrayStatus" = "ℹ️ وضعیت Xray: {{ .State }}\r\n"
|
||||
"username" = "👤 نام کاربری: {{ .Username }}\r\n"
|
||||
"time" = "⏰ زمان: {{ .Time }}\r\n"
|
||||
"inbound" = "📍 ورودی: {{ .Remark }}\r\n"
|
||||
"port" = "🔌 پورت: {{ .Port }}\r\n"
|
||||
"expire" = "📅 تاریخ انقضا: {{ .DateTime }}\r\n \r\n"
|
||||
"expireIn" = "📅 باقیمانده از انقضا: {{ .Time }}\r\n \r\n"
|
||||
"active" = "💡 فعال: {{ .Enable }}\r\n"
|
||||
"email" = "📧 ایمیل: {{ .Email }}\r\n"
|
||||
"upload" = "🔼 آپلود↑: {{ .Upload }}\r\n"
|
||||
"download" = "🔽 دانلود↓: {{ .Download }}\r\n"
|
||||
"total" = "🔄 کل: {{ .UpDown }} / {{ .Total }}\r\n"
|
||||
"TGUser" = "👤 کاربر تلگرام: {{ .TelegramID }}\r\n"
|
||||
"exhaustedMsg" = "🚨 {{ .Type }} به اتمام رسیده است:\r\n"
|
||||
"exhaustedCount" = "🚨 تعداد {{ .Type }} به اتمام رسیده:\r\n"
|
||||
"disabled" = "🛑 غیرفعال: {{ .Disabled }}\r\n"
|
||||
"depleteSoon" = "🔜 به زودی به پایان خواهد رسید: {{ .Deplete }}\r\n \r\n"
|
||||
"backupTime" = "🗄 زمان پشتیبانگیری: {{ .Time }}\r\n"
|
||||
"refreshedOn" = "🔄🕒 تازهسازی شده در: {{ .Time }}\r\n"
|
||||
|
||||
[tgbot.buttons]
|
||||
"closeKeyboard" = "❌ بستن کیبورد"
|
||||
"cancel" = "❌ لغو"
|
||||
"cancelReset" = "❌ لغو تنظیم مجدد"
|
||||
"cancelIpLimit" = "❌ لغو محدودیت IP"
|
||||
"confirmResetTraffic" = "✅ تأیید تنظیم مجدد ترافیک؟"
|
||||
"confirmClearIps" = "✅ تأیید پاکسازی آدرسهای IP؟"
|
||||
"confirmRemoveTGUser" = "✅ تأیید حذف کاربر تلگرام؟"
|
||||
"dbBackup" = "دریافت پشتیبان پایگاه داده"
|
||||
"serverUsage" = "استفاده از سرور"
|
||||
"getInbounds" = "دریافت ورودیها"
|
||||
"depleteSoon" = "به زودی به پایان خواهد رسید"
|
||||
"clientUsage" = "دریافت آمار کاربر"
|
||||
"commands" = "دستورات"
|
||||
"refresh" = "🔄 تازهسازی"
|
||||
"clearIPs" = "❌ پاکسازی آدرسها"
|
||||
"removeTGUser" = "❌ حذف کاربر تلگرام"
|
||||
"selectTGUser" = "👤 انتخاب کاربر تلگرام"
|
||||
"selectOneTGUser" = "👤 یک کاربر تلگرام را انتخاب کنید:"
|
||||
"resetTraffic" = "📈 تنظیم مجدد ترافیک"
|
||||
"resetExpire" = "📅 تنظیم مجدد تاریخ انقضا"
|
||||
"ipLog" = "🔢 لاگ آدرسهای IP"
|
||||
"ipLimit" = "🔢 محدودیت IP"
|
||||
"setTGUser" = "👤 تنظیم کاربر تلگرام"
|
||||
"toggle" = "🔘 فعال / غیرفعال"
|
||||
|
||||
[tgbot.answers]
|
||||
"errorOperation" = "❗ خطا در عملیات."
|
||||
"getInboundsFailed" = "❌ دریافت ورودیها با خطا مواجه شد."
|
||||
"canceled" = "❌ {{ .Email }} : عملیات لغو شد."
|
||||
"clientRefreshSuccess" = "✅ {{ .Email }} : کلاینت با موفقیت تازهسازی شد."
|
||||
"IpRefreshSuccess" = "✅ {{ .Email }} : آدرسها با موفقیت تازهسازی شدند."
|
||||
"TGIdRefreshSuccess" = "✅ {{ .Email }} : کاربر تلگرام کلاینت با موفقیت تازهسازی شد."
|
||||
"resetTrafficSuccess" = "✅ {{ .Email }} : ترافیک با موفقیت تنظیم مجدد شد."
|
||||
"expireResetSuccess" = "✅ {{ .Email }} : تاریخ انقضا با موفقیت تنظیم مجدد شد."
|
||||
"resetIpSuccess" = "✅ {{ .Email }} : محدودیت آدرس IP {{ .Count }} با موفقیت ذخیره شد."
|
||||
"clearIpSuccess" = "✅ {{ .Email }} : آدرسها با موفقیت پاکسازی شدند."
|
||||
"getIpLog" = "✅ {{ .Email }} : دریافت لاگ آدرسهای IP."
|
||||
"getUserInfo" = "✅ {{ .Email }} : دریافت اطلاعات کاربر تلگرام."
|
||||
"removedTGUserSuccess" = "✅ {{ .Email }} : کاربر تلگرام با موفقیت حذف شد."
|
||||
"enableSuccess" = "✅ {{ .Email }} : با موفقیت فعال شد."
|
||||
"disableSuccess" = "✅ {{ .Email }} : با موفقیت غیرفعال شد."
|
||||
"askToAddUserId" = "پیکربندی شما یافت نشد!\r\nلطفاً از مدیر خود بخواهید که شناسه کاربر تلگرام خود را در پیکربندی (های) خود استفاده کند.\r\n\r\nشناسه کاربری شما: <b>{{ .TgUserID }}</b>"
|
||||
"askToAddUserName" = "پیکربندی شما یافت نشد!\r\nلطفاً از مدیر خود بخواهید که نام کاربری یا شناسه کاربر تلگرام خود را در پیکربندی (های) خود استفاده کند.\r\n\r\nنام کاربری شما: <b>@{{ .TgUserName }}</b>\r\n\r\nشناسه کاربری شما: <b>{{ .TgUserID }}</b>"
|
||||
|
||||
@@ -1,126 +1,127 @@
|
||||
"username" = "имя пользователя"
|
||||
"password" = "пароль"
|
||||
"login" = "логин"
|
||||
"confirm" = "подтвердить"
|
||||
"cancel" = "отмена"
|
||||
"close" = "закрыть"
|
||||
"copy" = "копировать"
|
||||
"copied" = "скопировано"
|
||||
"download" = "скачать"
|
||||
"remark" = "примечание"
|
||||
"enable" = "включить"
|
||||
"protocol" = "протокол"
|
||||
"search" = "поиск"
|
||||
|
||||
"loading" = "загрузка"
|
||||
"second" = "секунда"
|
||||
"minute" = "минута"
|
||||
"hour" = "час"
|
||||
"day" = "день"
|
||||
"check" = "просмотр"
|
||||
"indefinite" = "бессрочно"
|
||||
"unlimited" = "безлимитно"
|
||||
"none" = "пусто"
|
||||
"qrCode" = "QR-код"
|
||||
"info" = "больше информации"
|
||||
"edit" = "изменить"
|
||||
"delete" = "удалить"
|
||||
"reset" = "обнулить"
|
||||
"copySuccess" = "скопировано"
|
||||
"sure" = "да"
|
||||
"username" = "Имя пользователя"
|
||||
"password" = "Пароль"
|
||||
"login" = "Логин"
|
||||
"confirm" = "Подтвердить"
|
||||
"cancel" = "Отмена"
|
||||
"close" = "Закрыть"
|
||||
"copy" = "Копировать"
|
||||
"copied" = "Скопировано"
|
||||
"download" = "Скачать"
|
||||
"remark" = "Примечание"
|
||||
"enable" = "Включить"
|
||||
"protocol" = "Протокол"
|
||||
"search" = "Поиск"
|
||||
"filter" = "Фильтр"
|
||||
"loading" = "Загрузка"
|
||||
"second" = "Секунда"
|
||||
"minute" = "Минута"
|
||||
"hour" = "Час"
|
||||
"day" = "День"
|
||||
"check" = "Проверить"
|
||||
"indefinite" = "Бессрочно"
|
||||
"unlimited" = "Безлимитно"
|
||||
"none" = "Пусто"
|
||||
"qrCode" = "QR код"
|
||||
"info" = "Информация"
|
||||
"edit" = "Изменить"
|
||||
"delete" = "Удалить"
|
||||
"reset" = "Сбросить"
|
||||
"copySuccess" = "Скопировано"
|
||||
"sure" = "Да"
|
||||
"encryption" = "Шифрование"
|
||||
"transmission" = "протокол передачи"
|
||||
"host" = "хост"
|
||||
"path" = "путь"
|
||||
"camouflage" = "маскировка"
|
||||
"status" = "статус"
|
||||
"enabled" = "включено"
|
||||
"disabled" = "отключено"
|
||||
"depleted" = "исчерпано"
|
||||
"depletingSoon" = "почти исчерпано"
|
||||
"domainName" = "домен"
|
||||
"additional" = "допольнительно"
|
||||
"monitor" = "порт IP"
|
||||
"certificate" = "сертификат"
|
||||
"fail" = "неудача"
|
||||
"success" = "успешно"
|
||||
"getVersion" = "узнать версию"
|
||||
"install" = "установка"
|
||||
"clients" = "клиенты"
|
||||
"usage" = "использование"
|
||||
"secretToken" = "секретный токен"
|
||||
"transmission" = "Протокол передачи"
|
||||
"host" = "Хост"
|
||||
"path" = "Путь"
|
||||
"camouflage" = "Маскировка"
|
||||
"status" = "Статус"
|
||||
"enabled" = "Включено"
|
||||
"disabled" = "Отключено"
|
||||
"depleted" = "Исчерпано"
|
||||
"depletingSoon" = "Почти исчерпано"
|
||||
"domainName" = "Домен"
|
||||
"additional" = "Дополнительно"
|
||||
"monitor" = "Порт IP"
|
||||
"certificate" = "Сертификат"
|
||||
"fail" = "Неудачно"
|
||||
"success" = "Успешно"
|
||||
"getVersion" = "Узнать версию"
|
||||
"install" = "Установка"
|
||||
"clients" = "Клиенты"
|
||||
"usage" = "Использование"
|
||||
"secretToken" = "Секретный токен"
|
||||
|
||||
[menu]
|
||||
"dashboard" = "статус системы"
|
||||
"inbounds" = "пользователи"
|
||||
"settings" = "настройки"
|
||||
"logout" = "выход"
|
||||
"link" = "другое"
|
||||
"dashboard" = "Статус системы"
|
||||
"inbounds" = "Подключения"
|
||||
"settings" = "Настройки панели"
|
||||
"logout" = "Выход"
|
||||
"link" = "Прочее"
|
||||
|
||||
[pages.login]
|
||||
"title" = "логин"
|
||||
"loginAgain" = "Время пребывания в сети вышло. Пожалуйста, войдите в систему снова."
|
||||
"title" = "Логин"
|
||||
"loginAgain" = "Время пребывания в сети вышло. Пожалуйста, войдите в систему снова"
|
||||
|
||||
[pages.login.toasts]
|
||||
"invalidFormData" = "Недопустимый формат данных."
|
||||
"emptyUsername" = "Введите имя пользователя."
|
||||
"emptyPassword" = "Введите пароль."
|
||||
"wrongUsernameOrPassword" = "Неверное имя пользователя или пароль."
|
||||
"successLogin" = "успешный вход"
|
||||
"invalidFormData" = "Недопустимый формат данных"
|
||||
"emptyUsername" = "Введите имя пользователя"
|
||||
"emptyPassword" = "Введите пароль"
|
||||
"wrongUsernameOrPassword" = "Неверное имя пользователя или пароль"
|
||||
"successLogin" = "Успешный вход"
|
||||
|
||||
[pages.index]
|
||||
"title" = "статус системы"
|
||||
"memory" = "память"
|
||||
"hard" = "жесткий диск"
|
||||
"xrayStatus" = "статус Xray"
|
||||
"stopXray" = "стоп"
|
||||
"restartXray" = "рестарт Xray"
|
||||
"xraySwitch" = "переключить версию"
|
||||
"xraySwitchClick" = "Выберите желаемую версию."
|
||||
"xraySwitchClickDesk" = "Выбирайте внимательно, так как старые версии могут быть несовместимы с текущими конфигурациями."
|
||||
"operationHours" = "Часы работы"
|
||||
"operationHoursDesc" = "Аптайм системы: время системы в сети."
|
||||
"title" = "Статус системы"
|
||||
"memory" = "Память"
|
||||
"hard" = "Жесткий диск"
|
||||
"xrayStatus" = "Статус Xray"
|
||||
"stopXray" = "Остановить Xray"
|
||||
"restartXray" = "Рестарт Xray"
|
||||
"xraySwitch" = "Переключить версию"
|
||||
"xraySwitchClick" = "Выберите желаемую версию"
|
||||
"xraySwitchClickDesk" = "Выбирайте внимательно, так как старые версии могут быть несовместимы с текущими конфигурациями"
|
||||
"operationHours" = "Время работы системы"
|
||||
"systemLoad" = "Системная нагрузка"
|
||||
"connectionCount" = "количество соединений"
|
||||
"connectionCountDesc" = "Всего подключений по всем сетям»."
|
||||
"upSpeed" = "Общая скорость upload."
|
||||
"downSpeed" = "Общая скорость download."
|
||||
"totalSent" = "Общий объем загруженных данных с момента запуска системы"
|
||||
"totalReceive" = "Общий объем полученных данных с момента запуска системы.."
|
||||
"xraySwitchVersionDialog" = "переключить версию Xray"
|
||||
"systemLoadDesc" = "средняя загрузка системы за последние 1, 5 и 15 минут"
|
||||
"connectionTcpCountDesc" = "Всего подключений TCP по всем сетевым картам."
|
||||
"connectionUdpCountDesc" = "Общее количество подключений UDP по всем сетевым картам."
|
||||
"connectionCount" = "Количество соединений"
|
||||
"upSpeed" = "Общая скорость upload для всех сетей"
|
||||
"downSpeed" = "Общая скорость download для всех сетей"
|
||||
"totalSent" = "Общий объем загруженных данных для всех сетей с момента запуска системы"
|
||||
"totalReceive" = "Общий объем полученных данных для всех сетей с момента запуска системы."
|
||||
"xraySwitchVersionDialog" = "Переключить версию Xray"
|
||||
"xraySwitchVersionDialogDesc" = "Вы точно хотите сменить версию Xray?"
|
||||
"dontRefresh" = "Установка. Не обновляйте эту страницу."
|
||||
"dontRefresh" = "Идёт установка, пожалуйста не обновляйте эту страницу"
|
||||
"logs" = "Логи"
|
||||
"config" = "Конфиг"
|
||||
"backup" = "Бекап и восстановление"
|
||||
"backupTitle" = "База данных бекапа и восстановления"
|
||||
"backupDescription" = "Не забудьте сделать резервную копию перед импортом новой базы данных."
|
||||
"config" = "Конфигурация"
|
||||
"backup" = "Бэкап и восстановление"
|
||||
"backupTitle" = "База данных бэкапа и восстановления"
|
||||
"backupDescription" = "Не забудьте сделать резервную копию перед импортом новой базы данных"
|
||||
"exportDatabase" = "Экспорт базы данных"
|
||||
"importDatabase" = "Импорт базы данных"
|
||||
|
||||
[pages.inbounds]
|
||||
"title" = "пользователи"
|
||||
"totalDownUp" = "Всего входящих/исходящих"
|
||||
"title" = "Подключения"
|
||||
"totalDownUp" = "Всего uploads/downloads"
|
||||
"totalUsage" = "Всего использовано"
|
||||
"inboundCount" = "Количество пользователей"
|
||||
"inboundCount" = "Количество подключений"
|
||||
"operate" = "Меню"
|
||||
"enable" = "Включить"
|
||||
"remark" = "Примечание"
|
||||
"protocol" = "Протокол"
|
||||
"port" = "Порт"
|
||||
"traffic" = "Траффик"
|
||||
"traffic" = "Трафик"
|
||||
"details" = "Подробнее"
|
||||
"transportConfig" = "Перенести"
|
||||
"transportConfig" = "Транспорт"
|
||||
"expireDate" = "Дата окончания"
|
||||
"resetTraffic" = "Обнулить траффик"
|
||||
"addInbound" = "Добавить пользователя"
|
||||
"resetTraffic" = "Сбросить трафик"
|
||||
"addInbound" = "Добавить подключение"
|
||||
"generalActions" = "Общие действия"
|
||||
"create" = "Создать"
|
||||
"update" = "Обновить"
|
||||
"modifyInbound" = "Изменить данные"
|
||||
"deleteInbound" = "Удалить пользователя"
|
||||
"deleteInboundContent" = "Подтвердите удаление пользователя?"
|
||||
"resetTrafficContent" = "Подтвердите обнуление траффика?"
|
||||
"modifyInbound" = "Изменить подключение"
|
||||
"deleteInbound" = "Удалить подключение"
|
||||
"deleteInboundContent" = "Подтвердите удаление подключения?"
|
||||
"resetTrafficContent" = "Подтвердите сброс трафика?"
|
||||
"copyLink" = "Копировать ключ"
|
||||
"address" = "Адрес"
|
||||
"network" = "Сеть"
|
||||
@@ -130,7 +131,7 @@
|
||||
"monitorDesc" = "Оставьте пустым по умолчанию"
|
||||
"meansNoLimit" = "Значит без ограничений"
|
||||
"totalFlow" = "Общий расход"
|
||||
"leaveBlankToNeverExpire" = "Оставьте пустым = бессрочно"
|
||||
"leaveBlankToNeverExpire" = "Оставьте пустым, чтобы не истекало"
|
||||
"noRecommendKeepDefault" = "Нет требований для сохранения настроек по умолчанию"
|
||||
"certificatePath" = "Путь файла сертификата"
|
||||
"certificateContent" = "Содержимое файла сертификата"
|
||||
@@ -138,39 +139,37 @@
|
||||
"publicKeyContent" = "Содержимое публичного ключа"
|
||||
"keyPath" = "Путь к приватному ключу"
|
||||
"keyContent" = "Содержимое приватного ключа"
|
||||
"clickOnQRcode" = "Нажмите на QR-код, чтобы скопировать"
|
||||
"clickOnQRcode" = "Нажмите на QR код, чтобы скопировать"
|
||||
"client" = "Клиент"
|
||||
"export" = "Поделиться ключом"
|
||||
"export" = "Экспорт ключей"
|
||||
"clone" = "Клонировать"
|
||||
"cloneInbound" = "Клонировать пользователя"
|
||||
"cloneInboundContent" = "Все настройки этого пользователя, кроме порта, порт прослушки и клиентов, будут клонированы."
|
||||
"cloneInbound" = "Клонировать"
|
||||
"cloneInboundContent" = "Все настройки этого подключения, кроме порта, IP прослушки и клиентов, будут клонированы"
|
||||
"cloneInboundOk" = "Клонировано"
|
||||
"resetAllTraffic" = "Обнулить весь траффик"
|
||||
"resetAllTrafficTitle" = "Обнуление всего траффика"
|
||||
"resetAllTrafficContent" = "Подтверждаете обнуление всего траффика пользователей?"
|
||||
"resetAllTrafficOkText" = "Подтвердить"
|
||||
"resetAllTrafficCancelText" = "Отмена"
|
||||
"resetInboundClientTraffics" = "Обнулить траффик пользователей"
|
||||
"resetInboundClientTrafficTitle" = "Обнуление траффика пользователей"
|
||||
"resetInboundClientTrafficContent" = "Вы уверены, что хотите обнулить весь трафик для этих пользователей?"
|
||||
"resetAllClientTraffics" = "Обнулить весь траффик пользователей"
|
||||
"resetAllClientTrafficTitle" = "Обнуление всего траффика пользователей"
|
||||
"resetAllClientTrafficContent" = "Подтверждаете обнуление всего траффика пользователей?"
|
||||
"resetAllTraffic" = "Сбросить трафик всех подключений"
|
||||
"resetAllTrafficTitle" = "Сброс трафика всех подключений"
|
||||
"resetAllTrafficContent" = "Подтверждаете сброс трафика всех подключений?"
|
||||
"resetInboundClientTraffics" = "Сбросить трафик пользователей"
|
||||
"resetInboundClientTrafficTitle" = "Сброс трафика пользователей"
|
||||
"resetInboundClientTrafficContent" = "Вы уверены, что хотите сбросить весь трафик для этих пользователей?"
|
||||
"resetAllClientTraffics" = "Сбросить трафик всех пользователей"
|
||||
"resetAllClientTrafficTitle" = "Сброс трафика всех пользователей"
|
||||
"resetAllClientTrafficContent" = "Подтверждаете сброс трафика всех пользователей?"
|
||||
"delDepletedClients" = "Удалить отключенных пользователей"
|
||||
"delDepletedClientsTitle" = "Удаление отключенных пользователей"
|
||||
"delDepletedClientsContent" = "Подтверждаете удаление отключенных пользователей?"
|
||||
"email" = "Email"
|
||||
"emailDesc" = "Пожалуйста, укажите уникальный Email"
|
||||
"IPLimit" = "ограничение по IP"
|
||||
"IPLimitDesc" = "Отключить ключ, если подключено больше введенного значения (введите 0, чтобы отключить ограничение IP-адресов)."
|
||||
"IPLimit" = "Ограничение по IP"
|
||||
"IPLimitDesc" = "Сбросить подключение, если подключено больше введенного значения (введите 0, чтобы отключить ограничение IP адресов)"
|
||||
"IPLimitlog" = "IP лог"
|
||||
"IPLimitlogDesc" = "Лог IP-адресов (перед включением лога IP-адресов, вы должны очистить список)."
|
||||
"IPLimitlogclear" = "Очистить список"
|
||||
"IPLimitlogDesc" = "Лог IP адресов (перед включением лога IP адресов, вы должны очистить список)"
|
||||
"IPLimitlogclear" = "Очистить лог"
|
||||
"setDefaultCert" = "Установить сертификат с панели"
|
||||
"xtlsDesc" = "Версия Xray должна быть не ниже 1.7.5"
|
||||
"realityDesc" = "Версия Xray должна быть 1.8.0 или выше."
|
||||
"telegramDesc" = "используйте Telegram ID (вы можете получить его у @userinfobot)"
|
||||
"subscriptionDesc" = "вы можете найти свою ссылку подписки в разделе «Подробнее», также вы можете использовать одно и то же имя для нескольких конфигов."
|
||||
"realityDesc" = "Версия Xray должна быть не ниже 1.8.0"
|
||||
"telegramDesc" = "Используйте Telegram ID без @ или ID пользователя (вы можете получить его у @userinfobot)"
|
||||
"subscriptionDesc" = "Вы можете найти свою ссылку подписки в разделе «Подробнее», также вы можете использовать одно и то же имя для нескольких конфигураций"
|
||||
|
||||
[pages.client]
|
||||
"add" = "Добавить пользователя"
|
||||
@@ -184,7 +183,7 @@
|
||||
"last" = "Последний"
|
||||
"prefix" = "Префикс"
|
||||
"postfix" = "Постфикс"
|
||||
"delayedStart" = "Начать со времени первого подключения"
|
||||
"delayedStart" = "Начать со момента первого подключения"
|
||||
"expireDays" = "Срок действия"
|
||||
"days" = "дней"
|
||||
|
||||
@@ -192,18 +191,18 @@
|
||||
"obtain" = "Получить"
|
||||
|
||||
[pages.inbounds.stream.general]
|
||||
"requestHeader" = "Требуется заголовок"
|
||||
"requestHeader" = "Заголовок запроса"
|
||||
"name" = "Имя"
|
||||
"value" = "Значение"
|
||||
|
||||
[pages.inbounds.stream.tcp]
|
||||
"requestVersion" = "Требуется версия"
|
||||
"requestMethod" = "Требуется метод"
|
||||
"requestPath" = "Требуется путь"
|
||||
"responseVersion" = "Указать версию"
|
||||
"responseStatus" = "Указать статус"
|
||||
"responseStatusDescription" = "Указать примечание статуса"
|
||||
"responseHeader" = "Указать заголовок"
|
||||
"requestVersion" = "Версия запроса"
|
||||
"requestMethod" = "Метод запроса"
|
||||
"requestPath" = "Путь запроса"
|
||||
"responseVersion" = "Версия ответа"
|
||||
"responseStatus" = "Статус ответа"
|
||||
"responseStatusDescription" = "Описание статуса ответа"
|
||||
"responseHeader" = "Заголовок ответа"
|
||||
|
||||
[pages.inbounds.stream.quic]
|
||||
"encryption" = "Шифрование"
|
||||
@@ -211,106 +210,152 @@
|
||||
[pages.settings]
|
||||
"title" = "Настройки"
|
||||
"save" = "Сохранить"
|
||||
"restartPanel" = "Рестарт панели"
|
||||
"restartPanelDesc" = "Подтвердите рестарт панели? ОК для рестарта панели через 3 сек. Если вы не можете пользоваться панелью после рестарта, пожалуйста, посмотрите лог панели на сервере"
|
||||
"infoDesc" = "Каждое сделанное здесь изменение необходимо сохранить. Пожалуйста, перезапустите панель, чтобы изменения вступили в силу"
|
||||
"restartPanel" = "Перезапуск панели"
|
||||
"restartPanelDesc" = "Подтвердите перезапуск панели? ОК для перезапуска панели через 3 сек. Если вы не можете пользоваться панелью после перезапуска, пожалуйста, посмотрите лог панели на сервере"
|
||||
"actions" = "Действия"
|
||||
"resetDefaultConfig" = "Сбросить всё по-умолчанию"
|
||||
"resetDefaultConfig" = "Сбросить на конфигурацию по-умолчанию"
|
||||
"panelSettings" = "Настройки панели"
|
||||
"securitySettings" = "Настройки безопасности"
|
||||
"xrayConfiguration" = "Конфигурация Xray"
|
||||
"TGBotSettings" = "Настройки Телеграм-бота"
|
||||
"panelListeningIP" = "IP-порт панели"
|
||||
"panelListeningIPDesc" = "Оставьте пустым для работы с любого IP. Перезагрузите панель для применения настроек."
|
||||
"TGBotSettings" = "Настройки Telegram бота"
|
||||
"panelListeningIP" = "IP адрес панели"
|
||||
"panelListeningIPDesc" = "Оставьте пустым для подключения с любого IP"
|
||||
"panelPort" = "Порт панели"
|
||||
"panelPortDesc" = "Перезагрузите панель для применения настроек."
|
||||
"panelPortDesc" = "Порт, используемый для отображения этой панели"
|
||||
"publicKeyPath" = "Путь к файлу публичного ключа сертификата панели"
|
||||
"publicKeyPathDesc" = "Введите полный путь, начинающийся с «/». Перезагрузите панель для применения настроек."
|
||||
"publicKeyPathDesc" = "Введите полный путь, начинающийся с "
|
||||
"privateKeyPath" = "Путь к файлу приватного ключа сертификата панели"
|
||||
"privateKeyPathDesc" = "Введите полный путь, начинающийся с «/». Перезагрузите панель для применения настроек."
|
||||
"panelUrlPath" = "Корневой путь URL-адреса панели"
|
||||
"panelUrlPathDesc" = "Должен начинаться с «/» и заканчиваться на «/». Перезагрузите панель для применения настроек."
|
||||
"oldUsername" = "Имя пользователя сейчас"
|
||||
"currentPassword" = "Пароль сейчас"
|
||||
"privateKeyPathDesc" = "Введите полный путь, начинающийся с "
|
||||
"panelUrlPath" = "Корневой путь URL адреса панели"
|
||||
"panelUrlPathDesc" = "Должен начинаться с «/» и заканчиваться на "
|
||||
"oldUsername" = "Текущее имя пользователя"
|
||||
"currentPassword" = "Текущий пароль"
|
||||
"newUsername" = "Новое имя пользователя"
|
||||
"newPassword" = "Новый пароль"
|
||||
"telegramBotEnable" = "Включить Телеграм-бота"
|
||||
"telegramBotEnableDesc" = "Перезагрузите панель для применения настроек."
|
||||
"telegramToken" = "Токен Телеграм-бота"
|
||||
"telegramTokenDesc" = "Перезагрузите панель для применения настроек."
|
||||
"telegramChatId" = "Телеграм-ID админа бота"
|
||||
"telegramChatIdDesc" = "Если несколько Телеграм-ID, разделить запятой. Используйте @userinfobot, чтобы получить Телеграм-ID. Перезагрузите панель для применения настроек."
|
||||
"telegramNotifyTime" = "Частота уведомлений телеграм-бота"
|
||||
"telegramNotifyTimeDesc" = "Используйте формат Crontab. Перезагрузите панель для применения настроек."
|
||||
"telegramBotEnable" = "Включить Telegram бота"
|
||||
"telegramBotEnableDesc" = "Подключайтесь к функциям этой панели через Telegram бота"
|
||||
"telegramToken" = "Токен Telegram бота"
|
||||
"telegramTokenDesc" = "Необходимо получить токен у менеджера ботов Telegram @botfather"
|
||||
"telegramChatId" = "Telegram ID админа бота"
|
||||
"telegramChatIdDesc" = "Несколько Telegram ID, разделённых запятой. Используйте @userinfobot, чтобы получить Telegram ID"
|
||||
"telegramNotifyTime" = "Частота уведомлений бота Telegram"
|
||||
"telegramNotifyTimeDesc" = "Используйте формат времени Crontab"
|
||||
"tgNotifyBackup" = "Резервное копирование базы данных"
|
||||
"tgNotifyBackupDesc" = "Включать файл резервной копии базы данных с уведомлением об отчете. Перезагрузите панель для применения настроек."
|
||||
"tgNotifyBackupDesc" = "Включать файл резервной копии базы данных с уведомлением об отчете"
|
||||
"sessionMaxAge" = "Продолжительность сессии"
|
||||
"sessionMaxAgeDesc" = "Продолжительность сессии в системе (значение: минута)"
|
||||
"expireTimeDiff" = "Порог истечения срока сессии для уведомления"
|
||||
"expireTimeDiffDesc" = "Получение уведомления об истечении срока действия сессии до достижения порогового значения (значение: день)"
|
||||
"trafficDiff" = "Порог траффика для уведомления"
|
||||
"trafficDiffDesc" = "Получение уведомления об исчерпании трафика до достижения порога (значение: ГБ)"
|
||||
"trafficDiff" = "Порог трафика для уведомления"
|
||||
"trafficDiffDesc" = "Получение уведомления об исчерпании трафика до достижения порога (значение: Гб)"
|
||||
"tgNotifyCpu" = "Порог нагрузки на ЦП для уведомления"
|
||||
"tgNotifyCpuDesc" = "Получение уведомления, если нагрузка на ЦП превышает этот порог (значение:%)"
|
||||
"timeZone" = "Временная зона"
|
||||
"timeZoneDesc" = "Запланированные задачи выполняются в соответствии со временем в этом часовом поясе. Перезагрузите панель для применения настроек."
|
||||
"tgNotifyCpuDesc" = "Получение уведомления, если нагрузка на ЦП превышает этот порог (значение: %)"
|
||||
"timeZone" = "Часовой пояс"
|
||||
"timeZoneDesc" = "Запланированные задачи выполняются в соответствии со временем в этом часовом поясе"
|
||||
"subSettings" = "Подписка"
|
||||
"subEnable" = "Включить службу"
|
||||
"subEnableDesc" = "Функция подписки с отдельной конфигурацией"
|
||||
"subListen" = "Прослушивание IP"
|
||||
"subListenDesc" = "Оставьте пустым по умолчанию, чтобы отслеживать все IP-адреса"
|
||||
"subPort" = "Порт подписки"
|
||||
"subPortDesc" = "Номер порта для обслуживания службы подписки не должен использоваться на сервере"
|
||||
"subCertPath" = "Путь к файлу открытого ключа сертификата подписки"
|
||||
"subCertPathDesc" = "Введите абсолютный путь, начинающийся с '/'"
|
||||
"subKeyPath" = "Путь к файлу закрытого ключа сертификата подписки"
|
||||
"subKeyPathDesc" = "Введите абсолютный путь, начинающийся с '/'"
|
||||
"subPath" = "Корневой путь URL-адреса подписки"
|
||||
"subPathDesc" = "Должен начинаться с '/' и заканчиваться на '/'"
|
||||
"subDomain" = "Домен прослушивания"
|
||||
"subDomainDesc" = "Оставьте пустым по умолчанию, чтобы отслеживать все домены и IP-адреса"
|
||||
"subUpdates" = "Интервалы обновления подписки"
|
||||
"subUpdatesDesc" = "Часовой интервал между обновлениями в клиентском приложении"
|
||||
|
||||
[pages.settings.templates]
|
||||
"title" = "Шаблоны"
|
||||
"basicTemplate" = "Базовые шаблоны"
|
||||
"advancedTemplate" = "Расширенные шаблоны"
|
||||
"completeTemplate" = "Конфигурация шаблона"
|
||||
"basicTemplate" = "Базовый шаблон"
|
||||
"advancedTemplate" = "Расширенный шаблон"
|
||||
"completeTemplate" = "Полный шаблон"
|
||||
"generalConfigs" = "Основные настройки"
|
||||
"generalConfigsDesc" = "Эти параметры не позволят пользователям подключаться к определенным протоколам и веб-сайтам."
|
||||
"countryConfigs" = "Настройки по гео"
|
||||
"countryConfigsDesc" = "Эти параметры не позволят пользователям подключаться к доменам определенной страны."
|
||||
"ipv4Configs" = "Настройки IPv4 "
|
||||
"ipv4ConfigsDesc" = "Эти параметры будут маршрутизироваться к целевым доменам только через IPv4."
|
||||
"warpConfigs" = "Настройки WARP"
|
||||
"warpConfigsDesc" = "Внимание: перед использованием этих параметров установите WARP в режиме прокси-сервера socks5 на свой сервер, следуя инструкциям на GitHub панели. WARP будет направлять трафик на веб-сайты через серверы Cloudflare."
|
||||
"generalConfigsDesc" = "Эти параметры описывают общие настройки"
|
||||
"blockConfigs" = "Блокировка конфигураций"
|
||||
"blockConfigsDesc" = "Эти параметры не позволят пользователям подключаться к определенным протоколам и веб-сайтам"
|
||||
"blockCountryConfigs" = "Конфигурации блокировки страны"
|
||||
"blockCountryConfigsDesc" = "Эти параметры не позволят пользователям подключаться к доменам определенной страны"
|
||||
"directCountryConfigs" = "Настройки прямого подключения для страны"
|
||||
"directCountryConfigsDesc" = "Эти параметры позволят пользователям подключаться напрямую к доменам определенной страны"
|
||||
"ipv4Configs" = "Настройки IPv4"
|
||||
"ipv4ConfigsDesc" = "Эти параметры позволят пользователям маршрутизироваться к целевым доменам только через IPv4"
|
||||
"warpConfigs" = "Настройки WARP"
|
||||
"warpConfigsDesc" = "Внимание: перед использованием этих параметров установите WARP в режиме прокси-сервера socks5 на свой сервер, следуя инструкциям на GitHub панели. WARP будет направлять трафик на веб-сайты через серверы Cloudflare"
|
||||
"xrayConfigTemplate" = "Шаблон конфигурации Xray"
|
||||
"xrayConfigTemplateDesc" = "Создание файла конфигурации Xray на основе этого шаблона. Перезагрузите панель для применения настроек."
|
||||
"xrayConfigTorrent" = "Запретить использование BitTorrent"
|
||||
"xrayConfigTorrentDesc" = "Измените конфигурацию, чтобы пользователи не использовали BitTorrent. Перезагрузите панель для применения настроек."
|
||||
"xrayConfigPrivateIp" = "Запрет частных диапазонов IP-адресов для подключения"
|
||||
"xrayConfigPrivateIpDesc" = "Измените конфигурацию, чтобы избежать подключения к диапазонам частных IP-адресов. Перезагрузите панель для применения настроек."
|
||||
"xrayConfigAds" = "Бокировка рекламы"
|
||||
"xrayConfigAdsDesc" = "Измените конфигурацию, чтобы заблокировать рекламу. Перезагрузите панель для применения настроек."
|
||||
"xrayConfigPorn" = "Блокировка порносайтов"
|
||||
"xrayConfigPornDesc" = "Измените конфигурацию, чтобы отключить подключения к порносайтам. Перезагрузите панель для применения настроек."
|
||||
"xrayConfigTemplateDesc" = "Создание файла конфигурации Xray на основе этого шаблона"
|
||||
"xrayConfigFreedomStrategy" = "Настройка стратегии протокола Freedom"
|
||||
"xrayConfigFreedomStrategyDesc" = "Установка стратегию вывода сети в протоколе Freedom"
|
||||
"xrayConfigRoutingStrategy" = "Настройка стратегии маршрутизации доменов"
|
||||
"xrayConfigRoutingStrategyDesc" = "Установка общей стратегии маршрутизации разрешения DNS"
|
||||
"xrayConfigTorrent" = "Запрет использования BitTorrent"
|
||||
"xrayConfigTorrentDesc" = "Изменение шаблона конфигурации, для предупреждения использования BitTorrent пользователями"
|
||||
"xrayConfigPrivateIp" = "Запрет частных диапазонов IP адресов для подключения"
|
||||
"xrayConfigPrivateIpDesc" = "Изменение шаблона конфигурации, для предупреждения подключения к диапазонам частных IP адресов"
|
||||
"xrayConfigAds" = "Блокировка рекламы"
|
||||
"xrayConfigAdsDesc" = "Изменение конфигурации, для блокировки рекламы"
|
||||
"xrayConfigFamily" = "Блокировать вредоносное ПО и контент для взрослых"
|
||||
"xrayConfigFamilyDesc" = "Резольверы DNS для блокировки вредоносных программ и контента для взрослых для защиты семьи"
|
||||
"xrayConfigSpeedtest" = "Блокировать сайты для проверки скорости"
|
||||
"xrayConfigSpeedtestDesc" = "Измените шаблон конфигурации, чтобы избежать подключения к веб-сайтам для тестирования скорости. Перезапустите панель, чтобы применить изменения."
|
||||
"xrayConfigIRIp" = "Отключить подключение к диапазонам IP-адресов Ирана"
|
||||
"xrayConfigIRIpDesc" = "Измените конфигурацию, чтобы отключить подключение к диапазонам IP-адресов Ирана. Перезагрузите панель для применения настроек."
|
||||
"xrayConfigIRDomain" = "Отключить подключение к доменам Ирана"
|
||||
"xrayConfigIRDomainDesc" = "Измените конфигурацию, чтобы отключить подключение к доменам Ирана. Перезагрузите панель для применения настроек."
|
||||
"xrayConfigChinaIp" = "Отключить подключение к диапазонам IP-адресов Китая"
|
||||
"xrayConfigChinaIpDesc" = "Измените конфигурацию, чтобы отключить подключение к диапазонам IP-адресов Китая. Перезагрузите панель для применения настроек."
|
||||
"xrayConfigChinaDomain" = "Отключить подключение к доменам Китая"
|
||||
"xrayConfigChinaDomainDesc" = "Измените конфигурацию, чтобы отключить подключение к доменам Китая. Перезагрузите панель для применения настроек."
|
||||
"xrayConfigRussiaIp" = "Отключить подключение к диапазонам IP-адресов России"
|
||||
"xrayConfigRussiaIpDesc" = "Измените конфигурацию, чтобы отключить соединения с диапазонами IP-адресов России. Перезагрузите панель для применения настроек."
|
||||
"xrayConfigRussiaDomain" = "Отключить подключение к доменам России"
|
||||
"xrayConfigRussiaDomainDesc" = "Измените конфигурацию, чтобы избежать подключения к доменам России. Перезагрузите панель для применения настроек."
|
||||
"xrayConfigSpeedtestDesc" = "Изменение шаблона конфигурации, для предупреждения подключения к веб-сайтам для тестирования скорости"
|
||||
"xrayConfigIRIp" = "Заблокировать подключения к диапазонам IP адресов Ирана"
|
||||
"xrayConfigIRIpDesc" = "Изменение конфигурации, чтобы заблокировать подключения к диапазонам IP адресов Ирана"
|
||||
"xrayConfigIRDomain" = "Заблокировать подключения к доменам Ирана"
|
||||
"xrayConfigIRDomainDesc" = "Изменение конфигурации, чтобы заблокировать подключения к доменам Ирана"
|
||||
"xrayConfigChinaIp" = "Заблокировать подключения к диапазонам IP адресов Китая"
|
||||
"xrayConfigChinaIpDesc" = "Изменение конфигурации, чтобы заблокировать подключения к диапазонам IP адресов Китая"
|
||||
"xrayConfigChinaDomain" = "Заблокировать подключения к доменам Китая"
|
||||
"xrayConfigChinaDomainDesc" = "Изменение конфигурации, чтобы заблокировать подключения к доменам Китая"
|
||||
"xrayConfigRussiaIp" = "Заблокировать подключения к диапазонам IP адресов России"
|
||||
"xrayConfigRussiaIpDesc" = "Изменение конфигурации, чтобы заблокировать подключения к диапазонами IP адресов России"
|
||||
"xrayConfigRussiaDomain" = "Заблокировать подключения к доменам России"
|
||||
"xrayConfigRussiaDomainDesc" = "Изменение конфигурации, чтобы заблокировать подключения к доменам России"
|
||||
"xrayConfigDirectIRIp" = "Прямое подключения к диапазонам IP адресов Ирана"
|
||||
"xrayConfigDirectIRIpDesc" = "Изменение шаблона конфигурации для прямого подключения к диапазонам IP адресов Ирана"
|
||||
"xrayConfigDirectIRDomain" = "Прямое подключение к доменам Ирана"
|
||||
"xrayConfigDirectIRDomainDesc" = "Изменение шаблон конфигурации для прямого подключения к доменам Ирана"
|
||||
"xrayConfigDirectChinaIp" = "Прямое подключение к диапазонам IP адресов Китая"
|
||||
"xrayConfigDirectChinaIpDesc" = "Изменение шаблона конфигурации для прямого подключения к диапазонам IP адресов Китая"
|
||||
"xrayConfigDirectChinaDomain" = "Прямое подключение к доменам Китая"
|
||||
"xrayConfigDirectChinaDomainDesc" = "Изменение шаблона конфигурации для прямого подключения к доменам Китая"
|
||||
"xrayConfigDirectRussiaIp" = "Прямое подключение к диапазонам IP адресов России"
|
||||
"xrayConfigDirectRussiaIpDesc" = "Изменение шаблона конфигурации для прямого подключения к диапазонам IP адресов России"
|
||||
"xrayConfigDirectRussiaDomain" = "Прямое подключение к доменам России"
|
||||
"xrayConfigDirectRussiaDomainDesc" = "Изменение шаблона конфигурации для прямого подключения к доменам России"
|
||||
"xrayConfigGoogleIPv4" = "Использовать IPv4 для Google"
|
||||
"xrayConfigGoogleIPv4Desc" = "Применить маршрутизацию Google для подключения к IPv4. Перезагрузите панель для применения настроек."
|
||||
"xrayConfigGoogleIPv4Desc" = "Добавить маршрутизацию для Google для подключения к IPv4"
|
||||
"xrayConfigNetflixIPv4" = "Использовать IPv4 для Netflix"
|
||||
"xrayConfigNetflixIPv4Desc" = "Применить маршрутизацию Netflix для подключения к IPv4. Перезагрузите панель для применения настроек."
|
||||
"xrayConfigGoogleWARP" = "Маршрутизация Google через WARP."
|
||||
"xrayConfigGoogleWARPDesc" = "Применить маршрутизацию Google через WARP. Перезагрузите панель для применения настроек."
|
||||
"xrayConfigOpenAIWARP" = "Маршрут OpenAI (ChatGPT) через WARP."
|
||||
"xrayConfigOpenAIWARPDesc" = "Применить маршрутизацию для OpenAI (ChatGPT) через WARP. Перезагрузите панель для применения настроек."
|
||||
"xrayConfigNetflixWARP" = "Маршрутизация Netflix через WARP."
|
||||
"xrayConfigNetflixWARPDesc" = "Применить маршрутизацию Netflix через WARP. Перезагрузите панель для применения настроек."
|
||||
"xrayConfigSpotifyWARP" = "Маршрутизация Spotify через WARP."
|
||||
"xrayConfigSpotifyWARPDesc" = "Применить маршрутизацию Spotify через WARP. Перезагрузите панель для применения настроек."
|
||||
"xrayConfigIRWARP" = "Маршрутизация доменов Ирана через WARP."
|
||||
"xrayConfigIRWARPDesc" = "Применить маршрутизацию для доменов Ирана через WARP. Перезагрузите панель для применения настроек."
|
||||
"xrayConfigNetflixIPv4Desc" = "Добавить маршрутизацию для Netflix для подключения к IPv4"
|
||||
"xrayConfigGoogleWARP" = "Маршрутизация Google через WARP"
|
||||
"xrayConfigGoogleWARPDesc" = "Добавить маршрутизацию для Google через WARP"
|
||||
"xrayConfigOpenAIWARP" = "Маршрутизация OpenAI (ChatGPT) через WARP"
|
||||
"xrayConfigOpenAIWARPDesc" = "Добавить маршрутизацию для OpenAI (ChatGPT) через WARP"
|
||||
"xrayConfigNetflixWARP" = "Маршрутизация Netflix через WARP"
|
||||
"xrayConfigNetflixWARPDesc" = "Добавить маршрутизацию для Netflix через WARP"
|
||||
"xrayConfigSpotifyWARP" = "Маршрутизация Spotify через WARP"
|
||||
"xrayConfigSpotifyWARPDesc" = "Добавить маршрутизацию для Spotify через WARP"
|
||||
"xrayConfigIRWARP" = "Маршрутизация доменов Ирана через WARP"
|
||||
"xrayConfigIRWARPDesc" = "Добавить маршрутизацию для доменов Ирана через WARP"
|
||||
"xrayConfigInbounds" = "Конфигурация подключений"
|
||||
"xrayConfigInboundsDesc" = "Изменение шаблона конфигурации, для подключения определенных пользователей. Перезагрузите панель для применения настроек."
|
||||
"xrayConfigInboundsDesc" = "Изменение шаблона конфигурации, для подключения определенных пользователей"
|
||||
"xrayConfigOutbounds" = "Конфигурация исходящих"
|
||||
"xrayConfigOutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить исходящие пути для этого сервера. Перезагрузите панель для применения настроек."
|
||||
"xrayConfigRoutings" = "Настройка правил маршрутизации."
|
||||
"xrayConfigRoutingsDesc" = "Изменение шаблона конфигурации, для определения правил маршрутизации для этого сервера. Перезагрузите панель для применения настроек."
|
||||
"xrayConfigOutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить исходящие пути для этого сервера"
|
||||
"xrayConfigRoutings" = "Настройка правил маршрутизации"
|
||||
"xrayConfigRoutingsDesc" = "Изменение шаблона конфигурации, для определения правил маршрутизации для этого сервера"
|
||||
"manualLists" = "Ручные списки"
|
||||
"manualListsDesc" = "Пожалуйста, используйте формат массива JSON"
|
||||
"manualBlockedIPs" = "Список заблокированных IP адресов"
|
||||
"manualBlockedDomains" = "Список заблокированных доменов"
|
||||
"manualDirectIPs" = "Список прямых IP адресов"
|
||||
"manualDirectDomains" = "Список прямых доменов"
|
||||
"manualIPv4Domains" = "Список доменов IPv4"
|
||||
"manualWARPDomains" = "Список доменов WARP"
|
||||
|
||||
[pages.settings.security]
|
||||
"admin" = "Админ"
|
||||
@@ -318,11 +363,123 @@
|
||||
"loginSecurity" = "Безопасность входа"
|
||||
"loginSecurityDesc" = "Включить дополнительные меры безопасности входа пользователя"
|
||||
"secretToken" = "Секретный токен"
|
||||
"secretTokenDesc" = "Пожалуйста, скопируйте и сохраните этот токен в безопасном месте. Этот токен необходим для входа в систему и не может быть восстановлен с помощью командного инструмента x-ui."
|
||||
"secretTokenDesc" = "Пожалуйста, скопируйте и сохраните этот токен в безопасном месте. Этот токен необходим для входа в систему и не может быть восстановлен с помощью инструмента x-ui"
|
||||
|
||||
[pages.settings.toasts]
|
||||
"modifySettings" = "Изменение настроек"
|
||||
"getSettings" = "Просмотр настроек"
|
||||
"modifyUser" = "Изменение пользователя "
|
||||
"modifyUser" = "Изменение пользователя"
|
||||
"originalUserPassIncorrect" = "Неверное имя пользователя или пароль"
|
||||
"userPassMustBeNotEmpty" = "Новое имя пользователя и новый пароль должны быть заполнены"
|
||||
|
||||
[tgbot]
|
||||
"keyboardClosed" = "❌ Закрыта настраиваемая клавиатура!"
|
||||
"noResult" = "❗ Нет результатов!"
|
||||
"noQuery" = "❌ Запрос не найден! Пожалуйста, повторите команду!"
|
||||
"wentWrong" = "❌ Что-то пошло не так!"
|
||||
"noIpRecord" = "❗ Нет записей об IP-адресе!"
|
||||
"noInbounds" = "❗ Входящих соединений не найдено!"
|
||||
"unlimited" = "♾ Неограниченно"
|
||||
"month" = "Месяц"
|
||||
"months" = "Месяцев"
|
||||
"day" = "День"
|
||||
"days" = "Дней"
|
||||
"unknown" = "Неизвестно"
|
||||
"inbounds" = "Входящие"
|
||||
"clients" = "Клиенты"
|
||||
|
||||
[tgbot.commands]
|
||||
"unknown" = "❗ Неизвестная команда"
|
||||
"pleaseChoose" = "👇 Пожалуйста, выберите:\r\n"
|
||||
"help" = "🤖 Добро пожаловать в этого бота! Он предназначен для предоставления вам конкретных данных с сервера и позволяет вносить необходимые изменения.\r\n\r\n"
|
||||
"start" = "👋 Привет, <i>{{ .Firstname }}</i>.\r\n"
|
||||
"welcome" = "🤖 Добро пожаловать в бота управления <b>{{ .Hostname }}</b>.\r\n"
|
||||
"status" = "✅ Бот работает нормально!"
|
||||
"usage" = "❗ Пожалуйста, укажите текст для поиска!"
|
||||
"helpAdminCommands" = "Поиск по электронной почте клиента:\r\n<code>/usage [Email]</code>\r\n \r\nПоиск входящих соединений (со статистикой клиента):\r\n<code>/inbound [Remark]</code>"
|
||||
"helpClientCommands" = "Для получения статистики используйте следующую команду:\r\n \r\n<code>/usage [UUID|Password]</code>\r\n \r\nИспользуйте UUID для vmess/vless и пароль для Trojan."
|
||||
|
||||
[tgbot.messages]
|
||||
"cpuThreshold" = "🔴 Загрузка процессора составляет {{ .Percent }}%, что превышает пороговое значение {{ .Threshold }}%"
|
||||
"selectUserFailed" = "❌ Ошибка при выборе пользователя!"
|
||||
"userSaved" = "✅ Пользователь Telegram сохранен."
|
||||
"loginSuccess" = "✅ Успешный вход в панель.\r\n"
|
||||
"loginFailed" = "❗️ Ошибка входа в панель.\r\n"
|
||||
"report" = "🕰 Запланированные отчеты: {{ .RunTime }}\r\n"
|
||||
"datetime" = "⏰ Дата и время: {{ .DateTime }}\r\n"
|
||||
"hostname" = "💻 Имя хоста: {{ .Hostname }}\r\n"
|
||||
"version" = "🚀 Версия X-UI: {{ .Version }}\r\n"
|
||||
"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n"
|
||||
"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n"
|
||||
"ip" = "🌐 IP: {{ .IP }}\r\n"
|
||||
"ips" = "🔢 IP-адреса: \r\n{{ .IPs }}\r\n"
|
||||
"serverUpTime" = "⏳ Время работы сервера: {{ .UpTime }} {{ .Unit }}\r\n"
|
||||
"serverLoad" = "📈 Загрузка сервера: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
|
||||
"serverMemory" = "📋 Память сервера: {{ .Current }}/{{ .Total }}\r\n"
|
||||
"tcpCount" = "🔹 Количество TCP-соединений: {{ .Count }}\r\n"
|
||||
"udpCount" = "🔸 Количество UDP-соединений: {{ .Count }}\r\n"
|
||||
"traffic" = "🚦 Трафик: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
|
||||
"xrayStatus" = "ℹ️ Состояние Xray: {{ .State }}\r\n"
|
||||
"username" = "👤 Имя пользователя: {{ .Username }}\r\n"
|
||||
"time" = "⏰ Время: {{ .Time }}\r\n"
|
||||
"inbound" = "📍 Входящий поток: {{ .Remark }}\r\n"
|
||||
"port" = "🔌 Порт: {{ .Port }}\r\n"
|
||||
"expire" = "📅 Дата окончания: {{ .DateTime }}\r\n \r\n"
|
||||
"expireIn" = "📅 Окончание через: {{ .Time }}\r\n \r\n"
|
||||
"active" = "💡 Активен: {{ .Enable }}\r\n"
|
||||
"email" = "📧 Email: {{ .Email }}\r\n"
|
||||
"upload" = "🔼 Загрузка↑: {{ .Upload }}\r\n"
|
||||
"download" = "🔽 Скачивание↓: {{ .Download }}\r\n"
|
||||
"total" = "🔄 Всего: {{ .UpDown }} / {{ .Total }}\r\n"
|
||||
"TGUser" = "👤 Пользователь Telegram: {{ .TelegramID }}\r\n"
|
||||
"exhaustedMsg" = "🚨 Исчерпаны {{ .Type }}:\r\n"
|
||||
"exhaustedCount" = "🚨 Количество исчерпанных {{ .Type }}:\r\n"
|
||||
"disabled" = "🛑 Отключено: {{ .Disabled }}\r\n"
|
||||
"depleteSoon" = "🔜 Скоро исчерпание: {{ .Deplete }}\r\n \r\n"
|
||||
"backupTime" = "🗄 Время резервного копирования: {{ .Time }}\r\n"
|
||||
"refreshedOn" = "🔄🕒 Обновлено: {{ .Time }}\r\n"
|
||||
|
||||
[tgbot.buttons]
|
||||
"closeKeyboard" = "❌ Закрыть клавиатуру"
|
||||
"cancel" = "❌ Отмена"
|
||||
"cancelReset" = "❌ Отменить сброс"
|
||||
"cancelIpLimit" = "❌ Отменить лимит IP"
|
||||
"confirmResetTraffic" = "✅ Подтвердить сброс трафика?"
|
||||
"confirmClearIps" = "✅ Подтвердить очистку IP?"
|
||||
"confirmRemoveTGUser" = "✅ Подтвердить удаление пользователя Telegram?"
|
||||
"dbBackup" = "Получить резервную копию DB"
|
||||
"serverUsage" = "Использование сервера"
|
||||
"getInbounds" = "Получить входящие потоки"
|
||||
"depleteSoon" = "Скоро исчерпание"
|
||||
"clientUsage" = "Получить использование"
|
||||
"commands" = "Команды"
|
||||
"refresh" = "🔄 Обновить"
|
||||
"clearIPs" = "❌ Очистить IP"
|
||||
"removeTGUser" = "❌ Удалить пользователя Telegram"
|
||||
"selectTGUser" = "👤 Выбрать пользователя Telegram"
|
||||
"selectOneTGUser" = "👤 Выберите пользователя Telegram:"
|
||||
"resetTraffic" = "📈 Сбросить трафик"
|
||||
"resetExpire" = "📅 Сбросить дату окончания"
|
||||
"ipLog" = "🔢 Лог IP"
|
||||
"ipLimit" = "🔢 Лимит IP"
|
||||
"setTGUser" = "👤 Установить пользователя Telegram"
|
||||
"toggle" = "🔘 Вкл./Выкл."
|
||||
|
||||
[tgbot.answers]
|
||||
"errorOperation" = "❗ Ошибка в операции."
|
||||
"getInboundsFailed" = "❌ Не удалось получить входящие потоки."
|
||||
"canceled" = "❌ {{ .Email }}: Операция отменена."
|
||||
"clientRefreshSuccess" = "✅ {{ .Email }}: Клиент успешно обновлен."
|
||||
"IpRefreshSuccess" = "✅ {{ .Email }}: IP-адреса успешно обновлены."
|
||||
"TGIdRefreshSuccess" = "✅ {{ .Email }}: Пользователь Telegram клиента успешно обновлен."
|
||||
"resetTrafficSuccess" = "✅ {{ .Email }}: Трафик успешно сброшен."
|
||||
"expireResetSuccess" = "✅ {{ .Email }}: Дни истечения успешно сброшены."
|
||||
"resetIpSuccess" = "✅ {{ .Email }}: Лимит IP ({{ .Count }}) успешно сохранен."
|
||||
"clearIpSuccess" = "✅ {{ .Email }}: IP-адреса успешно очищены."
|
||||
"getIpLog" = "✅ {{ .Email }}: Получен лог IP."
|
||||
"getUserInfo" = "✅ {{ .Email }}: Получена информация о пользователе Telegram."
|
||||
"removedTGUserSuccess" = "✅ {{ .Email }}: Пользователь Telegram успешно удален."
|
||||
"enableSuccess" = "✅ {{ .Email }}: Включено успешно."
|
||||
"disableSuccess" = "✅ {{ .Email }}: Отключено успешно."
|
||||
"askToAddUserId" = "Ваша конфигурация не найдена!\r\nПожалуйста, попросите администратора использовать ваш идентификатор пользователя Telegram в ваших конфигурациях.\r\n\r\nВаш идентификатор пользователя: <b>{{ .TgUserID }}</b>"
|
||||
"askToAddUserName" = "Ваша конфигурация не найдена!\r\nПожалуйста, попросите администратора использовать ваше имя пользователя или идентификатор пользователя Telegram в ваших конфигурациях.\r\n\r\nВаше имя пользователя: <b>@{{ .TgUserName }}</b>\r\n\r\nВаш идентификатор пользователя: <b>{{ .TgUserID }}</b>"
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"enable" = "启用"
|
||||
"protocol" = "协议"
|
||||
"search" = "搜尋"
|
||||
|
||||
"filter" = "过滤器"
|
||||
"loading" = "加载中"
|
||||
"second" = "秒"
|
||||
"minute" = "分钟"
|
||||
@@ -78,11 +78,12 @@
|
||||
"xraySwitch" = "切换版本"
|
||||
"xraySwitchClick" = "点击你想切换的版本"
|
||||
"xraySwitchClickDesk" = "请谨慎选择,旧版本可能配置不兼容"
|
||||
"operationHours" = "运行时间"
|
||||
"operationHoursDesc" = "系统自启动以来的运行时间"
|
||||
"operationHours" = "系统正常运行时间"
|
||||
"systemLoad" = "系统负载"
|
||||
"systemLoadDesc" = "过去 1、5 和 15 分钟的系统平均负载"
|
||||
"connectionTcpCountDesc" = "所有网卡的总 TCP 连接数。"
|
||||
"connectionUdpCountDesc" = "所有网卡的总 UDP 连接数。"
|
||||
"connectionCount" = "连接数"
|
||||
"connectionCountDesc" = "所有网卡的总连接数"
|
||||
"upSpeed" = "所有网卡的总上传速度"
|
||||
"downSpeed" = "所有网卡的总下载速度"
|
||||
"totalSent" = "系统启动以来所有网卡的总上传流量"
|
||||
@@ -209,6 +210,7 @@
|
||||
[pages.settings]
|
||||
"title" = "设置"
|
||||
"save" = "保存配置"
|
||||
"infoDesc" = "此处的所有更改都需要保存并重启面板才能生效"
|
||||
"restartPanel" = "重启面板"
|
||||
"restartPanelDesc" = "确定要重启面板吗?点击确定将于 3 秒后重启,若重启后无法访问面板,请前往服务器查看面板日志信息"
|
||||
"actions" = "动作"
|
||||
@@ -218,15 +220,15 @@
|
||||
"xrayConfiguration" = "xray 相关设置"
|
||||
"TGBotSettings" = "TG提醒相关设置"
|
||||
"panelListeningIP" = "面板监听 IP"
|
||||
"panelListeningIPDesc" = "默认留空监听所有 IP,重启面板生效"
|
||||
"panelListeningIPDesc" = "默认留空监听所有 IP"
|
||||
"panelPort" = "面板监听端口"
|
||||
"panelPortDesc" = "重启面板生效"
|
||||
"publicKeyPath" = "面板证书公钥文件路径"
|
||||
"publicKeyPathDesc" = "填写一个 '/' 开头的绝对路径,重启面板生效"
|
||||
"publicKeyPathDesc" = "填写一个 '/' 开头的绝对路径"
|
||||
"privateKeyPath" = "面板证书密钥文件路径"
|
||||
"privateKeyPathDesc" = "填写一个 '/' 开头的绝对路径,重启面板生效"
|
||||
"privateKeyPathDesc" = "填写一个 '/' 开头的绝对路径"
|
||||
"panelUrlPath" = "面板 url 根路径"
|
||||
"panelUrlPathDesc" = "必须以 '/' 开头,以 '/' 结尾,重启面板生效"
|
||||
"panelUrlPathDesc" = "必须以 '/' 开头,以 '/' 结尾"
|
||||
"oldUsername" = "原用户名"
|
||||
"currentPassword" = "原密码"
|
||||
"newUsername" = "新用户名"
|
||||
@@ -240,7 +242,7 @@
|
||||
"telegramNotifyTime" = "电报机器人通知时间"
|
||||
"telegramNotifyTimeDesc" = "采用Crontab定时格式,重启面板生效"
|
||||
"tgNotifyBackup" = "数据库备份"
|
||||
"tgNotifyBackupDesc" = "正在发送数据库备份文件和报告通知。重启面板生效"
|
||||
"tgNotifyBackupDesc" = "正在发送数据库备份文件和报告通知"
|
||||
"sessionMaxAge" = "会话最大年龄"
|
||||
"sessionMaxAgeDesc" = "您可以保持登录状态的时间(单位:分钟)"
|
||||
"expireTimeDiff" = "耗尽时间阈值"
|
||||
@@ -250,65 +252,110 @@
|
||||
"tgNotifyCpu" = "CPU 百分比警报阈值"
|
||||
"tgNotifyCpuDesc" = "如果 CPU 使用率超过此百分比(单位:%),此 talegram bot 将向您发送通知"
|
||||
"timeZone" = "时区"
|
||||
"timeZoneDesc" = "定时任务按照该时区的时间运行,重启面板生效"
|
||||
"timeZoneDesc" = "定时任务按照该时区的时间运行"
|
||||
"subSettings" = "订阅"
|
||||
"subEnable" = "启用服务"
|
||||
"subEnableDesc" = "具有单独配置的订阅功能"
|
||||
"subListen" = "监听IP"
|
||||
"subListenDesc" = "留空默认监听所有IP"
|
||||
"subPort" = "订阅端口"
|
||||
"subPortDesc" = "服务订阅服务的端口号必须在服务器中未使用"
|
||||
"subCertPath" = "订阅证书公钥文件路径"
|
||||
"subCertPathDesc" = "填写以'/'开头的绝对路径"
|
||||
"subKeyPath" = "订阅证书私钥文件路径"
|
||||
"subKeyPathDesc" = "填写以'/'开头的绝对路径"
|
||||
"subPath" = "订阅 URL 根路径"
|
||||
"subPathDesc" = "必须以'/'开始并以'/'结束"
|
||||
"subDomain" = "监听域"
|
||||
"subDomainDesc" = "留空默认监控所有域名和IP"
|
||||
"subUpdates" = "订阅更新间隔"
|
||||
"subUpdatesDesc" = "客户端应用程序更新之间的间隔时间"
|
||||
|
||||
[pages.settings.templates]
|
||||
"title" = "模板"
|
||||
"basicTemplate" = "基本模板"
|
||||
"advancedTemplate" = "高级模板部件"
|
||||
"completeTemplate" = "Xray 配置的完整模板"
|
||||
"generalConfigs" = "一般配置"
|
||||
"generalConfigsDesc" = "此选项将阻止用户连接到特定协议和网站。"
|
||||
"countryConfigs" = "国家配置"
|
||||
"countryConfigsDesc" = "此选项将阻止用户连接到特定国家/地区的域。"
|
||||
"generalConfigs" = "通用配置"
|
||||
"generalConfigsDesc" = "这些选项将提供一般调整"
|
||||
"blockConfigs" = "阻塞配置"
|
||||
"blockConfigsDesc" = "这些选项将阻止用户连接到特定协议和网站"
|
||||
"blockCountryConfigs" = "阻止国家配置"
|
||||
"blockCountryConfigsDesc" = "这些选项将阻止用户连接到特定国家/地区的域。"
|
||||
"directCountryConfigs" = "直接国家配置"
|
||||
"directCountryConfigsDesc" = "这些选项会将用户直接连接到特定国家/地区的域。"
|
||||
"ipv4Configs" = "IPv4 配置"
|
||||
"ipv4ConfigsDesc" = "此选项将仅通过 IPv4 路由到目标域。"
|
||||
"ipv4ConfigsDesc" = "此选项将仅通过 IPv4 路由到目标域"
|
||||
"warpConfigs" = "WARP 配置"
|
||||
"warpConfigsDesc" = "警告:在使用此选项之前,请按照面板 GitHub 上的步骤在您的服务器上以 socks5 代理模式安装 WARP。 WARP 将通过 Cloudflare 服务器将流量路由到网站。"
|
||||
"xrayConfigTemplate" = "xray 配置模板"
|
||||
"xrayConfigTemplateDesc" = "以该模型为基础生成最终的xray配置文件,重新启动面板生成效率"
|
||||
"warpConfigsDesc" = "注意:在使用这些选项之前,请按照面板 GitHub 上的步骤在您的服务器上以 socks5 代理模式安装 WARP。 WARP 将通过 Cloudflare 服务器将流量路由到网站。"
|
||||
"xrayConfigTemplate" = "Xray 配置模板"
|
||||
"xrayConfigTemplateDesc" = "以该模型为基础生成最终的Xray配置文件,重新启动面板生成效率"
|
||||
"xrayConfigFreedomStrategy" = "配置自由协议的策略"
|
||||
"xrayConfigFreedomStrategyDesc" = "在自由协议中设置网络输出策略"
|
||||
"xrayConfigRoutingStrategy" = "配置路由域策略"
|
||||
"xrayConfigRoutingStrategyDesc" = "设置DNS解析的整体路由策略"
|
||||
"xrayConfigTorrent" = "禁止使用 bittorrent"
|
||||
"xrayConfigTorrentDesc" = "更改配置模板避免用户使用bittorrent,重启面板生效"
|
||||
"xrayConfigTorrentDesc" = "更改配置模板避免用户使用bittorrent"
|
||||
"xrayConfigPrivateIp" = "禁止私人 IP 范围连接"
|
||||
"xrayConfigPrivateIpDesc" = "更改配置模板以避免连接私有 IP 范围,重启面板生效"
|
||||
"xrayConfigPrivateIpDesc" = "更改配置模板以避免连接私有 IP 范围"
|
||||
"xrayConfigAds" = "屏蔽广告"
|
||||
"xrayConfigAdsDesc" = "修改配置模板屏蔽广告,重启面板生效"
|
||||
"xrayConfigPorn" = "禁止色情网站连接"
|
||||
"xrayConfigPornDesc" = "更改配置模板避免连接色情网站,重启面板生效"
|
||||
"xrayConfigAdsDesc" = "修改配置模板屏蔽广告"
|
||||
"xrayConfigFamily" = "启用家庭友好配置"
|
||||
"xrayConfigFamilyDesc" = "避免为家人连接到不安全的网站"
|
||||
"xrayConfigSpeedtest" = "阻止测速网站"
|
||||
"xrayConfigSpeedtestDesc" = "更改配置模板以避免连接到速度测试网站。 重新启动面板以应用更改。"
|
||||
"xrayConfigIRIp" = "禁止伊朗 IP 范围连接"
|
||||
"xrayConfigIRIpDesc" = "修改配置模板避免连接伊朗IP段,重启面板生效"
|
||||
"xrayConfigIRIpDesc" = "修改配置模板避免连接伊朗IP段"
|
||||
"xrayConfigIRDomain" = "禁止伊朗域连接"
|
||||
"xrayConfigIRDomainDesc" = "更改配置模板避免连接伊朗域名,重启面板生效"
|
||||
"xrayConfigIRDomainDesc" = "更改配置模板避免连接伊朗域名"
|
||||
"xrayConfigChinaIp" = "禁止中国 IP 范围连接"
|
||||
"xrayConfigChinaIpDesc" = "修改配置模板避免连接中国IP段,重启面板生效"
|
||||
"xrayConfigChinaIpDesc" = "修改配置模板避免连接中国IP段"
|
||||
"xrayConfigChinaDomain" = "禁止中国域名连接"
|
||||
"xrayConfigChinaDomainDesc" = "更改配置模板避免连接中国域,重启面板生效"
|
||||
"xrayConfigChinaDomainDesc" = "更改配置模板避免连接中国域"
|
||||
"xrayConfigRussiaIp" = "禁止俄罗斯 IP 范围连接"
|
||||
"xrayConfigRussiaIpDesc" = "修改配置模板避免连接俄罗斯IP范围,重启面板生效"
|
||||
"xrayConfigRussiaIpDesc" = "修改配置模板避免连接俄罗斯IP范围"
|
||||
"xrayConfigRussiaDomain" = "禁止俄罗斯域连接"
|
||||
"xrayConfigRussiaDomainDesc" = "更改配置模板避免连接俄罗斯域,重启面板生效"
|
||||
"xrayConfigRussiaDomainDesc" = "更改配置模板避免连接俄罗斯域"
|
||||
"xrayConfigDirectIRIp" = "直接连接到伊朗 IP 范围"
|
||||
"xrayConfigDirectIRIpDesc" = "更改直接连接到伊朗 IP 范围的配置模板"
|
||||
"xrayConfigDirectIRDomain" = "直接连接到伊朗域"
|
||||
"xrayConfigDirectIRDomainDesc" = "更改直接连接到伊朗域的配置模板"
|
||||
"xrayConfigDirectChinaIp" = "直连中国IP范围"
|
||||
"xrayConfigDirectChinaIpDesc" = "更改直连中国 IP 范围的配置模板"
|
||||
"xrayConfigDirectChinaDomain" = "直连中国域名"
|
||||
"xrayConfigDirectChinaDomainDesc" = "修改中国域名直连配置模板"
|
||||
"xrayConfigDirectRussiaIp" = "直接连接到俄罗斯 IP 范围"
|
||||
"xrayConfigDirectRussiaIpDesc" = "更改直接连接到俄罗斯 IP 范围的配置模板"
|
||||
"xrayConfigDirectRussiaDomain" = "直接连接到俄罗斯域"
|
||||
"xrayConfigDirectRussiaDomainDesc" = "更改直接连接到俄罗斯域的配置模板"
|
||||
"xrayConfigGoogleIPv4" = "为谷歌使用 IPv4"
|
||||
"xrayConfigGoogleIPv4Desc" = "添加谷歌连接IPv4的路由,重启面板生效"
|
||||
"xrayConfigGoogleIPv4Desc" = "添加谷歌连接IPv4的路由"
|
||||
"xrayConfigNetflixIPv4" = "为 Netflix 使用 IPv4"
|
||||
"xrayConfigNetflixIPv4Desc" = "添加Netflix连接IPv4的路由,重启面板生效"
|
||||
"xrayConfigNetflixIPv4Desc" = "添加Netflix连接IPv4的路由"
|
||||
"xrayConfigGoogleWARP" = "将谷歌路由到 WARP"
|
||||
"xrayConfigGoogleWARPDesc" = "为谷歌添加路由到WARP,重启面板生效"
|
||||
"xrayConfigGoogleWARPDesc" = "为谷歌添加路由到WARP"
|
||||
"xrayConfigOpenAIWARP" = "将 OpenAI (ChatGPT) 路由到 WARP"
|
||||
"xrayConfigOpenAIWARPDesc" = "将OpenAI(ChatGPT)路由添加到WARP,重启面板生效"
|
||||
"xrayConfigOpenAIWARPDesc" = "将OpenAI(ChatGPT)路由添加到WARP"
|
||||
"xrayConfigNetflixWARP" = "将 Netflix 路由到 WARP"
|
||||
"xrayConfigNetflixWARPDesc" = "为Netflix添加路由到WARP,重启面板生效"
|
||||
"xrayConfigNetflixWARPDesc" = "为Netflix添加路由到WARP"
|
||||
"xrayConfigSpotifyWARP" = "将 Spotify 路由到 WARP"
|
||||
"xrayConfigSpotifyWARPDesc" = "为Spotify添加路由到WARP,重启面板生效"
|
||||
"xrayConfigSpotifyWARPDesc" = "为Spotify添加路由到WARP"
|
||||
"xrayConfigIRWARP" = "将伊朗域名路由到 WARP"
|
||||
"xrayConfigIRWARPDesc" = "将伊朗域的路由添加到 WARP。 重启面板生效"
|
||||
"xrayConfigInbounds" = "入站配置"
|
||||
"xrayConfigInboundsDesc" = "更改配置模板接受特殊客户端,重启面板生效"
|
||||
"xrayConfigInboundsDesc" = "更改配置模板接受特殊客户端"
|
||||
"xrayConfigOutbounds" = "出站配置"
|
||||
"xrayConfigOutboundsDesc" = "更改配置模板定义此服务器的传出方式,重启面板生效"
|
||||
"xrayConfigOutboundsDesc" = "更改配置模板定义此服务器的传出方式"
|
||||
"xrayConfigRoutings" = "路由规则配置"
|
||||
"xrayConfigRoutingsDesc" = "更改配置模板为该服务器定义路由规则,重启面板生效"
|
||||
"xrayConfigRoutingsDesc" = "更改配置模板为该服务器定义路由规则"
|
||||
"manualLists" = "手动列表"
|
||||
"manualListsDesc" = "请使用 JSON 数组格式"
|
||||
"manualBlockedIPs" = "被阻止的 IP 列表"
|
||||
"manualBlockedDomains" = "被阻止的域列表"
|
||||
"manualDirectIPs" = "直接 IP 列表"
|
||||
"manualDirectDomains" = "直接域列表"
|
||||
"manualIPv4Domains" = "IPv4 域名列表"
|
||||
"manualWARPDomains" = "WARP域名列表"
|
||||
|
||||
[pages.settings.security]
|
||||
"admin" = "行政"
|
||||
@@ -324,3 +371,115 @@
|
||||
"modifyUser" = "修改用户"
|
||||
"originalUserPassIncorrect" = "原用户名或原密码错误"
|
||||
"userPassMustBeNotEmpty" = "新用户名和新密码不能为空"
|
||||
|
||||
[tgbot]
|
||||
"keyboardClosed" = "❌ 自定义键盘已关闭!"
|
||||
"noResult" = "❗ 没有结果!"
|
||||
"noQuery" = "❌ 未找到查询!请重新使用命令!"
|
||||
"wentWrong" = "❌ 出了点问题!"
|
||||
"noIpRecord" = "❗ 没有IP记录!"
|
||||
"noInbounds" = "❗ 没有找到入站连接!"
|
||||
"unlimited" = "♾ 无限制"
|
||||
"month" = "月"
|
||||
"months" = "月"
|
||||
"day" = "天"
|
||||
"days" = "天"
|
||||
"unknown" = "未知"
|
||||
"inbounds" = "入站连接"
|
||||
"clients" = "客户端"
|
||||
|
||||
[tgbot.commands]
|
||||
"unknown" = "❗ 未知命令"
|
||||
"pleaseChoose" = "👇 请选择:\r\n"
|
||||
"help" = "🤖 欢迎使用本机器人!它旨在为您提供来自服务器的特定数据,并允许您进行必要的修改。\r\n\r\n"
|
||||
"start" = "👋 你好,<i>{{ .Firstname }}</i>。\r\n"
|
||||
"welcome" = "🤖 欢迎来到<b>{{ .Hostname }}</b>管理机器人。\r\n"
|
||||
"status" = "✅ 机器人正常运行!"
|
||||
"usage" = "❗ 请输入要搜索的文本!"
|
||||
"helpAdminCommands" = "搜索客户端邮箱:\r\n<code>/usage [Email]</code>\r\n \r\n搜索入站连接(包含客户端统计信息):\r\n<code>/inbound [Remark]</code>"
|
||||
"helpClientCommands" = "要搜索统计信息,请使用以下命令:\r\n \r\n<code>/usage [UUID|Password]</code>\r\n \r\n对于vmess/vless,请使用UUID;对于Trojan,请使用密码。"
|
||||
|
||||
[tgbot.messages]
|
||||
"cpuThreshold" = "🔴 CPU 使用率为 {{ .Percent }}%,超过阈值 {{ .Threshold }}%"
|
||||
"selectUserFailed" = "❌ 用户选择错误!"
|
||||
"userSaved" = "✅ 电报用户已保存。"
|
||||
"loginSuccess" = "✅ 成功登录到面板。\r\n"
|
||||
"loginFailed" = "❗️ 面板登录失败。\r\n"
|
||||
"report" = "🕰 定时报告:{{ .RunTime }}\r\n"
|
||||
"datetime" = "⏰ 日期时间:{{ .DateTime }}\r\n"
|
||||
"hostname" = "💻 主机名:{{ .Hostname }}\r\n"
|
||||
"version" = "🚀 X-UI 版本:{{ .Version }}\r\n"
|
||||
"ipv6" = "🌐 IPv6:{{ .IPv6 }}\r\n"
|
||||
"ipv4" = "🌐 IPv4:{{ .IPv4 }}\r\n"
|
||||
"ip" = "🌐 IP:{{ .IP }}\r\n"
|
||||
"ips" = "🔢 IP 地址:\r\n{{ .IPs }}\r\n"
|
||||
"serverUpTime" = "⏳ 服务器运行时间:{{ .UpTime }} {{ .Unit }}\r\n"
|
||||
"serverLoad" = "📈 服务器负载:{{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
|
||||
"serverMemory" = "📋 服务器内存:{{ .Current }}/{{ .Total }}\r\n"
|
||||
"tcpCount" = "🔹 TCP 连接数:{{ .Count }}\r\n"
|
||||
"udpCount" = "🔸 UDP 连接数:{{ .Count }}\r\n"
|
||||
"traffic" = "🚦 流量:{{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
|
||||
"xrayStatus" = "ℹ️ Xray 状态:{{ .State }}\r\n"
|
||||
"username" = "👤 用户名:{{ .Username }}\r\n"
|
||||
"time" = "⏰ 时间:{{ .Time }}\r\n"
|
||||
"inbound" = "📍 入站:{{ .Remark }}\r\n"
|
||||
"port" = "🔌 端口:{{ .Port }}\r\n"
|
||||
"expire" = "📅 过期日期:{{ .DateTime }}\r\n \r\n"
|
||||
"expireIn" = "📅 剩余时间:{{ .Time }}\r\n \r\n"
|
||||
"active" = "💡 激活:{{ .Enable }}\r\n"
|
||||
"email" = "📧 邮箱:{{ .Email }}\r\n"
|
||||
"upload" = "🔼 上传↑:{{ .Upload }}\r\n"
|
||||
"download" = "🔽 下载↓:{{ .Download }}\r\n"
|
||||
"total" = "🔄 总计:{{ .UpDown }} / {{ .Total }}\r\n"
|
||||
"TGUser" = "👤 电报用户:{{ .TelegramID }}\r\n"
|
||||
"exhaustedMsg" = "🚨 耗尽的{{ .Type }}:\r\n"
|
||||
"exhaustedCount" = "🚨 耗尽的{{ .Type }}数量:\r\n"
|
||||
"disabled" = "🛑 禁用:{{ .Disabled }}\r\n"
|
||||
"depleteSoon" = "🔜 即将耗尽:{{ .Deplete }}\r\n \r\n"
|
||||
"backupTime" = "🗄 备份时间:{{ .Time }}\r\n"
|
||||
"refreshedOn" = "🔄🕒 刷新时间:{{ .Time }}\r\n"
|
||||
|
||||
[tgbot.buttons]
|
||||
"closeKeyboard" = "❌ 关闭键盘"
|
||||
"cancel" = "❌ 取消"
|
||||
"cancelReset" = "❌ 取消重置"
|
||||
"cancelIpLimit" = "❌ 取消 IP 限制"
|
||||
"confirmResetTraffic" = "✅ 确认重置流量?"
|
||||
"confirmClearIps" = "✅ 确认清除 IP?"
|
||||
"confirmRemoveTGUser" = "✅ 确认移除 Telegram 用户?"
|
||||
"dbBackup" = "获取数据库备份"
|
||||
"serverUsage" = "服务器使用情况"
|
||||
"getInbounds" = "获取入站信息"
|
||||
"depleteSoon" = "即将耗尽"
|
||||
"clientUsage" = "获取使用情况"
|
||||
"commands" = "命令"
|
||||
"refresh" = "🔄 刷新"
|
||||
"clearIPs" = "❌ 清除 IP"
|
||||
"removeTGUser" = "❌ 移除 Telegram 用户"
|
||||
"selectTGUser" = "👤 选择 Telegram 用户"
|
||||
"selectOneTGUser" = "👤 选择一个 Telegram 用户:"
|
||||
"resetTraffic" = "📈 重置流量"
|
||||
"resetExpire" = "📅 重置过期天数"
|
||||
"ipLog" = "🔢 IP 日志"
|
||||
"ipLimit" = "🔢 IP 限制"
|
||||
"setTGUser" = "👤 设置 Telegram 用户"
|
||||
"toggle" = "🔘 启用/禁用"
|
||||
|
||||
[tgbot.answers]
|
||||
"errorOperation" = "❗ 操作错误。"
|
||||
"getInboundsFailed" = "❌ 获取入站信息失败。"
|
||||
"canceled" = "❌ {{ .Email }}:操作已取消。"
|
||||
"clientRefreshSuccess" = "✅ {{ .Email }}:客户端刷新成功。"
|
||||
"IpRefreshSuccess" = "✅ {{ .Email }}:IP 刷新成功。"
|
||||
"TGIdRefreshSuccess" = "✅ {{ .Email }}:客户端的 Telegram 用户刷新成功。"
|
||||
"resetTrafficSuccess" = "✅ {{ .Email }}:流量已重置成功。"
|
||||
"expireResetSuccess" = "✅ {{ .Email }}:过期天数已重置成功。"
|
||||
"resetIpSuccess" = "✅ {{ .Email }}:成功保存 IP 限制数量为 {{ .Count }}。"
|
||||
"clearIpSuccess" = "✅ {{ .Email }}:IP 已成功清除。"
|
||||
"getIpLog" = "✅ {{ .Email }}:获取 IP 日志。"
|
||||
"getUserInfo" = "✅ {{ .Email }}:获取 Telegram 用户信息。"
|
||||
"removedTGUserSuccess" = "✅ {{ .Email }}:Telegram 用户已成功移除。"
|
||||
"enableSuccess" = "✅ {{ .Email }}:已成功启用。"
|
||||
"disableSuccess" = "✅ {{ .Email }}:已成功禁用。"
|
||||
"askToAddUserId" = "未找到您的配置!\r\n请向管理员询问,在您的配置中使用您的 Telegram 用户ID。\r\n\r\n您的用户ID:<b>{{ .TgUserID }}</b>"
|
||||
"askToAddUserName" = "未找到您的配置!\r\n请向管理员询问,在您的配置中使用您的 Telegram 用户名或用户ID。\r\n\r\n您的用户名:<b>@{{ .TgUserName }}</b>\r\n\r\n您的用户ID:<b>{{ .TgUserID }}</b>"
|
||||
|
||||
117
web/web.go
117
web/web.go
@@ -18,16 +18,14 @@ import (
|
||||
"x-ui/util/common"
|
||||
"x-ui/web/controller"
|
||||
"x-ui/web/job"
|
||||
"x-ui/web/locale"
|
||||
"x-ui/web/network"
|
||||
"x-ui/web/service"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-contrib/sessions/cookie"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/robfig/cron/v3"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
//go:embed assets/*
|
||||
@@ -85,7 +83,6 @@ type Server struct {
|
||||
server *controller.ServerController
|
||||
panel *controller.XUIController
|
||||
api *controller.APIController
|
||||
sub *controller.SUBController
|
||||
|
||||
xrayService service.XrayService
|
||||
settingService service.SettingService
|
||||
@@ -202,13 +199,23 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
c.Header("Cache-Control", "max-age=31536000")
|
||||
}
|
||||
})
|
||||
err = s.initI18n(engine)
|
||||
|
||||
// init i18n
|
||||
err = locale.InitLocalizer(i18nFS, &s.settingService)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Apply locale middleware for i18n
|
||||
i18nWebFunc := func(key string, params ...string) string {
|
||||
return locale.I18n(locale.Web, key, params...)
|
||||
}
|
||||
engine.FuncMap["i18n"] = i18nWebFunc
|
||||
engine.Use(locale.LocalizerMiddleware())
|
||||
|
||||
// set static files and template
|
||||
if config.IsDebug() {
|
||||
// for develop
|
||||
// for development
|
||||
files, err := s.getHtmlFiles()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -216,12 +223,12 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
engine.LoadHTMLFiles(files...)
|
||||
engine.StaticFS(basePath+"assets", http.FS(os.DirFS("web/assets")))
|
||||
} else {
|
||||
// for prod
|
||||
t, err := s.getHtmlTemplate(engine.FuncMap)
|
||||
// for production
|
||||
template, err := s.getHtmlTemplate(engine.FuncMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
engine.SetHTMLTemplate(t)
|
||||
engine.SetHTMLTemplate(template)
|
||||
engine.StaticFS(basePath+"assets", http.FS(&wrapAssetsFS{FS: assetsFS}))
|
||||
}
|
||||
|
||||
@@ -234,92 +241,10 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
s.server = controller.NewServerController(g)
|
||||
s.panel = controller.NewXUIController(g)
|
||||
s.api = controller.NewAPIController(g)
|
||||
s.sub = controller.NewSUBController(g)
|
||||
|
||||
return engine, nil
|
||||
}
|
||||
|
||||
func (s *Server) initI18n(engine *gin.Engine) error {
|
||||
bundle := i18n.NewBundle(language.SimplifiedChinese)
|
||||
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
|
||||
err := fs.WalkDir(i18nFS, "translation", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
data, err := i18nFS.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = bundle.ParseMessageFileBytes(data, path)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
findI18nParamNames := func(key string) []string {
|
||||
names := make([]string, 0)
|
||||
keyLen := len(key)
|
||||
for i := 0; i < keyLen-1; i++ {
|
||||
if key[i:i+2] == "{{" { // 判断开头 "{{"
|
||||
j := i + 2
|
||||
isFind := false
|
||||
for ; j < keyLen-1; j++ {
|
||||
if key[j:j+2] == "}}" { // 结尾 "}}"
|
||||
isFind = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isFind {
|
||||
names = append(names, key[i+3:j])
|
||||
}
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
var localizer *i18n.Localizer
|
||||
|
||||
I18n := func(key string, params ...string) (string, error) {
|
||||
names := findI18nParamNames(key)
|
||||
if len(names) != len(params) {
|
||||
return "", common.NewError("find names:", names, "---------- params:", params, "---------- num not equal")
|
||||
}
|
||||
templateData := map[string]interface{}{}
|
||||
for i := range names {
|
||||
templateData[names[i]] = params[i]
|
||||
}
|
||||
return localizer.Localize(&i18n.LocalizeConfig{
|
||||
MessageID: key,
|
||||
TemplateData: templateData,
|
||||
})
|
||||
}
|
||||
|
||||
engine.FuncMap["i18n"] = I18n
|
||||
|
||||
engine.Use(func(c *gin.Context) {
|
||||
//accept := c.GetHeader("Accept-Language")
|
||||
|
||||
var lang string
|
||||
|
||||
if cookie, err := c.Request.Cookie("lang"); err == nil {
|
||||
lang = cookie.Value
|
||||
} else {
|
||||
lang = c.GetHeader("Accept-Language")
|
||||
}
|
||||
|
||||
localizer = i18n.NewLocalizer(bundle, lang)
|
||||
c.Set("localizer", localizer)
|
||||
c.Set("I18n", I18n)
|
||||
c.Next()
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) startTask() {
|
||||
err := s.xrayService.RestartXray(true)
|
||||
if err != nil {
|
||||
@@ -346,7 +271,7 @@ func (s *Server) startTask() {
|
||||
if (err == nil) && (isTgbotenabled) {
|
||||
runtime, err := s.settingService.GetTgbotRuntime()
|
||||
if err != nil || runtime == "" {
|
||||
logger.Errorf("Add NewStatsNotifyJob error[%s],Runtime[%s] invalid,wil run default", err, runtime)
|
||||
logger.Errorf("Add NewStatsNotifyJob error[%s], Runtime[%s] invalid, will run default", err, runtime)
|
||||
runtime = "@daily"
|
||||
}
|
||||
logger.Infof("Tg notify enabled,run at %s", runtime)
|
||||
@@ -356,12 +281,14 @@ func (s *Server) startTask() {
|
||||
return
|
||||
}
|
||||
|
||||
// check for Telegram bot callback query hash storage reset
|
||||
s.cron.AddJob("@every 2m", job.NewCheckHashStorageJob())
|
||||
|
||||
// Check CPU load and alarm to TgBot if threshold passes
|
||||
cpuThreshold, err := s.settingService.GetTgCpu()
|
||||
if (err == nil) && (cpuThreshold > 0) {
|
||||
s.cron.AddJob("@every 10s", job.NewCheckCpuJob())
|
||||
}
|
||||
|
||||
} else {
|
||||
s.cron.Remove(entry)
|
||||
}
|
||||
@@ -441,7 +368,7 @@ func (s *Server) Start() (err error) {
|
||||
isTgbotenabled, err := s.settingService.GetTgbotenabled()
|
||||
if (err == nil) && (isTgbotenabled) {
|
||||
tgBot := s.tgbotService.NewTgbot()
|
||||
tgBot.Start()
|
||||
tgBot.Start(i18nFS)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -453,7 +380,7 @@ func (s *Server) Stop() error {
|
||||
if s.cron != nil {
|
||||
s.cron.Stop()
|
||||
}
|
||||
if s.tgbotService.IsRunnging() {
|
||||
if s.tgbotService.IsRunning() {
|
||||
s.tgbotService.Stop()
|
||||
}
|
||||
var err1 error
|
||||
|
||||
75
x-ui.sh
75
x-ui.sh
@@ -517,7 +517,27 @@ install_acme() {
|
||||
return 0
|
||||
}
|
||||
|
||||
#method for standalone mode
|
||||
ssl_cert_issue_main() {
|
||||
echo "1) Get SSL"
|
||||
echo "2) Revoke"
|
||||
echo "3) Force Renew"
|
||||
read -p "Choose an option: " choice
|
||||
case "$choice" in
|
||||
1) ssl_cert_issue ;;
|
||||
2)
|
||||
local domain=""
|
||||
read -p "Please enter your domain name to revoke the certificate: " domain
|
||||
~/.acme.sh/acme.sh --revoke -d ${domain}
|
||||
LOGI "Certificate revoked"
|
||||
;;
|
||||
3)
|
||||
local domain=""
|
||||
read -p "Please enter your domain name to forcefully renew an SSL certificate: " domain
|
||||
~/.acme.sh/acme.sh --renew -d ${domain} --force ;;
|
||||
*) echo "Invalid choice" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
ssl_cert_issue() {
|
||||
#check for acme.sh first
|
||||
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
|
||||
@@ -547,9 +567,10 @@ ssl_cert_issue() {
|
||||
LOGD "your domain is:${domain},check it..."
|
||||
#here we need to judge whether there exists cert already
|
||||
local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}')
|
||||
|
||||
if [ ${currentCert} == ${domain} ]; then
|
||||
local certInfo=$(~/.acme.sh/acme.sh --list)
|
||||
LOGE "system already have certs here,can not issue again,current certs details:"
|
||||
LOGE "system already has certs here,can not issue again,current certs details:"
|
||||
LOGI "$certInfo"
|
||||
exit 1
|
||||
else
|
||||
@@ -618,27 +639,37 @@ warp_fixchatgpt() {
|
||||
|
||||
run_speedtest() {
|
||||
# Check if Speedtest is already installed
|
||||
if ! command -v speedtest &>/dev/null; then
|
||||
if ! command -v speedtest &> /dev/null; then
|
||||
# If not installed, install it
|
||||
if command -v dnf &>/dev/null; then
|
||||
sudo dnf install -y curl
|
||||
curl -s https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh | sudo bash
|
||||
sudo dnf install -y speedtest
|
||||
elif command -v yum &>/dev/null; then
|
||||
sudo yum install -y curl
|
||||
curl -s https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh | sudo bash
|
||||
sudo yum install -y speedtest
|
||||
elif command -v apt-get &>/dev/null; then
|
||||
sudo apt-get update && sudo apt-get install -y curl
|
||||
curl -s https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh | sudo bash
|
||||
sudo apt-get install -y speedtest
|
||||
elif command -v apt &>/dev/null; then
|
||||
sudo apt update && sudo apt install -y curl
|
||||
curl -s https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh | sudo bash
|
||||
sudo apt install -y speedtest
|
||||
else
|
||||
local pkg_manager=""
|
||||
local curl_install_cmd=""
|
||||
local speedtest_install_script=""
|
||||
|
||||
if command -v dnf &> /dev/null; then
|
||||
pkg_manager="dnf"
|
||||
curl_install_cmd="sudo dnf install -y curl"
|
||||
speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh"
|
||||
elif command -v yum &> /dev/null; then
|
||||
pkg_manager="yum"
|
||||
curl_install_cmd="sudo yum install -y curl"
|
||||
speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh"
|
||||
elif command -v apt-get &> /dev/null; then
|
||||
pkg_manager="apt-get"
|
||||
curl_install_cmd="sudo apt-get update && sudo apt-get install -y curl"
|
||||
speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh"
|
||||
elif command -v apt &> /dev/null; then
|
||||
pkg_manager="apt"
|
||||
curl_install_cmd="sudo apt update && sudo apt install -y curl"
|
||||
speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh"
|
||||
fi
|
||||
|
||||
if [[ -z $pkg_manager ]]; then
|
||||
echo "Error: Package manager not found. You may need to install Speedtest manually."
|
||||
return 1
|
||||
else
|
||||
$curl_install_cmd
|
||||
curl -s $speedtest_install_script | sudo bash
|
||||
sudo $pkg_manager install -y speedtest
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -687,7 +718,7 @@ show_menu() {
|
||||
${green}14.${plain} Disable x-ui On System Startup
|
||||
————————————————
|
||||
${green}15.${plain} Enable BBR
|
||||
${green}16.${plain} Apply for an SSL Certificate
|
||||
${green}16.${plain} SSL Certificate Management
|
||||
${green}17.${plain} Update Geo Files
|
||||
${green}18.${plain} Active Firewall and open ports
|
||||
${green}19.${plain} Install WARP
|
||||
@@ -746,7 +777,7 @@ show_menu() {
|
||||
enable_bbr
|
||||
;;
|
||||
16)
|
||||
ssl_cert_issue
|
||||
ssl_cert_issue_main
|
||||
;;
|
||||
17)
|
||||
update_geo
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"x-ui/config"
|
||||
"x-ui/util/common"
|
||||
@@ -20,6 +21,7 @@ import (
|
||||
"github.com/Workiva/go-datastructures/queue"
|
||||
statsservice "github.com/xtls/xray-core/app/stats/command"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
var trafficRegex = regexp.MustCompile("(inbound|outbound)>>>([^>]+)>>>traffic>>>(downlink|uplink)")
|
||||
@@ -49,8 +51,8 @@ func GetIranPath() string {
|
||||
return config.GetBinFolderPath() + "/iran.dat"
|
||||
}
|
||||
|
||||
func GetBlockedIPsPath() string {
|
||||
return config.GetBinFolderPath() + "/blockedIPs"
|
||||
func GetAllowedIPsPath() string {
|
||||
return config.GetBinFolderPath() + "/AllowedIPs"
|
||||
}
|
||||
|
||||
func stopProcess(p *Process) {
|
||||
@@ -171,7 +173,7 @@ func (p *process) Start() (err error) {
|
||||
return common.NewErrorf("Failed to write configuration file: %v", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command(GetBinaryPath(), "-c", configPath, "-restrictedIPsPath", GetBlockedIPsPath())
|
||||
cmd := exec.Command(GetBinaryPath(), "-c", configPath, "-restrictedIPsPath", GetAllowedIPsPath())
|
||||
p.cmd = cmd
|
||||
|
||||
stdReader, err := cmd.StdoutPipe()
|
||||
@@ -183,11 +185,11 @@ func (p *process) Start() (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
common.Recover("")
|
||||
stdReader.Close()
|
||||
}()
|
||||
defer wg.Done()
|
||||
reader := bufio.NewReaderSize(stdReader, 8192)
|
||||
for {
|
||||
line, _, err := reader.ReadLine()
|
||||
@@ -202,10 +204,7 @@ func (p *process) Start() (err error) {
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
common.Recover("")
|
||||
errReader.Close()
|
||||
}()
|
||||
defer wg.Done()
|
||||
reader := bufio.NewReaderSize(errReader, 8192)
|
||||
for {
|
||||
line, _, err := reader.ReadLine()
|
||||
@@ -224,6 +223,7 @@ func (p *process) Start() (err error) {
|
||||
if err != nil {
|
||||
p.exitErr = err
|
||||
}
|
||||
wg.Wait()
|
||||
}()
|
||||
|
||||
p.refreshVersion()
|
||||
@@ -243,7 +243,7 @@ func (p *process) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) {
|
||||
if p.apiPort == 0 {
|
||||
return nil, nil, common.NewError("xray api port wrong:", p.apiPort)
|
||||
}
|
||||
conn, err := grpc.Dial(fmt.Sprintf("127.0.0.1:%v", p.apiPort), grpc.WithInsecure())
|
||||
conn, err := grpc.Dial(fmt.Sprintf("127.0.0.1:%v", p.apiPort), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user