Compare commits

...

68 Commits

Author SHA1 Message Date
MHSanaei
31339d6bf8 v1.6.1 2023-05-25 19:16:09 +03:30
MHSanaei
8865443438 update dependencies 2023-05-25 19:15:22 +03:30
Tara Rostami
835deb77f4 Minor correction In UI (#537)
* Update antd.min.css

* Update settings.html

* Update custom.css
2023-05-25 16:21:29 +03:30
MHSanaei
6a7c3716ac fix MHz view 2023-05-25 16:13:54 +03:30
MHSanaei
15211f81b1 New - CPU Speed
logical Processors  removed
2023-05-25 15:48:23 +03:30
Tara Rostami
b3f7a6572e Minor changes in the UI (#535)
* Update antd.min.css

* Update settings.html
2023-05-25 13:15:24 +03:30
MHSanaei
6f28a3a2fe change blockedips to AllowedIps
now only those IPs that are allowed are able to establish a connection; other connections are dropped it will happen every 10 sec
after user offline that IPs will be removed from AllowedIps
2023-05-25 03:21:31 +03:30
MHSanaei
896cc5386c new - show cores, public ipv4 and ipv6
logical Processors
you can see them on first page
2023-05-25 03:11:09 +03:30
dependabot[bot]
76f70ce1e9 Bump github.com/pelletier/go-toml/v2 from 2.0.7 to 2.0.8 (#523)
Bumps [github.com/pelletier/go-toml/v2](https://github.com/pelletier/go-toml) from 2.0.7 to 2.0.8.
- [Release notes](https://github.com/pelletier/go-toml/releases)
- [Changelog](https://github.com/pelletier/go-toml/blob/v2/.goreleaser.yaml)
- [Commits](https://github.com/pelletier/go-toml/compare/v2.0.7...v2.0.8)

---
updated-dependencies:
- dependency-name: github.com/pelletier/go-toml/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-24 13:35:36 +03:30
dependabot[bot]
6aa3c8d4a2 Bump svenstaro/upload-release-action from 2.5.0 to 2.6.0 (#514)
Bumps [svenstaro/upload-release-action](https://github.com/svenstaro/upload-release-action) from 2.5.0 to 2.6.0.
- [Release notes](https://github.com/svenstaro/upload-release-action/releases)
- [Changelog](https://github.com/svenstaro/upload-release-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/svenstaro/upload-release-action/compare/2.5.0...2.6.0)

---
updated-dependencies:
- dependency-name: svenstaro/upload-release-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-24 01:37:30 +03:30
MHSanaei
aff9d0ea15 v1.6.0 2023-05-23 18:09:04 +03:30
MHSanaei
526426e2dd Alireza 💯
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-23 17:55:42 +03:30
MHSanaei
2223a21cfc bug fix - limit ip 2023-05-23 17:54:15 +03:30
MHSanaei
47ccc7b501 [feature] fallback link calculation
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-23 03:15:34 +03:30
MHSanaei
c38e1e0cfe a lot of improvement 2023-05-23 02:43:15 +03:30
MHSanaei
f36034541e update - SSL Certificate Management
Get SSL
Revoke
Force Renew
2023-05-23 02:34:36 +03:30
MHSanaei
783fa856c3 Fix deprecated gRPC Dial options for Insecure connections 2023-05-23 00:52:15 +03:30
MHSanaei
6139effb8a update - style
tcp,ws,tls
2023-05-22 22:36:15 +03:30
MHSanaei
8ae7f4a564 small fixes 2023-05-22 21:52:35 +03:30
MHSanaei
6f46f3e636 sub
remaining code
2023-05-22 21:51:52 +03:30
Hamidreza
ba278a4269 Merge pull request #509 from hamid-gh98/main
FIX restart redirect :)
2023-05-22 21:32:17 +03:30
Hamidreza Ghavami
3825e36ef7 FIX restart redirect :) 2023-05-22 22:30:35 +04:30
MHSanaei
f6e0e1b3cf translate update
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-22 18:16:24 +03:30
MHSanaei
769590d779 [feature] separate subscription service
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-22 18:06:34 +03:30
MHSanaei
1fa9101b40 [feature] add multi domain tls (CDN ready)
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-22 17:31:41 +03:30
MHSanaei
3f2e1aede9 features - random ShortId button 2023-05-22 16:44:27 +03:30
MHSanaei
235e6880c1 update - utils 2023-05-22 16:41:41 +03:30
MHSanaei
ffa23a43c6 update - URIComponent just for cookie value 2023-05-22 16:40:08 +03:30
MHSanaei
66e3c505e3 update dependencies 2023-05-22 14:41:23 +03:30
MHSanaei
0938519f49 bug fix - KeyContent 2023-05-22 11:39:21 +03:30
Ho3ein
5f489c3d08 Merge pull request #491 from hamid-gh98/main
[tgbot] Multi language + More...
2023-05-22 11:26:14 +03:30
Hamidreza Ghavami
f82d0051b2 Update hashstorage functionality 2023-05-21 08:33:08 +04:30
Hamidreza Ghavami
76267b23a0 fix tgbot 2023-05-21 07:41:59 +04:30
Hamidreza Ghavami
40a926a54a Update README.md 2023-05-21 06:50:57 +04:30
Hamidreza Ghavami
3a835fbeb8 FIX ERROR 'msg and reply are same' in tgbot 2023-05-21 05:48:19 +04:30
Hamidreza Ghavami
f6461b8386 FIX hashStorage remove expire job 2023-05-21 05:33:01 +04:30
Hamidreza Ghavami
586663c840 Update translations 2023-05-21 03:30:39 +04:30
Hamidreza Ghavami
82ead05093 Update tgbot locale + add I18nBot 2023-05-21 03:30:26 +04:30
Hamidreza Ghavami
d9b1b200ce rename I18n to I18nWeb 2023-05-21 03:29:27 +04:30
Hamidreza Ghavami
786a3ac992 FIX hashStorage 2023-05-20 21:46:42 +04:30
Hamidreza Ghavami
8c5648eb09 FIX callback query and BUTTON_DATA_INVALID error with hashStorage 2023-05-20 20:29:28 +04:30
Hamidreza Ghavami
4dfe527f20 init i18n in tgbot 2023-05-20 20:08:01 +04:30
Hamidreza Ghavami
980ebd99ca add tgBot localizer 2023-05-20 19:59:04 +04:30
Hamidreza Ghavami
9b7cddbec7 update to use WebI18n func 2023-05-20 19:51:45 +04:30
Hamidreza Ghavami
2bd706aa92 update I18n function for controller 2023-05-20 19:49:39 +04:30
Hamidreza Ghavami
95e0d9a468 create LocalizerMiddleware func 2023-05-20 19:46:34 +04:30
Hamidreza Ghavami
4865754b3d add localizer middleware to web.go 2023-05-20 19:46:05 +04:30
Hamidreza Ghavami
92eaff9608 replace new localizer to web.go 2023-05-20 19:43:59 +04:30
Hamidreza Ghavami
0b7aa8a9e0 Refactor i18n localizer 2023-05-20 19:41:08 +04:30
Hamidreza Ghavami
678962d4ca some fix and prune for tgbot 2023-05-20 19:39:01 +04:30
Hamidreza Ghavami
795835c54f Update translations 2023-05-20 19:21:10 +04:30
Hamidreza Ghavami
91360a3f49 add tgLang to settings 2023-05-20 19:20:54 +04:30
Hamidreza Ghavami
4831c2f1b2 Add tgLang option 2023-05-20 19:15:20 +04:30
Hamidreza Ghavami
3166d497f9 Update .gitignore 2023-05-20 19:03:31 +04:30
Hamidreza Ghavami
f50ccce9ec Add manual list for ipv4 and warp and fixed it 2023-05-20 19:02:37 +04:30
Hamidreza Ghavami
c7e300f14d FIX redirect after restart panel 2023-05-20 18:58:51 +04:30
Hamidreza Ghavami
419a1938ee update settings ui 2023-05-20 18:57:10 +04:30
Hamidreza Ghavami
a48745cb3e fix tls settings 2023-05-20 18:46:50 +04:30
Hamidreza Ghavami
ac9408c37f update sub remark for shadowsocks 2023-05-20 18:44:18 +04:30
dependabot[bot]
3d71289075 Bump github.com/Workiva/go-datastructures from 1.0.53 to 1.1.0 (#481)
Bumps [github.com/Workiva/go-datastructures](https://github.com/Workiva/go-datastructures) from 1.0.53 to 1.1.0.
- [Release notes](https://github.com/Workiva/go-datastructures/releases)
- [Commits](https://github.com/Workiva/go-datastructures/compare/v1.0.53...v1.1.0)

---
updated-dependencies:
- dependency-name: github.com/Workiva/go-datastructures
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-19 18:31:56 +03:30
Minobi
fbd3772788 Updated Russian translation of the web UI (#478) 2023-05-19 06:45:33 +03:30
Mahdi Nemati
334b28cddc Api for backup Telegram (#468) 2023-05-19 00:31:05 +03:30
MHSanaei
2fbfc88bc1 bug fix - xtls 2023-05-18 12:18:13 +03:30
MHSanaei
f781979d38 update - config 2023-05-18 11:52:53 +03:30
Ho3ein
aba37be6eb Merge pull request #467 from mmrabbani/patch-1
Simplifying ad blocking syntax
2023-05-18 00:59:01 +03:30
mmrabbani
30042bc047 Update traffic+block-ads+warp.json 2023-05-18 00:51:36 +03:30
mmrabbani
49f60f7775 Update traffic+block-ads+ipv4-google.json 2023-05-18 00:50:57 +03:30
mmrabbani
5f44c80cd5 Simplifying ad blocking syntax
I simplified the ad blocking syntax.
"category-ads-all" includes "category-ads". Also "category-ads" includes "google-ads" and "spotify-ads".
So if "category-ads-all" is blocked, there is no need to block 3 other lists (category-ads,google-ads,spotify-ads)

References:
https://github.com/v2fly/domain-list-community/blob/master/data/category-ads
https://github.com/v2fly/domain-list-community/blob/master/data/category-ads-all
2023-05-18 00:48:37 +03:30
68 changed files with 2876 additions and 1149 deletions

View File

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

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

View File

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

View File

@@ -1 +1 @@
1.5.0
1.6.1

View File

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

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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