Compare commits
68 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 |
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -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 }}
|
||||
@@ -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.5.0`:
|
||||
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.5.0
|
||||
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.5.0
|
||||
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
|
||||
}
|
||||
|
||||
25
go.mod
25
go.mod
@@ -3,7 +3,7 @@ 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
|
||||
@@ -11,28 +11,29 @@ require (
|
||||
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/andybalholm/brotli v1.0.5 // indirect
|
||||
github.com/bytedance/sonic v1.8.8 // 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
|
||||
@@ -44,26 +45,26 @@ require (
|
||||
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/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.5 // 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/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.47.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.2 // 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
|
||||
)
|
||||
|
||||
54
go.sum
54
go.sum
@@ -1,8 +1,8 @@
|
||||
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=
|
||||
@@ -10,8 +10,8 @@ github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P
|
||||
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=
|
||||
@@ -22,6 +22,8 @@ github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 h1:y7y0Oa6UawqTFP
|
||||
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=
|
||||
@@ -45,8 +47,8 @@ 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-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
@@ -94,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=
|
||||
@@ -115,8 +116,8 @@ 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=
|
||||
@@ -142,10 +143,11 @@ github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJ
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U=
|
||||
github.com/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=
|
||||
@@ -155,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=
|
||||
@@ -179,8 +182,9 @@ 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=
|
||||
@@ -191,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=
|
||||
@@ -202,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=
|
||||
@@ -221,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=
|
||||
@@ -242,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=
|
||||
@@ -259,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)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package sub
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"x-ui/database"
|
||||
"x-ui/database/model"
|
||||
"x-ui/logger"
|
||||
"x-ui/web/service"
|
||||
"x-ui/xray"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
@@ -15,7 +16,8 @@ import (
|
||||
|
||||
type SubService struct {
|
||||
address string
|
||||
inboundService InboundService
|
||||
inboundService service.InboundService
|
||||
settingServics service.SettingService
|
||||
}
|
||||
|
||||
func (s *SubService) GetSubs(subId string, host string) ([]string, []string, error) {
|
||||
@@ -29,13 +31,28 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, []string, 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,7 +83,8 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, []string, err
|
||||
}
|
||||
}
|
||||
headers = append(headers, fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000))
|
||||
headers = append(headers, "12")
|
||||
updateInterval, _ := s.settingServics.GetSubUpdates()
|
||||
headers = append(headers, fmt.Sprintf("%d", updateInterval))
|
||||
headers = append(headers, subId)
|
||||
return result, headers, nil
|
||||
}
|
||||
@@ -90,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":
|
||||
@@ -163,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{})
|
||||
@@ -185,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 != "" {
|
||||
@@ -192,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 {
|
||||
@@ -203,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)
|
||||
}
|
||||
@@ -214,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 {
|
||||
@@ -270,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{})
|
||||
@@ -294,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 {
|
||||
@@ -393,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()
|
||||
}
|
||||
@@ -404,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 {
|
||||
@@ -460,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{})
|
||||
@@ -484,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)
|
||||
@@ -580,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()
|
||||
}
|
||||
@@ -589,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)
|
||||
@@ -603,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")
|
||||
}
|
||||
|
||||
9
web/assets/ant-design-vue@1.7.2/antd.min.css
vendored
9
web/assets/ant-design-vue@1.7.2/antd.min.css
vendored
@@ -996,7 +996,8 @@ to{transform:scale(0) translate(50%,-50%);opacity:0}
|
||||
.ant-menu-horizontal .ant-menu-item,.ant-menu-horizontal .ant-menu-submenu{margin-top:-1px}
|
||||
.ant-menu-horizontal>.ant-menu-item-active,.ant-menu-horizontal>.ant-menu-item:hover,.ant-menu-horizontal>.ant-menu-submenu .ant-menu-submenu-title:hover{background-color:transparent}
|
||||
.ant-menu-item-selected,.ant-menu-item-selected>a,.ant-menu-item-selected>a:hover{color:#1890ff}
|
||||
.ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected{background: linear-gradient(90deg,#009670 0,#026247 100%);color: #fff;border-radius: 0.5rem}
|
||||
.ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected{background-color: #0a7557;background-image: linear-gradient( 270deg, rgba(123, 199, 77, 0) 30%, #00ab80, rgba(123, 199, 77, 0) 100% );background-repeat: no-repeat;animation: ma-bg-move linear 6.6s infinite;/*background: linear-gradient(90deg,#009670 0,#026247 100%);*/color: #fff;border-radius: 0.5rem}
|
||||
@-webkit-keyframes ma-bg-move {0% {background-position: -500px 0;}100% {background-position: 1000px 0;}}@keyframes ma-bg-move {0% {background-position: -500px 0;}50% {background-position: 1000px 0;}100% {background-position: 1000px 0;}}
|
||||
.ant-menu-vertical-right{border-left:1px solid #e8e8e8}
|
||||
.ant-menu-vertical-left.ant-menu-sub,.ant-menu-vertical-right.ant-menu-sub,.ant-menu-vertical.ant-menu-sub{min-width:160px;padding:0;border-right:0;transform-origin:0 0}
|
||||
.ant-menu-vertical-left.ant-menu-sub .ant-menu-item,.ant-menu-vertical-right.ant-menu-sub .ant-menu-item,.ant-menu-vertical.ant-menu-sub .ant-menu-item{left:0;margin-left:0;border-right:0}
|
||||
@@ -1081,8 +1082,8 @@ to{transform:scale(0) translate(50%,-50%);opacity:0}
|
||||
.ant-menu-dark .ant-menu-item-selected{color:#fff;border-right:0}
|
||||
.ant-menu-dark .ant-menu-item-selected:after{border-right:0}
|
||||
.ant-menu-dark .ant-menu-item-selected .anticon,.ant-menu-dark .ant-menu-item-selected .anticon+span,.ant-menu-dark .ant-menu-item-selected>a,.ant-menu-dark .ant-menu-item-selected>a:hover{color:#ffffff}
|
||||
.ant-menu-submenu-popup.ant-menu-dark .ant-menu-item-selected,.ant-menu.ant-menu-dark .ant-menu-item-selected{background-color:#15223a}
|
||||
.ant-menu-submenu-popup.ant-menu-dark .ant-menu-item-active,.ant-menu.ant-menu-dark .ant-menu-item-active{background-color:#38383800}
|
||||
.ant-menu-submenu-popup.ant-menu-dark .ant-menu-item-selected,.ant-menu.ant-menu-dark .ant-menu-item-selected{background-color:#0a7557}
|
||||
/*.ant-menu-submenu-popup.ant-menu-dark .ant-menu-item-active,.ant-menu.ant-menu-dark .ant-menu-item-active{background-color:#0a7557}*/
|
||||
.ant-menu-dark .ant-menu-item-disabled,.ant-menu-dark .ant-menu-item-disabled>a,.ant-menu-dark .ant-menu-submenu-disabled,.ant-menu-dark .ant-menu-submenu-disabled>a{color:hsla(0,0%,100%,.35)!important;opacity:.8}
|
||||
.ant-menu-dark .ant-menu-item-disabled>.ant-menu-submenu-title,.ant-menu-dark .ant-menu-submenu-disabled>.ant-menu-submenu-title{color:hsla(0,0%,100%,.35)!important}
|
||||
.ant-menu-dark .ant-menu-item-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-item-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:before,.ant-menu-dark .ant-menu-submenu-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-submenu-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:before{background:hsla(0,0%,100%,.35)!important}
|
||||
@@ -3231,7 +3232,7 @@ textarea.ant-input-number{max-width:100%;height:auto;min-height:32px;line-height
|
||||
.ant-input-number-handler-down-inner:before,.ant-input-number-handler-up-inner:before{display:none}
|
||||
.ant-input-number-handler-down-inner .ant-input-number-handler-down-inner-icon,.ant-input-number-handler-down-inner .ant-input-number-handler-up-inner-icon,.ant-input-number-handler-up-inner .ant-input-number-handler-down-inner-icon,.ant-input-number-handler-up-inner .ant-input-number-handler-up-inner-icon{display:block}
|
||||
.ant-input-number-focused,.ant-input-number:hover{border-color:rgb(0, 150, 112) !important;border-right-width:1px!important}
|
||||
.ant-input-number-focused{outline:0;box-shadow:0 0 0 2px rgba(24,144,255,.2)}
|
||||
.ant-input-number-focused{outline:0;box-shadow:rgba(0, 150, 112, 0.2) 0px 0px 0px 2px}
|
||||
.ant-input-number-disabled{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}
|
||||
.ant-input-number-disabled:hover{border-color:#d9d9d9;border-right-width:1px!important}
|
||||
.ant-input-number-disabled .ant-input-number-input{cursor:not-allowed}
|
||||
|
||||
@@ -213,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;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -706,7 +708,7 @@ class RealityStreamSettings extends XrayCommonClass {
|
||||
minClient = '',
|
||||
maxClient = '',
|
||||
maxTimediff = 0,
|
||||
shortIds = RandomUtil.randowShortId(),
|
||||
shortIds = RandomUtil.randomShortId(),
|
||||
settings= new RealityStreamSettings.Settings()
|
||||
){
|
||||
super();
|
||||
@@ -1507,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 '';
|
||||
}
|
||||
@@ -1537,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,25 +89,27 @@ 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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -81,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
|
||||
}
|
||||
|
||||
@@ -94,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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
@@ -77,7 +95,7 @@
|
||||
<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-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" }}'>
|
||||
@@ -88,19 +106,19 @@
|
||||
</a-form-item>
|
||||
<a-button type="primary" icon="import" @click="setDefaultCertData(index)">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
|
||||
</template>
|
||||
</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 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>
|
||||
@@ -122,7 +140,7 @@
|
||||
<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.tls.certs.length>1" type="primary" size="small" @click="inbound.stream.xtls.removeCert(index)" 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" }}'>
|
||||
@@ -133,14 +151,14 @@
|
||||
</a-form-item>
|
||||
<a-button type="primary" icon="import" @click="setDefaultCertXtls(index)">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
|
||||
</template>
|
||||
</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 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>
|
||||
|
||||
@@ -169,10 +187,12 @@
|
||||
<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"></a-input>
|
||||
<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,7 +29,7 @@
|
||||
<a-tag v-if="!isClientEnabled(record, client.email)" color="red">{{ i18n "depleted" }}</a-tag>
|
||||
</template>
|
||||
<template slot="traffic" slot-scope="text, client">
|
||||
<a-tag :color="statsColor(record, client.email)" @click="alert(usageColor(0,1024,512))">[[ 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 :color="statsColor(record, client.email)">[[client._totalGB]]GB</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>
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -105,6 +105,10 @@
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
<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>
|
||||
@@ -112,10 +116,6 @@
|
||||
<a-radio-button value="depleted">{{ i18n "depleted" }}</a-radio-button>
|
||||
<a-radio-button value="expiring">{{ i18n "depletingSoon" }}</a-radio-button>
|
||||
</a-radio-group>
|
||||
<a-switch v-model="enableFilter"
|
||||
checked-children='{{ i18n "search" }}' un-checked-children='{{ i18n "filter" }}'
|
||||
@change="toggleFilter">
|
||||
</a-switch>
|
||||
<a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
|
||||
:data-source="searchedInbounds"
|
||||
:loading="spinning" :scroll="{ x: 1300 }"
|
||||
@@ -175,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>
|
||||
@@ -343,7 +343,15 @@
|
||||
clientCount: {},
|
||||
isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false,
|
||||
refreshing: false,
|
||||
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000
|
||||
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
|
||||
subSettings: {
|
||||
enable : false,
|
||||
port: 0,
|
||||
path: '',
|
||||
domain: '',
|
||||
tls: false
|
||||
},
|
||||
tgBotEnable: false
|
||||
},
|
||||
methods: {
|
||||
loading(spinning = true) {
|
||||
@@ -353,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);
|
||||
@@ -365,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);
|
||||
@@ -745,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);
|
||||
@@ -852,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;
|
||||
|
||||
@@ -23,6 +23,52 @@
|
||||
:not(.ant-card-dark)>.ant-tabs-top-bar {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.alert-msg {
|
||||
color: rgb(194, 117, 18);
|
||||
font-weight: normal;
|
||||
font-size: 16px;
|
||||
padding: .5rem 1rem;
|
||||
text-align: center;
|
||||
background: rgb(255 145 0 / 15%);
|
||||
margin: 1.5rem 2.5rem 0rem 2.5rem;
|
||||
border-radius: .5rem;
|
||||
transition: all 0.5s;
|
||||
animation: signal 3s cubic-bezier(0.18, 0.89, 0.32, 1.28) infinite;
|
||||
}
|
||||
.alert-msg:hover {
|
||||
cursor: default;
|
||||
transition-duration: .3s;
|
||||
animation: signal 0.9s ease infinite;
|
||||
}
|
||||
@keyframes signal{
|
||||
0%{
|
||||
box-shadow: 0 0 0 0 rgba(194, 118, 18, 0.5);
|
||||
}
|
||||
50%{
|
||||
box-shadow: 0 0 0 6px rgba(0 ,0,0,0);
|
||||
}
|
||||
100%{
|
||||
box-shadow: 0 0 0 6px rgba(0 ,0,0,0);
|
||||
}
|
||||
}
|
||||
.alert-msg > i {
|
||||
color: inherit;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.collapse-title {
|
||||
color: inherit;
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
padding: 10px 20px;
|
||||
border-bottom: 2px solid;
|
||||
}
|
||||
|
||||
.collapse-title > i {
|
||||
color: inherit;
|
||||
font-size: 24px;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<a-layout id="app" v-cloak>
|
||||
@@ -35,8 +81,14 @@
|
||||
<a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n "pages.settings.save" }}</a-button>
|
||||
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.settings.restartPanel" }}</a-button>
|
||||
</a-space>
|
||||
<a-tabs style="margin:1rem 0.5rem;" default-active-key="1" :class="themeSwitcher.darkCardClass" >
|
||||
<a-tabs style="margin:1rem 0.5rem;" default-active-key="1" :class="themeSwitcher.darkCardClass">
|
||||
<a-tab-pane key="1" tab='{{ i18n "pages.settings.panelSettings"}}'>
|
||||
<a-row :xs="24" :sm="24" :lg="12">
|
||||
<h2 class="alert-msg">
|
||||
<a-icon type="warning"></a-icon>
|
||||
{{ i18n "pages.settings.infoDesc" }}
|
||||
</h2>
|
||||
</a-row>
|
||||
<a-list item-layout="horizontal" :style="themeSwitcher.textStyle">
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.panelListeningIP"}}' desc='{{ i18n "pages.settings.panelListeningIPDesc"}}' v-model="allSetting.webListen"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.settings.panelPort"}}' desc='{{ i18n "pages.settings.panelPortDesc"}}' v-model="allSetting.webPort" :min="0"></setting-list-item>
|
||||
@@ -72,12 +124,6 @@
|
||||
</a-row>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
<a-row :xs="24" :sm="24" :lg="12">
|
||||
<h2 style="color: inherit; font-weight: bold; font-size: 16px; padding: 5px 5px; text-align: center;">
|
||||
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
|
||||
{{ i18n "pages.settings.infoDesc" }}
|
||||
</h2>
|
||||
</a-row>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2" tab='{{ i18n "pages.settings.securitySettings"}}' style="padding: 20px;">
|
||||
<a-tabs class="ant-card-dark-securitybox-nohover" default-active-key="sec-1" :class="themeSwitcher.darkCardClass">
|
||||
@@ -144,8 +190,8 @@
|
||||
</a-space>
|
||||
<a-divider style="padding: 20px;">{{ i18n "pages.settings.templates.title"}} </a-divider>
|
||||
<a-row :xs="24" :sm="24" :lg="12">
|
||||
<h2 style="color: inherit; font-weight: bold; font-size: 16px; padding: 5px 5px; text-align: center;">
|
||||
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
|
||||
<h2 class="alert-msg">
|
||||
<a-icon type="warning"></a-icon>
|
||||
{{ i18n "pages.settings.infoDesc" }}
|
||||
</h2>
|
||||
</a-row>
|
||||
@@ -154,8 +200,8 @@
|
||||
<a-collapse>
|
||||
<a-collapse-panel header='{{ i18n "pages.settings.templates.generalConfigs"}}'>
|
||||
<a-row :xs="24" :sm="24" :lg="12">
|
||||
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
|
||||
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
|
||||
<h2 class="collapse-title">
|
||||
<a-icon type="warning"></a-icon>
|
||||
{{ i18n "pages.settings.templates.generalConfigsDesc" }}
|
||||
</h2>
|
||||
</a-row>
|
||||
@@ -199,8 +245,8 @@
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel header='{{ i18n "pages.settings.templates.blockConfigs"}}'>
|
||||
<a-row :xs="24" :sm="24" :lg="12">
|
||||
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
|
||||
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
|
||||
<h2 class="collapse-title">
|
||||
<a-icon type="warning"></a-icon>
|
||||
{{ i18n "pages.settings.templates.blockConfigsDesc" }}
|
||||
</h2>
|
||||
</a-row>
|
||||
@@ -212,8 +258,8 @@
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel header='{{ i18n "pages.settings.templates.blockCountryConfigs"}}'>
|
||||
<a-row :xs="24" :sm="24" :lg="12">
|
||||
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
|
||||
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
|
||||
<h2 class="collapse-title">
|
||||
<a-icon type="warning"></a-icon>
|
||||
{{ i18n "pages.settings.templates.blockCountryConfigsDesc" }}
|
||||
</h2>
|
||||
</a-row>
|
||||
@@ -226,8 +272,8 @@
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel header='{{ i18n "pages.settings.templates.directCountryConfigs"}}'>
|
||||
<a-row :xs="24" :sm="24" :lg="12">
|
||||
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
|
||||
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
|
||||
<h2 class="collapse-title">
|
||||
<a-icon type="warning"></a-icon>
|
||||
{{ i18n "pages.settings.templates.directCountryConfigsDesc" }}
|
||||
</h2>
|
||||
</a-row>
|
||||
@@ -240,8 +286,8 @@
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel header='{{ i18n "pages.settings.templates.ipv4Configs"}}'>
|
||||
<a-row :xs="24" :sm="24" :lg="12">
|
||||
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
|
||||
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
|
||||
<h2 class="collapse-title">
|
||||
<a-icon type="warning"></a-icon>
|
||||
{{ i18n "pages.settings.templates.ipv4ConfigsDesc" }}
|
||||
</h2>
|
||||
</a-row>
|
||||
@@ -250,8 +296,8 @@
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel header='{{ i18n "pages.settings.templates.warpConfigs"}}'>
|
||||
<a-row :xs="24" :sm="24" :lg="12">
|
||||
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
|
||||
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
|
||||
<h2 class="collapse-title">
|
||||
<a-icon type="warning"></a-icon>
|
||||
{{ i18n "pages.settings.templates.warpConfigsDesc" }}
|
||||
</h2>
|
||||
</a-row>
|
||||
@@ -262,8 +308,8 @@
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel header='{{ i18n "pages.settings.templates.manualLists"}}'>
|
||||
<a-row :xs="24" :sm="24" :lg="12">
|
||||
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
|
||||
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
|
||||
<h2 class="collapse-title">
|
||||
<a-icon type="warning"></a-icon>
|
||||
{{ i18n "pages.settings.templates.manualListsDesc" }}
|
||||
</h2>
|
||||
</a-row>
|
||||
@@ -271,6 +317,8 @@
|
||||
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualBlockedDomains"}}' v-model="manualBlockedDomains"></setting-list-item>
|
||||
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualDirectIPs"}}' v-model="manualDirectIPs"></setting-list-item>
|
||||
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualDirectDomains"}}' v-model="manualDirectDomains"></setting-list-item>
|
||||
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualIPv4Domains"}}' v-model="manualIPv4Domains"></setting-list-item>
|
||||
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualWARPDomains"}}' v-model="manualWARPDomains"></setting-list-item>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</a-tab-pane>
|
||||
@@ -295,6 +343,12 @@
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="4" tab='{{ i18n "pages.settings.TGBotSettings"}}'>
|
||||
<a-row :xs="24" :sm="24" :lg="12">
|
||||
<h2 class="alert-msg">
|
||||
<a-icon type="warning"></a-icon>
|
||||
{{ i18n "pages.settings.infoDesc" }}
|
||||
</h2>
|
||||
</a-row>
|
||||
<a-list item-layout="horizontal" :style="themeSwitcher.textStyle">
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.telegramBotEnable" }}' desc='{{ i18n "pages.settings.telegramBotEnableDesc" }}' v-model="allSetting.tgBotEnable"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.telegramToken"}}' desc='{{ i18n "pages.settings.telegramTokenDesc"}}' v-model="allSetting.tgBotToken"></setting-list-item>
|
||||
@@ -302,13 +356,48 @@
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.telegramNotifyTime"}}' desc='{{ i18n "pages.settings.telegramNotifyTimeDesc"}}' v-model="allSetting.tgRunTime"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.tgNotifyBackup" }}' desc='{{ i18n "pages.settings.tgNotifyBackupDesc" }}' v-model="allSetting.tgBotBackup"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.settings.tgNotifyCpu" }}' desc='{{ i18n "pages.settings.tgNotifyCpuDesc" }}' v-model="allSetting.tgCpu" :min="0" :max="100"></setting-list-item>
|
||||
<a-list-item>
|
||||
<a-row style="padding: 20px">
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta title="Telegram Bot Language" />
|
||||
</a-col>
|
||||
|
||||
<a-col :lg="24" :xl="12">
|
||||
<template>
|
||||
<a-select
|
||||
ref="selectBotLang"
|
||||
v-model="allSetting.tgLang"
|
||||
:dropdown-class-name="themeSwitcher.darkCardClass"
|
||||
style="width: 100%"
|
||||
>
|
||||
<a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs">
|
||||
<span role="img" aria-label="l.name" v-text="l.icon"></span>
|
||||
<span v-text="l.name"></span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="5" tab='{{ i18n "pages.settings.subSettings" }}'>
|
||||
<a-row :xs="24" :sm="24" :lg="12">
|
||||
<h2 style="color: inherit; font-weight: bold; font-size: 16px; padding: 5px 5px; text-align: center;">
|
||||
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
|
||||
<h2 class="alert-msg">
|
||||
<a-icon type="warning"></a-icon>
|
||||
{{ i18n "pages.settings.infoDesc" }}
|
||||
</h2>
|
||||
</a-row>
|
||||
<a-list item-layout="horizontal" :style="themeSwitcher.textStyle">
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.subEnable"}}' desc='{{ i18n "pages.settings.subEnableDesc"}}' v-model="allSetting.subEnable"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subListen"}}' desc='{{ i18n "pages.settings.subListenDesc"}}' v-model="allSetting.subListen"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.settings.subPort"}}' desc='{{ i18n "pages.settings.subPortDesc"}}' v-model.number="allSetting.subPort"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subPath"}}' desc='{{ i18n "pages.settings.subPathDesc"}}' v-model="allSetting.subPath"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subDomain"}}' desc='{{ i18n "pages.settings.subDomainDesc"}}' v-model="allSetting.subDomain"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subCertPath"}}' desc='{{ i18n "pages.settings.subCertPathDesc"}}' v-model="allSetting.subCertFile"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subKeyPath"}}' desc='{{ i18n "pages.settings.subKeyPathDesc"}}' v-model="allSetting.subKeyFile"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.settings.subUpdates"}}' desc='{{ i18n "pages.settings.subUpdatesDesc"}}' v-model="allSetting.subUpdates"></setting-list-item>
|
||||
</a-list>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-space>
|
||||
@@ -371,9 +460,6 @@
|
||||
domains: {
|
||||
ads: [
|
||||
"geosite:category-ads-all",
|
||||
"geosite:category-ads",
|
||||
"geosite:google-ads",
|
||||
"geosite:spotify-ads",
|
||||
"ext:iran.dat:ads"
|
||||
],
|
||||
speedtest: ["geosite:speedtest"],
|
||||
@@ -455,7 +541,12 @@
|
||||
if (msg.success) {
|
||||
this.loading(true);
|
||||
await PromiseUtil.sleep(5000);
|
||||
window.location.replace(this.allSetting.webBasePath + "panel/settings");
|
||||
let protocol = "http://";
|
||||
if (this.allSetting.webCertFile !== "") {
|
||||
protocol = "https://";
|
||||
}
|
||||
const { host } = window.location;
|
||||
window.location.replace(protocol + host + this.allSetting.webBasePath + "panel/settings");
|
||||
}
|
||||
},
|
||||
async fetchUserSecret() {
|
||||
@@ -587,30 +678,30 @@
|
||||
computed: {
|
||||
templateSettings: {
|
||||
get: function () { return this.allSetting.xrayTemplateConfig ? JSON.parse(this.allSetting.xrayTemplateConfig) : null; },
|
||||
set: function (newValue) { this.allSetting.xrayTemplateConfig = JSON.stringify(newValue, null, 2) },
|
||||
set: function (newValue) { this.allSetting.xrayTemplateConfig = JSON.stringify(newValue, null, 2); },
|
||||
},
|
||||
inboundSettings: {
|
||||
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.inbounds, null, 2) : null; },
|
||||
set: function (newValue) {
|
||||
newTemplateSettings = this.templateSettings;
|
||||
newTemplateSettings.inbounds = JSON.parse(newValue)
|
||||
this.templateSettings = newTemplateSettings
|
||||
newTemplateSettings.inbounds = JSON.parse(newValue);
|
||||
this.templateSettings = newTemplateSettings;
|
||||
},
|
||||
},
|
||||
outboundSettings: {
|
||||
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.outbounds, null, 2) : null; },
|
||||
set: function (newValue) {
|
||||
newTemplateSettings = this.templateSettings;
|
||||
newTemplateSettings.outbounds = JSON.parse(newValue)
|
||||
this.templateSettings = newTemplateSettings
|
||||
newTemplateSettings.outbounds = JSON.parse(newValue);
|
||||
this.templateSettings = newTemplateSettings;
|
||||
},
|
||||
},
|
||||
routingRuleSettings: {
|
||||
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.routing.rules, null, 2) : null; },
|
||||
set: function (newValue) {
|
||||
newTemplateSettings = this.templateSettings;
|
||||
newTemplateSettings.routing.rules = JSON.parse(newValue)
|
||||
this.templateSettings = newTemplateSettings
|
||||
newTemplateSettings.routing.rules = JSON.parse(newValue);
|
||||
this.templateSettings = newTemplateSettings;
|
||||
},
|
||||
},
|
||||
freedomStrategy: {
|
||||
@@ -685,6 +776,24 @@
|
||||
this.syncRulesWithOutbound("direct", this.directSettings);
|
||||
}
|
||||
},
|
||||
ipv4Domains: {
|
||||
get: function () {
|
||||
return this.templateRuleGetter({ outboundTag: "IPv4", property: "domain" });
|
||||
},
|
||||
set: function (newValue) {
|
||||
this.templateRuleSetter({ outboundTag: "IPv4", property: "domain", data: newValue });
|
||||
this.syncRulesWithOutbound("IPv4", this.ipv4Settings);
|
||||
}
|
||||
},
|
||||
warpDomains: {
|
||||
get: function () {
|
||||
return this.templateRuleGetter({ outboundTag: "WARP", property: "domain" });
|
||||
},
|
||||
set: function (newValue) {
|
||||
this.templateRuleSetter({ outboundTag: "WARP", property: "domain", data: newValue });
|
||||
this.syncRulesWithOutbound("WARP", this.warpSettings);
|
||||
}
|
||||
},
|
||||
manualBlockedIPs: {
|
||||
get: function () { return JSON.stringify(this.blockedIPs, null, 2); },
|
||||
set: debounce(function (value) { this.blockedIPs = JSON.parse(value); }, 1000)
|
||||
@@ -701,6 +810,14 @@
|
||||
get: function () { return JSON.stringify(this.directDomains, null, 2); },
|
||||
set: debounce(function (value) { this.directDomains = JSON.parse(value); }, 1000)
|
||||
},
|
||||
manualIPv4Domains: {
|
||||
get: function () { return JSON.stringify(this.ipv4Domains, null, 2); },
|
||||
set: debounce(function (value) { this.ipv4Domains = JSON.parse(value); }, 1000)
|
||||
},
|
||||
manualWARPDomains: {
|
||||
get: function () { return JSON.stringify(this.warpDomains, null, 2); },
|
||||
set: debounce(function (value) { this.warpDomains = JSON.parse(value); }, 1000)
|
||||
},
|
||||
torrentSettings: {
|
||||
get: function () {
|
||||
return doAllItemsExist(this.settingsData.protocols.bittorrent, this.blockedProtocols);
|
||||
@@ -766,40 +883,26 @@
|
||||
},
|
||||
GoogleIPv4Settings: {
|
||||
get: function () {
|
||||
return doAllItemsExist(this.settingsData.domains.google, this.templateRuleGetter({ outboundTag: "IPv4", property: "domain" }));
|
||||
return doAllItemsExist(this.settingsData.domains.google, this.ipv4Domains);
|
||||
},
|
||||
set: function (newValue) {
|
||||
oldData = this.templateRuleGetter({ outboundTag: "IPv4", property: "domain" });
|
||||
if (newValue) {
|
||||
oldData = [...oldData, ...this.settingsData.domains.google];
|
||||
this.ipv4Domains = [...this.ipv4Domains, ...this.settingsData.domains.google];
|
||||
} else {
|
||||
oldData = oldData.filter(data => !this.settingsData.domains.google.includes(data))
|
||||
this.ipv4Domains = this.ipv4Domains.filter(data => !this.settingsData.domains.google.includes(data));
|
||||
}
|
||||
this.templateRuleSetter({
|
||||
outboundTag: "IPv4",
|
||||
property: "domain",
|
||||
data: oldData
|
||||
});
|
||||
this.syncRulesWithOutbound("IPv4", this.ipv4Settings);
|
||||
},
|
||||
},
|
||||
NetflixIPv4Settings: {
|
||||
get: function () {
|
||||
return doAllItemsExist(this.settingsData.domains.netflix, this.templateRuleGetter({ outboundTag: "IPv4", property: "domain" }));
|
||||
return doAllItemsExist(this.settingsData.domains.netflix, this.ipv4Domains);
|
||||
},
|
||||
set: function (newValue) {
|
||||
oldData = this.templateRuleGetter({ outboundTag: "IPv4", property: "domain" });
|
||||
if (newValue) {
|
||||
oldData = [...oldData, ...this.settingsData.domains.netflix];
|
||||
this.ipv4Domains = [...this.ipv4Domains, ...this.settingsData.domains.netflix];
|
||||
} else {
|
||||
oldData = oldData.filter(data => !this.settingsData.domains.netflix.includes(data))
|
||||
this.ipv4Domains = this.ipv4Domains.filter(data => !this.settingsData.domains.netflix.includes(data));
|
||||
}
|
||||
this.templateRuleSetter({
|
||||
outboundTag: "IPv4",
|
||||
property: "domain",
|
||||
data: oldData
|
||||
});
|
||||
this.syncRulesWithOutbound("IPv4", this.ipv4Settings);
|
||||
},
|
||||
},
|
||||
IRIpSettings: {
|
||||
@@ -948,82 +1051,54 @@
|
||||
},
|
||||
GoogleWARPSettings: {
|
||||
get: function () {
|
||||
return doAllItemsExist(this.settingsData.domains.google, this.templateRuleGetter({ outboundTag: "WARP", property: "domain" }));
|
||||
return doAllItemsExist(this.settingsData.domains.google, this.warpDomains);
|
||||
},
|
||||
set: function (newValue) {
|
||||
oldData = this.templateRuleGetter({ outboundTag: "WARP", property: "domain" });
|
||||
if (newValue) {
|
||||
oldData = [...oldData, ...this.settingsData.domains.google];
|
||||
this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.google];
|
||||
} else {
|
||||
oldData = oldData.filter(data => !this.settingsData.domains.google.includes(data))
|
||||
this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.google.includes(data));
|
||||
}
|
||||
this.templateRuleSetter({
|
||||
outboundTag: "WARP",
|
||||
property: "domain",
|
||||
data: oldData
|
||||
});
|
||||
this.syncRulesWithOutbound("WARP", this.warpSettings);
|
||||
},
|
||||
},
|
||||
OpenAIWARPSettings: {
|
||||
get: function () {
|
||||
return doAllItemsExist(this.settingsData.domains.openai, this.templateRuleGetter({ outboundTag: "WARP", property: "domain" }));
|
||||
return doAllItemsExist(this.settingsData.domains.openai, this.warpDomains);
|
||||
},
|
||||
set: function (newValue) {
|
||||
oldData = this.templateRuleGetter({ outboundTag: "WARP", property: "domain" });
|
||||
if (newValue) {
|
||||
oldData = [...oldData, ...this.settingsData.domains.openai];
|
||||
this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.openai];
|
||||
} else {
|
||||
oldData = oldData.filter(data => !this.settingsData.domains.openai.includes(data))
|
||||
this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.openai.includes(data));
|
||||
}
|
||||
this.templateRuleSetter({
|
||||
outboundTag: "WARP",
|
||||
property: "domain",
|
||||
data: oldData
|
||||
});
|
||||
this.syncRulesWithOutbound("WARP", this.warpSettings);
|
||||
},
|
||||
},
|
||||
NetflixWARPSettings: {
|
||||
get: function () {
|
||||
return doAllItemsExist(this.settingsData.domains.netflix, this.templateRuleGetter({ outboundTag: "WARP", property: "domain" }));
|
||||
return doAllItemsExist(this.settingsData.domains.netflix, this.warpDomains);
|
||||
},
|
||||
set: function (newValue) {
|
||||
oldData = this.templateRuleGetter({ outboundTag: "WARP", property: "domain" });
|
||||
if (newValue) {
|
||||
oldData = [...oldData, ...this.settingsData.domains.netflix];
|
||||
this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.netflix];
|
||||
} else {
|
||||
oldData = oldData.filter(data => !this.settingsData.domains.netflix.includes(data))
|
||||
this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.netflix.includes(data));
|
||||
}
|
||||
this.templateRuleSetter({
|
||||
outboundTag: "WARP",
|
||||
property: "domain",
|
||||
data: oldData
|
||||
});
|
||||
this.syncRulesWithOutbound("WARP", this.warpSettings);
|
||||
},
|
||||
},
|
||||
SpotifyWARPSettings: {
|
||||
get: function () {
|
||||
return doAllItemsExist(this.settingsData.domains.spotify, this.templateRuleGetter({ outboundTag: "WARP", property: "domain" }));
|
||||
return doAllItemsExist(this.settingsData.domains.spotify, this.warpDomains);
|
||||
},
|
||||
set: function (newValue) {
|
||||
oldData = this.templateRuleGetter({ outboundTag: "WARP", property: "domain" });
|
||||
if (newValue) {
|
||||
oldData = [...oldData, ...this.settingsData.domains.spotify];
|
||||
this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.spotify];
|
||||
} else {
|
||||
oldData = oldData.filter(data => !this.settingsData.domains.spotify.includes(data))
|
||||
this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.spotify.includes(data));
|
||||
}
|
||||
this.templateRuleSetter({
|
||||
outboundTag: "WARP",
|
||||
property: "domain",
|
||||
data: oldData
|
||||
});
|
||||
this.syncRulesWithOutbound("WARP", this.warpSettings);
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -712,7 +712,7 @@ func (s *InboundService) GetClientByEmail(clientEmail string) (*xray.ClientTraff
|
||||
return nil, nil, common.NewError("Inbound Not Found For Email:", clientEmail)
|
||||
}
|
||||
|
||||
clients, err := s.getClients(inbound)
|
||||
clients, err := s.GetClients(inbound)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -737,7 +737,7 @@ func (s *InboundService) SetClientTelegramUserID(trafficId int, tgId string) err
|
||||
|
||||
clientEmail := traffic.Email
|
||||
|
||||
oldClients, err := s.getClients(inbound)
|
||||
oldClients, err := s.GetClients(inbound)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -791,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
|
||||
}
|
||||
@@ -847,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
|
||||
}
|
||||
@@ -901,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
|
||||
}
|
||||
@@ -1100,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")
|
||||
}
|
||||
@@ -1250,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
@@ -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."
|
||||
@@ -209,7 +210,7 @@
|
||||
[pages.settings]
|
||||
"title" = "Settings"
|
||||
"save" = "Save"
|
||||
"infoDesc" = "Every change made here needs to be saved. Please restart the panel for the changes to take effect."
|
||||
"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"
|
||||
@@ -252,6 +253,23 @@
|
||||
"tgNotifyCpuDesc" = "Receive notification if CPU usage exceeds this threshold (unit: %)"
|
||||
"timeZone" = "Time zone"
|
||||
"timeZoneDesc" = "Scheduled tasks run according to the time in this time zone."
|
||||
"subSettings" = "Subscription"
|
||||
"subEnable" = "Enable service"
|
||||
"subEnableDesc" = "Subscription feature with separate configuration"
|
||||
"subListen" = "Listening IP"
|
||||
"subListenDesc" = "Leave blank by default to monitor all IPs"
|
||||
"subPort" = "Subscription Port"
|
||||
"subPortDesc" = "Port number for serving the subscription service must be unused in server"
|
||||
"subCertPath" = "Subscription Certificate Public Key File Path"
|
||||
"subCertPathDesc" = "Fill in an absolute path starting with '/'"
|
||||
"subKeyPath" = "Subscription Certificate Private Key File Path"
|
||||
"subKeyPathDesc" = "Fill in an absolute path starting with '/'"
|
||||
"subPath" = "Subscription URL Root Path"
|
||||
"subPathDesc" = "Must start with '/' and end with '/'"
|
||||
"subDomain" = "Listening Domain"
|
||||
"subDomainDesc" = "Leave blank by default to monitor all domains and IPs"
|
||||
"subUpdates" = "Subscription update intervals"
|
||||
"subUpdatesDesc" = "Interval hours between updates in client application"
|
||||
|
||||
[pages.settings.templates]
|
||||
"title" = "Templates"
|
||||
@@ -336,6 +354,8 @@
|
||||
"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"
|
||||
@@ -351,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>"
|
||||
|
||||
@@ -78,11 +78,12 @@
|
||||
"xraySwitch" = "تغییر ورژن"
|
||||
"xraySwitchClick" = "ورژن مورد نظر را انتخاب کنید"
|
||||
"xraySwitchClickDesk" = "لطفا با دقت انتخاب کنید ، در صورت انتخاب اشتباه امکان قطعی سیستم وجود دارد "
|
||||
"operationHours" = "مدت فعالیت"
|
||||
"operationHoursDesc" = "مدت فعالیت سیستم بعد از روشن شدن"
|
||||
"systemLoad" = "بار روی سیستم"
|
||||
"operationHours" = "آپ تایم سیستم"
|
||||
"systemLoad" = "بار سیستم"
|
||||
"systemLoadDesc" = "میانگین بار سیستم برای 1، 5 و 15 دقیقه گذشته"
|
||||
"connectionTcpCountDesc" = "مجموع اتصالات TCP در تمام کارت های شبکه"
|
||||
"connectionUdpCountDesc" = "مجموع اتصالات UDP در تمام کارت های شبکه"
|
||||
"connectionCount" = "تعداد کانکشن ها"
|
||||
"connectionCountDesc" = "تعداد کانکشن ها برای کل شبکه"
|
||||
"upSpeed" = "سرعت آپلود در حال حاضر سیستم"
|
||||
"downSpeed" = "سرعت دانلود در حال حاضر سیستم"
|
||||
"totalSent" = "جمع کل ترافیک آپلود مصرفی"
|
||||
@@ -252,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" = "الگوها"
|
||||
@@ -336,6 +354,8 @@
|
||||
"manualBlockedDomains" = "لیست دامنه های مسدود شده"
|
||||
"manualDirectIPs" = "لیست آیپی های مستقیم"
|
||||
"manualDirectDomains" = "لیست دامنه های مستقیم"
|
||||
"manualIPv4Domains" = "لیست دامنههای IPv4"
|
||||
"manualWARPDomains" = "لیست دامنه های WARP"
|
||||
|
||||
[pages.settings.security]
|
||||
"admin" = "مدیر"
|
||||
@@ -351,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 [ایمیل]</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,64 +1,64 @@
|
||||
"username" = "имя пользователя"
|
||||
"password" = "пароль"
|
||||
"login" = "логин"
|
||||
"confirm" = "подтвердить"
|
||||
"cancel" = "отмена"
|
||||
"close" = "закрыть"
|
||||
"copy" = "копировать"
|
||||
"copied" = "скопировано"
|
||||
"download" = "скачать"
|
||||
"remark" = "примечание"
|
||||
"enable" = "включить"
|
||||
"protocol" = "протокол"
|
||||
"search" = "поиск"
|
||||
"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" = "да"
|
||||
"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" = "логин"
|
||||
"title" = "Логин"
|
||||
"loginAgain" = "Время пребывания в сети вышло. Пожалуйста, войдите в систему снова"
|
||||
|
||||
[pages.login.toasts]
|
||||
@@ -66,61 +66,62 @@
|
||||
"emptyUsername" = "Введите имя пользователя"
|
||||
"emptyPassword" = "Введите пароль"
|
||||
"wrongUsernameOrPassword" = "Неверное имя пользователя или пароль"
|
||||
"successLogin" = "успешный вход"
|
||||
"successLogin" = "Успешный вход"
|
||||
|
||||
[pages.index]
|
||||
"title" = "статус системы"
|
||||
"memory" = "память"
|
||||
"hard" = "жесткий диск"
|
||||
"xrayStatus" = "статус Xray"
|
||||
"stopXray" = "стоп"
|
||||
"restartXray" = "рестарт Xray"
|
||||
"xraySwitch" = "переключить версию"
|
||||
"title" = "Статус системы"
|
||||
"memory" = "Память"
|
||||
"hard" = "Жесткий диск"
|
||||
"xrayStatus" = "Статус Xray"
|
||||
"stopXray" = "Остановить Xray"
|
||||
"restartXray" = "Рестарт Xray"
|
||||
"xraySwitch" = "Переключить версию"
|
||||
"xraySwitchClick" = "Выберите желаемую версию"
|
||||
"xraySwitchClickDesk" = "Выбирайте внимательно, так как старые версии могут быть несовместимы с текущими конфигурациями"
|
||||
"operationHours" = "Часы работы"
|
||||
"operationHoursDesc" = "Аптайм системы: время системы в сети"
|
||||
"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" = "База данных бекапа и восстановления"
|
||||
"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,37 +139,37 @@
|
||||
"publicKeyContent" = "Содержимое публичного ключа"
|
||||
"keyPath" = "Путь к приватному ключу"
|
||||
"keyContent" = "Содержимое приватного ключа"
|
||||
"clickOnQRcode" = "Нажмите на QR-код, чтобы скопировать"
|
||||
"clickOnQRcode" = "Нажмите на QR код, чтобы скопировать"
|
||||
"client" = "Клиент"
|
||||
"export" = "Поделиться ключом"
|
||||
"export" = "Экспорт ключей"
|
||||
"clone" = "Клонировать"
|
||||
"cloneInbound" = "Клонировать пользователя"
|
||||
"cloneInboundContent" = "Все настройки этого пользователя, кроме порта, порт прослушки и клиентов, будут клонированы"
|
||||
"cloneInbound" = "Клонировать"
|
||||
"cloneInboundContent" = "Все настройки этого подключения, кроме порта, IP прослушки и клиентов, будут клонированы"
|
||||
"cloneInboundOk" = "Клонировано"
|
||||
"resetAllTraffic" = "Обнулить весь траффик"
|
||||
"resetAllTrafficTitle" = "Обнуление всего траффика"
|
||||
"resetAllTrafficContent" = "Подтверждаете обнуление всего траффика пользователей?"
|
||||
"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" = "Добавить пользователя"
|
||||
@@ -182,7 +183,7 @@
|
||||
"last" = "Последний"
|
||||
"prefix" = "Префикс"
|
||||
"postfix" = "Постфикс"
|
||||
"delayedStart" = "Начать со времени первого подключения"
|
||||
"delayedStart" = "Начать со момента первого подключения"
|
||||
"expireDays" = "Срок действия"
|
||||
"days" = "дней"
|
||||
|
||||
@@ -190,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" = "Шифрование"
|
||||
@@ -210,132 +211,151 @@
|
||||
"title" = "Настройки"
|
||||
"save" = "Сохранить"
|
||||
"infoDesc" = "Каждое сделанное здесь изменение необходимо сохранить. Пожалуйста, перезапустите панель, чтобы изменения вступили в силу"
|
||||
"restartPanel" = "Рестарт панели"
|
||||
"restartPanelDesc" = "Подтвердите рестарт панели? ОК для рестарта панели через 3 сек. Если вы не можете пользоваться панелью после рестарта, пожалуйста, посмотрите лог панели на сервере"
|
||||
"restartPanel" = "Перезапуск панели"
|
||||
"restartPanelDesc" = "Подтвердите перезапуск панели? ОК для перезапуска панели через 3 сек. Если вы не можете пользоваться панелью после перезапуска, пожалуйста, посмотрите лог панели на сервере"
|
||||
"actions" = "Действия"
|
||||
"resetDefaultConfig" = "Сбросить всё по-умолчанию"
|
||||
"resetDefaultConfig" = "Сбросить на конфигурацию по-умолчанию"
|
||||
"panelSettings" = "Настройки панели"
|
||||
"securitySettings" = "Настройки безопасности"
|
||||
"xrayConfiguration" = "Конфигурация Xray"
|
||||
"TGBotSettings" = "Настройки Телеграм-бота"
|
||||
"panelListeningIP" = "IP-порт панели"
|
||||
"panelListeningIPDesc" = "Оставьте пустым для работы с любого IP"
|
||||
"TGBotSettings" = "Настройки Telegram бота"
|
||||
"panelListeningIP" = "IP адрес панели"
|
||||
"panelListeningIPDesc" = "Оставьте пустым для подключения с любого IP"
|
||||
"panelPort" = "Порт панели"
|
||||
"panelPortDesc" = "Порт, используемый для отображения этой панели"
|
||||
"publicKeyPath" = "Путь к файлу публичного ключа сертификата панели"
|
||||
"publicKeyPathDesc" = "Введите полный путь, начинающийся с "
|
||||
"privateKeyPath" = "Путь к файлу приватного ключа сертификата панели"
|
||||
"privateKeyPathDesc" = "Введите полный путь, начинающийся с "
|
||||
"panelUrlPath" = "Корневой путь URL-адреса панели"
|
||||
"panelUrlPath" = "Корневой путь URL адреса панели"
|
||||
"panelUrlPathDesc" = "Должен начинаться с «/» и заканчиваться на "
|
||||
"oldUsername" = "Имя пользователя сейчас"
|
||||
"currentPassword" = "Пароль сейчас"
|
||||
"oldUsername" = "Текущее имя пользователя"
|
||||
"currentPassword" = "Текущий пароль"
|
||||
"newUsername" = "Новое имя пользователя"
|
||||
"newPassword" = "Новый пароль"
|
||||
"telegramBotEnable" = "Включить Телеграм-бота"
|
||||
"telegramBotEnableDesc" = "Подключайтесь к возможностям этой панели через Telegram-бота"
|
||||
"telegramToken" = "Токен Телеграм-бота"
|
||||
"telegramTokenDesc" = "Получить токен необходимо у менеджера ботов Telegram @botfather"
|
||||
"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" = "Включать файл резервной копии базы данных с уведомлением об отчете"
|
||||
"sessionMaxAge" = "Продолжительность сессии"
|
||||
"sessionMaxAgeDesc" = "Продолжительность сессии в системе (значение: минута)"
|
||||
"expireTimeDiff" = "Порог истечения срока сессии для уведомления"
|
||||
"expireTimeDiffDesc" = "Получение уведомления об истечении срока действия сессии до достижения порогового значения (значение: день)"
|
||||
"trafficDiff" = "Порог траффика для уведомления"
|
||||
"trafficDiffDesc" = "Получение уведомления об исчерпании трафика до достижения порога (значение: ГБ)"
|
||||
"trafficDiff" = "Порог трафика для уведомления"
|
||||
"trafficDiffDesc" = "Получение уведомления об исчерпании трафика до достижения порога (значение: Гб)"
|
||||
"tgNotifyCpu" = "Порог нагрузки на ЦП для уведомления"
|
||||
"tgNotifyCpuDesc" = "Получение уведомления, если нагрузка на ЦП превышает этот порог (значение:%)"
|
||||
"timeZone" = "Временная зона"
|
||||
"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" = "Эти параметры не позволят пользователям подключаться к определенным протоколам и веб-сайтам"
|
||||
"generalConfigsDesc" = "Эти параметры описывают общие настройки"
|
||||
"blockConfigs" = "Блокировка конфигураций"
|
||||
"blockConfigsDesc" = "Эти параметры не позволят пользователям подключаться к определенным протоколам и веб-сайтам"
|
||||
"blockCountryConfigs" = "Заблокировать конфигурации страны"
|
||||
"blockCountryConfigs" = "Конфигурации блокировки страны"
|
||||
"blockCountryConfigsDesc" = "Эти параметры не позволят пользователям подключаться к доменам определенной страны"
|
||||
"directCountryConfigs" = "Прямые настройки страны"
|
||||
"directCountryConfigsDesc" = "Эти параметры будут подключать пользователей напрямую к доменам определенной страны"
|
||||
"ipv4Configs" = "Настройки IPv4 "
|
||||
"ipv4ConfigsDesc" = "Эти параметры будут маршрутизироваться к целевым доменам только через IPv4"
|
||||
"warpConfigs" = "Настройки WARP"
|
||||
"directCountryConfigs" = "Настройки прямого подключения для страны"
|
||||
"directCountryConfigsDesc" = "Эти параметры позволят пользователям подключаться напрямую к доменам определенной страны"
|
||||
"ipv4Configs" = "Настройки IPv4"
|
||||
"ipv4ConfigsDesc" = "Эти параметры позволят пользователям маршрутизироваться к целевым доменам только через IPv4"
|
||||
"warpConfigs" = "Настройки WARP"
|
||||
"warpConfigsDesc" = "Внимание: перед использованием этих параметров установите WARP в режиме прокси-сервера socks5 на свой сервер, следуя инструкциям на GitHub панели. WARP будет направлять трафик на веб-сайты через серверы Cloudflare"
|
||||
"xrayConfigTemplate" = "Шаблон конфигурации Xray"
|
||||
"xrayConfigTemplateDesc" = "Создание файла конфигурации Xray на основе этого шаблона"
|
||||
"xrayConfigFreedomStrategy" = "Настроить стратегию протокола Freedom"
|
||||
"xrayConfigFreedomStrategyDesc" = "Установить стратегию вывода сети в протоколе Freedom"
|
||||
"xrayConfigRoutingStrategy" = "Настроить доменную стратегию маршрутизации"
|
||||
"xrayConfigRoutingStrategyDesc" = "Установить общую стратегию маршрутизации разрешения DNS"
|
||||
"xrayConfigTorrent" = "Запретить использование BitTorrent"
|
||||
"xrayConfigTorrentDesc" = "Измените конфигурацию, чтобы пользователи не использовали BitTorrent"
|
||||
"xrayConfigPrivateIp" = "Запрет частных диапазонов IP-адресов для подключения"
|
||||
"xrayConfigPrivateIpDesc" = "Измените конфигурацию, чтобы избежать подключения к диапазонам частных IP-адресов"
|
||||
"xrayConfigAds" = "Бокировка рекламы"
|
||||
"xrayConfigAdsDesc" = "Измените конфигурацию, чтобы заблокировать рекламу"
|
||||
"xrayConfigFreedomStrategy" = "Настройка стратегии протокола Freedom"
|
||||
"xrayConfigFreedomStrategyDesc" = "Установка стратегию вывода сети в протоколе Freedom"
|
||||
"xrayConfigRoutingStrategy" = "Настройка стратегии маршрутизации доменов"
|
||||
"xrayConfigRoutingStrategyDesc" = "Установка общей стратегии маршрутизации разрешения DNS"
|
||||
"xrayConfigTorrent" = "Запрет использования BitTorrent"
|
||||
"xrayConfigTorrentDesc" = "Изменение шаблона конфигурации, для предупреждения использования BitTorrent пользователями"
|
||||
"xrayConfigPrivateIp" = "Запрет частных диапазонов IP адресов для подключения"
|
||||
"xrayConfigPrivateIpDesc" = "Изменение шаблона конфигурации, для предупреждения подключения к диапазонам частных IP адресов"
|
||||
"xrayConfigAds" = "Блокировка рекламы"
|
||||
"xrayConfigAdsDesc" = "Изменение конфигурации, для блокировки рекламы"
|
||||
"xrayConfigFamily" = "Блокировать вредоносное ПО и контент для взрослых"
|
||||
"xrayConfigFamilyDesc" = "Преобразователи DNS для блокировки вредоносных программ и контента для взрослых для защиты семьи"
|
||||
"xrayConfigFamilyDesc" = "Резольверы DNS для блокировки вредоносных программ и контента для взрослых для защиты семьи"
|
||||
"xrayConfigSpeedtest" = "Блокировать сайты для проверки скорости"
|
||||
"xrayConfigSpeedtestDesc" = "Измените шаблон конфигурации, чтобы избежать подключения к веб-сайтам для тестирования скорости"
|
||||
"xrayConfigIRIp" = "Отключить подключение к диапазонам IP-адресов Ирана"
|
||||
"xrayConfigIRIpDesc" = "Измените конфигурацию, чтобы отключить подключение к диапазонам IP-адресов Ирана"
|
||||
"xrayConfigIRDomain" = "Отключить подключение к доменам Ирана"
|
||||
"xrayConfigIRDomainDesc" = "Измените конфигурацию, чтобы отключить подключение к доменам Ирана"
|
||||
"xrayConfigChinaIp" = "Отключить подключение к диапазонам IP-адресов Китая"
|
||||
"xrayConfigChinaIpDesc" = "Измените конфигурацию, чтобы отключить подключение к диапазонам IP-адресов Китая"
|
||||
"xrayConfigChinaDomain" = "Отключить подключение к доменам Китая"
|
||||
"xrayConfigChinaDomainDesc" = "Измените конфигурацию, чтобы отключить подключение к доменам Китая"
|
||||
"xrayConfigRussiaIp" = "Отключить подключение к диапазонам IP-адресов России"
|
||||
"xrayConfigRussiaIpDesc" = "Измените конфигурацию, чтобы отключить соединения с диапазонами IP-адресов России"
|
||||
"xrayConfigRussiaDomain" = "Отключить подключение к доменам России"
|
||||
"xrayConfigRussiaDomainDesc" = "Измените конфигурацию, чтобы избежать подключения к доменам России"
|
||||
"xrayConfigDirectIRIp" = "Прямое подключение к диапазонам IP-адресов Ирана"
|
||||
"xrayConfigDirectIRIpDesc" = "Измените шаблон конфигурации для прямого подключения к диапазонам IP-адресов Ирана"
|
||||
"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-адресов Китая"
|
||||
"xrayConfigDirectIRDomainDesc" = "Изменение шаблон конфигурации для прямого подключения к доменам Ирана"
|
||||
"xrayConfigDirectChinaIp" = "Прямое подключение к диапазонам IP адресов Китая"
|
||||
"xrayConfigDirectChinaIpDesc" = "Изменение шаблона конфигурации для прямого подключения к диапазонам IP адресов Китая"
|
||||
"xrayConfigDirectChinaDomain" = "Прямое подключение к доменам Китая"
|
||||
"xrayConfigDirectChinaDomainDesc" = "Измените шаблон конфигурации для прямого подключения к доменам Китая"
|
||||
"xrayConfigDirectRussiaIp" = "Прямое подключение к диапазонам IP-адресов России"
|
||||
"xrayConfigDirectRussiaIpDesc" = "Изменить шаблон конфигурации для прямого подключения к диапазонам IP-адресов России"
|
||||
"xrayConfigDirectChinaDomainDesc" = "Изменение шаблона конфигурации для прямого подключения к доменам Китая"
|
||||
"xrayConfigDirectRussiaIp" = "Прямое подключение к диапазонам IP адресов России"
|
||||
"xrayConfigDirectRussiaIpDesc" = "Изменение шаблона конфигурации для прямого подключения к диапазонам IP адресов России"
|
||||
"xrayConfigDirectRussiaDomain" = "Прямое подключение к доменам России"
|
||||
"xrayConfigDirectRussiaDomainDesc" = "Изменить шаблон конфигурации для прямого подключения к доменам России"
|
||||
"xrayConfigDirectRussiaDomainDesc" = "Изменение шаблона конфигурации для прямого подключения к доменам России"
|
||||
"xrayConfigGoogleIPv4" = "Использовать IPv4 для Google"
|
||||
"xrayConfigGoogleIPv4Desc" = "Применить маршрутизацию Google для подключения к IPv4"
|
||||
"xrayConfigGoogleIPv4Desc" = "Добавить маршрутизацию для Google для подключения к IPv4"
|
||||
"xrayConfigNetflixIPv4" = "Использовать IPv4 для Netflix"
|
||||
"xrayConfigNetflixIPv4Desc" = "Применить маршрутизацию Netflix для подключения к IPv4"
|
||||
"xrayConfigNetflixIPv4Desc" = "Добавить маршрутизацию для Netflix для подключения к IPv4"
|
||||
"xrayConfigGoogleWARP" = "Маршрутизация Google через WARP"
|
||||
"xrayConfigGoogleWARPDesc" = "Применить маршрутизацию Google через WARP"
|
||||
"xrayConfigOpenAIWARP" = "Маршрут OpenAI (ChatGPT) через WARP"
|
||||
"xrayConfigOpenAIWARPDesc" = "Применить маршрутизацию для OpenAI (ChatGPT) через WARP"
|
||||
"xrayConfigGoogleWARPDesc" = "Добавить маршрутизацию для Google через WARP"
|
||||
"xrayConfigOpenAIWARP" = "Маршрутизация 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"
|
||||
"xrayConfigIRWARPDesc" = "Добавить маршрутизацию для доменов Ирана через WARP"
|
||||
"xrayConfigInbounds" = "Конфигурация подключений"
|
||||
"xrayConfigInboundsDesc" = "Изменение шаблона конфигурации, для подключения определенных пользователей"
|
||||
"xrayConfigOutbounds" = "Конфигурация исходящих"
|
||||
"xrayConfigOutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить исходящие пути для этого сервера"
|
||||
"xrayConfigRoutings" = "Настройка правил маршрутизации"
|
||||
"xrayConfigRoutingsDesc" = "Изменение шаблона конфигурации, для определения правил маршрутизации для этого сервера"
|
||||
"manualLists" = "ручные списки"
|
||||
"manualLists" = "Ручные списки"
|
||||
"manualListsDesc" = "Пожалуйста, используйте формат массива JSON"
|
||||
"manualBlockedIPs" = "Список заблокированных IP-адресов"
|
||||
"manualBlockedIPs" = "Список заблокированных IP адресов"
|
||||
"manualBlockedDomains" = "Список заблокированных доменов"
|
||||
"manualDirectIPs" = "Список прямых IP-адресов"
|
||||
"manualDirectIPs" = "Список прямых IP адресов"
|
||||
"manualDirectDomains" = "Список прямых доменов"
|
||||
"manualIPv4Domains" = "Список доменов IPv4"
|
||||
"manualWARPDomains" = "Список доменов WARP"
|
||||
|
||||
[pages.settings.security]
|
||||
"admin" = "Админ"
|
||||
@@ -343,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>"
|
||||
|
||||
@@ -78,11 +78,12 @@
|
||||
"xraySwitch" = "切换版本"
|
||||
"xraySwitchClick" = "点击你想切换的版本"
|
||||
"xraySwitchClickDesk" = "请谨慎选择,旧版本可能配置不兼容"
|
||||
"operationHours" = "运行时间"
|
||||
"operationHoursDesc" = "系统自启动以来的运行时间"
|
||||
"operationHours" = "系统正常运行时间"
|
||||
"systemLoad" = "系统负载"
|
||||
"systemLoadDesc" = "过去 1、5 和 15 分钟的系统平均负载"
|
||||
"connectionTcpCountDesc" = "所有网卡的总 TCP 连接数。"
|
||||
"connectionUdpCountDesc" = "所有网卡的总 UDP 连接数。"
|
||||
"connectionCount" = "连接数"
|
||||
"connectionCountDesc" = "所有网卡的总连接数"
|
||||
"upSpeed" = "所有网卡的总上传速度"
|
||||
"downSpeed" = "所有网卡的总下载速度"
|
||||
"totalSent" = "系统启动以来所有网卡的总上传流量"
|
||||
@@ -252,6 +253,23 @@
|
||||
"tgNotifyCpuDesc" = "如果 CPU 使用率超过此百分比(单位:%),此 talegram bot 将向您发送通知"
|
||||
"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" = "模板"
|
||||
@@ -336,6 +354,8 @@
|
||||
"manualBlockedDomains" = "被阻止的域列表"
|
||||
"manualDirectIPs" = "直接 IP 列表"
|
||||
"manualDirectDomains" = "直接域列表"
|
||||
"manualIPv4Domains" = "IPv4 域名列表"
|
||||
"manualWARPDomains" = "WARP域名列表"
|
||||
|
||||
[pages.settings.security]
|
||||
"admin" = "行政"
|
||||
@@ -351,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