Compare commits
72 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5db35c32de | ||
|
|
abb79bd978 | ||
|
|
38c318737b | ||
|
|
c1ed6d8454 | ||
|
|
26a0481d82 | ||
|
|
304510aefc | ||
|
|
3ef04201cc | ||
|
|
de26dbbc96 | ||
|
|
e1da43053d | ||
|
|
3bb90cbf24 | ||
|
|
099f2fc52b | ||
|
|
d8e0c958e7 | ||
|
|
9f18d60b9c | ||
|
|
0e5de1aec8 | ||
|
|
91ebe7008d | ||
|
|
72f868506d | ||
|
|
56850165a4 | ||
|
|
a784a94806 | ||
|
|
8ba46a99ba | ||
|
|
472694a611 | ||
|
|
7c980343f1 | ||
|
|
2dd203e174 | ||
|
|
7084812515 | ||
|
|
64df14f8d5 | ||
|
|
838d0c2625 | ||
|
|
e51c59995c | ||
|
|
c07b2c73d7 | ||
|
|
c0580bccb5 | ||
|
|
45469e9f64 | ||
|
|
87acb81496 | ||
|
|
16be454f6d | ||
|
|
ef24174a38 | ||
|
|
f2c28822c1 | ||
|
|
48d6362a69 | ||
|
|
3f2adbd70a | ||
|
|
706c39452b | ||
|
|
8b855a7cb5 | ||
|
|
80759c8951 | ||
|
|
e55f3c37fd | ||
|
|
c87c1017d8 | ||
|
|
43aea38641 | ||
|
|
88744d92b3 | ||
|
|
7b38d02ff0 | ||
|
|
3da6c4d7d9 | ||
|
|
606360ae03 | ||
|
|
e2fd84a6ae | ||
|
|
f56dd43999 | ||
|
|
f0f5163a83 | ||
|
|
373628a6a3 | ||
|
|
a790efb18d | ||
|
|
868224ae97 | ||
|
|
60169bd055 | ||
|
|
33db9d0f90 | ||
|
|
27d020709e | ||
|
|
77be5cf7d8 | ||
|
|
c9d768a086 | ||
|
|
9c0718bc44 | ||
|
|
826c7264b5 | ||
|
|
162349f8c8 | ||
|
|
a6dfdcdd31 | ||
|
|
03a6c131f9 | ||
|
|
8dad9a4338 | ||
|
|
f0d4dbf838 | ||
|
|
3152d5f191 | ||
|
|
17f64462d2 | ||
|
|
bbec13c0da | ||
|
|
466ad1605b | ||
|
|
a068c350ee | ||
|
|
0605221628 | ||
|
|
3856c4d0f9 | ||
|
|
557a9d020a | ||
|
|
14d7cb812e |
24
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,24 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Create a report to help us improve
|
|
||||||
title: ''
|
|
||||||
labels: bug
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Describe the 3x-ui bug**
|
|
||||||
A clear and concise description of what the bug is.
|
|
||||||
|
|
||||||
**Expected behavior**
|
|
||||||
A clear and concise description of what you expected to happen.
|
|
||||||
|
|
||||||
**Screenshots**
|
|
||||||
If applicable, add screenshots to help explain your problem.
|
|
||||||
|
|
||||||
**Server (please complete the following information):**
|
|
||||||
- OS: [e.g. iOS]
|
|
||||||
- Version [e.g. 22]
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context about the problem here.
|
|
||||||
56
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
name: Issue Report
|
||||||
|
description: "Create a report to help us improve."
|
||||||
|
body:
|
||||||
|
- type: checkboxes
|
||||||
|
id: terms
|
||||||
|
attributes:
|
||||||
|
label: Welcome
|
||||||
|
options:
|
||||||
|
- label: Yes, I'm using the latest major release. Only such installations are supported.
|
||||||
|
required: true
|
||||||
|
- label: Yes, I'm using the supported system. Only such systems are supported.
|
||||||
|
required: true
|
||||||
|
- label: Yes, I have read all WIKI document,nothing can help me in my problem.
|
||||||
|
required: true
|
||||||
|
- label: Yes, I've searched similar issues on GitHub and didn't find any.
|
||||||
|
required: true
|
||||||
|
- label: Yes, I've included all information below (version, config, log, etc).
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: problem
|
||||||
|
attributes:
|
||||||
|
label: Description of the problem,screencshot would be good
|
||||||
|
placeholder: Your problem description
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: Version of 3x-ui
|
||||||
|
value: |-
|
||||||
|
<details>
|
||||||
|
|
||||||
|
```console
|
||||||
|
# Paste here
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: log
|
||||||
|
attributes:
|
||||||
|
label: x-ui log reports or xray log
|
||||||
|
value: |-
|
||||||
|
<details>
|
||||||
|
|
||||||
|
```console
|
||||||
|
# paste log here
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
3
.github/workflows/release.yml
vendored
@@ -6,7 +6,7 @@ jobs:
|
|||||||
name: build x-ui amd64 version
|
name: build x-ui amd64 version
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3.4.0
|
- uses: actions/checkout@v3.5.0
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v4.0.0
|
uses: actions/setup-go@v4.0.0
|
||||||
with:
|
with:
|
||||||
@@ -27,6 +27,7 @@ jobs:
|
|||||||
rm -f Xray-linux-64.zip geoip.dat geosite.dat
|
rm -f Xray-linux-64.zip geoip.dat geosite.dat
|
||||||
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
|
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
|
||||||
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
|
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
|
||||||
|
wget https://github.com/bootmortis/iran-hosted-domains/releases/latest/download/iran.dat
|
||||||
mv xray xray-linux-amd64
|
mv xray xray-linux-amd64
|
||||||
cd ..
|
cd ..
|
||||||
cd ..
|
cd ..
|
||||||
|
|||||||
21
README.md
@@ -1,8 +1,8 @@
|
|||||||
# 3x-ui
|
# 3x-ui
|
||||||

|
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||||

|
[](#)
|
||||||

|
[](#)
|
||||||

|
[](#)
|
||||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||||
|
|
||||||
|
|
||||||
@@ -34,6 +34,7 @@ certbot renew --dry-run
|
|||||||
- Port: 2053
|
- Port: 2053
|
||||||
- username and password will be generated randomly if you skip to modify your own security(x-ui "7")
|
- username and password will be generated randomly if you skip to modify your own security(x-ui "7")
|
||||||
- database path: /etc/x-ui/x-ui.db
|
- database path: /etc/x-ui/x-ui.db
|
||||||
|
- xray config path: /usr/local/x-ui/bin/config.json
|
||||||
|
|
||||||
before you set ssl on settings
|
before you set ssl on settings
|
||||||
- http:// ip or domain:2053/xui
|
- http:// ip or domain:2053/xui
|
||||||
@@ -44,8 +45,9 @@ After you set ssl on settings
|
|||||||
# Enable Traffic For Users:
|
# Enable Traffic For Users:
|
||||||
|
|
||||||
**copy and paste to xray Configuration :** (you don't need to do this if you have a fresh install)
|
**copy and paste to xray Configuration :** (you don't need to do this if you have a fresh install)
|
||||||
- [for enable traffic](https://raw.githubusercontent.com/mhsanaei/3x-ui/main/media/for%20enable%20traffic.txt)
|
- [enable traffic](./media/enable-traffic.txt)
|
||||||
- [for enable traffic+block all iran ip address](https://raw.githubusercontent.com/mhsanaei/3x-ui/main/media/for%20enable%20traffic%2Bblock%20all%20iran%20ip.txt)
|
- [enable traffic+block all IR IP address](./media/enable-traffic+block-IR-IP.txt)
|
||||||
|
- [enable traffic+block all IR domain](./media/enable-traffic+block-IR-domain.txt)
|
||||||
|
|
||||||
# Features
|
# Features
|
||||||
|
|
||||||
@@ -60,6 +62,7 @@ After you set ssl on settings
|
|||||||
- Support https access panel (self-provided domain name + ssl certificate)
|
- Support https access panel (self-provided domain name + ssl certificate)
|
||||||
- Support one-click SSL certificate application and automatic renewal
|
- Support one-click SSL certificate application and automatic renewal
|
||||||
- For more advanced configuration items, please refer to the panel
|
- For more advanced configuration items, please refer to the panel
|
||||||
|
- fix api routes (user setting will create with api)
|
||||||
|
|
||||||
# Tg robot use
|
# Tg robot use
|
||||||
|
|
||||||
@@ -76,6 +79,8 @@ Set the robot-related parameters in the panel background, including:
|
|||||||
|
|
||||||
Reference syntax:
|
Reference syntax:
|
||||||
|
|
||||||
|
- 30 * * * * * //Notify at the 30s of each point
|
||||||
|
- 0 */10 * * * * //Notify at the first second of each 10 minutes
|
||||||
- @hourly // hourly notification
|
- @hourly // hourly notification
|
||||||
- @daily // Daily notification (00:00 in the morning)
|
- @daily // Daily notification (00:00 in the morning)
|
||||||
- @every 8h // notify every 8 hours
|
- @every 8h // notify every 8 hours
|
||||||
@@ -86,13 +91,13 @@ Reference syntax:
|
|||||||
- Login notification
|
- Login notification
|
||||||
- CPU threshold notification
|
- CPU threshold notification
|
||||||
- Threshold for Expiration time and Traffic to report in advance
|
- Threshold for Expiration time and Traffic to report in advance
|
||||||
- Support client report if client's telegram username is added to the end of `email` like 'test123@telegram_username'
|
- Support client report menu if client's telegram username added to the user's configurations
|
||||||
- Support telegram traffic report searched with UID (VMESS/VLESS) or Password (TROJAN) - anonymously
|
- Support telegram traffic report searched with UID (VMESS/VLESS) or Password (TROJAN) - anonymously
|
||||||
- Menu based bot
|
- Menu based bot
|
||||||
- Search client by email ( only admin )
|
- Search client by email ( only admin )
|
||||||
- Check all inbounds
|
- Check all inbounds
|
||||||
- Check server status
|
- Check server status
|
||||||
- Check Exhausted users
|
- Check depleted users
|
||||||
- Receive backup by request and in periodic reports
|
- Receive backup by request and in periodic reports
|
||||||
|
|
||||||
# A Special Thanks To
|
# A Special Thanks To
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
1.0.9
|
1.2.0
|
||||||
|
|||||||
@@ -44,9 +44,9 @@ type Inbound struct {
|
|||||||
Sniffing string `json:"sniffing" form:"sniffing"`
|
Sniffing string `json:"sniffing" form:"sniffing"`
|
||||||
}
|
}
|
||||||
type InboundClientIps struct {
|
type InboundClientIps struct {
|
||||||
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
|
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||||
ClientEmail string `json:"clientEmail" form:"clientEmail" gorm:"unique"`
|
ClientEmail string `json:"clientEmail" form:"clientEmail" gorm:"unique"`
|
||||||
Ips string `json:"ips" form:"ips"`
|
Ips string `json:"ips" form:"ips"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
|
func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
|
||||||
@@ -73,10 +73,14 @@ type Setting struct {
|
|||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
Flow string `json:"flow"`
|
||||||
AlterIds uint16 `json:"alterId"`
|
AlterIds uint16 `json:"alterId"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
LimitIP int `json:"limitIp"`
|
LimitIP int `json:"limitIp"`
|
||||||
Security string `json:"security"`
|
|
||||||
TotalGB int64 `json:"totalGB" form:"totalGB"`
|
TotalGB int64 `json:"totalGB" form:"totalGB"`
|
||||||
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
|
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
|
||||||
|
Enable bool `json:"enable" form:"enable"`
|
||||||
|
TgID string `json:"tgId" form:"tgId"`
|
||||||
|
SubID string `json:"subId" form:"subId"`
|
||||||
}
|
}
|
||||||
|
|||||||
7
go.mod
@@ -8,14 +8,15 @@ require (
|
|||||||
github.com/gin-gonic/gin v1.9.0
|
github.com/gin-gonic/gin v1.9.0
|
||||||
github.com/go-cmd/cmd v1.4.1
|
github.com/go-cmd/cmd v1.4.1
|
||||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
|
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
|
||||||
|
github.com/goccy/go-json v0.10.0
|
||||||
github.com/nicksnyder/go-i18n/v2 v2.2.1
|
github.com/nicksnyder/go-i18n/v2 v2.2.1
|
||||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||||
github.com/pelletier/go-toml/v2 v2.0.7
|
github.com/pelletier/go-toml/v2 v2.0.7
|
||||||
github.com/robfig/cron/v3 v3.0.1
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
github.com/shirou/gopsutil/v3 v3.23.2
|
github.com/shirou/gopsutil/v3 v3.23.3
|
||||||
github.com/xtls/xray-core v1.8.0
|
github.com/xtls/xray-core v1.8.0
|
||||||
go.uber.org/atomic v1.10.0
|
go.uber.org/atomic v1.10.0
|
||||||
golang.org/x/text v0.8.0
|
golang.org/x/text v0.9.0
|
||||||
google.golang.org/grpc v1.54.0
|
google.golang.org/grpc v1.54.0
|
||||||
gorm.io/driver/sqlite v1.4.4
|
gorm.io/driver/sqlite v1.4.4
|
||||||
gorm.io/gorm v1.24.6
|
gorm.io/gorm v1.24.6
|
||||||
@@ -30,7 +31,6 @@ require (
|
|||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.11.2 // indirect
|
github.com/go-playground/validator/v10 v10.11.2 // indirect
|
||||||
github.com/goccy/go-json v0.10.0 // indirect
|
|
||||||
github.com/golang/protobuf v1.5.3 // indirect
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
github.com/gorilla/context v1.1.1 // indirect
|
github.com/gorilla/context v1.1.1 // indirect
|
||||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||||
@@ -47,6 +47,7 @@ require (
|
|||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pires/go-proxyproto v0.6.2 // indirect
|
github.com/pires/go-proxyproto v0.6.2 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect
|
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect
|
||||||
|
github.com/shoenig/go-m1cpu v0.1.4 // indirect
|
||||||
github.com/tklauser/go-sysconf v0.3.11 // indirect
|
github.com/tklauser/go-sysconf v0.3.11 // indirect
|
||||||
github.com/tklauser/numcpus v0.6.0 // indirect
|
github.com/tklauser/numcpus v0.6.0 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
|||||||
13
go.sum
@@ -137,8 +137,12 @@ github.com/sagernet/sing v0.1.7 h1:g4vjr3q8SUlBZSx97Emz5OBfSMBxxW5Q8C2PfdoSo08=
|
|||||||
github.com/sagernet/sing-shadowsocks v0.1.1 h1:uFK2rlVeD/b1xhDwSMbUI2goWc6fOKxp+ZeKHZq6C9Q=
|
github.com/sagernet/sing-shadowsocks v0.1.1 h1:uFK2rlVeD/b1xhDwSMbUI2goWc6fOKxp+ZeKHZq6C9Q=
|
||||||
github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c h1:vK2wyt9aWYHHvNLWniwijBu/n4pySypiKRhN32u/JGo=
|
github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c h1:vK2wyt9aWYHHvNLWniwijBu/n4pySypiKRhN32u/JGo=
|
||||||
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U=
|
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U=
|
||||||
github.com/shirou/gopsutil/v3 v3.23.2 h1:PAWSuiAszn7IhPMBtXsbSCafej7PqUOvY6YywlQUExU=
|
github.com/shirou/gopsutil/v3 v3.23.3 h1:Syt5vVZXUDXPEXpIBt5ziWsJ4LdSAAxF4l/xZeQgSEE=
|
||||||
github.com/shirou/gopsutil/v3 v3.23.2/go.mod h1:gv0aQw33GLo3pG8SiWKiQrbDzbRY1K80RyZJ7V4Th1M=
|
github.com/shirou/gopsutil/v3 v3.23.3/go.mod h1:lSBNN6t3+D6W5e5nXTxc8KIMMVxAcS+6IJlffjRRlMU=
|
||||||
|
github.com/shoenig/go-m1cpu v0.1.4 h1:SZPIgRM2sEF9NJy50mRHu9PKGwxyyTTJIWvCtgVbozs=
|
||||||
|
github.com/shoenig/go-m1cpu v0.1.4/go.mod h1:Wwvst4LR89UxjeFtLRMrpgRiyY4xPsejnVZym39dbAQ=
|
||||||
|
github.com/shoenig/test v0.6.3 h1:GVXWJFk9PiOjN0KoJ7VrJGH6uLPnqxR7/fe3HUPfE0c=
|
||||||
|
github.com/shoenig/test v0.6.3/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
@@ -211,7 +215,6 @@ 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.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
@@ -221,8 +224,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
|||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
|||||||
27
install.sh
@@ -66,14 +66,33 @@ else
|
|||||||
echo -e "${red}Failed to check the OS version, please contact the author!${plain}" && exit 1
|
echo -e "${red}Failed to check the OS version, please contact the author!${plain}" && exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# This function installs the base packages required for most scripts
|
||||||
install_base() {
|
install_base() {
|
||||||
if [[ "${release}" == "centos" ]]; then
|
# Store the package names in a variable for easy modification
|
||||||
yum install wget curl tar -y
|
local packages="wget curl tar"
|
||||||
|
|
||||||
|
# Check for the package managers and install the packages if they are not already installed
|
||||||
|
if ! command -v wget >/dev/null 2>&1 || ! command -v curl >/dev/null 2>&1 || ! command -v tar >/dev/null 2>&1; then
|
||||||
|
if command -v apt >/dev/null 2>&1; then
|
||||||
|
apt-get update && apt-get install -y $packages
|
||||||
|
elif command -v dnf >/dev/null 2>&1; then
|
||||||
|
dnf install -y $packages
|
||||||
|
elif command -v yum >/dev/null 2>&1; then
|
||||||
|
yum install -y $packages
|
||||||
|
else
|
||||||
|
echo "ERROR: No package managers found. Please install wget, curl, and tar manually."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Print a confirmation message after the installation is complete
|
||||||
|
echo "The following packages have been successfully installed: $packages"
|
||||||
else
|
else
|
||||||
apt install wget curl tar -y
|
# Print a message confirming that the packages are already installed
|
||||||
|
echo "The following packages are already installed: $packages"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#This function will be called when user installed x-ui out of sercurity
|
#This function will be called when user installed x-ui out of sercurity
|
||||||
config_after_install() {
|
config_after_install() {
|
||||||
echo -e "${yellow}Install/update finished! For security it's recommended to modify panel settings ${plain}"
|
echo -e "${yellow}Install/update finished! For security it's recommended to modify panel settings ${plain}"
|
||||||
|
|||||||
10
main.go
@@ -136,7 +136,7 @@ func updateTgbotEnableSts(status bool) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateTgbotSetting(tgBotToken string, tgBotChatid int, tgBotRuntime string) {
|
func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime string) {
|
||||||
err := database.InitDB(config.GetDBPath())
|
err := database.InitDB(config.GetDBPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
@@ -165,7 +165,7 @@ func updateTgbotSetting(tgBotToken string, tgBotChatid int, tgBotRuntime string)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if tgBotChatid != 0 {
|
if tgBotChatid != "" {
|
||||||
err := settingService.SetTgBotChatId(tgBotChatid)
|
err := settingService.SetTgBotChatId(tgBotChatid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
@@ -224,7 +224,7 @@ func main() {
|
|||||||
var username string
|
var username string
|
||||||
var password string
|
var password string
|
||||||
var tgbottoken string
|
var tgbottoken string
|
||||||
var tgbotchatid int
|
var tgbotchatid string
|
||||||
var enabletgbot bool
|
var enabletgbot bool
|
||||||
var tgbotRuntime string
|
var tgbotRuntime string
|
||||||
var reset bool
|
var reset bool
|
||||||
@@ -236,7 +236,7 @@ func main() {
|
|||||||
settingCmd.StringVar(&password, "password", "", "set login password")
|
settingCmd.StringVar(&password, "password", "", "set login password")
|
||||||
settingCmd.StringVar(&tgbottoken, "tgbottoken", "", "set telegrame bot token")
|
settingCmd.StringVar(&tgbottoken, "tgbottoken", "", "set telegrame bot token")
|
||||||
settingCmd.StringVar(&tgbotRuntime, "tgbotRuntime", "", "set telegrame bot cron time")
|
settingCmd.StringVar(&tgbotRuntime, "tgbotRuntime", "", "set telegrame bot cron time")
|
||||||
settingCmd.IntVar(&tgbotchatid, "tgbotchatid", 0, "set telegrame bot chat id")
|
settingCmd.StringVar(&tgbotchatid, "tgbotchatid", "", "set telegrame bot chat id")
|
||||||
settingCmd.BoolVar(&enabletgbot, "enabletgbot", false, "enable telegram bot notify")
|
settingCmd.BoolVar(&enabletgbot, "enabletgbot", false, "enable telegram bot notify")
|
||||||
|
|
||||||
oldUsage := flag.Usage
|
oldUsage := flag.Usage
|
||||||
@@ -287,7 +287,7 @@ func main() {
|
|||||||
if show {
|
if show {
|
||||||
showSetting(show)
|
showSetting(show)
|
||||||
}
|
}
|
||||||
if (tgbottoken != "") || (tgbotchatid != 0) || (tgbotRuntime != "") {
|
if (tgbottoken != "") || (tgbotchatid != "") || (tgbotRuntime != "") {
|
||||||
updateTgbotSetting(tgbottoken, tgbotchatid, tgbotRuntime)
|
updateTgbotSetting(tgbottoken, tgbotchatid, tgbotRuntime)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|||||||
BIN
media/1.png
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 50 KiB |
BIN
media/2.png
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 36 KiB |
BIN
media/3.png
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 32 KiB |
BIN
media/4.png
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 159 KiB |
@@ -3,7 +3,6 @@
|
|||||||
"loglevel": "warning",
|
"loglevel": "warning",
|
||||||
"access": "./access.log"
|
"access": "./access.log"
|
||||||
},
|
},
|
||||||
|
|
||||||
"api": {
|
"api": {
|
||||||
"services": [
|
"services": [
|
||||||
"HandlerService",
|
"HandlerService",
|
||||||
@@ -57,17 +56,23 @@
|
|||||||
"type": "field"
|
"type": "field"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ip": [
|
|
||||||
"geoip:private",
|
|
||||||
"geoip:ir"
|
|
||||||
],
|
|
||||||
"outboundTag": "blocked",
|
"outboundTag": "blocked",
|
||||||
|
"protocol": [
|
||||||
|
"bittorrent"
|
||||||
|
],
|
||||||
"type": "field"
|
"type": "field"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"outboundTag": "blocked",
|
"outboundTag": "blocked",
|
||||||
"protocol": [
|
"ip": [
|
||||||
"bittorrent"
|
"geoip:private"
|
||||||
|
],
|
||||||
|
"type": "field"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"outboundTag": "blocked",
|
||||||
|
"ip": [
|
||||||
|
"geoip:ir"
|
||||||
],
|
],
|
||||||
"type": "field"
|
"type": "field"
|
||||||
}
|
}
|
||||||
84
media/enable-traffic+block-IR-domain.txt
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
{
|
||||||
|
"log": {
|
||||||
|
"loglevel": "warning",
|
||||||
|
"access": "./access.log"
|
||||||
|
},
|
||||||
|
"api": {
|
||||||
|
"services": [
|
||||||
|
"HandlerService",
|
||||||
|
"LoggerService",
|
||||||
|
"StatsService"
|
||||||
|
],
|
||||||
|
"tag": "api"
|
||||||
|
},
|
||||||
|
"inbounds": [
|
||||||
|
{
|
||||||
|
"listen": "127.0.0.1",
|
||||||
|
"port": 62789,
|
||||||
|
"protocol": "dokodemo-door",
|
||||||
|
"settings": {
|
||||||
|
"address": "127.0.0.1"
|
||||||
|
},
|
||||||
|
"tag": "api"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outbounds": [
|
||||||
|
{
|
||||||
|
"protocol": "freedom",
|
||||||
|
"settings": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"protocol": "blackhole",
|
||||||
|
"settings": {},
|
||||||
|
"tag": "blocked"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"policy": {
|
||||||
|
"levels": {
|
||||||
|
"0": {
|
||||||
|
"statsUserUplink": true,
|
||||||
|
"statsUserDownlink": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"system": {
|
||||||
|
"statsInboundDownlink": true,
|
||||||
|
"statsInboundUplink": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"routing": {
|
||||||
|
"domainStrategy": "IPIfNonMatch",
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"inboundTag": [
|
||||||
|
"api"
|
||||||
|
],
|
||||||
|
"outboundTag": "api",
|
||||||
|
"type": "field"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": [
|
||||||
|
"geoip:private"
|
||||||
|
],
|
||||||
|
"outboundTag": "blocked",
|
||||||
|
"type": "field"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"outboundTag": "blocked",
|
||||||
|
"protocol": [
|
||||||
|
"bittorrent"
|
||||||
|
],
|
||||||
|
"type": "field"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"outboundTag": "blocked",
|
||||||
|
"domain": [
|
||||||
|
"regexp:.+.ir$",
|
||||||
|
"ext:iran.dat:ir",
|
||||||
|
"ext:iran.dat:other"
|
||||||
|
],
|
||||||
|
"type": "field"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"stats": {}
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
"loglevel": "warning",
|
"loglevel": "warning",
|
||||||
"access": "./access.log"
|
"access": "./access.log"
|
||||||
},
|
},
|
||||||
|
|
||||||
"api": {
|
"api": {
|
||||||
"services": [
|
"services": [
|
||||||
"HandlerService",
|
"HandlerService",
|
||||||
@@ -47,6 +46,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"routing": {
|
"routing": {
|
||||||
|
"domainStrategy": "IPIfNonMatch",
|
||||||
"rules": [
|
"rules": [
|
||||||
{
|
{
|
||||||
"inboundTag": [
|
"inboundTag": [
|
||||||
@@ -56,10 +56,10 @@
|
|||||||
"type": "field"
|
"type": "field"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"outboundTag": "blocked",
|
||||||
"ip": [
|
"ip": [
|
||||||
"geoip:private"
|
"geoip:private"
|
||||||
],
|
],
|
||||||
"outboundTag": "blocked",
|
|
||||||
"type": "field"
|
"type": "field"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
//go:build darwin
|
||||||
// +build darwin
|
// +build darwin
|
||||||
|
|
||||||
package sys
|
package sys
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
//go:build linux
|
||||||
// +build linux
|
// +build linux
|
||||||
|
|
||||||
package sys
|
package sys
|
||||||
|
|||||||
4490
web/assets/ant-design-vue@1.7.2/antd.min.css
vendored
@@ -156,6 +156,12 @@
|
|||||||
padding:16px;
|
padding:16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-table-expand-icon-th,
|
||||||
|
.ant-table-row-expand-icon-cell {
|
||||||
|
width: 30px;
|
||||||
|
min-width: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
.ant-menu-dark,
|
.ant-menu-dark,
|
||||||
.ant-menu-dark .ant-menu-sub,
|
.ant-menu-dark .ant-menu-sub,
|
||||||
.ant-layout-header,
|
.ant-layout-header,
|
||||||
@@ -174,6 +180,7 @@
|
|||||||
|
|
||||||
.ant-card-dark:hover {
|
.ant-card-dark:hover {
|
||||||
border-color: #e8e8e8;
|
border-color: #e8e8e8;
|
||||||
|
box-shadow: 0 2px 8px rgba(255,255,255,.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-card-dark .ant-table-thead th {
|
.ant-card-dark .ant-table-thead th {
|
||||||
@@ -216,20 +223,25 @@
|
|||||||
|
|
||||||
.ant-card-dark .ant-table-tbody>tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)>td,
|
.ant-card-dark .ant-table-tbody>tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)>td,
|
||||||
.ant-card-dark .ant-select-dropdown-menu-item:hover:not(.ant-select-dropdown-menu-item-disabled),
|
.ant-card-dark .ant-select-dropdown-menu-item:hover:not(.ant-select-dropdown-menu-item-disabled),
|
||||||
.ant-card-dark .ant-calendar-date:hover {
|
.ant-card-dark .ant-calendar-date:hover,
|
||||||
|
.ant-card-dark .ant-select-dropdown-menu-item-active,
|
||||||
|
.ant-card-dark li.ant-calendar-time-picker-select-option-selected {
|
||||||
background-color: #004488;
|
background-color: #004488;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-card-dark tbody .ant-table-expanded-row {
|
.ant-card-dark tbody .ant-table-expanded-row,
|
||||||
|
.ant-card-dark .ant-calendar-time-picker-inner {
|
||||||
color: hsla(0,0%,100%,.65);
|
color: hsla(0,0%,100%,.65);
|
||||||
background-color: #1a212a;
|
background-color: #1a212a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-card-dark .ant-input,
|
.ant-card-dark .ant-input,
|
||||||
.ant-card-dark .ant-input-number,
|
.ant-card-dark .ant-input-number,
|
||||||
|
.ant-card-dark .ant-input-number-handler-wrap,
|
||||||
.ant-card-dark .ant-calendar-input,
|
.ant-card-dark .ant-calendar-input,
|
||||||
.ant-card-dark .ant-select-dropdown-menu-item-selected,
|
.ant-card-dark .ant-select-dropdown-menu-item-selected,
|
||||||
.ant-card-dark .ant-select-selection {
|
.ant-card-dark .ant-select-selection,
|
||||||
|
.ant-card-dark .ant-calendar-picker-clear {
|
||||||
color: hsla(0,0%,100%,.65);
|
color: hsla(0,0%,100%,.65);
|
||||||
background-color: #2e3b52;
|
background-color: #2e3b52;
|
||||||
}
|
}
|
||||||
@@ -239,6 +251,12 @@
|
|||||||
background-color: #161b22;
|
background-color: #161b22;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-dropdown-menu-dark,
|
||||||
|
.ant-card-dark .ant-modal-content {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.65);
|
||||||
|
box-shadow: 0 2px 8px rgba(255,255,255,.15);
|
||||||
|
}
|
||||||
|
|
||||||
.ant-card-dark .ant-modal-content,
|
.ant-card-dark .ant-modal-content,
|
||||||
.ant-card-dark .ant-modal-body,
|
.ant-card-dark .ant-modal-body,
|
||||||
.ant-card-dark .ant-modal-header,
|
.ant-card-dark .ant-modal-header,
|
||||||
@@ -280,6 +298,12 @@
|
|||||||
border: 1px solid hsla(0,0%,100%,.30);
|
border: 1px solid hsla(0,0%,100%,.30);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-card-dark .ant-tag {
|
||||||
|
color: hsla(0,0%,100%,.65);
|
||||||
|
background: rgba(255,255,255,.04);
|
||||||
|
border-color: #434343;
|
||||||
|
}
|
||||||
|
|
||||||
.ant-card-dark .ant-tag-blue {
|
.ant-card-dark .ant-tag-blue {
|
||||||
color: #3c9ae8;
|
color: #3c9ae8;
|
||||||
background: #111d2c;
|
background: #111d2c;
|
||||||
@@ -334,6 +358,29 @@
|
|||||||
color: hsla(0,0%,100%,.65);
|
color: hsla(0,0%,100%,.65);
|
||||||
background-color: #073763;
|
background-color: #073763;
|
||||||
border-color: #1890ff;
|
border-color: #1890ff;
|
||||||
text-shadow: 0 -1px 0 rgba(0,0,0,.12);
|
text-shadow: 0 -1px 0 rgba(255,255,255,.12);
|
||||||
box-shadow: 0 2px 0 rgba(0,0,0,.045);
|
box-shadow: 0 2px 0 rgba(255,255,255,.045);
|
||||||
|
}
|
||||||
|
.ant-card-dark .ant-btn-primary:hover {
|
||||||
|
background-color: #40a9ff;
|
||||||
|
border-color: #40a9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-dark .ant-popover-content {
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 8px rgba(255,255,255,.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-dark .ant-popover-inner {
|
||||||
|
background: #222a37;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-dark .ant-popover-title,
|
||||||
|
.ant-dark .ant-popover-inner-content {
|
||||||
|
color: hsla(0,0%,100%,.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-dark .ant-popover-placement-top>.ant-popover-content>.ant-popover-arrow {
|
||||||
|
border-color: transparent #2e3b52 #2e3b52 transparent;
|
||||||
}
|
}
|
||||||
BIN
web/assets/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
@@ -4,7 +4,7 @@ supportLangs = [
|
|||||||
value : "en-US",
|
value : "en-US",
|
||||||
icon : "🇺🇸"
|
icon : "🇺🇸"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : "Farsi",
|
name : "Farsi",
|
||||||
value : "fa_IR",
|
value : "fa_IR",
|
||||||
icon : "🇮🇷"
|
icon : "🇮🇷"
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ class DBInbound {
|
|||||||
this.remark = "";
|
this.remark = "";
|
||||||
this.enable = true;
|
this.enable = true;
|
||||||
this.expiryTime = 0;
|
this.expiryTime = 0;
|
||||||
this.iplimit = 0;
|
this.limitIp = 0;
|
||||||
|
|
||||||
this.listen = "";
|
this.listen = "";
|
||||||
this.port = 0;
|
this.port = 0;
|
||||||
this.protocol = "";
|
this.protocol = "";
|
||||||
@@ -109,10 +110,6 @@ class DBInbound {
|
|||||||
get isExpiry() {
|
get isExpiry() {
|
||||||
return this.expiryTime < new Date().getTime();
|
return this.expiryTime < new Date().getTime();
|
||||||
}
|
}
|
||||||
get isDBInboundEmpty() {
|
|
||||||
const inbound = this.toInbound();
|
|
||||||
return inbound.isInboundEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
toInbound() {
|
toInbound() {
|
||||||
let settings = {};
|
let settings = {};
|
||||||
@@ -159,6 +156,7 @@ class DBInbound {
|
|||||||
const inbound = this.toInbound();
|
const inbound = this.toInbound();
|
||||||
return inbound.genLink(this.address, this.remark, clientIndex);
|
return inbound.genLink(this.address, this.remark, clientIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
get genInboundLinks() {
|
get genInboundLinks() {
|
||||||
const inbound = this.toInbound();
|
const inbound = this.toInbound();
|
||||||
return inbound.genInboundLinks(this.address, this.remark);
|
return inbound.genInboundLinks(this.address, this.remark);
|
||||||
@@ -173,10 +171,14 @@ class AllSetting {
|
|||||||
this.webCertFile = "";
|
this.webCertFile = "";
|
||||||
this.webKeyFile = "";
|
this.webKeyFile = "";
|
||||||
this.webBasePath = "/";
|
this.webBasePath = "/";
|
||||||
|
this.expireDiff = "";
|
||||||
|
this.trafficDiff = "";
|
||||||
this.tgBotEnable = false;
|
this.tgBotEnable = false;
|
||||||
this.tgBotToken = "";
|
this.tgBotToken = "";
|
||||||
this.tgBotChatId = 0;
|
this.tgBotChatId = "";
|
||||||
this.tgRunTime = "";
|
this.tgRunTime = "@daily";
|
||||||
|
this.tgBotBackup = false;
|
||||||
|
this.tgCpu = "";
|
||||||
this.xrayTemplateConfig = "";
|
this.xrayTemplateConfig = "";
|
||||||
|
|
||||||
this.timeLocation = "Asia/Tehran";
|
this.timeLocation = "Asia/Tehran";
|
||||||
|
|||||||
@@ -17,16 +17,15 @@ const VmessMethods = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const SSMethods = {
|
const SSMethods = {
|
||||||
// AES_256_CFB: 'aes-256-cfb',
|
AES_128_GCM: 'aes-128-gcm',
|
||||||
// AES_128_CFB: 'aes-128-cfb',
|
AES_256_GCM: 'aes-256-gcm',
|
||||||
// CHACHA20: 'chacha20',
|
CHACHA20_POLY1305: 'chacha20-poly1305',
|
||||||
// CHACHA20_IETF: 'chacha20-ietf',
|
CHACHA20_IETF_POLY1305: 'chacha20-ietf-poly1305',
|
||||||
CHACHA20_POLY1305: 'chacha20-poly1305',
|
XCHACHA20_POLY1305: 'xchacha20-poly1305',
|
||||||
AES_256_GCM: 'aes-256-gcm',
|
XCHACHA20_IETF_POLY1305: 'xchacha20-ietf-poly1305',
|
||||||
AES_128_GCM: 'aes-128-gcm',
|
|
||||||
BLAKE3_AES_128_GCM: '2022-blake3-aes-128-gcm',
|
BLAKE3_AES_128_GCM: '2022-blake3-aes-128-gcm',
|
||||||
BLAKE3_AES_256_GCM: '2022-blake3-aes-256-gcm',
|
BLAKE3_AES_256_GCM: '2022-blake3-aes-256-gcm',
|
||||||
BLAKE3_CHACHA20_POLY1305: '2022-blake3-chacha20-poly1305',
|
BLAKE3_CHACHA20_POLY1305: '2022-blake3-chacha20-poly1305',
|
||||||
};
|
};
|
||||||
|
|
||||||
const RULE_IP = {
|
const RULE_IP = {
|
||||||
@@ -92,7 +91,11 @@ const UTLS_FINGERPRINT = {
|
|||||||
UTLS_RANDOMIZED: "randomized",
|
UTLS_RANDOMIZED: "randomized",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const bytesToHex = e => Array.from(e).map(e => e.toString(16).padStart(2, 0)).join('');
|
||||||
|
const hexToBytes = e => new Uint8Array(e.match(/[0-9a-f]{2}/gi).map(e => parseInt(e, 16)));
|
||||||
|
|
||||||
const ALPN_OPTION = {
|
const ALPN_OPTION = {
|
||||||
|
H3: "h3",
|
||||||
H2: "h2",
|
H2: "h2",
|
||||||
HTTP1: "http/1.1",
|
HTTP1: "http/1.1",
|
||||||
};
|
};
|
||||||
@@ -106,7 +109,6 @@ Object.freeze(XTLS_FLOW_CONTROL);
|
|||||||
Object.freeze(TLS_FLOW_CONTROL);
|
Object.freeze(TLS_FLOW_CONTROL);
|
||||||
Object.freeze(TLS_VERSION_OPTION);
|
Object.freeze(TLS_VERSION_OPTION);
|
||||||
Object.freeze(TLS_CIPHER_OPTION);
|
Object.freeze(TLS_CIPHER_OPTION);
|
||||||
Object.freeze(UTLS_FINGERPRINT);
|
|
||||||
Object.freeze(ALPN_OPTION);
|
Object.freeze(ALPN_OPTION);
|
||||||
|
|
||||||
class XrayCommonClass {
|
class XrayCommonClass {
|
||||||
@@ -167,27 +169,25 @@ class XrayCommonClass {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class TcpStreamSettings extends XrayCommonClass {
|
class TcpStreamSettings extends XrayCommonClass {
|
||||||
constructor(
|
constructor(acceptProxyProtocol=false,
|
||||||
type = 'none',
|
type='none',
|
||||||
acceptProxyProtocol = false,
|
request=new TcpStreamSettings.TcpRequest(),
|
||||||
request = new TcpStreamSettings.TcpRequest(),
|
response=new TcpStreamSettings.TcpResponse(),
|
||||||
response = new TcpStreamSettings.TcpResponse(),
|
) {
|
||||||
) {
|
|
||||||
super();
|
super();
|
||||||
|
this.acceptProxyProtocol = acceptProxyProtocol;
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.request = request;
|
this.request = request;
|
||||||
this.response = response;
|
this.response = response;
|
||||||
this.acceptProxyProtocol = acceptProxyProtocol;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromJson(json = {}) {
|
static fromJson(json={}) {
|
||||||
let header = json.header;
|
let header = json.header;
|
||||||
if (!header) {
|
if (!header) {
|
||||||
header = {};
|
header = {};
|
||||||
}
|
}
|
||||||
return new TcpStreamSettings(
|
return new TcpStreamSettings(json.acceptProxyProtocol,
|
||||||
header.type,
|
header.type,
|
||||||
json.acceptProxyProtocol,
|
|
||||||
TcpStreamSettings.TcpRequest.fromJson(header.request),
|
TcpStreamSettings.TcpRequest.fromJson(header.request),
|
||||||
TcpStreamSettings.TcpResponse.fromJson(header.response),
|
TcpStreamSettings.TcpResponse.fromJson(header.response),
|
||||||
);
|
);
|
||||||
@@ -195,21 +195,21 @@ class TcpStreamSettings extends XrayCommonClass {
|
|||||||
|
|
||||||
toJson() {
|
toJson() {
|
||||||
return {
|
return {
|
||||||
|
acceptProxyProtocol: this.acceptProxyProtocol,
|
||||||
header: {
|
header: {
|
||||||
type: this.type,
|
type: this.type,
|
||||||
request: this.type === 'http' ? this.request.toJson() : undefined,
|
request: this.type === 'http' ? this.request.toJson() : undefined,
|
||||||
response: this.type === 'http' ? this.response.toJson() : undefined,
|
response: this.type === 'http' ? this.response.toJson() : undefined,
|
||||||
},
|
},
|
||||||
acceptProxyProtocol: this.acceptProxyProtocol,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TcpStreamSettings.TcpRequest = class extends XrayCommonClass {
|
TcpStreamSettings.TcpRequest = class extends XrayCommonClass {
|
||||||
constructor(version = '1.1',
|
constructor(version='1.1',
|
||||||
method = 'GET',
|
method='GET',
|
||||||
path = ['/'],
|
path=['/'],
|
||||||
headers = [],
|
headers=[],
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
this.version = version;
|
this.version = version;
|
||||||
@@ -243,7 +243,7 @@ TcpStreamSettings.TcpRequest = class extends XrayCommonClass {
|
|||||||
this.headers.splice(index, 1);
|
this.headers.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromJson(json = {}) {
|
static fromJson(json={}) {
|
||||||
return new TcpStreamSettings.TcpRequest(
|
return new TcpStreamSettings.TcpRequest(
|
||||||
json.version,
|
json.version,
|
||||||
json.method,
|
json.method,
|
||||||
@@ -262,10 +262,10 @@ TcpStreamSettings.TcpRequest = class extends XrayCommonClass {
|
|||||||
};
|
};
|
||||||
|
|
||||||
TcpStreamSettings.TcpResponse = class extends XrayCommonClass {
|
TcpStreamSettings.TcpResponse = class extends XrayCommonClass {
|
||||||
constructor(version = '1.1',
|
constructor(version='1.1',
|
||||||
status = '200',
|
status='200',
|
||||||
reason = 'OK',
|
reason='OK',
|
||||||
headers = [],
|
headers=[],
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
this.version = version;
|
this.version = version;
|
||||||
@@ -282,7 +282,7 @@ TcpStreamSettings.TcpResponse = class extends XrayCommonClass {
|
|||||||
this.headers.splice(index, 1);
|
this.headers.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromJson(json = {}) {
|
static fromJson(json={}) {
|
||||||
return new TcpStreamSettings.TcpResponse(
|
return new TcpStreamSettings.TcpResponse(
|
||||||
json.version,
|
json.version,
|
||||||
json.status,
|
json.status,
|
||||||
@@ -475,9 +475,13 @@ class GrpcStreamSettings extends XrayCommonClass {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class TlsStreamSettings extends XrayCommonClass {
|
class TlsStreamSettings extends XrayCommonClass {
|
||||||
constructor(serverName = '', minVersion = TLS_VERSION_OPTION.TLS10, maxVersion = TLS_VERSION_OPTION.TLS13,
|
constructor(serverName='',
|
||||||
cipherSuites = '',
|
minVersion = TLS_VERSION_OPTION.TLS12,
|
||||||
certificates = [new TlsStreamSettings.Cert()], alpn=[''] ,settings=[new TlsStreamSettings.Settings()]) {
|
maxVersion = TLS_VERSION_OPTION.TLS13,
|
||||||
|
cipherSuites = '',
|
||||||
|
certificates=[new TlsStreamSettings.Cert()],
|
||||||
|
alpn=[],
|
||||||
|
settings=[new TlsStreamSettings.Settings()]) {
|
||||||
super();
|
super();
|
||||||
this.server = serverName;
|
this.server = serverName;
|
||||||
this.minVersion = minVersion;
|
this.minVersion = minVersion;
|
||||||
@@ -485,7 +489,7 @@ class TlsStreamSettings extends XrayCommonClass {
|
|||||||
this.cipherSuites = cipherSuites;
|
this.cipherSuites = cipherSuites;
|
||||||
this.certs = certificates;
|
this.certs = certificates;
|
||||||
this.alpn = alpn;
|
this.alpn = alpn;
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
addCert(cert) {
|
addCert(cert) {
|
||||||
@@ -498,15 +502,15 @@ class TlsStreamSettings extends XrayCommonClass {
|
|||||||
|
|
||||||
static fromJson(json={}) {
|
static fromJson(json={}) {
|
||||||
let certs;
|
let certs;
|
||||||
let settings;
|
let settings;
|
||||||
if (!ObjectUtil.isEmpty(json.certificates)) {
|
if (!ObjectUtil.isEmpty(json.certificates)) {
|
||||||
certs = json.certificates.map(cert => TlsStreamSettings.Cert.fromJson(cert));
|
certs = json.certificates.map(cert => TlsStreamSettings.Cert.fromJson(cert));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ObjectUtil.isEmpty(json.settings)) {
|
if (!ObjectUtil.isEmpty(json.settings)) {
|
||||||
let values = json.settings[0];
|
let values = json.settings[0];
|
||||||
settings = [new TlsStreamSettings.Settings(values.allowInsecure , values.fingerprint, values.serverName)];
|
settings = [new TlsStreamSettings.Settings(values.allowInsecure , values.fingerprint, values.serverName)];
|
||||||
}
|
}
|
||||||
|
|
||||||
return new TlsStreamSettings(
|
return new TlsStreamSettings(
|
||||||
json.serverName,
|
json.serverName,
|
||||||
json.minVersion,
|
json.minVersion,
|
||||||
@@ -514,7 +518,7 @@ class TlsStreamSettings extends XrayCommonClass {
|
|||||||
json.cipherSuites,
|
json.cipherSuites,
|
||||||
certs,
|
certs,
|
||||||
json.alpn,
|
json.alpn,
|
||||||
settings,
|
settings,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -527,7 +531,6 @@ class TlsStreamSettings extends XrayCommonClass {
|
|||||||
certificates: TlsStreamSettings.toJsonArray(this.certs),
|
certificates: TlsStreamSettings.toJsonArray(this.certs),
|
||||||
alpn: this.alpn,
|
alpn: this.alpn,
|
||||||
settings: TlsStreamSettings.toJsonArray(this.settings),
|
settings: TlsStreamSettings.toJsonArray(this.settings),
|
||||||
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -574,44 +577,96 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
|
|||||||
};
|
};
|
||||||
|
|
||||||
TlsStreamSettings.Settings = class extends XrayCommonClass {
|
TlsStreamSettings.Settings = class extends XrayCommonClass {
|
||||||
constructor(allowInsecure = false, fingerprint = '', serverName = '') {
|
constructor(allowInsecure = false, fingerprint = '', serverName = '') {
|
||||||
super();
|
super();
|
||||||
this.allowInsecure = allowInsecure;
|
this.allowInsecure = allowInsecure;
|
||||||
this.fingerprint = fingerprint;
|
this.fingerprint = fingerprint;
|
||||||
this.serverName = serverName;
|
this.serverName = serverName;
|
||||||
}
|
}
|
||||||
static fromJson(json = {}) {
|
static fromJson(json = {}) {
|
||||||
return new TlsStreamSettings.Settings(
|
return new TlsStreamSettings.Settings(
|
||||||
json.allowInsecure,
|
json.allowInsecure,
|
||||||
json.fingerprint,
|
json.fingerprint,
|
||||||
json.servername,
|
json.servername,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
toJson() {
|
toJson() {
|
||||||
return {
|
return {
|
||||||
allowInsecure: this.allowInsecure,
|
allowInsecure: this.allowInsecure,
|
||||||
fingerprint: this.fingerprint,
|
fingerprint: this.fingerprint,
|
||||||
serverName: this.serverName,
|
serverName: this.serverName,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class RealityStreamSettings extends XrayCommonClass {
|
||||||
|
constructor(show = false,xver = 0, fingerprint = UTLS_FINGERPRINT.UTLS_FIREFOX, dest = 'github.io:443', serverNames = 'github.io,www.github.io,', privateKey = RandomUtil.randomX25519PrivateKey(), publicKey = '', minClient = '',
|
||||||
|
maxClient = '', maxTimediff = 0, shortIds = RandomUtil.randowShortId()) {
|
||||||
|
super();
|
||||||
|
this.show = show;
|
||||||
|
this.xver = xver;
|
||||||
|
this.fingerprint = fingerprint;
|
||||||
|
this.dest = dest;
|
||||||
|
this.serverNames = serverNames instanceof Array ? serverNames.join(",") : serverNames;
|
||||||
|
this.privateKey = privateKey;
|
||||||
|
this.publicKey = RandomUtil.randomX25519PublicKey(this.privateKey);
|
||||||
|
this.minClient = minClient;
|
||||||
|
this.maxClient = maxClient;
|
||||||
|
this.maxTimediff = maxTimediff;
|
||||||
|
this.shortIds = shortIds instanceof Array ? shortIds.join(",") : shortIds;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson(json = {}) {
|
||||||
|
return new RealityStreamSettings(
|
||||||
|
json.show,
|
||||||
|
json.xver,
|
||||||
|
json.fingerprint,
|
||||||
|
json.dest,
|
||||||
|
json.serverNames,
|
||||||
|
json.privateKey,
|
||||||
|
json.publicKey,
|
||||||
|
json.minClient,
|
||||||
|
json.maxClient,
|
||||||
|
json.maxTimediff,
|
||||||
|
json.shortIds
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
toJson() {
|
||||||
|
return {
|
||||||
|
show: this.show,
|
||||||
|
xver: this.xver,
|
||||||
|
fingerprint: this.fingerprint,
|
||||||
|
dest: this.dest,
|
||||||
|
serverNames: this.serverNames.split(/,|,|\s+/),
|
||||||
|
privateKey: this.privateKey,
|
||||||
|
publicKey: this.publicKey,
|
||||||
|
minClient: this.minClient,
|
||||||
|
maxClient: this.maxClient,
|
||||||
|
maxTimediff: this.maxTimediff,
|
||||||
|
shortIds: this.shortIds.split(/,|,|\s+/)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class StreamSettings extends XrayCommonClass {
|
class StreamSettings extends XrayCommonClass {
|
||||||
constructor(network='tcp',
|
constructor(network='tcp',
|
||||||
security='none',
|
security='none',
|
||||||
tlsSettings=new TlsStreamSettings(),
|
tlsSettings=new TlsStreamSettings(),
|
||||||
tcpSettings=new TcpStreamSettings(),
|
realitySettings = new RealityStreamSettings(),
|
||||||
kcpSettings=new KcpStreamSettings(),
|
tcpSettings=new TcpStreamSettings(),
|
||||||
wsSettings=new WsStreamSettings(),
|
kcpSettings=new KcpStreamSettings(),
|
||||||
httpSettings=new HttpStreamSettings(),
|
wsSettings=new WsStreamSettings(),
|
||||||
quicSettings=new QuicStreamSettings(),
|
httpSettings=new HttpStreamSettings(),
|
||||||
grpcSettings=new GrpcStreamSettings(),
|
quicSettings=new QuicStreamSettings(),
|
||||||
) {
|
grpcSettings=new GrpcStreamSettings(),
|
||||||
|
) {
|
||||||
super();
|
super();
|
||||||
this.network = network;
|
this.network = network;
|
||||||
this.security = security;
|
this.security = security;
|
||||||
this.tls = tlsSettings;
|
this.tls = tlsSettings;
|
||||||
|
this.reality = realitySettings;
|
||||||
this.tcp = tcpSettings;
|
this.tcp = tcpSettings;
|
||||||
this.kcp = kcpSettings;
|
this.kcp = kcpSettings;
|
||||||
this.ws = wsSettings;
|
this.ws = wsSettings;
|
||||||
@@ -644,17 +699,34 @@ class StreamSettings extends XrayCommonClass {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromJson(json={}) {
|
//for Reality
|
||||||
let tls;
|
get isReality() {
|
||||||
if (json.security === "xtls") {
|
return this.security === "reality";
|
||||||
tls = TlsStreamSettings.fromJson(json.XTLSSettings);
|
}
|
||||||
|
|
||||||
|
set isReality(isReality) {
|
||||||
|
if (isReality) {
|
||||||
|
this.security = "reality";
|
||||||
} else {
|
} else {
|
||||||
|
this.security = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson(json = {}) {
|
||||||
|
let tls, reality;
|
||||||
|
if (json.security === "xtls") {
|
||||||
|
tls = TlsStreamSettings.fromJson(json.xtlsSettings);
|
||||||
|
} else if (json.security === "tls") {
|
||||||
tls = TlsStreamSettings.fromJson(json.tlsSettings);
|
tls = TlsStreamSettings.fromJson(json.tlsSettings);
|
||||||
}
|
}
|
||||||
|
if (json.security === "reality") {
|
||||||
|
reality = RealityStreamSettings.fromJson(json.realitySettings)
|
||||||
|
}
|
||||||
return new StreamSettings(
|
return new StreamSettings(
|
||||||
json.network,
|
json.network,
|
||||||
json.security,
|
json.security,
|
||||||
tls,
|
tls,
|
||||||
|
reality,
|
||||||
TcpStreamSettings.fromJson(json.tcpSettings),
|
TcpStreamSettings.fromJson(json.tcpSettings),
|
||||||
KcpStreamSettings.fromJson(json.kcpSettings),
|
KcpStreamSettings.fromJson(json.kcpSettings),
|
||||||
WsStreamSettings.fromJson(json.wsSettings),
|
WsStreamSettings.fromJson(json.wsSettings),
|
||||||
@@ -672,6 +744,7 @@ class StreamSettings extends XrayCommonClass {
|
|||||||
tlsSettings: this.isTls ? this.tls.toJson() : undefined,
|
tlsSettings: this.isTls ? this.tls.toJson() : undefined,
|
||||||
XTLSSettings: this.isXTLS ? this.tls.toJson() : undefined,
|
XTLSSettings: this.isXTLS ? this.tls.toJson() : undefined,
|
||||||
tcpSettings: network === 'tcp' ? this.tcp.toJson() : undefined,
|
tcpSettings: network === 'tcp' ? this.tcp.toJson() : undefined,
|
||||||
|
realitySettings: this.isReality ? this.reality.toJson() : undefined,
|
||||||
kcpSettings: network === 'kcp' ? this.kcp.toJson() : undefined,
|
kcpSettings: network === 'kcp' ? this.kcp.toJson() : undefined,
|
||||||
wsSettings: network === 'ws' ? this.ws.toJson() : undefined,
|
wsSettings: network === 'ws' ? this.ws.toJson() : undefined,
|
||||||
httpSettings: network === 'http' ? this.http.toJson() : undefined,
|
httpSettings: network === 'http' ? this.http.toJson() : undefined,
|
||||||
@@ -734,15 +807,18 @@ class Inbound extends XrayCommonClass {
|
|||||||
this._protocol = protocol;
|
this._protocol = protocol;
|
||||||
this.settings = Inbound.Settings.getSettings(protocol);
|
this.settings = Inbound.Settings.getSettings(protocol);
|
||||||
if (protocol === Protocols.TROJAN) {
|
if (protocol === Protocols.TROJAN) {
|
||||||
this.tls = false;
|
this.tls = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get tls() {
|
get tls() {
|
||||||
return this.stream.security === 'tls';
|
return this.stream.security === 'tls';
|
||||||
}
|
}
|
||||||
|
|
||||||
set tls(isTls) {
|
set tls(isTls) {
|
||||||
if (isTls) {
|
if (isTls) {
|
||||||
|
this.xtls = false;
|
||||||
|
this.reality = false;
|
||||||
this.stream.security = 'tls';
|
this.stream.security = 'tls';
|
||||||
} else {
|
} else {
|
||||||
this.stream.security = 'none';
|
this.stream.security = 'none';
|
||||||
@@ -755,12 +831,32 @@ class Inbound extends XrayCommonClass {
|
|||||||
|
|
||||||
set XTLS(isXTLS) {
|
set XTLS(isXTLS) {
|
||||||
if (isXTLS) {
|
if (isXTLS) {
|
||||||
|
this.xtls = false;
|
||||||
|
this.reality = false;
|
||||||
this.stream.security = 'xtls';
|
this.stream.security = 'xtls';
|
||||||
} else {
|
} else {
|
||||||
this.stream.security = 'none';
|
this.stream.security = 'none';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//for Reality
|
||||||
|
get reality() {
|
||||||
|
if (this.stream.security === "reality") {
|
||||||
|
return this.network === "tcp" || this.network === "grpc" || this.network === "http";
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
set reality(isReality) {
|
||||||
|
if (isReality) {
|
||||||
|
this.tls = false;
|
||||||
|
this.xtls = false;
|
||||||
|
this.stream.security = "reality";
|
||||||
|
} else {
|
||||||
|
this.stream.security = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get network() {
|
get network() {
|
||||||
return this.stream.network;
|
return this.stream.network;
|
||||||
}
|
}
|
||||||
@@ -919,16 +1015,16 @@ class Inbound extends XrayCommonClass {
|
|||||||
isExpiry(index) {
|
isExpiry(index) {
|
||||||
switch (this.protocol) {
|
switch (this.protocol) {
|
||||||
case Protocols.VMESS:
|
case Protocols.VMESS:
|
||||||
if(this.settings.vmesses[index]._expiryTime != null)
|
if(this.settings.vmesses[index].expiryTime > 0)
|
||||||
return this.settings.vmesses[index]._expiryTime < new Date().getTime();
|
return this.settings.vmesses[index].expiryTime < new Date().getTime();
|
||||||
return false
|
return false
|
||||||
case Protocols.VLESS:
|
case Protocols.VLESS:
|
||||||
if(this.settings.vlesses[index]._expiryTime != null)
|
if(this.settings.vlesses[index].expiryTime > 0)
|
||||||
return this.settings.vlesses[index]._expiryTime < new Date().getTime();
|
return this.settings.vlesses[index].expiryTime < new Date().getTime();
|
||||||
return false
|
return false
|
||||||
case Protocols.TROJAN:
|
case Protocols.TROJAN:
|
||||||
if(this.settings.trojans[index]._expiryTime != null)
|
if(this.settings.trojans[index].expiryTime > 0)
|
||||||
return this.settings.trojans[index]._expiryTime < new Date().getTime();
|
return this.settings.trojans[index].expiryTime < new Date().getTime();
|
||||||
return false
|
return false
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
@@ -940,7 +1036,6 @@ class Inbound extends XrayCommonClass {
|
|||||||
case Protocols.VMESS:
|
case Protocols.VMESS:
|
||||||
case Protocols.VLESS:
|
case Protocols.VLESS:
|
||||||
case Protocols.TROJAN:
|
case Protocols.TROJAN:
|
||||||
case Protocols.SHADOWSOCKS:
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
@@ -958,9 +1053,19 @@ class Inbound extends XrayCommonClass {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
canEnableReality() {
|
||||||
|
switch (this.protocol) {
|
||||||
|
case Protocols.VLESS:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.network === "tcp" || this.network === "grpc" || this.network === "http";
|
||||||
|
}
|
||||||
|
|
||||||
//this is used for xtls-rprx-vision
|
//this is used for xtls-rprx-vision
|
||||||
canEnableTlsFlow() {
|
canEnableTlsFlow() {
|
||||||
if ((this.stream.security === 'tls') && (this.network === "tcp")) {
|
if (((this.stream.security === 'tls') || (this.stream.security === 'reality')) && (this.network === "tcp")) {
|
||||||
switch (this.protocol) {
|
switch (this.protocol) {
|
||||||
case Protocols.VLESS:
|
case Protocols.VLESS:
|
||||||
return true;
|
return true;
|
||||||
@@ -975,7 +1080,6 @@ class Inbound extends XrayCommonClass {
|
|||||||
return this.canEnableTls();
|
return this.canEnableTls();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
canEnableXTLS() {
|
canEnableXTLS() {
|
||||||
switch (this.protocol) {
|
switch (this.protocol) {
|
||||||
case Protocols.VLESS:
|
case Protocols.VLESS:
|
||||||
@@ -991,7 +1095,7 @@ class Inbound extends XrayCommonClass {
|
|||||||
switch (this.protocol) {
|
switch (this.protocol) {
|
||||||
case Protocols.VMESS:
|
case Protocols.VMESS:
|
||||||
case Protocols.VLESS:
|
case Protocols.VLESS:
|
||||||
case Protocols.TROJAN:
|
case Protocols.TROJAN:
|
||||||
case Protocols.SHADOWSOCKS:
|
case Protocols.SHADOWSOCKS:
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
@@ -1081,7 +1185,7 @@ class Inbound extends XrayCommonClass {
|
|||||||
host: host,
|
host: host,
|
||||||
path: path,
|
path: path,
|
||||||
tls: this.stream.security,
|
tls: this.stream.security,
|
||||||
sni: this.stream.tls.settings[0]['serverName'],
|
sni: this.stream.tls.settings[0]['serverName'],
|
||||||
fp: this.stream.tls.settings[0]['fingerprint'],
|
fp: this.stream.tls.settings[0]['fingerprint'],
|
||||||
alpn: this.stream.tls.alpn.join(','),
|
alpn: this.stream.tls.alpn.join(','),
|
||||||
allowInsecure: this.stream.tls.settings[0].allowInsecure,
|
allowInsecure: this.stream.tls.settings[0].allowInsecure,
|
||||||
@@ -1151,7 +1255,7 @@ class Inbound extends XrayCommonClass {
|
|||||||
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
|
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
|
||||||
address = this.stream.tls.server;
|
address = this.stream.tls.server;
|
||||||
}
|
}
|
||||||
if (this.stream.tls.settings[0]['serverName'] !== ''){
|
if (this.stream.tls.settings[0]['serverName'] !== ''){
|
||||||
params.set("sni", this.stream.tls.settings[0]['serverName']);
|
params.set("sni", this.stream.tls.settings[0]['serverName']);
|
||||||
}
|
}
|
||||||
if (type === "tcp" && this.settings.vlesses[clientIndex].flow.length > 0) {
|
if (type === "tcp" && this.settings.vlesses[clientIndex].flow.length > 0) {
|
||||||
@@ -1159,17 +1263,37 @@ class Inbound extends XrayCommonClass {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.XTLS) {
|
if (this.XTLS) {
|
||||||
params.set("security", "tls");
|
params.set("security", "xtls");
|
||||||
params.set("alpn", this.stream.tls.alpn);
|
params.set("alpn", this.stream.tls.alpn);
|
||||||
if(this.stream.tls.settings[0].allowInsecure){
|
if(this.stream.tls.settings[0].allowInsecure){
|
||||||
params.set("allowInsecure", "1");
|
params.set("allowInsecure", "1");
|
||||||
}
|
}
|
||||||
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
|
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
|
||||||
address = this.stream.tls.server;
|
address = this.stream.tls.server;
|
||||||
}
|
}
|
||||||
params.set("flow", this.settings.vlesses[clientIndex].flow);
|
params.set("flow", this.settings.vlesses[clientIndex].flow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.reality) {
|
||||||
|
params.set("security", "reality");
|
||||||
|
if (!ObjectUtil.isArrEmpty(this.stream.reality.serverNames)) {
|
||||||
|
params.set("sni", this.stream.reality.serverNames.split(/,|,|\s+/)[0]);
|
||||||
|
}
|
||||||
|
if (this.stream.reality.publicKey != "") {
|
||||||
|
//params.set("pbk", Ed25519.getPublicKey(this.stream.reality.privateKey));
|
||||||
|
params.set("pbk", this.stream.reality.publicKey);
|
||||||
|
}
|
||||||
|
if (this.stream.network === 'tcp') {
|
||||||
|
params.set("flow", this.settings.vlesses[clientIndex].flow);
|
||||||
|
}
|
||||||
|
if (this.stream.reality.shortIds != "") {
|
||||||
|
params.set("sid", this.stream.reality.shortIds);
|
||||||
|
}
|
||||||
|
if (this.stream.reality.fingerprint != "") {
|
||||||
|
params.set("fp", this.stream.reality.fingerprint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const link = `vless://${uuid}@${address}:${port}`;
|
const link = `vless://${uuid}@${address}:${port}`;
|
||||||
const url = new URL(link);
|
const url = new URL(link);
|
||||||
@@ -1180,18 +1304,13 @@ class Inbound extends XrayCommonClass {
|
|||||||
return url.toString();
|
return url.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
genSSLink(address = '', remark = '') {
|
genSSLink(address='', remark='') {
|
||||||
let settings = this.settings;
|
let settings = this.settings;
|
||||||
const server = this.stream.tls.server;
|
const server = this.stream.tls.server;
|
||||||
if (!ObjectUtil.isEmpty(server)) {
|
if (!ObjectUtil.isEmpty(server)) {
|
||||||
address = server;
|
address = server;
|
||||||
}
|
}
|
||||||
if (settings.method == SSMethods.BLAKE3_AES_128_GCM || settings.method == SSMethods.BLAKE3_AES_256_GCM || settings.method == SSMethods.BLAKE3_CHACHA20_POLY1305) {
|
return 'ss://' + safeBase64(settings.method + ':' + settings.password) + `@${address}:${this.port}#${encodeURIComponent(remark)}`;
|
||||||
return `ss://${settings.method}:${settings.password}@${address}:${this.port}#${encodeURIComponent(remark)}`;
|
|
||||||
} else {
|
|
||||||
return 'ss://' + safeBase64(settings.method + ':' + settings.password + '@' + address + ':' + this.port)
|
|
||||||
+ '#' + encodeURIComponent(remark);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
genTrojanLink(address = '', remark = '', clientIndex = 0) {
|
genTrojanLink(address = '', remark = '', clientIndex = 0) {
|
||||||
@@ -1199,7 +1318,7 @@ class Inbound extends XrayCommonClass {
|
|||||||
const port = this.port;
|
const port = this.port;
|
||||||
const type = this.stream.network;
|
const type = this.stream.network;
|
||||||
const params = new Map();
|
const params = new Map();
|
||||||
params.set("type", this.stream.network);
|
params.set("type", this.stream.network);
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "tcp":
|
case "tcp":
|
||||||
const tcp = this.stream.tcp;
|
const tcp = this.stream.tcp;
|
||||||
@@ -1254,23 +1373,23 @@ class Inbound extends XrayCommonClass {
|
|||||||
}
|
}
|
||||||
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
|
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
|
||||||
address = this.stream.tls.server;
|
address = this.stream.tls.server;
|
||||||
}
|
|
||||||
if (this.stream.tls.settings[0]['serverName'] !== ''){
|
|
||||||
params.set("sni", this.stream.tls.settings[0]['serverName']);
|
|
||||||
}
|
}
|
||||||
|
if (this.stream.tls.settings[0]['serverName'] !== ''){
|
||||||
|
params.set("sni", this.stream.tls.settings[0]['serverName']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.XTLS) {
|
if (this.XTLS) {
|
||||||
params.set("security", "tls");
|
params.set("security", "xtls");
|
||||||
params.set("alpn", this.stream.tls.alpn);
|
params.set("alpn", this.stream.tls.alpn);
|
||||||
if(this.stream.tls.settings[0].allowInsecure){
|
if(this.stream.tls.settings[0].allowInsecure){
|
||||||
params.set("allowInsecure", "1");
|
params.set("allowInsecure", "1");
|
||||||
}
|
}
|
||||||
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
|
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
|
||||||
address = this.stream.tls.server;
|
address = this.stream.tls.server;
|
||||||
}
|
}
|
||||||
params.set("flow", this.settings.trojans[clientIndex].flow);
|
params.set("flow", this.settings.trojans[clientIndex].flow);
|
||||||
}
|
}
|
||||||
|
|
||||||
const link = `trojan://${settings.trojans[clientIndex].password}@${address}:${this.port}#${encodeURIComponent(remark)}`;
|
const link = `trojan://${settings.trojans[clientIndex].password}@${address}:${this.port}#${encodeURIComponent(remark)}`;
|
||||||
const url = new URL(link);
|
const url = new URL(link);
|
||||||
@@ -1302,8 +1421,9 @@ class Inbound extends XrayCommonClass {
|
|||||||
default: return '';
|
default: return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
genInboundLinks(address = '', remark = '') {
|
genInboundLinks(address = '', remark = '') {
|
||||||
let link = '';
|
let link = '';
|
||||||
switch (this.protocol) {
|
switch (this.protocol) {
|
||||||
case Protocols.VMESS:
|
case Protocols.VMESS:
|
||||||
case Protocols.VLESS:
|
case Protocols.VLESS:
|
||||||
@@ -1316,7 +1436,7 @@ class Inbound extends XrayCommonClass {
|
|||||||
return (this.genSSLink(address, remark) + '\r\n');
|
return (this.genSSLink(address, remark) + '\r\n');
|
||||||
default: return '';
|
default: return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromJson(json={}) {
|
static fromJson(json={}) {
|
||||||
return new Inbound(
|
return new Inbound(
|
||||||
@@ -1431,7 +1551,7 @@ Inbound.VmessSettings = class extends Inbound.Settings {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
Inbound.VmessSettings.Vmess = class extends XrayCommonClass {
|
Inbound.VmessSettings.Vmess = class extends XrayCommonClass {
|
||||||
constructor(id=RandomUtil.randomUUID(), alterId=0, email=RandomUtil.randomText(),limitIp=0, totalGB=0, expiryTime='') {
|
constructor(id=RandomUtil.randomUUID(), alterId=0, email=RandomUtil.randomText(),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId='') {
|
||||||
super();
|
super();
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.alterId = alterId;
|
this.alterId = alterId;
|
||||||
@@ -1439,6 +1559,9 @@ Inbound.VmessSettings.Vmess = class extends XrayCommonClass {
|
|||||||
this.limitIp = limitIp;
|
this.limitIp = limitIp;
|
||||||
this.totalGB = totalGB;
|
this.totalGB = totalGB;
|
||||||
this.expiryTime = expiryTime;
|
this.expiryTime = expiryTime;
|
||||||
|
this.enable = enable;
|
||||||
|
this.tgId = tgId;
|
||||||
|
this.subId = subId;
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromJson(json={}) {
|
static fromJson(json={}) {
|
||||||
@@ -1449,13 +1572,18 @@ Inbound.VmessSettings.Vmess = class extends XrayCommonClass {
|
|||||||
json.limitIp,
|
json.limitIp,
|
||||||
json.totalGB,
|
json.totalGB,
|
||||||
json.expiryTime,
|
json.expiryTime,
|
||||||
|
json.enable,
|
||||||
|
json.tgId,
|
||||||
|
json.subId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
get _expiryTime() {
|
get _expiryTime() {
|
||||||
if (this.expiryTime === 0 || this.expiryTime === "") {
|
if (this.expiryTime === 0 || this.expiryTime === "") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (this.expiryTime < 0){
|
||||||
|
return this.expiryTime / -86400000;
|
||||||
|
}
|
||||||
return moment(this.expiryTime);
|
return moment(this.expiryTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1483,7 +1611,7 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
|
|||||||
fallbacks=[],) {
|
fallbacks=[],) {
|
||||||
super(protocol);
|
super(protocol);
|
||||||
this.vlesses = vlesses;
|
this.vlesses = vlesses;
|
||||||
this.decryption = 'none';
|
this.decryption = 'none'; // Using decryption is not implemented here
|
||||||
this.fallbacks = fallbacks;
|
this.fallbacks = fallbacks;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1495,6 +1623,7 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
|
|||||||
this.fallbacks.splice(index, 1);
|
this.fallbacks.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// decryption should be set to static value
|
||||||
static fromJson(json={}) {
|
static fromJson(json={}) {
|
||||||
return new Inbound.VLESSSettings(
|
return new Inbound.VLESSSettings(
|
||||||
Protocols.VLESS,
|
Protocols.VLESS,
|
||||||
@@ -1514,8 +1643,7 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
|
|||||||
|
|
||||||
};
|
};
|
||||||
Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
|
Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
|
||||||
|
constructor(id=RandomUtil.randomUUID(), flow='', email=RandomUtil.randomText(),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId='') {
|
||||||
constructor(id=RandomUtil.randomUUID(), flow='', email=RandomUtil.randomText(),limitIp=0, totalGB=0, expiryTime='') {
|
|
||||||
super();
|
super();
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.flow = flow;
|
this.flow = flow;
|
||||||
@@ -1523,7 +1651,9 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
|
|||||||
this.limitIp = limitIp;
|
this.limitIp = limitIp;
|
||||||
this.totalGB = totalGB;
|
this.totalGB = totalGB;
|
||||||
this.expiryTime = expiryTime;
|
this.expiryTime = expiryTime;
|
||||||
|
this.enable = enable;
|
||||||
|
this.tgId = tgId;
|
||||||
|
this.subId = subId;
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromJson(json={}) {
|
static fromJson(json={}) {
|
||||||
@@ -1534,14 +1664,19 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
|
|||||||
json.limitIp,
|
json.limitIp,
|
||||||
json.totalGB,
|
json.totalGB,
|
||||||
json.expiryTime,
|
json.expiryTime,
|
||||||
|
json.enable,
|
||||||
|
json.tgId,
|
||||||
|
json.subId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get _expiryTime() {
|
get _expiryTime() {
|
||||||
if (this.expiryTime === 0 || this.expiryTime === "") {
|
if (this.expiryTime === 0 || this.expiryTime === "") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (this.expiryTime < 0){
|
||||||
|
return this.expiryTime / -86400000;
|
||||||
|
}
|
||||||
return moment(this.expiryTime);
|
return moment(this.expiryTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1561,7 +1696,7 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
Inbound.VLESSSettings.Fallback = class extends XrayCommonClass {
|
Inbound.VLESSSettings.Fallback = class extends XrayCommonClass {
|
||||||
constructor(name="", alpn='', path='', dest='', xver=0) {
|
constructor(name="", alpn=[], path='', dest='', xver=0) {
|
||||||
super();
|
super();
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.alpn = alpn;
|
this.alpn = alpn;
|
||||||
@@ -1601,8 +1736,8 @@ Inbound.VLESSSettings.Fallback = class extends XrayCommonClass {
|
|||||||
|
|
||||||
Inbound.TrojanSettings = class extends Inbound.Settings {
|
Inbound.TrojanSettings = class extends Inbound.Settings {
|
||||||
constructor(protocol,
|
constructor(protocol,
|
||||||
trojans=[new Inbound.TrojanSettings.Trojan()],
|
trojans=[new Inbound.TrojanSettings.Trojan()],
|
||||||
fallbacks=[],) {
|
fallbacks=[],) {
|
||||||
super(protocol);
|
super(protocol);
|
||||||
this.trojans = trojans;
|
this.trojans = trojans;
|
||||||
this.fallbacks = fallbacks;
|
this.fallbacks = fallbacks;
|
||||||
@@ -1631,7 +1766,7 @@ Inbound.TrojanSettings = class extends Inbound.Settings {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
|
Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
|
||||||
constructor(password=RandomUtil.randomSeq(10), flow='', email=RandomUtil.randomText(),limitIp=0, totalGB=0, expiryTime='') {
|
constructor(password=RandomUtil.randomSeq(10), flow='', email=RandomUtil.randomText(),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId='') {
|
||||||
super();
|
super();
|
||||||
this.password = password;
|
this.password = password;
|
||||||
this.flow = flow;
|
this.flow = flow;
|
||||||
@@ -1639,6 +1774,9 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
|
|||||||
this.limitIp = limitIp;
|
this.limitIp = limitIp;
|
||||||
this.totalGB = totalGB;
|
this.totalGB = totalGB;
|
||||||
this.expiryTime = expiryTime;
|
this.expiryTime = expiryTime;
|
||||||
|
this.enable = enable;
|
||||||
|
this.tgId = tgId;
|
||||||
|
this.subId = subId;
|
||||||
}
|
}
|
||||||
|
|
||||||
toJson() {
|
toJson() {
|
||||||
@@ -1649,10 +1787,13 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
|
|||||||
limitIp: this.limitIp,
|
limitIp: this.limitIp,
|
||||||
totalGB: this.totalGB,
|
totalGB: this.totalGB,
|
||||||
expiryTime: this.expiryTime,
|
expiryTime: this.expiryTime,
|
||||||
|
enable: this.enable,
|
||||||
|
tgId: this.tgId,
|
||||||
|
subId: this.subId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromJson(json={}) {
|
static fromJson(json = {}) {
|
||||||
return new Inbound.TrojanSettings.Trojan(
|
return new Inbound.TrojanSettings.Trojan(
|
||||||
json.password,
|
json.password,
|
||||||
json.flow,
|
json.flow,
|
||||||
@@ -1660,7 +1801,9 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
|
|||||||
json.limitIp,
|
json.limitIp,
|
||||||
json.totalGB,
|
json.totalGB,
|
||||||
json.expiryTime,
|
json.expiryTime,
|
||||||
|
json.enable,
|
||||||
|
json.tgId,
|
||||||
|
json.subId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1668,6 +1811,9 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
|
|||||||
if (this.expiryTime === 0 || this.expiryTime === "") {
|
if (this.expiryTime === 0 || this.expiryTime === "") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (this.expiryTime < 0){
|
||||||
|
return this.expiryTime / -86400000;
|
||||||
|
}
|
||||||
return moment(this.expiryTime);
|
return moment(this.expiryTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1689,7 +1835,7 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Inbound.TrojanSettings.Fallback = class extends XrayCommonClass {
|
Inbound.TrojanSettings.Fallback = class extends XrayCommonClass {
|
||||||
constructor(name="", alpn='', path='', dest='', xver=0) {
|
constructor(name="", alpn=[], path='', dest='', xver=0) {
|
||||||
super();
|
super();
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.alpn = alpn;
|
this.alpn = alpn;
|
||||||
@@ -1729,9 +1875,9 @@ Inbound.TrojanSettings.Fallback = class extends XrayCommonClass {
|
|||||||
|
|
||||||
Inbound.ShadowsocksSettings = class extends Inbound.Settings {
|
Inbound.ShadowsocksSettings = class extends Inbound.Settings {
|
||||||
constructor(protocol,
|
constructor(protocol,
|
||||||
method = SSMethods.BLAKE3_AES_256_GCM,
|
method=SSMethods.BLAKE3_AES_256_GCM,
|
||||||
password = RandomUtil.randomSeq(44),
|
password=RandomUtil.randomSeq(44),
|
||||||
network = 'tcp,udp'
|
network='tcp,udp'
|
||||||
) {
|
) {
|
||||||
super(protocol);
|
super(protocol);
|
||||||
this.method = method;
|
this.method = method;
|
||||||
@@ -1739,7 +1885,7 @@ Inbound.ShadowsocksSettings = class extends Inbound.Settings {
|
|||||||
this.network = network;
|
this.network = network;
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromJson(json = {}) {
|
static fromJson(json={}) {
|
||||||
return new Inbound.ShadowsocksSettings(
|
return new Inbound.ShadowsocksSettings(
|
||||||
Protocols.SHADOWSOCKS,
|
Protocols.SHADOWSOCKS,
|
||||||
json.method,
|
json.method,
|
||||||
@@ -1763,7 +1909,7 @@ Inbound.DokodemoSettings = class extends Inbound.Settings {
|
|||||||
this.address = address;
|
this.address = address;
|
||||||
this.port = port;
|
this.port = port;
|
||||||
this.network = network;
|
this.network = network;
|
||||||
this.followRedirect = followRedirect;
|
this.followRedirect = followRedirect;
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromJson(json={}) {
|
static fromJson(json={}) {
|
||||||
@@ -1772,7 +1918,7 @@ Inbound.DokodemoSettings = class extends Inbound.Settings {
|
|||||||
json.address,
|
json.address,
|
||||||
json.port,
|
json.port,
|
||||||
json.network,
|
json.network,
|
||||||
json.followRedirect,
|
json.followRedirect,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1781,7 +1927,7 @@ Inbound.DokodemoSettings = class extends Inbound.Settings {
|
|||||||
address: this.address,
|
address: this.address,
|
||||||
port: this.port,
|
port: this.port,
|
||||||
network: this.network,
|
network: this.network,
|
||||||
followRedirect: this.followRedirect,
|
followRedirect: this.followRedirect,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -89,6 +89,31 @@ const seq = [
|
|||||||
'U', 'V', 'W', 'X', 'Y', 'Z'
|
'U', 'V', 'W', 'X', 'Y', 'Z'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const shortIdSeq = [
|
||||||
|
'a', 'b', 'c', 'd', 'e', 'f',
|
||||||
|
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
||||||
|
];
|
||||||
|
|
||||||
|
const x25519Map = new Map(
|
||||||
|
[
|
||||||
|
['EH2FWe-Ij_FFAa2u9__-aiErLvVIneP601GOCdlyPWw', "goY3OtfaA4UYbiz7Hn0NysI5QJrK0VT_Chg6RLgUPQU"],
|
||||||
|
['cKI_6DoMSP1IepeWWXrG3G9nkehl94KYBhagU50g2U0', "VigpKFbSLnHLzBWobZaS1IBmw--giJ51w92y723ajnU"],
|
||||||
|
['qM2SNyK3NyHB6deWpEP3ITyCGKQFRTna_mlKP0w1QH0', "HYyIGuyNFslmcnNT7mrDdmuXwn4cm7smE_FZbYguKHQ"],
|
||||||
|
['qCWg5GMEDFd3n1nxDswlIpOHoPUXMLuMOIiLUVzubkI', "rJFC3dUjJxMnVZiUGzmf_LFsJUwFWY-CU5RQgFOHCWM"],
|
||||||
|
['4NOBxDrEsOhNI3Y3EnVIy_TN-uyBoAjQw6QM0YsOi0s', "CbcY9qc4YuMDJDyyL0OITlU824TBg1O84ClPy27e2RM"],
|
||||||
|
['eBvFb0M4HpSOwWjtXV8zliiEs_hg56zX4a2LpuuqpEI', "CjulQ2qVIky7ImIfysgQhNX7s_drGLheCGSkVHcLZhc"],
|
||||||
|
['yEpOzQV04NNcycWVeWtRNTzv5TS-ynTuKRacZCH-6U8', "O9RSr5gSdok2K_tobQnf_scyKVqnCx6C4Jrl7_rCZEQ"],
|
||||||
|
['CNt6TAUVCwqM6xIBHyni0K3Zqbn2htKQLvLb6XDgh0s', "d9cGLVBrDFS02L2OvkqyqwFZ1Ux3AHs28ehl4Rwiyl0"],
|
||||||
|
['EInKw-6Wr0rAHXlxxDuZU5mByIzcD3Z-_iWPzXlUL1k', "LlYD2nNVAvyjNvjZGZh4R8PkMIwkc6EycPTvR2LE0nQ"],
|
||||||
|
['GKIKo7rcXVyle-EUHtGIDtYnDsI6osQmOUl3DTJRAGc', "VcqHivYGGoBkcxOI6cSSjQmneltstkb2OhvO53dyhEM"],
|
||||||
|
['-FVDzv68IC17fJVlNDlhrrgX44WeBfbhwjWpCQVXGHE', "PGG2EYOvsFt2lAQTD7lqHeRxz2KxvllEDKcUrtizPBU"],
|
||||||
|
['0H3OJEYEu6XW7woqy7cKh2vzg6YHkbF_xSDTHKyrsn4', "mzevpYbS8kXengBY5p7tt56QE4tS3lwlwRemmkcQeyc"],
|
||||||
|
['8F8XywN6ci44ES6em2Z0fYYxyptB9uaXY9Hc1WSSPE4', "qCZUdWQZ2H33vWXnOkG8NpxBeq3qn5QWXlfCOWBNkkc"],
|
||||||
|
['IN0dqfkC10dj-ifRHrg2PmmOrzYs697ajGMwcLbu-1g', "2UW_EO3r7uczPGUUlpJBnMDpDmWUHE2yDzCmXS4sckE"],
|
||||||
|
['uIcmks5rAhvBe4dRaJOdeSqgxLGGMZhsGk4J4PEKL2s', "F9WJV_74IZp0Ide4hWjiJXk9FRtBUBkUr3mzU-q1lzk"],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
class RandomUtil {
|
class RandomUtil {
|
||||||
|
|
||||||
static randomIntRange(min, max) {
|
static randomIntRange(min, max) {
|
||||||
@@ -107,6 +132,14 @@ class RandomUtil {
|
|||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static randomShortIdSeq(count) {
|
||||||
|
let str = '';
|
||||||
|
for (let i = 0; i < count; ++i) {
|
||||||
|
str += shortIdSeq[this.randomInt(16)];
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
static randomLowerAndNum(count) {
|
static randomLowerAndNum(count) {
|
||||||
let str = '';
|
let str = '';
|
||||||
for (let i = 0; i < count; ++i) {
|
for (let i = 0; i < count; ++i) {
|
||||||
@@ -137,6 +170,26 @@ class RandomUtil {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static randowShortId() {
|
||||||
|
let str = '';
|
||||||
|
str += this.randomShortIdSeq(8)
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
static randomX25519PrivateKey() {
|
||||||
|
let num = x25519Map.size;
|
||||||
|
let index = this.randomInt(num);
|
||||||
|
let cntr = 0;
|
||||||
|
for (let key of x25519Map.keys()) {
|
||||||
|
if (cntr++ === index) {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static randomX25519PublicKey(key) {
|
||||||
|
return x25519Map.get(key)
|
||||||
|
}
|
||||||
static randomText() {
|
static randomText() {
|
||||||
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
|
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
|
||||||
var string = '';
|
var string = '';
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import "github.com/gin-gonic/gin"
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
type APIController struct {
|
type APIController struct {
|
||||||
BaseController
|
BaseController
|
||||||
|
|
||||||
inboundController *InboundController
|
inboundController *InboundController
|
||||||
settingController *SettingController
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAPIController(g *gin.RouterGroup) *APIController {
|
func NewAPIController(g *gin.RouterGroup) *APIController {
|
||||||
@@ -20,21 +17,26 @@ func (a *APIController) initRouter(g *gin.RouterGroup) {
|
|||||||
g = g.Group("/xui/API/inbounds")
|
g = g.Group("/xui/API/inbounds")
|
||||||
g.Use(a.checkLogin)
|
g.Use(a.checkLogin)
|
||||||
|
|
||||||
g.GET("/", a.inbounds)
|
g.POST("/list", a.getAllInbounds)
|
||||||
g.GET("/get/:id", a.inbound)
|
g.GET("/get/:id", a.getSingleInbound)
|
||||||
g.POST("/add", a.addInbound)
|
g.POST("/add", a.addInbound)
|
||||||
g.POST("/del/:id", a.delInbound)
|
g.POST("/del/:id", a.delInbound)
|
||||||
g.POST("/update/:id", a.updateInbound)
|
g.POST("/update/:id", a.updateInbound)
|
||||||
|
g.POST("/clientIps/:email", a.getClientIps)
|
||||||
|
g.POST("/clearClientIps/:email", a.clearClientIps)
|
||||||
|
g.POST("/addClient/", a.addInboundClient)
|
||||||
|
g.POST("/delClient/:email", a.delInboundClient)
|
||||||
|
g.POST("/updateClient/:index", a.updateInboundClient)
|
||||||
|
g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
|
||||||
|
g.POST("/resetAllTraffics", a.resetAllTraffics)
|
||||||
|
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
|
||||||
|
|
||||||
a.inboundController = NewInboundController(g)
|
a.inboundController = NewInboundController(g)
|
||||||
}
|
}
|
||||||
|
func (a *APIController) getAllInbounds(c *gin.Context) {
|
||||||
|
|
||||||
func (a *APIController) inbounds(c *gin.Context) {
|
|
||||||
a.inboundController.getInbounds(c)
|
a.inboundController.getInbounds(c)
|
||||||
}
|
}
|
||||||
func (a *APIController) inbound(c *gin.Context) {
|
func (a *APIController) getSingleInbound(c *gin.Context) {
|
||||||
a.inboundController.getInbound(c)
|
a.inboundController.getInbound(c)
|
||||||
}
|
}
|
||||||
func (a *APIController) addInbound(c *gin.Context) {
|
func (a *APIController) addInbound(c *gin.Context) {
|
||||||
@@ -46,3 +48,29 @@ func (a *APIController) delInbound(c *gin.Context) {
|
|||||||
func (a *APIController) updateInbound(c *gin.Context) {
|
func (a *APIController) updateInbound(c *gin.Context) {
|
||||||
a.inboundController.updateInbound(c)
|
a.inboundController.updateInbound(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *APIController) getClientIps(c *gin.Context) {
|
||||||
|
a.inboundController.getClientIps(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APIController) clearClientIps(c *gin.Context) {
|
||||||
|
a.inboundController.clearClientIps(c)
|
||||||
|
}
|
||||||
|
func (a *APIController) addInboundClient(c *gin.Context) {
|
||||||
|
a.inboundController.addInboundClient(c)
|
||||||
|
}
|
||||||
|
func (a *APIController) delInboundClient(c *gin.Context) {
|
||||||
|
a.inboundController.delInboundClient(c)
|
||||||
|
}
|
||||||
|
func (a *APIController) updateInboundClient(c *gin.Context) {
|
||||||
|
a.inboundController.updateInboundClient(c)
|
||||||
|
}
|
||||||
|
func (a *APIController) resetClientTraffic(c *gin.Context) {
|
||||||
|
a.inboundController.resetClientTraffic(c)
|
||||||
|
}
|
||||||
|
func (a *APIController) resetAllTraffics(c *gin.Context) {
|
||||||
|
a.inboundController.resetAllTraffics(c)
|
||||||
|
}
|
||||||
|
func (a *APIController) resetAllClientTraffics(c *gin.Context) {
|
||||||
|
a.inboundController.resetAllClientTraffics(c)
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,7 +33,12 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
|
|||||||
g.POST("/update/:id", a.updateInbound)
|
g.POST("/update/:id", a.updateInbound)
|
||||||
g.POST("/clientIps/:email", a.getClientIps)
|
g.POST("/clientIps/:email", a.getClientIps)
|
||||||
g.POST("/clearClientIps/:email", a.clearClientIps)
|
g.POST("/clearClientIps/:email", a.clearClientIps)
|
||||||
g.POST("/resetClientTraffic/:email", a.resetClientTraffic)
|
g.POST("/addClient/", a.addInboundClient)
|
||||||
|
g.POST("/delClient/:email", a.delInboundClient)
|
||||||
|
g.POST("/updateClient/:index", a.updateInboundClient)
|
||||||
|
g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
|
||||||
|
g.POST("/resetAllTraffics", a.resetAllTraffics)
|
||||||
|
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,10 +129,11 @@ func (a *InboundController) updateInbound(c *gin.Context) {
|
|||||||
a.xrayService.SetToNeedRestart()
|
a.xrayService.SetToNeedRestart()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *InboundController) getClientIps(c *gin.Context) {
|
func (a *InboundController) getClientIps(c *gin.Context) {
|
||||||
email := c.Param("email")
|
email := c.Param("email")
|
||||||
|
|
||||||
ips , err := a.inboundService.GetInboundClientIps(email)
|
ips, err := a.inboundService.GetInboundClientIps(email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonObj(c, "No IP Record", nil)
|
jsonObj(c, "No IP Record", nil)
|
||||||
return
|
return
|
||||||
@@ -144,13 +150,109 @@ func (a *InboundController) clearClientIps(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
jsonMsg(c, "Log Cleared", nil)
|
jsonMsg(c, "Log Cleared", nil)
|
||||||
}
|
}
|
||||||
|
func (a *InboundController) addInboundClient(c *gin.Context) {
|
||||||
|
inbound := &model.Inbound{}
|
||||||
|
err := c.ShouldBind(inbound)
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.inboundService.AddInboundClient(inbound)
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, "something worng!", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonMsg(c, "Client added", nil)
|
||||||
|
if err == nil {
|
||||||
|
a.xrayService.SetToNeedRestart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *InboundController) delInboundClient(c *gin.Context) {
|
||||||
|
email := c.Param("email")
|
||||||
|
inbound := &model.Inbound{}
|
||||||
|
err := c.ShouldBind(inbound)
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.inboundService.DelInboundClient(inbound, email)
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, "something worng!", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonMsg(c, "Client deleted", nil)
|
||||||
|
if err == nil {
|
||||||
|
a.xrayService.SetToNeedRestart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *InboundController) updateInboundClient(c *gin.Context) {
|
||||||
|
index, err := strconv.Atoi(c.Param("index"))
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
inbound := &model.Inbound{}
|
||||||
|
err = c.ShouldBind(inbound)
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.inboundService.UpdateInboundClient(inbound, index)
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, "something worng!", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonMsg(c, "Client updated", nil)
|
||||||
|
if err == nil {
|
||||||
|
a.xrayService.SetToNeedRestart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (a *InboundController) resetClientTraffic(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.revise"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
email := c.Param("email")
|
email := c.Param("email")
|
||||||
|
|
||||||
err := a.inboundService.ResetClientTraffic(email)
|
err = a.inboundService.ResetClientTraffic(id, email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonMsg(c, "something worng!", err)
|
jsonMsg(c, "something worng!", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
jsonMsg(c, "traffic reseted", nil)
|
jsonMsg(c, "traffic reseted", nil)
|
||||||
|
if err == nil {
|
||||||
|
a.xrayService.SetToNeedRestart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *InboundController) resetAllTraffics(c *gin.Context) {
|
||||||
|
err := a.inboundService.ResetAllTraffics()
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, "something worng!", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonMsg(c, "All traffics reseted", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *InboundController) resetAllClientTraffics(c *gin.Context) {
|
||||||
|
id, err := strconv.Atoi(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.inboundService.ResetAllClientTraffics(id)
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, "something worng!", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonMsg(c, "All traffics of client reseted", nil)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
"x-ui/logger"
|
"x-ui/logger"
|
||||||
"x-ui/web/job"
|
|
||||||
"x-ui/web/service"
|
"x-ui/web/service"
|
||||||
"x-ui/web/session"
|
"x-ui/web/session"
|
||||||
|
|
||||||
@@ -20,6 +19,7 @@ type IndexController struct {
|
|||||||
BaseController
|
BaseController
|
||||||
|
|
||||||
userService service.UserService
|
userService service.UserService
|
||||||
|
tgbot service.Tgbot
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIndexController(g *gin.RouterGroup) *IndexController {
|
func NewIndexController(g *gin.RouterGroup) *IndexController {
|
||||||
@@ -60,13 +60,13 @@ func (a *IndexController) login(c *gin.Context) {
|
|||||||
user := a.userService.CheckUser(form.Username, form.Password)
|
user := a.userService.CheckUser(form.Username, form.Password)
|
||||||
timeStr := time.Now().Format("2006-01-02 15:04:05")
|
timeStr := time.Now().Format("2006-01-02 15:04:05")
|
||||||
if user == nil {
|
if user == nil {
|
||||||
job.NewStatsNotifyJob().UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0)
|
a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0)
|
||||||
logger.Infof("wrong username or password: \"%s\" \"%s\"", form.Username, form.Password)
|
logger.Infof("wrong username or password: \"%s\" \"%s\"", form.Username, form.Password)
|
||||||
pureJsonMsg(c, false, I18n(c, "pages.login.toasts.wrongUsernameOrPassword"))
|
pureJsonMsg(c, false, I18n(c, "pages.login.toasts.wrongUsernameOrPassword"))
|
||||||
return
|
return
|
||||||
} else {
|
} 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))
|
||||||
job.NewStatsNotifyJob().UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 1)
|
a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = session.SetLoginUser(c, user)
|
err = session.SetLoginUser(c, user)
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"time"
|
"time"
|
||||||
"x-ui/web/global"
|
"x-ui/web/global"
|
||||||
"x-ui/web/service"
|
"x-ui/web/service"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ServerController struct {
|
type ServerController struct {
|
||||||
@@ -37,6 +38,7 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
|
|||||||
g.POST("/stopXrayService", a.stopXrayService)
|
g.POST("/stopXrayService", a.stopXrayService)
|
||||||
g.POST("/restartXrayService", a.restartXrayService)
|
g.POST("/restartXrayService", a.restartXrayService)
|
||||||
g.POST("/installXray/:version", a.installXray)
|
g.POST("/installXray/:version", a.installXray)
|
||||||
|
g.POST("/logs/:count", a.getLogs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ServerController) refreshStatus() {
|
func (a *ServerController) refreshStatus() {
|
||||||
@@ -87,13 +89,13 @@ func (a *ServerController) installXray(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *ServerController) stopXrayService(c *gin.Context) {
|
func (a *ServerController) stopXrayService(c *gin.Context) {
|
||||||
a.lastGetStatusTime = time.Now()
|
a.lastGetStatusTime = time.Now()
|
||||||
err := a.serverService.StopXrayService()
|
err := a.serverService.StopXrayService()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonMsg(c, "", err)
|
jsonMsg(c, "", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
jsonMsg(c, "Xray stoped",err)
|
jsonMsg(c, "Xray stoped", err)
|
||||||
|
|
||||||
}
|
}
|
||||||
func (a *ServerController) restartXrayService(c *gin.Context) {
|
func (a *ServerController) restartXrayService(c *gin.Context) {
|
||||||
@@ -102,6 +104,16 @@ func (a *ServerController) restartXrayService(c *gin.Context) {
|
|||||||
jsonMsg(c, "", err)
|
jsonMsg(c, "", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
jsonMsg(c, "Xray restarted",err)
|
jsonMsg(c, "Xray restarted", err)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *ServerController) getLogs(c *gin.Context) {
|
||||||
|
count := c.Param("count")
|
||||||
|
logs, err := a.serverService.GetLogs(count)
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, I18n(c, "getLogs"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonObj(c, logs, nil)
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"time"
|
"time"
|
||||||
"x-ui/web/entity"
|
"x-ui/web/entity"
|
||||||
"x-ui/web/service"
|
"x-ui/web/service"
|
||||||
"x-ui/web/session"
|
"x-ui/web/session"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type updateUserForm struct {
|
type updateUserForm struct {
|
||||||
@@ -32,6 +33,7 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) {
|
|||||||
g = g.Group("/setting")
|
g = g.Group("/setting")
|
||||||
|
|
||||||
g.POST("/all", a.getAllSetting)
|
g.POST("/all", a.getAllSetting)
|
||||||
|
g.POST("/defaultSettings", a.getDefaultSettings)
|
||||||
g.POST("/update", a.updateSetting)
|
g.POST("/update", a.updateSetting)
|
||||||
g.POST("/updateUser", a.updateUser)
|
g.POST("/updateUser", a.updateUser)
|
||||||
g.POST("/restartPanel", a.restartPanel)
|
g.POST("/restartPanel", a.restartPanel)
|
||||||
@@ -46,6 +48,36 @@ func (a *SettingController) getAllSetting(c *gin.Context) {
|
|||||||
jsonObj(c, allSetting, nil)
|
jsonObj(c, allSetting, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *SettingController) getDefaultSettings(c *gin.Context) {
|
||||||
|
expireDiff, err := a.settingService.GetExpireDiff()
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, I18n(c, "pages.setting.toasts.getSetting"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
trafficDiff, err := a.settingService.GetTrafficDiff()
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, I18n(c, "pages.setting.toasts.getSetting"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defaultCert, err := a.settingService.GetCertFile()
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, I18n(c, "pages.setting.toasts.getSetting"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defaultKey, err := a.settingService.GetKeyFile()
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, I18n(c, "pages.setting.toasts.getSetting"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"expireDiff": expireDiff,
|
||||||
|
"trafficDiff": trafficDiff,
|
||||||
|
"defaultCert": defaultCert,
|
||||||
|
"defaultKey": defaultKey,
|
||||||
|
}
|
||||||
|
jsonObj(c, result, nil)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *SettingController) updateSetting(c *gin.Context) {
|
func (a *SettingController) updateSetting(c *gin.Context) {
|
||||||
allSetting := &entity.AllSetting{}
|
allSetting := &entity.AllSetting{}
|
||||||
err := c.ShouldBind(allSetting)
|
err := c.ShouldBind(allSetting)
|
||||||
|
|||||||
42
web/controller/sub.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"strings"
|
||||||
|
"x-ui/web/service"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SUBController struct {
|
||||||
|
BaseController
|
||||||
|
|
||||||
|
subService service.SubService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSUBController(g *gin.RouterGroup) *SUBController {
|
||||||
|
a := &SUBController{}
|
||||||
|
a.initRouter(g)
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SUBController) initRouter(g *gin.RouterGroup) {
|
||||||
|
g = g.Group("/sub")
|
||||||
|
|
||||||
|
g.GET("/:subid", a.subs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SUBController) subs(c *gin.Context) {
|
||||||
|
subId := c.Param("subid")
|
||||||
|
host := strings.Split(c.Request.Host, ":")[0]
|
||||||
|
subs, err := a.subService.GetSubs(subId, host)
|
||||||
|
if err != nil {
|
||||||
|
c.String(400, "Error!")
|
||||||
|
} else {
|
||||||
|
result := ""
|
||||||
|
for _, sub := range subs {
|
||||||
|
result += sub + "\n"
|
||||||
|
}
|
||||||
|
c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,13 +32,16 @@ type AllSetting struct {
|
|||||||
WebCertFile string `json:"webCertFile" form:"webCertFile"`
|
WebCertFile string `json:"webCertFile" form:"webCertFile"`
|
||||||
WebKeyFile string `json:"webKeyFile" form:"webKeyFile"`
|
WebKeyFile string `json:"webKeyFile" form:"webKeyFile"`
|
||||||
WebBasePath string `json:"webBasePath" form:"webBasePath"`
|
WebBasePath string `json:"webBasePath" form:"webBasePath"`
|
||||||
|
ExpireDiff int `json:"expireDiff" form:"expireDiff"`
|
||||||
|
TrafficDiff int `json:"trafficDiff" form:"trafficDiff"`
|
||||||
TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"`
|
TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"`
|
||||||
TgBotToken string `json:"tgBotToken" form:"tgBotToken"`
|
TgBotToken string `json:"tgBotToken" form:"tgBotToken"`
|
||||||
TgBotChatId int `json:"tgBotChatId" form:"tgBotChatId"`
|
TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"`
|
||||||
TgRunTime string `json:"tgRunTime" form:"tgRunTime"`
|
TgRunTime string `json:"tgRunTime" form:"tgRunTime"`
|
||||||
|
TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"`
|
||||||
|
TgCpu int `json:"tgCpu" form:"tgCpu"`
|
||||||
XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"`
|
XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"`
|
||||||
|
TimeLocation string `json:"timeLocation" form:"timeLocation"`
|
||||||
TimeLocation string `json:"timeLocation" form:"timeLocation"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AllSetting) CheckValid() error {
|
func (s *AllSetting) CheckValid() error {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<link rel="stylesheet" href="{{ .base_path }}assets/ant-design-vue@1.7.2/antd.min.css">
|
<link rel="stylesheet" href="{{ .base_path }}assets/ant-design-vue@1.7.2/antd.min.css">
|
||||||
<link rel="stylesheet" href="{{ .base_path }}assets/element-ui@2.15.0/theme-chalk/display.css">
|
<link rel="stylesheet" href="{{ .base_path }}assets/element-ui@2.15.0/theme-chalk/display.css">
|
||||||
<link rel="stylesheet" href="{{ .base_path }}assets/css/custom.css?{{ .cur_ver }}">
|
<link rel="stylesheet" href="{{ .base_path }}assets/css/custom.css?{{ .cur_ver }}">
|
||||||
|
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
|
||||||
<style>
|
<style>
|
||||||
[v-cloak] {
|
[v-cloak] {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{{define "promptModal"}}
|
{{define "promptModal"}}
|
||||||
<a-modal id="prompt-modal" v-model="promptModal.visible" :title="promptModal.title"
|
<a-modal id="prompt-modal" v-model="promptModal.visible" :title="promptModal.title"
|
||||||
:closable="true" @ok="promptModal.ok" :mask-closable="false"
|
:closable="true" @ok="promptModal.ok" :mask-closable="false"
|
||||||
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||||
:ok-text="promptModal.okText" cancel-text='{{ i18n "cancel" }}'>
|
:ok-text="promptModal.okText" cancel-text='{{ i18n "cancel" }}'>
|
||||||
<a-input id="prompt-modal-input" :type="promptModal.type"
|
<a-input id="prompt-modal-input" :type="promptModal.type"
|
||||||
v-model="promptModal.value"
|
v-model="promptModal.value"
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
{{define "qrcodeModal"}}
|
{{define "qrcodeModal"}}
|
||||||
<a-modal id="qrcode-modal" v-model="qrModal.visible" :title="qrModal.title"
|
<a-modal id="qrcode-modal" v-model="qrModal.visible" :title="qrModal.title"
|
||||||
:closable="true" width="300px" :ok-text="qrModal.okText"
|
:closable="true"
|
||||||
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||||
cancel-text='{{ i18n "close" }}' :ok-button-props="{attrs:{id:'qr-modal-ok-btn'}}">
|
:footer="null"
|
||||||
<canvas id="qrCode" style="width: 100%; height: 100%;"></canvas>
|
width="300px">
|
||||||
|
<a-tag color="green" style="margin-bottom: 10px;display: block;text-align: center;" >{{ i18n "pages.inbounds.clickOnQRcode" }}</a-tag>
|
||||||
|
<canvas @click="copyToClipboard()" id="qrCode" style="width: 100%; height: 100%;"></canvas>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -13,17 +15,15 @@
|
|||||||
content: '',
|
content: '',
|
||||||
inbound: new Inbound(),
|
inbound: new Inbound(),
|
||||||
dbInbound: new DBInbound(),
|
dbInbound: new DBInbound(),
|
||||||
okText: '',
|
|
||||||
copyText: '',
|
copyText: '',
|
||||||
qrcode: null,
|
qrcode: null,
|
||||||
clipboard: null,
|
clipboard: null,
|
||||||
visible: false,
|
visible: false,
|
||||||
show: function (title='', content='', dbInbound=new DBInbound(),okText='{{ i18n "copy" }}', copyText='') {
|
show: function (title='', content='', dbInbound=new DBInbound(), copyText='') {
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.content = content;
|
this.content = content;
|
||||||
this.dbInbound = dbInbound;
|
this.dbInbound = dbInbound;
|
||||||
this.inbound = dbInbound.toInbound();
|
this.inbound = dbInbound.toInbound();
|
||||||
this.okText = okText;
|
|
||||||
if (ObjectUtil.isEmpty(copyText)) {
|
if (ObjectUtil.isEmpty(copyText)) {
|
||||||
this.copyText = content;
|
this.copyText = content;
|
||||||
} else {
|
} else {
|
||||||
@@ -31,12 +31,6 @@
|
|||||||
}
|
}
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
qrModalApp.$nextTick(() => {
|
qrModalApp.$nextTick(() => {
|
||||||
if (this.clipboard === null) {
|
|
||||||
this.clipboard = new ClipboardJS('#qr-modal-ok-btn', {
|
|
||||||
text: () => this.copyText,
|
|
||||||
});
|
|
||||||
this.clipboard.on('success', () => app.$message.success('{{ i18n "copied" }}'));
|
|
||||||
}
|
|
||||||
if (this.qrcode === null) {
|
if (this.qrcode === null) {
|
||||||
this.qrcode = new QRious({
|
this.qrcode = new QRious({
|
||||||
element: document.querySelector('#qrCode'),
|
element: document.querySelector('#qrCode'),
|
||||||
@@ -58,6 +52,17 @@
|
|||||||
data: {
|
data: {
|
||||||
qrModal: qrModal,
|
qrModal: qrModal,
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
copyToClipboard() {
|
||||||
|
this.qrModal.clipboard = new ClipboardJS('#qrCode', {
|
||||||
|
text: () => this.qrModal.copyText,
|
||||||
|
});
|
||||||
|
this.qrModal.clipboard.on('success', () => {
|
||||||
|
app.$message.success('{{ i18n "copied" }}')
|
||||||
|
this.qrModal.clipboard.destroy();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{{define "textModal"}}
|
{{define "textModal"}}
|
||||||
<a-modal id="text-modal" v-model="txtModal.visible" :title="txtModal.title"
|
<a-modal id="text-modal" v-model="txtModal.visible" :title="txtModal.title"
|
||||||
:closable="true" ok-text='{{ i18n "copy" }}' cancel-text='{{ i18n "close" }}'
|
:closable="true" ok-text='{{ i18n "copy" }}' cancel-text='{{ i18n "close" }}'
|
||||||
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||||
:ok-button-props="{attrs:{id:'txt-modal-ok-btn'}}">
|
:ok-button-props="{attrs:{id:'txt-modal-ok-btn'}}">
|
||||||
<a-button v-if="!ObjectUtil.isEmpty(txtModal.fileName)" type="primary" style="margin-bottom: 10px;"
|
<a-button v-if="!ObjectUtil.isEmpty(txtModal.fileName)" type="primary" style="margin-bottom: 10px;"
|
||||||
:href="'data:application/text;charset=utf-8,' + encodeURIComponent(txtModal.content)" :download="txtModal.fileName">
|
:href="'data:application/text;charset=utf-8,' + encodeURIComponent(txtModal.content)" :download="txtModal.fileName">
|
||||||
@@ -32,7 +32,6 @@
|
|||||||
});
|
});
|
||||||
this.clipboard.on('success', () => app.$message.success('{{ i18n "copied" }}'));
|
this.clipboard.on('success', () => app.$message.success('{{ i18n "copied" }}'));
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
close: function () {
|
close: function () {
|
||||||
@@ -41,7 +40,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const textModalApp = new Vue({
|
const textModalApp = new Vue({
|
||||||
delimiters: ['[[', ']]'],
|
delimiters: ['[[', ']]'],
|
||||||
el: '#text-modal',
|
el: '#text-modal',
|
||||||
data: {
|
data: {
|
||||||
txtModal: txtModal,
|
txtModal: txtModal,
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
<a-layout-content>
|
<a-layout-content>
|
||||||
<a-row type="flex" justify="center">
|
<a-row type="flex" justify="center">
|
||||||
<a-col :xs="22" :sm="20" :md="16" :lg="12" :xl="8">
|
<a-col :xs="22" :sm="20" :md="16" :lg="12" :xl="8">
|
||||||
<h1>3x-ui {{ i18n "pages.login.title" }}</h1>
|
<h1>{{ i18n "pages.login.title" }}</h1>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
<a-row type="flex" justify="center">
|
<a-row type="flex" justify="center">
|
||||||
@@ -63,25 +63,21 @@
|
|||||||
<a-form-item>
|
<a-form-item>
|
||||||
|
|
||||||
<a-row justify="center" class="selectLang">
|
<a-row justify="center" class="selectLang">
|
||||||
<a-col :span="4"><span>Language : </span></a-col>
|
<a-col :span="5"><span>Language :</span></a-col>
|
||||||
|
|
||||||
<a-col :span="6">
|
<a-col :span="7">
|
||||||
<a-select
|
<a-select
|
||||||
ref="selectLang"
|
ref="selectLang"
|
||||||
v-model="lang"
|
v-model="lang"
|
||||||
@change="setLang(lang)"
|
@change="setLang(lang)"
|
||||||
>
|
>
|
||||||
<a-select-option :value="l.value" label="China" v-for="l in supportLangs" >
|
<a-select-option :value="l.value" label="English" v-for="l in supportLangs" >
|
||||||
<span role="img" aria-label="l.name" v-text="l.icon"></span>
|
<span role="img" aria-label="l.name" v-text="l.icon"></span>
|
||||||
<span v-text="l.name"></span>
|
<span v-text="l.name"></span>
|
||||||
</a-select-option>
|
</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-col>
|
</a-col>
|
||||||
|
|
||||||
</a-row>
|
</a-row>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
</a-col>
|
</a-col>
|
||||||
|
|||||||
184
web/html/xui/client_bulk_modal.html
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
{{define "clientsBulkModal"}}
|
||||||
|
<a-modal id="client-bulk-modal" v-model="clientsBulkModal.visible" :title="clientsBulkModal.title" @ok="clientsBulkModal.ok"
|
||||||
|
:confirm-loading="clientsBulkModal.confirmLoading" :closable="true" :mask-closable="false"
|
||||||
|
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||||
|
:ok-text="clientsBulkModal.okText" cancel-text='{{ i18n "close" }}'>
|
||||||
|
<a-form layout="inline">
|
||||||
|
<a-form-item label='{{ i18n "pages.client.method" }}'>
|
||||||
|
<a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid" style="width: 350px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||||
|
<a-select-option :value="0">Random</a-select-option>
|
||||||
|
<a-select-option :value="1">Random+Prefix</a-select-option>
|
||||||
|
<a-select-option :value="2">Random+Prefix+Num</a-select-option>
|
||||||
|
<a-select-option :value="3">Random+Prefix+Num+Postfix</a-select-option>
|
||||||
|
<a-select-option :value="4">Prefix+Num+Postfix [ BE CAREFUL! ]</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item><br />
|
||||||
|
<a-form-item v-if="clientsBulkModal.emailMethod>1">
|
||||||
|
<span slot="label">{{ i18n "pages.client.first" }}</span>
|
||||||
|
<a-input-number v-model="clientsBulkModal.firstNum" :min="1"></a-input-number>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item v-if="clientsBulkModal.emailMethod>1">
|
||||||
|
<span slot="label">{{ i18n "pages.client.last" }}</span>
|
||||||
|
<a-input-number v-model="clientsBulkModal.lastNum" :min="clientsBulkModal.firstNum"></a-input-number>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item v-if="clientsBulkModal.emailMethod>0">
|
||||||
|
<span slot="label">{{ i18n "pages.client.prefix" }}</span>
|
||||||
|
<a-input v-model="clientsBulkModal.emailPrefix" style="width: 120px"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item v-if="clientsBulkModal.emailMethod>2">
|
||||||
|
<span slot="label">{{ i18n "pages.client.postfix" }}</span>
|
||||||
|
<a-input v-model="clientsBulkModal.emailPostfix" style="width: 120px"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item v-if="clientsBulkModal.emailMethod < 2">
|
||||||
|
<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 label="Subscription">
|
||||||
|
<a-input v-model.trim="clientsBulkModal.subId"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Telegram ID">
|
||||||
|
<a-input v-model.trim="clientsBulkModal.tgId"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item>
|
||||||
|
<span slot="label">
|
||||||
|
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">
|
||||||
|
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
||||||
|
</template>
|
||||||
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</span>
|
||||||
|
<a-input-number v-model="clientsBulkModal.totalGB" :min="0"></a-input-number>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="{{ i18n "pages.client.delayedStart" }}">
|
||||||
|
<a-switch v-model="clientsBulkModal.delayedStart" @click="clientsBulkModal.expiryTime=0"></a-switch>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="{{ i18n "pages.client.expireDays" }}" v-if="clientsBulkModal.delayedStart">
|
||||||
|
<a-input type="number" v-model.number="delayedExpireDays" :min="0"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item v-else>
|
||||||
|
<span slot="label">
|
||||||
|
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">
|
||||||
|
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
||||||
|
</template>
|
||||||
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</span>
|
||||||
|
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
||||||
|
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
|
||||||
|
v-model="clientsBulkModal.expiryTime" style="width: 300px;"></a-date-picker>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-modal>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
const clientsBulkModal = {
|
||||||
|
visible: false,
|
||||||
|
confirmLoading: false,
|
||||||
|
title: '',
|
||||||
|
okText: '',
|
||||||
|
confirm: null,
|
||||||
|
dbInbound: new DBInbound(),
|
||||||
|
inbound: new Inbound(),
|
||||||
|
clients: [],
|
||||||
|
quantity: 1,
|
||||||
|
totalGB: 0,
|
||||||
|
expiryTime: '',
|
||||||
|
emailMethod: 0,
|
||||||
|
firstNum: 1,
|
||||||
|
lastNum: 1,
|
||||||
|
emailPrefix: "",
|
||||||
|
emailPostfix: "",
|
||||||
|
subId: "",
|
||||||
|
tgId: "",
|
||||||
|
delayedStart: false,
|
||||||
|
ok() {
|
||||||
|
method=clientsBulkModal.emailMethod;
|
||||||
|
if(method>1){
|
||||||
|
start=clientsBulkModal.firstNum;
|
||||||
|
end=clientsBulkModal.lastNum + 1;
|
||||||
|
} else {
|
||||||
|
start=0;
|
||||||
|
end=clientsBulkModal.quantity;
|
||||||
|
}
|
||||||
|
prefix = (method>0 && clientsBulkModal.emailPrefix.length>0) ? clientsBulkModal.emailPrefix : "";
|
||||||
|
useNum=(method>1);
|
||||||
|
postfix = (method>2 && clientsBulkModal.emailPostfix.length>0) ? clientsBulkModal.emailPostfix : "";
|
||||||
|
for (let i = start; i < end; i++) {
|
||||||
|
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;
|
||||||
|
newClient._totalGB = clientsBulkModal.totalGB;
|
||||||
|
newClient._expiryTime = clientsBulkModal.expiryTime;
|
||||||
|
clientsBulkModal.clients.push(newClient);
|
||||||
|
}
|
||||||
|
ObjectUtil.execute(clientsBulkModal.confirm, clientsBulkModal.inbound, clientsBulkModal.dbInbound);
|
||||||
|
},
|
||||||
|
show({ title='', okText='{{ i18n "sure" }}', dbInbound=null, confirm=(inbound, dbInbound)=>{} }) {
|
||||||
|
this.visible = true;
|
||||||
|
this.title = title;
|
||||||
|
this.okText = okText;
|
||||||
|
this.confirm = confirm;
|
||||||
|
this.quantity = 1;
|
||||||
|
this.totalGB = 0;
|
||||||
|
this.expiryTime = 0;
|
||||||
|
this.emailMethod= 0;
|
||||||
|
this.firstNum= 1;
|
||||||
|
this.lastNum= 1;
|
||||||
|
this.emailPrefix= "";
|
||||||
|
this.emailPostfix= "";
|
||||||
|
this.subId= "";
|
||||||
|
this.tgId= "";
|
||||||
|
this.dbInbound = new DBInbound(dbInbound);
|
||||||
|
this.inbound = dbInbound.toInbound();
|
||||||
|
this.clients = this.getClients(this.inbound.protocol, this.inbound.settings);
|
||||||
|
this.delayedStart = false;
|
||||||
|
},
|
||||||
|
getClients(protocol, clientSettings) {
|
||||||
|
switch(protocol){
|
||||||
|
case Protocols.VMESS: return clientSettings.vmesses;
|
||||||
|
case Protocols.VLESS: return clientSettings.vlesses;
|
||||||
|
case Protocols.TROJAN: return clientSettings.trojans;
|
||||||
|
default: return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
newClient(protocol) {
|
||||||
|
switch (protocol) {
|
||||||
|
case Protocols.VMESS: return new Inbound.VmessSettings.Vmess();
|
||||||
|
case Protocols.VLESS: return new Inbound.VLESSSettings.VLESS();
|
||||||
|
case Protocols.TROJAN: return new Inbound.TrojanSettings.Trojan();
|
||||||
|
default: return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
clientsBulkModal.visible = false;
|
||||||
|
clientsBulkModal.loading(false);
|
||||||
|
},
|
||||||
|
loading(loading) {
|
||||||
|
clientsBulkModal.confirmLoading = loading;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const clientsBulkModalApp = new Vue({
|
||||||
|
delimiters: ['[[', ']]'],
|
||||||
|
el: '#client-bulk-modal',
|
||||||
|
data: {
|
||||||
|
clientsBulkModal,
|
||||||
|
get inbound() {
|
||||||
|
return this.clientsBulkModal.inbound;
|
||||||
|
},
|
||||||
|
get delayedExpireDays() {
|
||||||
|
return this.clientsBulkModal.expiryTime < 0 ? this.clientsBulkModal.expiryTime / -86400000 : 0;
|
||||||
|
},
|
||||||
|
set delayedExpireDays(days){
|
||||||
|
this.clientsBulkModal.expiryTime = -86400000 * days;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{{end}}
|
||||||
145
web/html/xui/client_modal.html
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
{{define "clientsModal"}}
|
||||||
|
<a-modal id="client-modal" v-model="clientModal.visible" :title="clientModal.title" @ok="clientModal.ok"
|
||||||
|
:confirm-loading="clientModal.confirmLoading" :closable="true" :mask-closable="false"
|
||||||
|
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||||
|
:ok-text="clientModal.okText" cancel-text='{{ i18n "close" }}'>
|
||||||
|
{{template "form/client"}}
|
||||||
|
</a-modal>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
const clientModal = {
|
||||||
|
visible: false,
|
||||||
|
confirmLoading: false,
|
||||||
|
title: '',
|
||||||
|
okText: '',
|
||||||
|
dbInbound: new DBInbound(),
|
||||||
|
inbound: new Inbound(),
|
||||||
|
clients: [],
|
||||||
|
clientStats: [],
|
||||||
|
index: null,
|
||||||
|
clientIps: null,
|
||||||
|
isExpired: false,
|
||||||
|
delayedStart: false,
|
||||||
|
ok() {
|
||||||
|
ObjectUtil.execute(clientModal.confirm, clientModal.inbound, clientModal.dbInbound, clientModal.index);
|
||||||
|
},
|
||||||
|
show({ title='', okText='{{ i18n "sure" }}', index=null, dbInbound=null, confirm=(index, dbInbound)=>{}, isEdit=false }) {
|
||||||
|
this.visible = true;
|
||||||
|
this.title = title;
|
||||||
|
this.okText = okText;
|
||||||
|
this.isEdit = isEdit;
|
||||||
|
this.dbInbound = new DBInbound(dbInbound);
|
||||||
|
this.inbound = dbInbound.toInbound();
|
||||||
|
this.clients = this.getClients(this.inbound.protocol, this.inbound.settings);
|
||||||
|
this.index = index === null ? this.clients.length : index;
|
||||||
|
this.isExpired = isEdit ? this.inbound.isExpiry(this.index) : false;
|
||||||
|
this.delayedStart = false;
|
||||||
|
if (!isEdit){
|
||||||
|
this.addClient(this.inbound.protocol, this.clients);
|
||||||
|
} else {
|
||||||
|
if (this.clients[index].expiryTime < 0){
|
||||||
|
this.delayedStart = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.clientStats = this.dbInbound.clientStats.find(row => row.email === this.clients[this.index].email);
|
||||||
|
this.confirm = confirm;
|
||||||
|
},
|
||||||
|
getClients(protocol, clientSettings) {
|
||||||
|
switch(protocol){
|
||||||
|
case Protocols.VMESS: return clientSettings.vmesses;
|
||||||
|
case Protocols.VLESS: return clientSettings.vlesses;
|
||||||
|
case Protocols.TROJAN: return clientSettings.trojans;
|
||||||
|
default: return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addClient(protocol, clients) {
|
||||||
|
switch (protocol) {
|
||||||
|
case Protocols.VMESS: return clients.push(new Inbound.VmessSettings.Vmess());
|
||||||
|
case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS());
|
||||||
|
case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan());
|
||||||
|
default: return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
clientModal.visible = false;
|
||||||
|
clientModal.loading(false);
|
||||||
|
},
|
||||||
|
loading(loading) {
|
||||||
|
clientModal.confirmLoading = loading;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const clientModalApp = new Vue({
|
||||||
|
delimiters: ['[[', ']]'],
|
||||||
|
el: '#client-modal',
|
||||||
|
data: {
|
||||||
|
clientModal,
|
||||||
|
get inbound() {
|
||||||
|
return this.clientModal.inbound;
|
||||||
|
},
|
||||||
|
get client() {
|
||||||
|
return this.clientModal.clients[this.clientModal.index];
|
||||||
|
},
|
||||||
|
get clientStats() {
|
||||||
|
return this.clientModal.clientStats;
|
||||||
|
},
|
||||||
|
get isEdit() {
|
||||||
|
return this.clientModal.isEdit;
|
||||||
|
},
|
||||||
|
get isTrafficExhausted() {
|
||||||
|
if(!clientStats) return false
|
||||||
|
if(clientStats.total <= 0) return false
|
||||||
|
if(clientStats.up + clientStats.down < clientStats.total) return false
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
get isExpiry() {
|
||||||
|
return this.clientModal.isExpired
|
||||||
|
},
|
||||||
|
get statsColor() {
|
||||||
|
if(!clientStats) return 'blue'
|
||||||
|
if(clientStats.total <= 0) return 'blue'
|
||||||
|
else if(clientStats.total > 0 && (clientStats.down+clientStats.up) < clientStats.total) return 'cyan'
|
||||||
|
else return 'red'
|
||||||
|
},
|
||||||
|
get delayedExpireDays() {
|
||||||
|
return this.client && this.client.expiryTime < 0 ? this.client.expiryTime / -86400000 : 0;
|
||||||
|
},
|
||||||
|
set delayedExpireDays(days){
|
||||||
|
this.client.expiryTime = -86400000 * days;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getNewEmail(client) {
|
||||||
|
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
|
||||||
|
var string = '';
|
||||||
|
var len = 6 + Math.floor(Math.random() * 5);
|
||||||
|
for(var ii=0; ii<len; ii++){
|
||||||
|
string += chars[Math.floor(Math.random() * chars.length)];
|
||||||
|
}
|
||||||
|
client.email = string;
|
||||||
|
},
|
||||||
|
async getDBClientIps(email,event) {
|
||||||
|
const msg = await HttpUtil.post('/xui/inbound/clientIps/'+ email);
|
||||||
|
if (!msg.success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
ips = JSON.parse(msg.obj)
|
||||||
|
ips = ips.join(",")
|
||||||
|
event.target.value = ips
|
||||||
|
} catch (error) {
|
||||||
|
// text
|
||||||
|
event.target.value = msg.obj
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async clearDBClientIps(email) {
|
||||||
|
const msg = await HttpUtil.post('/xui/inbound/clearClientIps/'+ email);
|
||||||
|
if (!msg.success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.getElementById("clientIPs").value = ""
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{{end}}
|
||||||
@@ -13,22 +13,8 @@
|
|||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
<!--<a-menu-item key="{{ .base_path }}xui/clients">-->
|
<!--<a-menu-item key="{{ .base_path }}xui/clients">-->
|
||||||
<!-- <a-icon type="laptop"></a-icon>-->
|
<!-- <a-icon type="laptop"></a-icon>-->
|
||||||
<!-- <span>client</span>-->
|
<!-- <span>Client</span>-->
|
||||||
<!--</a-menu-item>-->
|
<!--</a-menu-item>-->
|
||||||
<a-sub-menu>
|
|
||||||
<template slot="title">
|
|
||||||
<a-icon type="link"></a-icon>
|
|
||||||
<span>{{ i18n "menu.link"}}</span>
|
|
||||||
</template>
|
|
||||||
<a-menu-item key="https://github.com/mhsanaei/3x-ui/">
|
|
||||||
<a-icon type="github"></a-icon>
|
|
||||||
<span>Github</span>
|
|
||||||
</a-menu-item>
|
|
||||||
<a-menu-item key="https://t.me/panel3xui">
|
|
||||||
<a-icon type="usergroup-add"></a-icon>
|
|
||||||
<span>Telegram</span>
|
|
||||||
</a-menu-item>
|
|
||||||
</a-sub-menu>
|
|
||||||
<a-menu-item key="{{ .base_path }}logout">
|
<a-menu-item key="{{ .base_path }}logout">
|
||||||
<a-icon type="logout"></a-icon>
|
<a-icon type="logout"></a-icon>
|
||||||
<span>{{ i18n "menu.logout"}}</span>
|
<span>{{ i18n "menu.logout"}}</span>
|
||||||
@@ -41,7 +27,7 @@
|
|||||||
<a-menu :theme="siderDrawer.theme" mode="inline" selected-keys="">
|
<a-menu :theme="siderDrawer.theme" mode="inline" selected-keys="">
|
||||||
<a-menu-item mode="inline">
|
<a-menu-item mode="inline">
|
||||||
<a-icon type="bg-colors"></a-icon>
|
<a-icon type="bg-colors"></a-icon>
|
||||||
<a-switch size="small" :default-checked="siderDrawer.isDarkTheme"
|
<a-switch :default-checked="siderDrawer.isDarkTheme"
|
||||||
checked-children="☀"
|
checked-children="☀"
|
||||||
un-checked-children="🌙"
|
un-checked-children="🌙"
|
||||||
@change="siderDrawer.changeTheme()"></a-switch>
|
@change="siderDrawer.changeTheme()"></a-switch>
|
||||||
@@ -55,11 +41,12 @@
|
|||||||
<a-drawer id="sider-drawer" placement="left" :closable="false"
|
<a-drawer id="sider-drawer" placement="left" :closable="false"
|
||||||
@close="siderDrawer.close()"
|
@close="siderDrawer.close()"
|
||||||
:visible="siderDrawer.visible"
|
:visible="siderDrawer.visible"
|
||||||
|
:wrap-class-name="siderDrawer.isDarkTheme ? 'ant-drawer-dark' : ''"
|
||||||
:wrap-style="{ padding: 0 }">
|
:wrap-style="{ padding: 0 }">
|
||||||
<div class="drawer-handle" @click="siderDrawer.change()" slot="handle">
|
<div class="drawer-handle" @click="siderDrawer.change()" slot="handle">
|
||||||
<a-icon :type="siderDrawer.visible ? 'close' : 'menu-fold'"></a-icon>
|
<a-icon :type="siderDrawer.visible ? 'close' : 'menu-fold'"></a-icon>
|
||||||
</div>
|
</div>
|
||||||
<a-menu mode="inline" selected-keys="">
|
<a-menu :theme="siderDrawer.theme" mode="inline" selected-keys="">
|
||||||
<a-menu-item mode="inline">
|
<a-menu-item mode="inline">
|
||||||
<a-icon type="bg-colors"></a-icon>
|
<a-icon type="bg-colors"></a-icon>
|
||||||
<a-switch :default-checked="siderDrawer.isDarkTheme"
|
<a-switch :default-checked="siderDrawer.isDarkTheme"
|
||||||
@@ -68,19 +55,17 @@
|
|||||||
@change="siderDrawer.changeTheme()"></a-switch>
|
@change="siderDrawer.changeTheme()"></a-switch>
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
</a-menu>
|
</a-menu>
|
||||||
<a-menu mode="inline" :selected-keys="['{{ .request_uri }}']"
|
<a-menu :theme="siderDrawer.theme" mode="inline" :selected-keys="['{{ .request_uri }}']"
|
||||||
@click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key">
|
@click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key">
|
||||||
{{template "menuItems" .}}
|
{{template "menuItems" .}}
|
||||||
</a-menu>
|
</a-menu>
|
||||||
</a-drawer>
|
</a-drawer>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
|
|
||||||
const darkClass = "ant-card-dark";
|
const darkClass = "ant-card-dark";
|
||||||
const bgDarkStyle = "background-color: #242c3a";
|
const bgDarkStyle = "background-color: #242c3a";
|
||||||
const siderDrawer = {
|
const siderDrawer = {
|
||||||
visible: false,
|
visible: false,
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
isDarkTheme: localStorage.getItem("dark-mode") === 'true' ? true : false,
|
isDarkTheme: localStorage.getItem("dark-mode") === 'true' ? true : false,
|
||||||
show() {
|
show() {
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
@@ -90,7 +75,7 @@
|
|||||||
},
|
},
|
||||||
change() {
|
change() {
|
||||||
this.visible = !this.visible;
|
this.visible = !this.visible;
|
||||||
},
|
},
|
||||||
toggleCollapsed() {
|
toggleCollapsed() {
|
||||||
this.collapsed = !this.collapsed;
|
this.collapsed = !this.collapsed;
|
||||||
},
|
},
|
||||||
|
|||||||
125
web/html/xui/form/client.html
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
{{define "form/client"}}
|
||||||
|
<a-form layout="inline" v-if="client">
|
||||||
|
<template v-if="isEdit">
|
||||||
|
<a-tag v-if="isExpiry || isTrafficExhausted" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag>
|
||||||
|
</template>
|
||||||
|
<a-form-item>
|
||||||
|
<span slot="label">
|
||||||
|
<span>{{ i18n "pages.inbounds.Email" }}</span>
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">
|
||||||
|
<span>{{ i18n "pages.inbounds.EmailDesc" }}</span>
|
||||||
|
</template>
|
||||||
|
<a-icon type="sync" @click="getNewEmail(client)"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</span>
|
||||||
|
<a-input v-model.trim="client.email" style="width: 150px;" ></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="{{ i18n "pages.inbounds.enable" }}">
|
||||||
|
<a-switch v-model="client.enable"></a-switch>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Password" v-if="inbound.protocol === Protocols.TROJAN">
|
||||||
|
<a-input v-model.trim="client.password" style="width: 150px;" ></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="ID" v-if="inbound.protocol === Protocols.VMESS || inbound.protocol === Protocols.VLESS">
|
||||||
|
<a-input v-model.trim="client.id" style="width: 300px;"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label='{{ i18n "additional" }} ID' v-if="inbound.protocol === Protocols.VMESS">
|
||||||
|
<a-input type="number" v-model.number="client.alterId" style="width: 70px;"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Subscription" v-if="client.email">
|
||||||
|
<a-input v-model.trim="client.subId"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Telegram Username" v-if="client.email">
|
||||||
|
<a-input v-model.trim="client.tgId"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item>
|
||||||
|
<span slot="label">
|
||||||
|
<span>{{ i18n "pages.inbounds.IPLimit" }}</span>
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">
|
||||||
|
<span>{{ i18n "pages.inbounds.IPLimitDesc" }}</span>
|
||||||
|
</template>
|
||||||
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</span>
|
||||||
|
<a-input type="number" v-model.number="client.limitIp" min="0" style="width: 70px;" ></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item v-if="client.email && client.limitIp > 0 && isEdit">
|
||||||
|
<span slot="label">
|
||||||
|
<span>{{ i18n "pages.inbounds.IPLimitlog" }}</span>
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">
|
||||||
|
<span>{{ i18n "pages.inbounds.IPLimitlogDesc" }}</span>
|
||||||
|
</template>
|
||||||
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">
|
||||||
|
<span>{{ i18n "pages.inbounds.IPLimitlogclear" }}</span>
|
||||||
|
</template>
|
||||||
|
<span style="color: #FF4D4F">
|
||||||
|
<a-icon type="delete" @click="clearDBClientIps(client.email)"></a-icon>
|
||||||
|
</span>
|
||||||
|
</a-tooltip>
|
||||||
|
</span>
|
||||||
|
<a-form layout="block">
|
||||||
|
<a-textarea id="clientIPs" readonly @click="getDBClientIps(client.email,$event)" placeholder="Click To Get IPs" :auto-size="{ minRows: 2, maxRows: 10 }">
|
||||||
|
</a-textarea>
|
||||||
|
</a-form>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item v-if="inbound.XTLS" label="Flow">
|
||||||
|
<a-select v-model="client.flow" style="width: 150px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||||
|
<a-select-option value="">{{ i18n "none" }}</a-select-option>
|
||||||
|
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item v-else-if="inbound.canEnableTlsFlow()" label="Flow" layout="inline">
|
||||||
|
<a-select v-model="client.flow" style="width: 150px">
|
||||||
|
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
|
||||||
|
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item>
|
||||||
|
<span slot="label">
|
||||||
|
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">
|
||||||
|
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
||||||
|
</template>
|
||||||
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</span>
|
||||||
|
<a-input-number v-model="client._totalGB":min="0" style="width: 70px;"></a-input-number>
|
||||||
|
<template v-if="isEdit && clientStats">
|
||||||
|
<span>{{ i18n "usage" }}:</span>
|
||||||
|
<a-tag :color="statsColor">
|
||||||
|
[[ sizeFormat(clientStats.up) ]] /
|
||||||
|
[[ sizeFormat(clientStats.down) ]]
|
||||||
|
([[ sizeFormat(clientStats.up + clientStats.down) ]])
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="{{ i18n "pages.client.delayedStart" }}">
|
||||||
|
<a-switch v-model="clientModal.delayedStart" @click="client._expiryTime=0"></a-switch>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="{{ i18n "pages.client.expireDays" }}" v-if="clientModal.delayedStart">
|
||||||
|
<a-input type="number" v-model.number="delayedExpireDays" :min="0"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item v-else>
|
||||||
|
<span slot="label">
|
||||||
|
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">
|
||||||
|
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
||||||
|
</template>
|
||||||
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</span>
|
||||||
|
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
||||||
|
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
|
||||||
|
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
|
||||||
|
<a-tag color="red" v-if="isExpiry">Expired</a-tag>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
{{end}}
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
<a-switch v-model="dbInbound.enable"></a-switch>
|
<a-switch v-model="dbInbound.enable"></a-switch>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label='{{ i18n "protocol" }}'>
|
<a-form-item label='{{ i18n "protocol" }}'>
|
||||||
<a-select v-model="inbound.protocol" style="width: 160px;">
|
<a-select v-model="inbound.protocol" style="width: 160px;" :disabled="isEdit" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||||
<a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p ]]</a-select-option>
|
<a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p ]]</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
@@ -50,6 +50,7 @@
|
|||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</span>
|
</span>
|
||||||
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
||||||
|
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
|
||||||
v-model="dbInbound._expiryTime" style="width: 300px;"></a-date-picker>
|
v-model="dbInbound._expiryTime" style="width: 300px;"></a-date-picker>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
|
|||||||
@@ -7,11 +7,14 @@
|
|||||||
<a-input type="number" v-model.number="inbound.settings.port"></a-input>
|
<a-input type="number" v-model.number="inbound.settings.port"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label='{{ i18n "pages.inbounds.network"}}'>
|
<a-form-item label='{{ i18n "pages.inbounds.network"}}'>
|
||||||
<a-select v-model="inbound.settings.network" style="width: 100px;">
|
<a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||||
<a-select-option value="tcp,udp">tcp+udp</a-select-option>
|
<a-select-option value="tcp,udp">TCP+UDP</a-select-option>
|
||||||
<a-select-option value="tcp">tcp</a-select-option>
|
<a-select-option value="tcp">TCP</a-select-option>
|
||||||
<a-select-option value="udp">udp</a-select-option>
|
<a-select-option value="udp">UDP</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
<a-form-item label="FollowRedirect">
|
||||||
|
<a-switch v-model="inbound.settings.followRedirect"></a-switch>
|
||||||
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -1,18 +1,18 @@
|
|||||||
{{define "form/shadowsocks"}}
|
{{define "form/shadowsocks"}}
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-form-item label='{{ i18n "encryption" }}'>
|
<a-form-item label='{{ i18n "encryption" }}'>
|
||||||
<a-select v-model="inbound.settings.method" style="width: 165px;">
|
<a-select v-model="inbound.settings.method" style="width: 250px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||||
<a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
|
<a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label='{{ i18n "password" }}'>
|
<a-form-item label='{{ i18n "password" }}'>
|
||||||
<a-input v-model.trim="inbound.settings.password"></a-input>
|
<a-input v-model.trim="inbound.settings.password" style="width: 250px;"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label='{{ i18n "pages.inbounds.network" }}'>
|
<a-form-item label='{{ i18n "pages.inbounds.network" }}'>
|
||||||
<a-select v-model="inbound.settings.network" style="width: 100px;">
|
<a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||||
<a-select-option value="tcp,udp">tcp+udp</a-select-option>
|
<a-select-option value="tcp,udp">TCP+UDP</a-select-option>
|
||||||
<a-select-option value="tcp">tcp</a-select-option>
|
<a-select-option value="tcp">TCP</a-select-option>
|
||||||
<a-select-option value="udp">udp</a-select-option>
|
<a-select-option value="udp">UDP</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{{define "form/socks"}}
|
{{define "form/socks"}}
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<!-- <a-form-item label="密码认证">-->
|
<!-- <a-form-item label="Password authentication">-->
|
||||||
<a-form-item label='{{ i18n "password" }}'>
|
<a-form-item label='{{ i18n "password" }}'>
|
||||||
<a-switch :checked="inbound.settings.auth === 'password'"
|
<a-switch :checked="inbound.settings.auth === 'password'"
|
||||||
@change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"></a-switch>
|
@change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"></a-switch>
|
||||||
|
|||||||
@@ -1,68 +1,38 @@
|
|||||||
{{define "form/trojan"}}
|
{{define "form/trojan"}}
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<label style="color: green;">{{ i18n "clients"}}</label>
|
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.trojans.slice(0,1)" v-if="!isEdit">
|
||||||
<a-collapse activeKey="0" v-for="(trojan, index) in inbound.settings.trojans"
|
<a-collapse-panel header="{{ i18n "pages.inbounds.client" }}">
|
||||||
:key="`trojan-${index}`">
|
|
||||||
|
|
||||||
<a-collapse-panel :class="getHeaderStyle(trojan.email)" :header="getHeaderText(trojan.email)">
|
|
||||||
<a-tag v-if="isExpiry(index) || ((getUpStats(trojan.email) + getDownStats(trojan.email)) > trojan.totalGB && trojan.totalGB != 0)" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag>
|
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<span slot="label">
|
<span slot="label">
|
||||||
Email
|
<span>{{ i18n "pages.inbounds.Email" }}</span>
|
||||||
<a-tooltip>
|
<a-tooltip>
|
||||||
<template slot="title">
|
<template slot="title">
|
||||||
The Email Must Be Completely Unique
|
<span>{{ i18n "pages.inbounds.EmailDesc" }}</span>
|
||||||
</template>
|
</template>
|
||||||
<!--Renew Svg Icon-->
|
<a-icon @click="getNewEmail(client)" type="sync"> </a-icon>
|
||||||
<svg
|
|
||||||
@click="getNewEmail(trojan)"
|
|
||||||
xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="anticon anticon-question-circle" viewBox="0 0 16 16"> <path d="M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41zm-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9z"/> <path fill-rule="evenodd" d="M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z"/> </svg>
|
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</span>
|
</span>
|
||||||
<a-input v-model.trim="trojan.email" style="width: 150px;"></a-input>
|
<a-input v-model.trim="client.email" style="width: 150px;" ></a-input>
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label="Password" >
|
|
||||||
<a-input v-model.trim="trojan.password" style="width: 150px;"></a-input>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item>
|
|
||||||
<span slot="label">
|
|
||||||
IP Count Limit
|
|
||||||
<a-tooltip>
|
|
||||||
<template slot="title">
|
|
||||||
disable inbound if more than entered count (0 for disable limit ip)
|
|
||||||
</template>
|
|
||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
|
||||||
</a-tooltip>
|
|
||||||
</span>
|
|
||||||
<a-input type="number" v-model.number="trojan.limitIp" min="0" style="width: 70px;"></a-input>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item v-if="trojan.email && trojan.limitIp > 0 && isEdit">
|
|
||||||
<span slot="label">
|
|
||||||
IP log
|
|
||||||
<a-tooltip>
|
|
||||||
<template slot="title">
|
|
||||||
IPs history Log (before enabling inbound after it has been disabled by IP limit, you should clear the log)
|
|
||||||
</template>
|
|
||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
|
||||||
</a-tooltip>
|
|
||||||
<a-tooltip>
|
|
||||||
<template slot="title">
|
|
||||||
clear the log
|
|
||||||
</template>
|
|
||||||
<span style="color: #FF4D4F">
|
|
||||||
<a-icon type="delete" @click="clearDBClientIps(trojan.email,$event)"></a-icon>
|
|
||||||
</span>
|
|
||||||
</a-tooltip>
|
|
||||||
</span>
|
|
||||||
<a-form layout="block">
|
|
||||||
<a-textarea readonly @click="getDBClientIps(trojan.email,$event)" placeholder="Click To Get IPs" :auto-size="{ minRows: 2, maxRows: 10 }">
|
|
||||||
</a-textarea>
|
|
||||||
</a-form>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
|
<a-form-item label="Password">
|
||||||
|
<a-input v-model.trim="client.password" style="width: 150px;"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item>
|
||||||
|
<span slot="label">
|
||||||
|
<span>{{ i18n "pages.inbounds.IPLimit" }}</span>
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">
|
||||||
|
<span>{{ i18n "pages.inbounds.IPLimitDesc" }}</span>
|
||||||
|
</template>
|
||||||
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</span>
|
||||||
|
<a-input type="number" v-model.number="client.limitIp" min="0" style="width: 70px;" ></a-input>
|
||||||
|
</a-form-item>
|
||||||
<a-form-item v-if="inbound.XTLS" label="Flow">
|
<a-form-item v-if="inbound.XTLS" label="Flow">
|
||||||
<a-select v-model="trojan.flow" style="width: 150px">
|
<a-select v-model="client.flow" style="width: 150px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||||
<a-select-option value="">{{ i18n "none" }}</a-select-option>
|
<a-select-option value="">{{ i18n "none" }}</a-select-option>
|
||||||
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
@@ -77,7 +47,7 @@
|
|||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</span>
|
</span>
|
||||||
<a-input-number v-model="trojan._totalGB" :min="0"></a-input-number>
|
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<span slot="label">
|
<span slot="label">
|
||||||
@@ -90,40 +60,24 @@
|
|||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</span>
|
</span>
|
||||||
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
||||||
v-model="trojan._expiryTime" style="width: 170px;"></a-date-picker>
|
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
|
||||||
|
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form layout="inline">
|
|
||||||
<a-tooltip v-if="trojan._totalGB > 0">
|
|
||||||
<template slot="title">
|
|
||||||
{{ i18n "pages.inbounds.resetTraffic" }}
|
|
||||||
</template>
|
|
||||||
<span style="color: #FF4D4F">
|
|
||||||
<a-icon type="delete" @click="resetClientTraffic(trojan,$event)"></a-icon>
|
|
||||||
</span>
|
|
||||||
</a-tooltip>
|
|
||||||
<a-tag color="blue">[[ sizeFormat(getUpStats(trojan.email)) ]] / [[ sizeFormat(getDownStats(trojan.email)) ]]</a-tag>
|
|
||||||
<a-tag v-if="trojan._totalGB > 0" color="red">used : [[ sizeFormat(getUpStats(trojan.email) + getDownStats(trojan.email)) ]]</a-tag>
|
|
||||||
<a-tag v-show="inbound.settings.trojans.length > 1" @click="removeClient(index, inbound.settings.trojans)">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22" width="22" height="22" class="mt-2 cursor-pointer">
|
|
||||||
<path fill="none" d="M0 0h24v24H0z" />
|
|
||||||
<path fill="#EC4899"
|
|
||||||
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm0-9.414l2.828-2.829 1.415 1.415L13.414 12l2.829 2.828-1.415 1.415L12 13.414l-2.828 2.829-1.415-1.415L10.586 12 7.757 9.172l1.415-1.415L12 10.586z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a-tag>
|
|
||||||
</a-form>
|
|
||||||
</a-collapse-panel>
|
</a-collapse-panel>
|
||||||
</a-collapse>
|
</a-collapse>
|
||||||
<a-tag @click="addClient(inbound.protocol, inbound.settings.trojans)">
|
<a-collapse v-else>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" class="ml-2 cursor-pointer">
|
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.trojans.length">
|
||||||
<path fill="none" d="M0 0h24v24H0z" />
|
<table width="100%">
|
||||||
<path fill="green"
|
<tr class="client-table-header">
|
||||||
d="M11 11V7h2v4h4v2h-4v4h-2v-4H7v-2h4zm1 11C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16z"
|
<th v-for="col in Object.keys(inbound.settings.trojans[0]).slice(0, 3)">[[ col ]]</th>
|
||||||
/>
|
</tr>
|
||||||
</svg>
|
<tr v-for="(client, index) in inbound.settings.trojans" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
|
||||||
</a-tag>
|
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
|
||||||
|
</tr>
|
||||||
<template v-if="inbound.isTcp && inbound.tls || inbound.XTLS">
|
</table>
|
||||||
|
</a-collapse-panel>
|
||||||
|
</a-collapse>
|
||||||
|
<template v-if="inbound.isTcp">
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-form-item label="Fallbacks">
|
<a-form-item label="Fallbacks">
|
||||||
<a-row>
|
<a-row>
|
||||||
@@ -135,26 +89,26 @@
|
|||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
|
|
||||||
<!-- trojan fallbacks -->
|
<!-- trojan fallbacks -->
|
||||||
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline">
|
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline">
|
||||||
<a-divider>
|
<a-divider>
|
||||||
fallback[[ index + 1 ]]
|
fallback[[ index + 1 ]]
|
||||||
<a-icon type="delete" @click="() => inbound.settings.delTrojanFallback(index)"
|
<a-icon type="delete" @click="() => inbound.settings.delTrojanFallback(index)"
|
||||||
style="color: rgb(255, 77, 79);cursor: pointer;"/>
|
style="color: rgb(255, 77, 79);cursor: pointer;"/>
|
||||||
</a-divider>
|
</a-divider>
|
||||||
<a-form-item label="name">
|
<a-form-item label="Name">
|
||||||
<a-input v-model="fallback.name"></a-input>
|
<a-input v-model="fallback.name"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="alpn">
|
<a-form-item label="Alpn">
|
||||||
<a-input v-model="fallback.alpn"></a-input>
|
<a-input v-model="fallback.alpn"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="path">
|
<a-form-item label="Path">
|
||||||
<a-input v-model="fallback.path"></a-input>
|
<a-input v-model="fallback.path"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="dest">
|
<a-form-item label="Dest">
|
||||||
<a-input v-model="fallback.dest"></a-input>
|
<a-input v-model="fallback.dest"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="xver">
|
<a-form-item label="xVer">
|
||||||
<a-input type="number" v-model.number="fallback.xver"></a-input>
|
<a-input type="number" v-model.number="fallback.xver"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
|
<a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
|
||||||
|
|||||||
@@ -1,76 +1,44 @@
|
|||||||
{{define "form/vless"}}
|
{{define "form/vless"}}
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<label style="color: green;">{{ i18n "clients"}}</label>
|
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit">
|
||||||
<a-collapse activeKey="0" v-for="(vless, index) in inbound.settings.vlesses"
|
<a-collapse-panel header="{{ i18n "pages.inbounds.client" }}">
|
||||||
:key="`vless-${index}`">
|
|
||||||
|
|
||||||
<a-collapse-panel :class="getHeaderStyle(vless.email)" :header="getHeaderText(vless.email)">
|
|
||||||
<a-tag v-if="isExpiry(index) || ((getUpStats(vless.email) + getDownStats(vless.email)) > vless.totalGB && vless.totalGB != 0)" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag>
|
|
||||||
|
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<span slot="label">
|
<span slot="label">
|
||||||
Email
|
<span>{{ i18n "pages.inbounds.Email" }}</span>
|
||||||
<a-tooltip>
|
<a-tooltip>
|
||||||
<template slot="title">
|
<template slot="title">
|
||||||
The Email Must Be Completely Unique
|
<span>{{ i18n "pages.inbounds.EmailDesc" }}</span>
|
||||||
</template>
|
</template>
|
||||||
<!--Renew Svg Icon-->
|
<a-icon type="sync" @click="getNewEmail(client)"></a-icon>
|
||||||
<svg
|
|
||||||
@click="getNewEmail(vless)"
|
|
||||||
xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="anticon anticon-question-circle" viewBox="0 0 16 16"> <path d="M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41zm-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9z"/> <path fill-rule="evenodd" d="M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z"/> </svg>
|
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</span>
|
</span>
|
||||||
<a-input v-model.trim="vless.email" style="width: 150px;"></a-input>
|
<a-input v-model.trim="client.email" style="width: 150px;" ></a-input>
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label="ID">
|
|
||||||
<a-input v-model.trim="vless.id" style="width: 300px;" ></a-input>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item>
|
|
||||||
<span slot="label">
|
|
||||||
IP Count Limit
|
|
||||||
<a-tooltip>
|
|
||||||
<template slot="title">
|
|
||||||
disable inbound if more than entered count (0 for disable limit ip)
|
|
||||||
</template>
|
|
||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
|
||||||
</a-tooltip>
|
|
||||||
</span>
|
|
||||||
<a-input type="number" v-model.number="vless.limitIp" min="0" style="width: 70px;"></a-input>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item v-if="vless.email && vless.limitIp > 0 && isEdit">
|
|
||||||
<span slot="label">
|
|
||||||
IP log
|
|
||||||
<a-tooltip>
|
|
||||||
<template slot="title">
|
|
||||||
IPs history Log (before enabling inbound after it has been disabled by IP limit, you should clear the log)
|
|
||||||
</template>
|
|
||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
|
||||||
</a-tooltip>
|
|
||||||
<a-tooltip>
|
|
||||||
<template slot="title">
|
|
||||||
clear the log
|
|
||||||
</template>
|
|
||||||
<span style="color: #FF4D4F">
|
|
||||||
<a-icon type="delete" @click="clearDBClientIps(vless.email,$event)"></a-icon>
|
|
||||||
</span>
|
|
||||||
</a-tooltip>
|
|
||||||
</span>
|
|
||||||
<a-form layout="block">
|
|
||||||
|
|
||||||
<a-textarea readonly @click="getDBClientIps(vless.email,$event)" placeholder="Click To Get IPs" :auto-size="{ minRows: 2, maxRows: 10 }">
|
|
||||||
</a-textarea>
|
|
||||||
</a-form>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
|
<a-form-item label="ID">
|
||||||
|
<a-input v-model.trim="client.id" style="width: 300px;" ></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item>
|
||||||
|
<span slot="label">
|
||||||
|
<span>{{ i18n "pages.inbounds.IPLimit" }}</span>
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">
|
||||||
|
<span>{{ i18n "pages.inbounds.IPLimitDesc" }}</span>
|
||||||
|
</template>
|
||||||
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</span>
|
||||||
|
<a-input type="number" v-model.number="client.limitIp" min="0" style="width: 70px;" ></a-input>
|
||||||
|
</a-form-item>
|
||||||
<a-form-item v-if="inbound.XTLS" label="Flow">
|
<a-form-item v-if="inbound.XTLS" label="Flow">
|
||||||
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 150px">
|
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 150px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||||
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
|
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
|
||||||
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item v-else-if="inbound.canEnableTlsFlow()" label="Flow" layout="inline">
|
<a-form-item v-else-if="inbound.canEnableTlsFlow()" label="Flow" layout="inline">
|
||||||
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 150px">
|
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 150px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||||
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
|
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
|
||||||
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
@@ -85,7 +53,7 @@
|
|||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</span>
|
</span>
|
||||||
<a-input-number v-model="vless._totalGB" :min="0"></a-input-number>
|
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<span slot="label">
|
<span slot="label">
|
||||||
@@ -98,41 +66,24 @@
|
|||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</span>
|
</span>
|
||||||
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
||||||
v-model="vless._expiryTime" style="width: 300px;"></a-date-picker>
|
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
|
||||||
|
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form layout="inline">
|
|
||||||
<a-tooltip v-if="vless._totalGB > 0">
|
|
||||||
<template slot="title">
|
|
||||||
{{ i18n "pages.inbounds.resetTraffic" }}
|
|
||||||
</template>
|
|
||||||
<span style="color: #FF4D4F">
|
|
||||||
<a-icon type="delete" @click="resetClientTraffic(vless,$event)"></a-icon>
|
|
||||||
</span>
|
|
||||||
</a-tooltip>
|
|
||||||
<a-tag color="blue">[[ sizeFormat(getUpStats(vless.email)) ]] / [[ sizeFormat(getDownStats(vless.email)) ]]</a-tag>
|
|
||||||
<a-tag v-if="vless._totalGB > 0" color="red">used : [[ sizeFormat(getUpStats(vless.email) + getDownStats(vless.email)) ]]</a-tag>
|
|
||||||
|
|
||||||
<a-tag v-show="inbound.settings.vlesses.length > 1" @click="removeClient(index, inbound.settings.vlesses)">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22" width="22" height="22" class="mt-2 cursor-pointer">
|
|
||||||
<path fill="none" d="M0 0h24v24H0z" />
|
|
||||||
<path fill="#EC4899"
|
|
||||||
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm0-9.414l2.828-2.829 1.415 1.415L13.414 12l2.829 2.828-1.415 1.415L12 13.414l-2.828 2.829-1.415-1.415L10.586 12 7.757 9.172l1.415-1.415L12 10.586z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a-tag>
|
|
||||||
</a-form>
|
|
||||||
</a-collapse-panel>
|
</a-collapse-panel>
|
||||||
</a-collapse>
|
</a-collapse>
|
||||||
<a-tag @click="addClient(inbound.protocol, inbound.settings.vlesses)">
|
<a-collapse v-else>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" class="ml-2 cursor-pointer">
|
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.vlesses.length">
|
||||||
<path fill="none" d="M0 0h24v24H0z" />
|
<table width="100%">
|
||||||
<path fill="green"
|
<tr class="client-table-header">
|
||||||
d="M11 11V7h2v4h4v2h-4v4h-2v-4H7v-2h4zm1 11C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16z"
|
<th v-for="col in Object.keys(inbound.settings.vlesses[0]).slice(0, 3)">[[ col ]]</th>
|
||||||
/>
|
</tr>
|
||||||
</svg>
|
<tr v-for="(client, index) in inbound.settings.vlesses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
|
||||||
</a-tag>
|
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
|
||||||
|
</tr>
|
||||||
<template v-if="inbound.isTcp && inbound.tls || inbound.XTLS">
|
</table>
|
||||||
|
</a-collapse-panel>
|
||||||
|
</a-collapse>
|
||||||
|
<template v-if="inbound.isTcp">
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-form-item label="Fallbacks">
|
<a-form-item label="Fallbacks">
|
||||||
<a-row>
|
<a-row>
|
||||||
@@ -144,26 +95,26 @@
|
|||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
|
|
||||||
<!-- vless fallbacks -->
|
<!-- vless fallbacks -->
|
||||||
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline">
|
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline">
|
||||||
<a-divider>
|
<a-divider>
|
||||||
fallback[[ index + 1 ]]
|
fallback[[ index + 1 ]]
|
||||||
<a-icon type="delete" @click="() => inbound.settings.delFallback(index)"
|
<a-icon type="delete" @click="() => inbound.settings.delFallback(index)"
|
||||||
style="color: rgb(255, 77, 79);cursor: pointer;"/>
|
style="color: rgb(255, 77, 79);cursor: pointer;"/>
|
||||||
</a-divider>
|
</a-divider>
|
||||||
<a-form-item label="name">
|
<a-form-item label="Name">
|
||||||
<a-input v-model="fallback.name"></a-input>
|
<a-input v-model="fallback.name"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="alpn">
|
<a-form-item label="Alpn">
|
||||||
<a-input v-model="fallback.alpn"></a-input>
|
<a-input v-model="fallback.alpn"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="path">
|
<a-form-item label="Path">
|
||||||
<a-input v-model="fallback.path"></a-input>
|
<a-input v-model="fallback.path"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="dest">
|
<a-form-item label="Dest">
|
||||||
<a-input v-model="fallback.dest"></a-input>
|
<a-input v-model="fallback.dest"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="xver">
|
<a-form-item label="xVer">
|
||||||
<a-input type="number" v-model.number="fallback.xver"></a-input>
|
<a-input type="number" v-model.number="fallback.xver"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
|
<a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
|
||||||
|
|||||||
@@ -1,67 +1,39 @@
|
|||||||
{{define "form/vmess"}}
|
{{define "form/vmess"}}
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<label style="color: green;">{{ i18n "clients"}}</label>
|
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vmesses.slice(0,1)" v-if="!isEdit">
|
||||||
<a-collapse activeKey="0" v-for="(vmess, index) in inbound.settings.vmesses"
|
<a-collapse-panel header="{{ i18n "pages.inbounds.client" }}">
|
||||||
:key="`vmess-${index}`">
|
|
||||||
<a-collapse-panel :class="getHeaderStyle(vmess.email)" :header="getHeaderText(vmess.email)">
|
|
||||||
<a-tag v-if="isExpiry(index) || ((getUpStats(vmess.email) + getDownStats(vmess.email)) > vmess.totalGB && vmess.totalGB != 0)" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag>
|
|
||||||
|
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<span slot="label">
|
<span slot="label">
|
||||||
Email
|
<span>{{ i18n "pages.inbounds.Email" }}</span>
|
||||||
<a-tooltip>
|
<a-tooltip>
|
||||||
<template slot="title">
|
<template slot="title">
|
||||||
The Email Must Be Completely Unique
|
<span>{{ i18n "pages.inbounds.EmailDesc" }}</span>
|
||||||
</template>
|
</template>
|
||||||
<!--Renew Svg Icon-->
|
<a-icon type="sync" @click="getNewEmail(client)"></a-icon>
|
||||||
<svg
|
|
||||||
@click="getNewEmail(vmess)"
|
|
||||||
xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="anticon anticon-question-circle" viewBox="0 0 16 16"> <path d="M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41zm-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9z"/> <path fill-rule="evenodd" d="M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z"/> </svg>
|
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</span>
|
</span>
|
||||||
<a-input v-model.trim="vmess.email" style="width: 150px;"></a-input>
|
<a-input v-model.trim="client.email" style="width: 150px;"></a-input>
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label="ID">
|
|
||||||
<a-input v-model.trim="vmess.id" style="width: 300px;" ></a-input>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label='{{ i18n "additional" }} ID'>
|
|
||||||
<a-input type="number" v-model.number="vmess.alterId"></a-input>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item>
|
|
||||||
<span slot="label">
|
|
||||||
IP Count Limit
|
|
||||||
<a-tooltip>
|
|
||||||
<template slot="title">
|
|
||||||
disable inbound if more than entered count (0 for disable limit ip)
|
|
||||||
</template>
|
|
||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
|
||||||
</a-tooltip>
|
|
||||||
</span>
|
|
||||||
<a-input type="number" v-model.number="vmess.limitIp" min="0" style="width: 70px;" ></a-input>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item v-if="vmess.email && vmess.limitIp > 0 && isEdit">
|
|
||||||
<span slot="label">
|
|
||||||
IP Log
|
|
||||||
<a-tooltip>
|
|
||||||
<template slot="title">
|
|
||||||
IPs history Log (before enabling inbound after it has been disabled by IP limit, you should clear the log)
|
|
||||||
</template>
|
|
||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
|
||||||
</a-tooltip>
|
|
||||||
<a-tooltip>
|
|
||||||
<template slot="title">
|
|
||||||
clear the log
|
|
||||||
</template>
|
|
||||||
<span style="color: #FF4D4F">
|
|
||||||
<a-icon type="delete" @click="clearDBClientIps(vmess.email,$event)"></a-icon>
|
|
||||||
</span>
|
|
||||||
</a-tooltip>
|
|
||||||
</span>
|
|
||||||
<a-textarea readonly @click="getDBClientIps(vmess.email,$event)" placeholder="Click To Get IPs" :auto-size="{ minRows: 2, maxRows: 10 }">
|
|
||||||
</a-textarea>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
|
<a-form-item label="ID">
|
||||||
|
<a-input v-model.trim="client.id" style="width: 300px;"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label='{{ i18n "additional" }} ID'>
|
||||||
|
<a-input type="number" v-model.number="client.alterId" style="width: 70px;"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item>
|
||||||
|
<span slot="label">
|
||||||
|
<span>{{ i18n "pages.inbounds.IPLimit" }}</span>
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">
|
||||||
|
<span>{{ i18n "pages.inbounds.IPLimitDesc" }}</span>
|
||||||
|
</template>
|
||||||
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</span>
|
||||||
|
<a-input type="number" v-model.number="client.limitIp" min="0" style="width: 70px;"></a-input>
|
||||||
|
</a-form-item>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<span slot="label">
|
<span slot="label">
|
||||||
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
|
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
|
||||||
@@ -72,7 +44,7 @@
|
|||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</span>
|
</span>
|
||||||
<a-input-number v-model="vmess._totalGB" :min="0"></a-input-number>
|
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<span slot="label">
|
<span slot="label">
|
||||||
@@ -85,43 +57,23 @@
|
|||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</span>
|
</span>
|
||||||
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
||||||
v-model="vmess._expiryTime" style="width: 300px;"></a-date-picker>
|
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
|
||||||
|
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form layout="inline">
|
|
||||||
<a-tooltip v-if="vmess._totalGB > 0">
|
|
||||||
<template slot="title">
|
|
||||||
{{ i18n "pages.inbounds.resetTraffic" }}
|
|
||||||
</template>
|
|
||||||
<span style="color: #FF4D4F">
|
|
||||||
<a-icon type="delete" @click="resetClientTraffic(vmess,$event)"></a-icon>
|
|
||||||
</span>
|
|
||||||
</a-tooltip>
|
|
||||||
<a-tag color="blue">[[ sizeFormat(getUpStats(vmess.email)) ]] / [[ sizeFormat(getDownStats(vmess.email)) ]]</a-tag>
|
|
||||||
<a-tag v-if="vmess._totalGB > 0" color="red">used : [[ sizeFormat(getUpStats(vmess.email) + getDownStats(vmess.email)) ]]</a-tag>
|
|
||||||
<a-tag v-show="inbound.settings.vmesses.length > 1" @click="removeClient(index, inbound.settings.vmesses)">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22" width="22" height="22" class="mt-2 cursor-pointer">
|
|
||||||
<path fill="none" d="M0 0h24v24H0z" />
|
|
||||||
<path fill="#EC4899"
|
|
||||||
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm0-9.414l2.828-2.829 1.415 1.415L13.414 12l2.829 2.828-1.415 1.415L12 13.414l-2.828 2.829-1.415-1.415L10.586 12 7.757 9.172l1.415-1.415L12 10.586z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a-tag>
|
|
||||||
</a-form>
|
|
||||||
|
|
||||||
|
|
||||||
</a-collapse-panel>
|
</a-collapse-panel>
|
||||||
|
|
||||||
</a-collapse>
|
</a-collapse>
|
||||||
|
<a-collapse v-else>
|
||||||
<a-tag @click="addClient(inbound.protocol, inbound.settings.vmesses)">
|
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.vmesses.length">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" class="ml-2 cursor-pointer">
|
<table width="100%">
|
||||||
<path fill="none" d="M0 0h24v24H0z" />
|
<tr class="client-table-header">
|
||||||
<path fill="green"
|
<th v-for="col in Object.keys(inbound.settings.vmesses[0]).slice(0, 3)">[[ col ]]</th>
|
||||||
d="M11 11V7h2v4h4v2h-4v4h-2v-4H7v-2h4zm1 11C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16z"
|
</tr>
|
||||||
/>
|
<tr v-for="(client, index) in inbound.settings.vmesses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
|
||||||
</svg>
|
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
|
||||||
</a-tag>
|
</tr>
|
||||||
|
</table>
|
||||||
|
</a-collapse-panel>
|
||||||
|
</a-collapse>
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-form-item label='{{ i18n "pages.inbounds.disableInsecureEncryption" }}'>
|
<a-form-item label='{{ i18n "pages.inbounds.disableInsecureEncryption" }}'>
|
||||||
<a-switch v-model.number="inbound.settings.disableInsecure"></a-switch>
|
<a-switch v-model.number="inbound.settings.disableInsecure"></a-switch>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{{define "form/streamQUIC"}}
|
{{define "form/streamQUIC"}}
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-form-item label='{{ i18n "pages.inbounds.stream.quic.encryption" }}'>
|
<a-form-item label='{{ i18n "pages.inbounds.stream.quic.encryption" }}'>
|
||||||
<a-select v-model="inbound.stream.quic.security" style="width: 165px;">
|
<a-select v-model="inbound.stream.quic.security" style="width: 165px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||||
<a-select-option value="none">none</a-select-option>
|
<a-select-option value="none">none</a-select-option>
|
||||||
<a-select-option value="aes-128-gcm">aes-128-gcm</a-select-option>
|
<a-select-option value="aes-128-gcm">aes-128-gcm</a-select-option>
|
||||||
<a-select-option value="chacha20-poly1305">chacha20-poly1305</a-select-option>
|
<a-select-option value="chacha20-poly1305">chacha20-poly1305</a-select-option>
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
<a-input v-model.trim="inbound.stream.quic.key"></a-input>
|
<a-input v-model.trim="inbound.stream.quic.key"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label='{{ i18n "camouflage" }}'>
|
<a-form-item label='{{ i18n "camouflage" }}'>
|
||||||
<a-select v-model="inbound.stream.quic.type" style="width: 280px;">
|
<a-select v-model="inbound.stream.quic.type" style="width: 280px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||||
<a-select-option value="none">none(not camouflage)</a-select-option>
|
<a-select-option value="none">none(not camouflage)</a-select-option>
|
||||||
<a-select-option value="srtp">srtp(camouflage video call)</a-select-option>
|
<a-select-option value="srtp">srtp(camouflage video call)</a-select-option>
|
||||||
<a-select-option value="utp">utp(camouflage BT download)</a-select-option>
|
<a-select-option value="utp">utp(camouflage BT download)</a-select-option>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<!-- select stream network -->
|
<!-- select stream network -->
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-form-item label='{{ i18n "transmission" }}'>
|
<a-form-item label='{{ i18n "transmission" }}'>
|
||||||
<a-select v-model="inbound.stream.network" @change="streamNetworkChange">
|
<a-select v-model="inbound.stream.network" @change="streamNetworkChange" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||||
<a-select-option value="tcp">TCP</a-select-option>
|
<a-select-option value="tcp">TCP</a-select-option>
|
||||||
<a-select-option value="kcp">KCP</a-select-option>
|
<a-select-option value="kcp">KCP</a-select-option>
|
||||||
<a-select-option value="ws">WS</a-select-option>
|
<a-select-option value="ws">WS</a-select-option>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<a-form-item label="AcceptProxyProtocol">
|
<a-form-item label="AcceptProxyProtocol">
|
||||||
<a-switch v-model="inbound.stream.tcp.acceptProxyProtocol"></a-switch>
|
<a-switch v-model="inbound.stream.tcp.acceptProxyProtocol"></a-switch>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="HTTP Camouflage">
|
<a-form-item label="HTTP {{ i18n "camouflage" }}">
|
||||||
<a-switch
|
<a-switch
|
||||||
:checked="inbound.stream.tcp.type === 'http'"
|
:checked="inbound.stream.tcp.type === 'http'"
|
||||||
@change="checked => inbound.stream.tcp.type = checked ? 'http' : 'none'">
|
@change="checked => inbound.stream.tcp.type = checked ? 'http' : 'none'">
|
||||||
|
|||||||
@@ -1,17 +1,38 @@
|
|||||||
{{define "form/tlsSettings"}}
|
{{define "form/tlsSettings"}}
|
||||||
<!-- tls enable -->
|
<!-- tls enable -->
|
||||||
<a-form layout="inline" v-if="inbound.canSetTls()">
|
<a-form layout="inline" v-if="inbound.canSetTls()">
|
||||||
<a-form-item label="TLS">
|
<a-form-item v-if="inbound.canEnableTls()" label="TLS">
|
||||||
<a-switch v-model="inbound.tls">
|
<a-switch v-model="inbound.tls">
|
||||||
</a-switch>
|
</a-switch>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item v-if="inbound.canEnableXTLS()" label="XTLS">
|
<a-form-item v-if="inbound.canEnableReality()">
|
||||||
|
<span slot="label">
|
||||||
|
Reality
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">
|
||||||
|
<span>{{ i18n "pages.inbounds.Realitydec" }}</span>
|
||||||
|
</template>
|
||||||
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</span>
|
||||||
|
<a-switch v-model="inbound.reality"></a-switch>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item v-if="inbound.canEnableXTLS()">
|
||||||
|
<span slot="label">
|
||||||
|
XTLS
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">
|
||||||
|
<span>{{ i18n "pages.inbounds.XTLSdec" }}</span>
|
||||||
|
</template>
|
||||||
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</span>
|
||||||
<a-switch v-model="inbound.XTLS"></a-switch>
|
<a-switch v-model="inbound.XTLS"></a-switch>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
|
|
||||||
<!-- tls settings -->
|
<!-- tls settings -->
|
||||||
<a-form v-if="inbound.tls || inbound.XTLS"layout="inline">
|
<a-form v-if="inbound.tls || inbound.XTLS" layout="inline">
|
||||||
<a-form-item label="SNI" placeholder="Server Name Indication" v-if="inbound.tls">
|
<a-form-item label="SNI" placeholder="Server Name Indication" v-if="inbound.tls">
|
||||||
<a-input v-model.trim="inbound.stream.tls.settings[0].serverName"></a-input>
|
<a-input v-model.trim="inbound.stream.tls.settings[0].serverName"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
@@ -22,25 +43,25 @@
|
|||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="MinVersion">
|
<a-form-item label="MinVersion">
|
||||||
<a-select v-model="inbound.stream.tls.minVersion" style="width: 60px">
|
<a-select v-model="inbound.stream.tls.minVersion" style="width: 60px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||||
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
|
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="MaxVersion">
|
<a-form-item label="MaxVersion">
|
||||||
<a-select v-model="inbound.stream.tls.maxVersion" style="width: 60px">
|
<a-select v-model="inbound.stream.tls.maxVersion" style="width: 60px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||||
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
|
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="uTLS" v-if="inbound.tls" >
|
<a-form-item label="uTLS" v-if="inbound.tls" >
|
||||||
<a-select v-model="inbound.stream.tls.settings[0].fingerprint" style="width: 135px">
|
<a-select v-model="inbound.stream.tls.settings[0].fingerprint" style="width: 135px">
|
||||||
<a-select-option value=''>None</a-select-option>
|
<a-select-option value=''>None</a-select-option>
|
||||||
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
|
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label='{{ i18n "domainName" }}'>
|
<a-form-item label='{{ i18n "domainName" }}'>
|
||||||
<a-input v-model.trim="inbound.stream.tls.server"></a-input>
|
<a-input v-model.trim="inbound.stream.tls.server"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="Alpn">
|
<a-form-item label="Alpn">
|
||||||
<a-checkbox-group v-model="inbound.stream.tls.alpn" style="width:200px">
|
<a-checkbox-group v-model="inbound.stream.tls.alpn" style="width:200px">
|
||||||
<a-checkbox v-for="key in ALPN_OPTION" :value="key">[[ key ]]</a-checkbox>
|
<a-checkbox v-for="key in ALPN_OPTION" :value="key">[[ key ]]</a-checkbox>
|
||||||
</a-checkbox-group>
|
</a-checkbox-group>
|
||||||
@@ -61,6 +82,7 @@
|
|||||||
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
|
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
|
||||||
<a-input v-model.trim="inbound.stream.tls.certs[0].keyFile" style="width:300px;"></a-input>
|
<a-input v-model.trim="inbound.stream.tls.certs[0].keyFile" style="width:300px;"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
<a-button @click="setDefaultCertData">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
|
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
|
||||||
@@ -71,4 +93,33 @@
|
|||||||
</a-form-item>
|
</a-form-item>
|
||||||
</template>
|
</template>
|
||||||
</a-form>
|
</a-form>
|
||||||
|
<a-form v-else-if="inbound.reality" layout="inline">
|
||||||
|
<a-form-item label="show">
|
||||||
|
<a-switch v-model="inbound.stream.reality.show">
|
||||||
|
</a-switch>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="xver">
|
||||||
|
<a-input type="number" v-model.number="inbound.stream.reality.xver" :min="0" style="width: 60px"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="uTLS" >
|
||||||
|
<a-select v-model="inbound.stream.reality.fingerprint" style="width: 135px">
|
||||||
|
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="dest">
|
||||||
|
<a-input v-model.trim="inbound.stream.reality.dest" style="width: 360px"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="serverNames">
|
||||||
|
<a-input v-model.trim="inbound.stream.reality.serverNames" style="width: 360px"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="privateKey">
|
||||||
|
<a-input v-model.trim="inbound.stream.reality.privateKey" style="width: 360px"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="publicKey">
|
||||||
|
<a-input v-model.trim="inbound.stream.reality.publicKey" style="width: 360px"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="shortIds">
|
||||||
|
<a-input v-model.trim="inbound.stream.reality.shortIds"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -1,24 +1,35 @@
|
|||||||
{{define "client_row"}}
|
{{define "client_table"}}
|
||||||
<template slot="actions" slot-scope="text, client, index">
|
<template slot="actions" slot-scope="text, client, index">
|
||||||
<a-tooltip>
|
<a-tooltip>
|
||||||
<template slot="title">{{ i18n "qrCode" }}</template>
|
<template slot="title">{{ i18n "qrCode" }}</template>
|
||||||
<a-icon style="font-size: 24px;" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record,index);"></a-icon>
|
<a-icon style="font-size: 24px;" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record,index);"></a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">{{ i18n "pages.client.edit" }}</template>
|
||||||
|
<a-icon style="font-size: 24px;" type="edit" @click="openEditClient(record.id,client);"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
<a-tooltip>
|
<a-tooltip>
|
||||||
<template slot="title">{{ i18n "info" }}</template>
|
<template slot="title">{{ i18n "info" }}</template>
|
||||||
<a-icon style="font-size: 24px;" type="info-circle" @click="showInfo(record,index);"></a-icon>
|
<a-icon style="font-size: 24px;" type="info-circle" @click="showInfo(record,index);"></a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<a-tooltip>
|
<a-tooltip>
|
||||||
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
|
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
|
||||||
<a-icon style="font-size: 24px;" type="retweet" @click="resetClientTraffic(client,record,$event)" v-if="client.email != ''"></a-icon>
|
<a-icon style="font-size: 24px;" type="retweet" @click="resetClientTraffic(client,record.id)" v-if="client.email.length > 0"></a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title"><span style="color: #FF4D4F"> {{ i18n "delete"}}</span></template>
|
||||||
|
<a-icon style="font-size: 24px;" type="delete" v-if="isRemovable(record.id)" @click="delClient(record.id,client)"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
|
<template slot="enable" slot-scope="text, client, index">
|
||||||
|
<a-switch v-model="client.enable" @change="switchEnableClient(record.id,client)"></a-switch>
|
||||||
</template>
|
</template>
|
||||||
<template slot="client" slot-scope="text, client">
|
<template slot="client" slot-scope="text, client">
|
||||||
[[ client.email ]]
|
[[ client.email ]]
|
||||||
<a-tag v-if="!isClientEnabled(record, client.email)" color="red">{{ i18n "disabled" }}</a-tag>
|
<a-tag v-if="!isClientEnabled(record, client.email)" color="red">{{ i18n "depleted" }}</a-tag>
|
||||||
</template>
|
</template>
|
||||||
<template slot="traffic" slot-scope="text, client">
|
<template slot="traffic" slot-scope="text, client">
|
||||||
<a-tag color="blue">[[ sizeFormat(getUpStats(record, client.email)) ]] / [[ sizeFormat(getDownStats(record, client.email)) ]]</a-tag>
|
<a-tag color="blue">[[ sizeFormat(getUpStats(record, client.email)) ]] / [[ sizeFormat(getDownStats(record, client.email)) ]]</a-tag>
|
||||||
<template v-if="client._totalGB > 0">
|
<template v-if="client._totalGB > 0">
|
||||||
<a-tag v-if="isTrafficExhausted(record, client.email)" color="red">[[client._totalGB]]GB</a-tag>
|
<a-tag v-if="isTrafficExhausted(record, client.email)" color="red">[[client._totalGB]]GB</a-tag>
|
||||||
<a-tag v-else color="cyan">[[client._totalGB]]GB</a-tag>
|
<a-tag v-else color="cyan">[[client._totalGB]]GB</a-tag>
|
||||||
@@ -26,11 +37,12 @@
|
|||||||
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
|
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
|
||||||
</template>
|
</template>
|
||||||
<template slot="expiryTime" slot-scope="text, client, index">
|
<template slot="expiryTime" slot-scope="text, client, index">
|
||||||
<template v-if="client._expiryTime > 0">
|
<template v-if="client.expiryTime > 0">
|
||||||
<a-tag :color="isExpiry(record, index)? 'red' : 'blue'">
|
<a-tag :color="isExpiry(record, index)? 'red' : 'blue'">
|
||||||
[[ DateUtil.formatMillis(client._expiryTime) ]]
|
[[ DateUtil.formatMillis(client._expiryTime) ]]
|
||||||
</a-tag>
|
</a-tag>
|
||||||
</template>
|
</template>
|
||||||
|
<a-tag v-else-if="client.expiryTime < 0" color="cyan">[[ client._expiryTime ]] {{ i18n "pages.client.days" }}</a-tag>
|
||||||
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
|
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
|
||||||
</template>
|
</template>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
v-model="infoModal.visible" title='{{ i18n "pages.inbounds.details"}}'
|
v-model="infoModal.visible" title='{{ i18n "pages.inbounds.details"}}'
|
||||||
:closable="true"
|
:closable="true"
|
||||||
:mask-closable="true"
|
:mask-closable="true"
|
||||||
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||||
:footer="null"
|
:footer="null"
|
||||||
width="600px"
|
width="600px"
|
||||||
>
|
>
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</table>
|
</table>
|
||||||
</td></tr>
|
</td></tr>
|
||||||
<tr colspan="2">
|
<tr colspan="2" v-if="dbInbound.hasLink()">
|
||||||
<td v-if="inbound.tls">
|
<td v-if="inbound.tls">
|
||||||
tls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
|
tls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
|
||||||
tls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
|
tls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
|
||||||
@@ -57,20 +57,30 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
<template v-if="infoModal.clientSettings">
|
||||||
<a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
|
<a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
|
||||||
<table style="margin-bottom: 10px; width: 100%;">
|
<table style="margin-bottom: 10px;">
|
||||||
<tr><th>[[ Object.keys(infoModal.clientSettings)[0] ]]</th><th>[[ Object.keys(infoModal.clientSettings)[1] ]]</th><th>[[ Object.keys(infoModal.clientSettings)[2] ]]</th></tr>
|
<tr v-for="col,index in Object.keys(infoModal.clientSettings).slice(0, 3)">
|
||||||
|
<td>[[ col ]]</td>
|
||||||
|
<td><a-tag color="green">[[ infoModal.clientSettings[col] ]]</a-tag></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><a-tag color="green">[[ Object.values(infoModal.clientSettings)[0] ]]</a-tag></td>
|
<td>{{ i18n "status" }}</td>
|
||||||
<td><a-tag color="green">[[ Object.values(infoModal.clientSettings)[1] ]]</a-tag></td>
|
<td>
|
||||||
<td><a-tag color="green">[[ Object.values(infoModal.clientSettings)[2] ]]</a-tag></td>
|
<a-tag v-if="isEnable" color="blue">{{ i18n "enabled" }}</a-tag>
|
||||||
|
<a-tag v-else color="red">{{ i18n "disabled" }}</a-tag>
|
||||||
|
<a-tag v-if="!isActive" color="red">{{ i18n "depleted" }}</a-tag>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<table style="margin-bottom: 10px; width: 100%;">
|
<table style="margin-bottom: 10px; width: 100%;">
|
||||||
<tr><th>{{ i18n "usage" }}</th><th>{{ i18n "pages.inbounds.totalFlow" }}</th><th>{{ i18n "pages.inbounds.expireDate" }}</th><th>{{ i18n "enable" }}</th></tr>
|
<tr>
|
||||||
|
<th>{{ i18n "usage" }}</th>
|
||||||
|
<th>{{ i18n "pages.inbounds.totalFlow" }}</th>
|
||||||
|
<th>{{ i18n "pages.inbounds.expireDate" }}</th>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a-tag :color="statsColor(infoModal.clientStats)">
|
<a-tag v-if="infoModal.clientStats" :color="statsColor(infoModal.clientStats)">
|
||||||
[[ sizeFormat(infoModal.clientStats['up']) ]] /
|
[[ sizeFormat(infoModal.clientStats['up']) ]] /
|
||||||
[[ sizeFormat(infoModal.clientStats['down']) ]]
|
[[ sizeFormat(infoModal.clientStats['down']) ]]
|
||||||
([[ sizeFormat(infoModal.clientStats['up'] + infoModal.clientStats['down']) ]])
|
([[ sizeFormat(infoModal.clientStats['up'] + infoModal.clientStats['down']) ]])
|
||||||
@@ -86,14 +96,82 @@
|
|||||||
[[ DateUtil.formatMillis(infoModal.clientSettings.expiryTime) ]]
|
[[ DateUtil.formatMillis(infoModal.clientSettings.expiryTime) ]]
|
||||||
</a-tag>
|
</a-tag>
|
||||||
</template>
|
</template>
|
||||||
|
<a-tag v-else-if="infoModal.clientSettings.expiryTime < 0" color="cyan">[[ infoModal.clientSettings.expiryTime / -86400000 ]] {{ i18n "pages.client.days" }}</a-tag>
|
||||||
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
|
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
|
||||||
<a-tag v-if="infoModal.clientStats.enable" color="blue">{{ i18n "enabled" }}</a-tag>
|
|
||||||
<a-tag v-else color="red">{{ i18n "disabled" }}</a-tag>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</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>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="infoModal.clientSettings.tgId">
|
||||||
|
<td>Telegram Username</td>
|
||||||
|
<td><a :href="[[ tgBase + infoModal.clientSettings.tgId ]]" target="_blank">@[[ infoModal.clientSettings.tgId ]]</a></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<a-divider></a-divider>
|
||||||
|
<table v-if="inbound.protocol == Protocols.SHADOWSOCKS" style="margin-bottom: 10px; width: 100%;">
|
||||||
|
<tr>
|
||||||
|
<th>{{ i18n "encryption" }}</th>
|
||||||
|
<th>{{ i18n "password" }}</th>
|
||||||
|
<th>{{ i18n "pages.inbounds.network" }}</th>
|
||||||
|
</tr><tr>
|
||||||
|
<td><a-tag color="green">[[ inbound.settings.method ]]</a-tag></td>
|
||||||
|
<td><a-tag color="blue">[[ inbound.settings.password ]]</a-tag></td>
|
||||||
|
<td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<table v-if="inbound.protocol == Protocols.DOKODEMO" style="margin-bottom: 10px; width: 100%;">
|
||||||
|
<tr>
|
||||||
|
<th>{{ i18n "pages.inbounds.targetAddress" }}</th>
|
||||||
|
<th>{{ i18n "pages.inbounds.destinationPort" }}</th>
|
||||||
|
<th>{{ i18n "pages.inbounds.network" }}</th>
|
||||||
|
<th>FollowRedirect</th>
|
||||||
|
</tr><tr>
|
||||||
|
<td><a-tag color="green">[[ inbound.settings.address ]]</a-tag></td>
|
||||||
|
<td><a-tag color="blue">[[ inbound.settings.port ]]</a-tag></td>
|
||||||
|
<td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td>
|
||||||
|
<td><a-tag color="blue">[[ inbound.settings.followRedirect ]]</a-tag></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</table>
|
||||||
|
<table v-if="inbound.protocol == Protocols.SOCKS" style="margin-bottom: 10px; width: 100%;">
|
||||||
|
<tr>
|
||||||
|
<th>{{ i18n "password" }} Auth</th>
|
||||||
|
<th>{{ i18n "pages.inbounds.enable" }} udp</th>
|
||||||
|
<th>IP</th>
|
||||||
|
</tr><tr>
|
||||||
|
<td><a-tag color="green">[[ inbound.settings.auth ]]</a-tag></td>
|
||||||
|
<td><a-tag color="blue">[[ inbound.settings.udp]]</a-tag></td>
|
||||||
|
<td><a-tag color="green">[[ inbound.settings.ip ]]</a-tag></td>
|
||||||
|
</tr><tr v-if="inbound.settings.auth == 'password'">
|
||||||
|
<td> </td>
|
||||||
|
<td>{{ i18n "username" }}</td>
|
||||||
|
<td>{{ i18n "password" }}</td>
|
||||||
|
</tr><tr v-for="account,index in inbound.settings.accounts">
|
||||||
|
<td><a-tag color="green">[[ index ]]</a-tag></td>
|
||||||
|
<td><a-tag color="blue">[[ account.user ]]</a-tag></td>
|
||||||
|
<td><a-tag color="green">[[ account.pass ]]</a-tag></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</table>
|
||||||
|
<table v-if="inbound.protocol == Protocols.HTTP" style="margin-bottom: 10px; width: 100%;">
|
||||||
|
<tr>
|
||||||
|
<th> </th>
|
||||||
|
<th>{{ i18n "username" }}</th>
|
||||||
|
<th>{{ i18n "password" }}</th>
|
||||||
|
</tr><tr v-for="account,index in inbound.settings.accounts">
|
||||||
|
<td><a-tag color="green">[[ index ]]</a-tag></td>
|
||||||
|
<td><a-tag color="blue">[[ account.user ]]</a-tag></td>
|
||||||
|
<td><a-tag color="green">[[ account.pass ]]</a-tag></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
<div v-if="dbInbound.hasLink()">
|
<div v-if="dbInbound.hasLink()">
|
||||||
<a-divider>URL</a-divider>
|
<a-divider>URL</a-divider>
|
||||||
<p>[[ infoModal.link ]]</p>
|
<p>[[ infoModal.link ]]</p>
|
||||||
@@ -101,44 +179,35 @@
|
|||||||
</div>
|
</div>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
const infoModal = {
|
const infoModal = {
|
||||||
visible: false,
|
visible: false,
|
||||||
inbound: new Inbound(),
|
inbound: new Inbound(),
|
||||||
dbInbound: new DBInbound(),
|
dbInbound: new DBInbound(),
|
||||||
clientSettings: new Inbound.Settings(),
|
settings: null,
|
||||||
|
clientSettings: null,
|
||||||
clientStats: [],
|
clientStats: [],
|
||||||
upStats: 0,
|
upStats: 0,
|
||||||
downStats: 0,
|
downStats: 0,
|
||||||
clipboard: null,
|
clipboard: null,
|
||||||
link: null,
|
link: null,
|
||||||
index: 0,
|
index: null,
|
||||||
isExpired: false,
|
isExpired: false,
|
||||||
show(dbInbound, index=0) {
|
show(dbInbound, index) {
|
||||||
this.index = index;
|
this.index = index;
|
||||||
this.inbound = dbInbound.toInbound();
|
this.inbound = dbInbound.toInbound();
|
||||||
this.dbInbound = new DBInbound(dbInbound);
|
this.dbInbound = new DBInbound(dbInbound);
|
||||||
this.link = dbInbound.genLink(index);
|
this.link = dbInbound.genLink(index);
|
||||||
this.clientSettings = Object.values(JSON.parse(this.inbound.settings).clients)[index];
|
this.settings = JSON.parse(this.inbound.settings);
|
||||||
this.clientStats = dbInbound.clientStats;
|
this.clientSettings = this.settings.clients ? Object.values(this.settings.clients)[index] : null;
|
||||||
this.isExpired = this.inbound.isExpiry(index);
|
this.isExpired = this.inbound.isExpiry(index);
|
||||||
if(dbInbound.clientStats.length > 0)
|
this.clientStats = this.settings.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : [];
|
||||||
{
|
|
||||||
for (const key in dbInbound.clientStats) {
|
|
||||||
if (Object.hasOwnProperty.call(dbInbound.clientStats, key)) {
|
|
||||||
if(dbInbound.clientStats[key]['email'] == this.clientSettings.email)
|
|
||||||
this.clientStats = dbInbound.clientStats[key];
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
infoModalApp.$nextTick(() => {
|
infoModalApp.$nextTick(() => {
|
||||||
if (this.clipboard === null) {
|
if (this.clipboard === null) {
|
||||||
this.clipboard = new ClipboardJS('#copy-url-link', {
|
this.clipboard = new ClipboardJS('#copy-url-link', {
|
||||||
text: () => this.link,
|
text: () => this.link,
|
||||||
});
|
});
|
||||||
this.clipboard.on('success', () => app.$message.success('{{ i18n "copySuccess" }}'));
|
this.clipboard.on('success', () => app.$message.success('{{ i18n "copied" }}'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -146,6 +215,7 @@
|
|||||||
infoModal.visible = false;
|
infoModal.visible = false;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const infoModalApp = new Vue({
|
const infoModalApp = new Vue({
|
||||||
delimiters: ['[[', ']]'],
|
delimiters: ['[[', ']]'],
|
||||||
el: '#inbound-info-modal',
|
el: '#inbound-info-modal',
|
||||||
@@ -156,32 +226,45 @@
|
|||||||
},
|
},
|
||||||
get inbound() {
|
get inbound() {
|
||||||
return this.infoModal.inbound;
|
return this.infoModal.inbound;
|
||||||
}
|
},
|
||||||
|
get isActive() {
|
||||||
|
if(infoModal.clientStats){
|
||||||
|
return infoModal.clientStats.enable;
|
||||||
|
}
|
||||||
|
return infoModal.dbInbound.isEnable;
|
||||||
|
},
|
||||||
|
get isEnable() {
|
||||||
|
if(infoModal.clientSettings){
|
||||||
|
return infoModal.clientSettings.enable;
|
||||||
|
}
|
||||||
|
return infoModal.dbInbound.isEnable;
|
||||||
|
},
|
||||||
|
get subBase() {
|
||||||
|
return window.location.protocol + "//" + window.location.hostname + (window.location.port ? ":" + window.location.port:"") + "/sub/";
|
||||||
|
},
|
||||||
|
get tgBase() {
|
||||||
|
return "https://t.me/"
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
setQrCode(elmentId,index) {
|
|
||||||
content = infoModal.inbound.genLink(infoModal.dbInbound.address,infoModal.dbInbound.remark,index)
|
|
||||||
new QRious({
|
|
||||||
element: document.querySelector('#'+elmentId),
|
|
||||||
size: 260,
|
|
||||||
value: content,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
copyTextToClipboard(elmentId,content) {
|
copyTextToClipboard(elmentId,content) {
|
||||||
this.infoModal.clipboard = new ClipboardJS('#' + elmentId, {
|
this.infoModal.clipboard = new ClipboardJS('#' + elmentId, {
|
||||||
text: () => content,
|
text: () => content,
|
||||||
});
|
});
|
||||||
this.infoModal.clipboard.on('success', () => {
|
this.infoModal.clipboard.on('success', () => {
|
||||||
app.$message.success('{{ i18n "copySuccess" }}')
|
app.$message.success('{{ i18n "copied" }}')
|
||||||
this.infoModal.clipboard.destroy();
|
this.infoModal.clipboard.destroy();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
statsColor(stats) {
|
statsColor(stats) {
|
||||||
|
if(!stats) return 'blue'
|
||||||
if(stats['total'] === 0) return 'blue'
|
if(stats['total'] === 0) return 'blue'
|
||||||
else if(stats['total'] > 0 && (stats['down']+stats['up']) < stats['total']) return 'cyan'
|
else if(stats['total'] > 0 && (stats['down']+stats['up']) < stats['total']) return 'cyan'
|
||||||
else return 'red'
|
else return 'red'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{{define "inboundModal"}}
|
{{define "inboundModal"}}
|
||||||
<a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title" @ok="inModal.ok"
|
<a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title" @ok="inModal.ok"
|
||||||
:confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false"
|
:confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false"
|
||||||
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||||
:ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
|
:ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
|
||||||
{{template "form/inbound"}}
|
{{template "form/inbound"}}
|
||||||
</a-modal>
|
</a-modal>
|
||||||
@@ -89,93 +89,25 @@
|
|||||||
removeClient(index, clients) {
|
removeClient(index, clients) {
|
||||||
clients.splice(index, 1);
|
clients.splice(index, 1);
|
||||||
},
|
},
|
||||||
async getDBClientIps(email, event) {
|
|
||||||
const msg = await HttpUtil.post('/xui/inbound/clientIps/' + email);
|
|
||||||
if (!msg.success) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
let ips = JSON.parse(msg.obj);
|
|
||||||
ips = ips.join(",");
|
|
||||||
event.target.value = ips;
|
|
||||||
} catch (error) {
|
|
||||||
event.target.value = msg.obj;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async clearDBClientIps(email,event) {
|
|
||||||
const msg = await HttpUtil.post('/xui/inbound/clearClientIps/'+ email);
|
|
||||||
if (!msg.success) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
event.target.value = ""
|
|
||||||
},
|
|
||||||
async resetClientTraffic(client, event) {
|
|
||||||
const msg = await HttpUtil.post(`/xui/inbound/resetClientTraffic/${client.email}`);
|
|
||||||
if (!msg.success) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const clientStats = this.inbound.clientStats;
|
|
||||||
if (clientStats.length > 0) {
|
|
||||||
for (let i = 0; i < clientStats.length; i++) {
|
|
||||||
if (clientStats[i].email === client.email) {
|
|
||||||
clientStats[i].up = 0;
|
|
||||||
clientStats[i].down = 0;
|
|
||||||
break; // Stop looping once we've found the matching client.
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isExpiry(index) {
|
isExpiry(index) {
|
||||||
return this.inbound.isExpiry(index)
|
return this.inbound.isExpiry(index)
|
||||||
},
|
},
|
||||||
getUpStats(email) {
|
|
||||||
clientStats = this.inbound.clientStats
|
|
||||||
if(clientStats.length > 0)
|
|
||||||
{
|
|
||||||
for (const key in clientStats) {
|
|
||||||
if (Object.hasOwnProperty.call(clientStats, key)) {
|
|
||||||
if(clientStats[key]['email'] == email)
|
|
||||||
return clientStats[key]['up']
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getDownStats(email) {
|
|
||||||
clientStats = this.inbound.clientStats
|
|
||||||
if(clientStats.length > 0)
|
|
||||||
{
|
|
||||||
for (const key in clientStats) {
|
|
||||||
if (Object.hasOwnProperty.call(clientStats, key)) {
|
|
||||||
if(clientStats[key]['email'] == email)
|
|
||||||
return clientStats[key]['down']
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isClientEnable(email) {
|
isClientEnable(email) {
|
||||||
clientStats = this.dbInbound.clientStats ? this.dbInbound.clientStats.find(stats => stats.email === email) : null
|
clientStats = this.dbInbound.clientStats ? this.dbInbound.clientStats.find(stats => stats.email === email) : null
|
||||||
return clientStats ? clientStats['enable'] : true
|
return clientStats ? clientStats['enable'] : true
|
||||||
},
|
},
|
||||||
getHeaderText(email) {
|
setDefaultCertData(){
|
||||||
if(email == "")
|
inModal.inbound.stream.tls.certs[0].certFile = app.defaultCert;
|
||||||
return "Add Client"
|
inModal.inbound.stream.tls.certs[0].keyFile = app.defaultKey;
|
||||||
|
|
||||||
return email + (this.isClientEnable(email) == true ? ' Active' : ' Deactive')
|
|
||||||
},
|
|
||||||
getHeaderStyle(email) {
|
|
||||||
return (this.isClientEnable(email) == true ? '' : 'deactive-client')
|
|
||||||
},
|
},
|
||||||
getNewEmail(client) {
|
getNewEmail(client) {
|
||||||
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
|
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
|
||||||
var string = '';
|
var string = '';
|
||||||
var len = 7 + Math.floor(Math.random() * 5)
|
var len = 6 + Math.floor(Math.random() * 5);
|
||||||
for(var ii=0; ii<len; ii++){
|
for(var ii=0; ii<len; ii++){
|
||||||
string += chars[Math.floor(Math.random() * chars.length)];
|
string += chars[Math.floor(Math.random() * chars.length)];
|
||||||
}
|
}
|
||||||
client.email = string
|
client.email = string;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,22 +27,38 @@
|
|||||||
<a-card hoverable style="margin-bottom: 20px;" :class="siderDrawer.isDarkTheme ? darkClass : ''">
|
<a-card hoverable style="margin-bottom: 20px;" :class="siderDrawer.isDarkTheme ? darkClass : ''">
|
||||||
<a-row>
|
<a-row>
|
||||||
<a-col :xs="24" :sm="24" :lg="12">
|
<a-col :xs="24" :sm="24" :lg="12">
|
||||||
{{ i18n "pages.inbounds.totalDownUp" }}:
|
{{ i18n "pages.inbounds.totalDownUp" }}:
|
||||||
<a-tag color="green">[[ sizeFormat(total.up) ]] / [[ sizeFormat(total.down) ]]</a-tag>
|
<a-tag color="green">[[ sizeFormat(total.up) ]] / [[ sizeFormat(total.down) ]]</a-tag>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :sm="24" :lg="12">
|
<a-col :xs="24" :sm="24" :lg="12">
|
||||||
{{ i18n "pages.inbounds.totalUsage" }}:
|
{{ i18n "pages.inbounds.totalUsage" }}:
|
||||||
<a-tag color="green">[[ sizeFormat(total.up + total.down) ]]</a-tag>
|
<a-tag color="green">[[ sizeFormat(total.up + total.down) ]]</a-tag>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :sm="24" :lg="12">
|
<a-col :xs="24" :sm="24" :lg="12">
|
||||||
{{ i18n "pages.inbounds.inboundCount" }}:
|
{{ i18n "pages.inbounds.inboundCount" }}:
|
||||||
<a-tag color="green">[[ dbInbounds.length ]]</a-tag>
|
<a-tag color="green">[[ dbInbounds.length ]]</a-tag>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :sm="24" :lg="12">
|
<a-col :xs="24" :sm="24" :lg="12">
|
||||||
{{ i18n "clients" }}:
|
{{ i18n "clients" }}:
|
||||||
<a-tag color="green">[[ total.clients ]]</a-tag>
|
<a-tag color="green">[[ total.clients ]]</a-tag>
|
||||||
<a-tag color="blue">{{ i18n "enabled" }} [[ total.active ]]</a-tag>
|
<a-popover title="{{ i18n "disabled" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
|
||||||
<a-tag color="red">{{ i18n "disabled" }} [[ total.deactive ]]</a-tag>
|
<template slot="content">
|
||||||
|
<p v-for="clientEmail in total.deactive">[[ clientEmail ]]</p>
|
||||||
|
</template>
|
||||||
|
<a-tag v-if="total.deactive.length">[[ total.deactive.length ]]</a-tag>
|
||||||
|
</a-popover>
|
||||||
|
<a-popover title="{{ i18n "depleted" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
|
||||||
|
<template slot="content">
|
||||||
|
<p v-for="clientEmail in total.depleted">[[ clientEmail ]]</p>
|
||||||
|
</template>
|
||||||
|
<a-tag color="red" v-if="total.depleted.length">[[ total.depleted.length ]]</a-tag>
|
||||||
|
</a-popover>
|
||||||
|
<a-popover title="{{ i18n "depletingSoon" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
|
||||||
|
<template slot="content">
|
||||||
|
<p v-for="clientEmail in total.expiring">[[ clientEmail ]]</p>
|
||||||
|
</template>
|
||||||
|
<a-tag color="orange" v-if="total.expiring.length">[[ total.expiring.length ]]</a-tag>
|
||||||
|
</a-popover>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
</a-card>
|
</a-card>
|
||||||
@@ -50,10 +66,11 @@
|
|||||||
<transition name="list" appear>
|
<transition name="list" appear>
|
||||||
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
|
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
|
||||||
<div slot="title">
|
<div slot="title">
|
||||||
<a-button type="primary" @click="openAddInbound">Add Inbound</a-button>
|
<a-button type="primary" icon="plus" @click="openAddInbound">{{ i18n "pages.inbounds.addInbound" }}</a-button>
|
||||||
<a-button type="primary" @click="exportAllLinks" class="copy-btn">Export Links</a-button>
|
<a-button type="primary" icon="export" @click="exportAllLinks">{{ i18n "pages.inbounds.export" }}</a-button>
|
||||||
|
<a-button type="primary" icon="reload" @click="resetAllTraffic">{{ i18n "pages.inbounds.resetAllTraffic" }}</a-button>
|
||||||
</div>
|
</div>
|
||||||
<a-input v-model.lazy="searchKey" placeholder="{{ i18n "search" }}" autofocus style="max-width: 300px"></a-input>
|
<a-input v-model.lazy="searchKey" placeholder="{{ i18n "search" }}" autofocus style="max-width: 300px"></a-input>
|
||||||
<a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
|
<a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
|
||||||
:data-source="searchedInbounds"
|
:data-source="searchedInbounds"
|
||||||
:loading="spinning" :scroll="{ x: 1300 }"
|
:loading="spinning" :scroll="{ x: 1300 }"
|
||||||
@@ -64,7 +81,7 @@
|
|||||||
<a-icon type="edit" style="font-size: 25px" @click="openEditInbound(dbInbound.id);"></a-icon>
|
<a-icon type="edit" style="font-size: 25px" @click="openEditInbound(dbInbound.id);"></a-icon>
|
||||||
<a-dropdown :trigger="['click']">
|
<a-dropdown :trigger="['click']">
|
||||||
<a @click="e => e.preventDefault()">{{ i18n "pages.inbounds.operate" }}</a>
|
<a @click="e => e.preventDefault()">{{ i18n "pages.inbounds.operate" }}</a>
|
||||||
<a-menu slot="overlay" @click="a => clickAction(a, dbInbound)">
|
<a-menu slot="overlay" @click="a => clickAction(a, dbInbound)" :theme="siderDrawer.theme">
|
||||||
<a-menu-item v-if="dbInbound.isSS" key="qrcode">
|
<a-menu-item v-if="dbInbound.isSS" key="qrcode">
|
||||||
<a-icon type="qrcode"></a-icon>
|
<a-icon type="qrcode"></a-icon>
|
||||||
{{ i18n "qrCode" }}
|
{{ i18n "qrCode" }}
|
||||||
@@ -73,15 +90,36 @@
|
|||||||
<a-icon type="edit"></a-icon>
|
<a-icon type="edit"></a-icon>
|
||||||
{{ i18n "edit" }}
|
{{ i18n "edit" }}
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
<template v-if="dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess">
|
<template v-if="dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess">
|
||||||
|
<a-menu-item key="addClient">
|
||||||
|
<a-icon type="user-add"></a-icon>
|
||||||
|
{{ i18n "pages.client.add"}}
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item key="addBulkClient">
|
||||||
|
<a-icon type="usergroup-add"></a-icon>
|
||||||
|
{{ i18n "pages.client.bulk"}}
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item key="resetClients">
|
||||||
|
<a-icon type="file-done"></a-icon>
|
||||||
|
{{ i18n "pages.inbounds.resetAllClientTraffics"}}
|
||||||
|
</a-menu-item>
|
||||||
<a-menu-item key="export">
|
<a-menu-item key="export">
|
||||||
<a-icon type="export"></a-icon>
|
<a-icon type="export"></a-icon>
|
||||||
{{ i18n "pages.inbounds.export"}}
|
{{ i18n "pages.inbounds.export"}}
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<a-menu-item key="showInfo">
|
||||||
|
<a-icon type="info-circle"></a-icon>
|
||||||
|
{{ i18n "info"}}
|
||||||
|
</a-menu-item>
|
||||||
|
</template>
|
||||||
<a-menu-item key="resetTraffic">
|
<a-menu-item key="resetTraffic">
|
||||||
<a-icon type="retweet"></a-icon> {{ i18n "pages.inbounds.resetTraffic" }}
|
<a-icon type="retweet"></a-icon> {{ i18n "pages.inbounds.resetTraffic" }}
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
|
<a-menu-item key="clone">
|
||||||
|
<a-icon type="block"></a-icon> {{ i18n "pages.inbounds.Clone"}}
|
||||||
|
</a-menu-item>
|
||||||
<a-menu-item key="delete">
|
<a-menu-item key="delete">
|
||||||
<span style="color: #FF4D4F">
|
<span style="color: #FF4D4F">
|
||||||
<a-icon type="delete"></a-icon> {{ i18n "delete"}}
|
<a-icon type="delete"></a-icon> {{ i18n "delete"}}
|
||||||
@@ -91,7 +129,36 @@
|
|||||||
</a-dropdown>
|
</a-dropdown>
|
||||||
</template>
|
</template>
|
||||||
<template slot="protocol" slot-scope="text, dbInbound">
|
<template slot="protocol" slot-scope="text, dbInbound">
|
||||||
<a-tag color="blue">[[ dbInbound.protocol ]]</a-tag>
|
<a-tag style="margin:0;" color="blue">[[ dbInbound.protocol ]]</a-tag>
|
||||||
|
<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>
|
||||||
|
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isReality" color="cyan">Reality</a-tag>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<template slot="clients" slot-scope="text, dbInbound">
|
||||||
|
<template v-if="clientCount[dbInbound.id]">
|
||||||
|
<a-tag style="margin:0;" color="green">[[ clientCount[dbInbound.id].clients ]]</a-tag>
|
||||||
|
<a-popover title="{{ i18n "disabled" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
|
||||||
|
<template slot="content">
|
||||||
|
<p v-for="clientEmail in clientCount[dbInbound.id].deactive">[[ clientEmail ]]</p>
|
||||||
|
</template>
|
||||||
|
<a-tag style="margin:0; padding: 0 2px;" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag>
|
||||||
|
</a-popover>
|
||||||
|
<a-popover title="{{ i18n "depleted" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
|
||||||
|
<template slot="content">
|
||||||
|
<p v-for="clientEmail in clientCount[dbInbound.id].depleted">[[ clientEmail ]]</p>
|
||||||
|
</template>
|
||||||
|
<a-tag style="margin:0; padding: 0 2px;" color="red" v-if="clientCount[dbInbound.id].depleted.length">[[ clientCount[dbInbound.id].depleted.length ]]</a-tag>
|
||||||
|
</a-popover>
|
||||||
|
<a-popover title="{{ i18n "depletingSoon" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
|
||||||
|
<template slot="content">
|
||||||
|
<p v-for="clientEmail in clientCount[dbInbound.id].expiring">[[ clientEmail ]]</p>
|
||||||
|
</template>
|
||||||
|
<a-tag style="margin:0; padding: 0 2px;" color="orange" v-if="clientCount[dbInbound.id].expiring.length">[[ clientCount[dbInbound.id].expiring.length ]]</a-tag>
|
||||||
|
</a-popover>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<template slot="traffic" slot-scope="text, dbInbound">
|
<template slot="traffic" slot-scope="text, dbInbound">
|
||||||
<a-tag color="blue">[[ sizeFormat(dbInbound.up) ]] / [[ sizeFormat(dbInbound.down) ]]</a-tag>
|
<a-tag color="blue">[[ sizeFormat(dbInbound.up) ]] / [[ sizeFormat(dbInbound.down) ]]</a-tag>
|
||||||
@@ -101,16 +168,8 @@
|
|||||||
</template>
|
</template>
|
||||||
<a-tag v-else color="green">{{ i18n "unlimited" }}</a-tag>
|
<a-tag v-else color="green">{{ i18n "unlimited" }}</a-tag>
|
||||||
</template>
|
</template>
|
||||||
<template slot="stream" slot-scope="text, dbInbound, index">
|
|
||||||
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
|
|
||||||
<a-tag color="green">[[ inbounds[index].stream.network ]]</a-tag>
|
|
||||||
<a-tag v-if="inbounds[index].stream.isTls" color="blue">tls</a-tag>
|
|
||||||
<a-tag v-if="inbounds[index].stream.isXTls" color="blue">xtls</a-tag>
|
|
||||||
</template>
|
|
||||||
<template v-else>{{ i18n "none" }}</template>
|
|
||||||
</template>
|
|
||||||
<template slot="enable" slot-scope="text, dbInbound">
|
<template slot="enable" slot-scope="text, dbInbound">
|
||||||
<a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound)"></a-switch>
|
<a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound.id)"></a-switch>
|
||||||
</template>
|
</template>
|
||||||
<template slot="expiryTime" slot-scope="text, dbInbound">
|
<template slot="expiryTime" slot-scope="text, dbInbound">
|
||||||
<template v-if="dbInbound.expiryTime > 0">
|
<template v-if="dbInbound.expiryTime > 0">
|
||||||
@@ -131,7 +190,7 @@
|
|||||||
:data-source="getInboundClients(record)"
|
:data-source="getInboundClients(record)"
|
||||||
:pagination="false"
|
:pagination="false"
|
||||||
>
|
>
|
||||||
{{template "client_row"}}
|
{{template "client_table"}}
|
||||||
</a-table>
|
</a-table>
|
||||||
<a-table
|
<a-table
|
||||||
v-else-if="record.protocol === Protocols.TROJAN"
|
v-else-if="record.protocol === Protocols.TROJAN"
|
||||||
@@ -140,16 +199,7 @@
|
|||||||
:data-source="getInboundClients(record)"
|
:data-source="getInboundClients(record)"
|
||||||
:pagination="false"
|
:pagination="false"
|
||||||
>
|
>
|
||||||
{{template "client_row"}}
|
{{template "client_table"}}
|
||||||
</a-table>
|
|
||||||
<a-table
|
|
||||||
v-else
|
|
||||||
:row-key="client => client.id"
|
|
||||||
:columns="innerOneColumns"
|
|
||||||
:data-source="record"
|
|
||||||
:pagination="false"
|
|
||||||
>
|
|
||||||
{{template "client_row"}}
|
|
||||||
</a-table>
|
</a-table>
|
||||||
</template>
|
</template>
|
||||||
</a-table>
|
</a-table>
|
||||||
@@ -173,7 +223,7 @@
|
|||||||
width: 40,
|
width: 40,
|
||||||
scopedSlots: { customRender: 'enable' },
|
scopedSlots: { customRender: 'enable' },
|
||||||
}, {
|
}, {
|
||||||
title: "Id",
|
title: "ID",
|
||||||
align: 'center',
|
align: 'center',
|
||||||
dataIndex: "id",
|
dataIndex: "id",
|
||||||
width: 30,
|
width: 30,
|
||||||
@@ -182,26 +232,26 @@
|
|||||||
align: 'center',
|
align: 'center',
|
||||||
width: 80,
|
width: 80,
|
||||||
dataIndex: "remark",
|
dataIndex: "remark",
|
||||||
}, {
|
|
||||||
title: '{{ i18n "pages.inbounds.protocol" }}',
|
|
||||||
align: 'center',
|
|
||||||
width: 50,
|
|
||||||
scopedSlots: { customRender: 'protocol' },
|
|
||||||
}, {
|
}, {
|
||||||
title: '{{ i18n "pages.inbounds.port" }}',
|
title: '{{ i18n "pages.inbounds.port" }}',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
dataIndex: "port",
|
dataIndex: "port",
|
||||||
width: 40,
|
width: 40,
|
||||||
|
}, {
|
||||||
|
title: '{{ i18n "pages.inbounds.protocol" }}',
|
||||||
|
align: 'left',
|
||||||
|
width: 80,
|
||||||
|
scopedSlots: { customRender: 'protocol' },
|
||||||
|
}, {
|
||||||
|
title: '{{ i18n "clients" }}',
|
||||||
|
align: 'left',
|
||||||
|
width: 50,
|
||||||
|
scopedSlots: { customRender: 'clients' },
|
||||||
}, {
|
}, {
|
||||||
title: '{{ i18n "pages.inbounds.traffic" }}↑|↓',
|
title: '{{ i18n "pages.inbounds.traffic" }}↑|↓',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
width: 150,
|
width: 120,
|
||||||
scopedSlots: { customRender: 'traffic' },
|
scopedSlots: { customRender: 'traffic' },
|
||||||
},{
|
|
||||||
title: '{{ i18n "pages.inbounds.transportConfig" }}',
|
|
||||||
align: 'center',
|
|
||||||
width: 60,
|
|
||||||
scopedSlots: { customRender: 'stream' },
|
|
||||||
}, {
|
}, {
|
||||||
title: '{{ i18n "pages.inbounds.expireDate" }}',
|
title: '{{ i18n "pages.inbounds.expireDate" }}',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
@@ -210,24 +260,21 @@
|
|||||||
}];
|
}];
|
||||||
|
|
||||||
const innerColumns = [
|
const innerColumns = [
|
||||||
{ title: '', width: 70, scopedSlots: { customRender: 'actions' } },
|
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } },
|
||||||
{ title: '{{ i18n "pages.inbounds.client" }}', width: 60, scopedSlots: { customRender: 'client' } },
|
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 30, scopedSlots: { customRender: 'enable' } },
|
||||||
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 100, scopedSlots: { customRender: 'traffic' } },
|
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
|
||||||
|
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 120, scopedSlots: { customRender: 'traffic' } },
|
||||||
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
|
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
|
||||||
{ title: 'UID', width: 150, dataIndex: "id" },
|
{ title: 'UID', width: 120, dataIndex: "id" },
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const innerTrojanColumns = [
|
const innerTrojanColumns = [
|
||||||
{ title: '', width: 70, scopedSlots: { customRender: 'actions' } },
|
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } },
|
||||||
{ title: '{{ i18n "pages.inbounds.client" }}', width: 60, scopedSlots: { customRender: 'client' } },
|
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 30, scopedSlots: { customRender: 'enable' } },
|
||||||
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 100, scopedSlots: { customRender: 'traffic' } },
|
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
|
||||||
|
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 120, scopedSlots: { customRender: 'traffic' } },
|
||||||
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
|
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
|
||||||
{ title: 'Password', width: 100, dataIndex: "password" },
|
{ title: 'Password', width: 120, dataIndex: "password" },
|
||||||
];
|
|
||||||
|
|
||||||
const innerOneColumns = [
|
|
||||||
{ title: '', width: 70, scopedSlots: { customRender: 'actions' } },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const app = new Vue({
|
const app = new Vue({
|
||||||
@@ -240,6 +287,11 @@
|
|||||||
dbInbounds: [],
|
dbInbounds: [],
|
||||||
searchKey: '',
|
searchKey: '',
|
||||||
searchedInbounds: [],
|
searchedInbounds: [],
|
||||||
|
expireDiff: 0,
|
||||||
|
trafficDiff: 0,
|
||||||
|
defaultCert: '',
|
||||||
|
defaultKey: '',
|
||||||
|
clientCount: {},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
loading(spinning=true) {
|
loading(spinning=true) {
|
||||||
@@ -253,6 +305,19 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setInbounds(msg.obj);
|
this.setInbounds(msg.obj);
|
||||||
|
this.searchKey = '';
|
||||||
|
},
|
||||||
|
async getDefaultSettings() {
|
||||||
|
this.loading();
|
||||||
|
const msg = await HttpUtil.post('/xui/setting/defaultSettings');
|
||||||
|
this.loading(false);
|
||||||
|
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;
|
||||||
},
|
},
|
||||||
setInbounds(dbInbounds) {
|
setInbounds(dbInbounds) {
|
||||||
this.inbounds.splice(0);
|
this.inbounds.splice(0);
|
||||||
@@ -260,11 +325,48 @@
|
|||||||
this.searchedInbounds.splice(0);
|
this.searchedInbounds.splice(0);
|
||||||
for (const inbound of dbInbounds) {
|
for (const inbound of dbInbounds) {
|
||||||
const dbInbound = new DBInbound(inbound);
|
const dbInbound = new DBInbound(inbound);
|
||||||
this.inbounds.push(dbInbound.toInbound());
|
to_inbound = dbInbound.toInbound()
|
||||||
|
this.inbounds.push(to_inbound);
|
||||||
this.dbInbounds.push(dbInbound);
|
this.dbInbounds.push(dbInbound);
|
||||||
this.searchedInbounds.push(dbInbound);
|
this.searchedInbounds.push(dbInbound);
|
||||||
|
if([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN].includes(inbound.protocol) ){
|
||||||
|
this.clientCount[inbound.id] = this.getClientCounts(inbound,to_inbound);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
getClientCounts(dbInbound,inbound){
|
||||||
|
let clientCount = 0,active = [], deactive = [], depleted = [], expiring = [];
|
||||||
|
clients = this.getClients(dbInbound.protocol, inbound.settings);
|
||||||
|
clientStats = dbInbound.clientStats
|
||||||
|
now = new Date().getTime()
|
||||||
|
if(clients){
|
||||||
|
clientCount = clients.length;
|
||||||
|
if(dbInbound.enable){
|
||||||
|
clients.forEach(client => {
|
||||||
|
client.enable ? active.push(client.email) : deactive.push(client.email);
|
||||||
|
});
|
||||||
|
clientStats.forEach(client => {
|
||||||
|
if(!client.enable) {
|
||||||
|
depleted.push(client.email);
|
||||||
|
} else {
|
||||||
|
if ((client.expiryTime > 0 && (client.expiryTime-now < this.expireDiff)) ||
|
||||||
|
(client.total > 0 && (client.total-(client.up+client.down) < this.trafficDiff ))) expiring.push(client.email);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
clients.forEach(client => {
|
||||||
|
deactive.push(client.email);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
clients: clientCount,
|
||||||
|
active: active,
|
||||||
|
deactive: deactive,
|
||||||
|
depleted: depleted,
|
||||||
|
expiring: expiring,
|
||||||
|
};
|
||||||
|
},
|
||||||
searchInbounds(key) {
|
searchInbounds(key) {
|
||||||
if (ObjectUtil.isEmpty(key)) {
|
if (ObjectUtil.isEmpty(key)) {
|
||||||
this.searchedInbounds = this.dbInbounds.slice();
|
this.searchedInbounds = this.dbInbounds.slice();
|
||||||
@@ -293,19 +395,67 @@
|
|||||||
case "qrcode":
|
case "qrcode":
|
||||||
this.showQrcode(dbInbound);
|
this.showQrcode(dbInbound);
|
||||||
break;
|
break;
|
||||||
case "export":
|
case "showInfo":
|
||||||
this.inboundLinks(dbInbound.id);
|
this.showInfo(dbInbound);
|
||||||
break;
|
break;
|
||||||
case "edit":
|
case "edit":
|
||||||
this.openEditInbound(dbInbound.id);
|
this.openEditInbound(dbInbound.id);
|
||||||
break;
|
break;
|
||||||
|
case "addClient":
|
||||||
|
this.openAddClient(dbInbound.id)
|
||||||
|
break;
|
||||||
|
case "addBulkClient":
|
||||||
|
this.openAddBulkClient(dbInbound.id)
|
||||||
|
break;
|
||||||
|
case "export":
|
||||||
|
this.inboundLinks(dbInbound.id);
|
||||||
|
break;
|
||||||
case "resetTraffic":
|
case "resetTraffic":
|
||||||
this.resetTraffic(dbInbound);
|
this.resetTraffic(dbInbound.id);
|
||||||
|
break;
|
||||||
|
case "resetClients":
|
||||||
|
this.resetAllClientTraffics(dbInbound.id);
|
||||||
|
break;
|
||||||
|
case "clone":
|
||||||
|
this.openCloneInbound(dbInbound);
|
||||||
break;
|
break;
|
||||||
case "delete":
|
case "delete":
|
||||||
this.delInbound(dbInbound);
|
this.delInbound(dbInbound.id);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
openCloneInbound(dbInbound) {
|
||||||
|
this.$confirm({
|
||||||
|
title: '{{ i18n "pages.inbounds.cloneInbound"}} ' + dbInbound.remark,
|
||||||
|
content: '{{ i18n "pages.inbounds.cloneInboundContent"}}',
|
||||||
|
okText: '{{ i18n "pages.inbounds.cloneInboundOk"}}',
|
||||||
|
cancelText: '{{ i18n "cancel" }}',
|
||||||
|
onOk: () => {
|
||||||
|
const baseInbound = dbInbound.toInbound();
|
||||||
|
dbInbound.up = 0;
|
||||||
|
dbInbound.down = 0;
|
||||||
|
this.cloneInbound(baseInbound, dbInbound);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async cloneInbound(baseInbound, dbInbound) {
|
||||||
|
const inbound = new Inbound();
|
||||||
|
const data = {
|
||||||
|
up: dbInbound.up,
|
||||||
|
down: dbInbound.down,
|
||||||
|
total: dbInbound.total,
|
||||||
|
remark: dbInbound.remark + " - Cloned",
|
||||||
|
enable: dbInbound.enable,
|
||||||
|
expiryTime: dbInbound.expiryTime,
|
||||||
|
|
||||||
|
listen: inbound.listen,
|
||||||
|
port: inbound.port,
|
||||||
|
protocol: baseInbound.protocol,
|
||||||
|
settings: inbound.settings.toString(),
|
||||||
|
streamSettings: baseInbound.stream.toString(),
|
||||||
|
sniffing: baseInbound.canSniffing() ? baseInbound.sniffing.toString() : '{}',
|
||||||
|
};
|
||||||
|
await this.submit('/xui/inbound/add', data, inModal);
|
||||||
},
|
},
|
||||||
openAddInbound() {
|
openAddInbound() {
|
||||||
inModal.show({
|
inModal.show({
|
||||||
@@ -320,8 +470,8 @@
|
|||||||
isEdit: false
|
isEdit: false
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
openEditInbound(dbInbound_id) {
|
openEditInbound(dbInboundId) {
|
||||||
dbInbound = this.dbInbounds.find(row => row.id === dbInbound_id);
|
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||||
const inbound = dbInbound.toInbound();
|
const inbound = dbInbound.toInbound();
|
||||||
inModal.show({
|
inModal.show({
|
||||||
title: '{{ i18n "pages.inbounds.modifyInbound"}}',
|
title: '{{ i18n "pages.inbounds.modifyInbound"}}',
|
||||||
@@ -350,9 +500,10 @@
|
|||||||
port: inbound.port,
|
port: inbound.port,
|
||||||
protocol: inbound.protocol,
|
protocol: inbound.protocol,
|
||||||
settings: inbound.settings.toString(),
|
settings: inbound.settings.toString(),
|
||||||
streamSettings: inbound.stream.toString(),
|
|
||||||
sniffing: inbound.canSniffing() ? inbound.sniffing.toString() : '{}',
|
|
||||||
};
|
};
|
||||||
|
if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString();
|
||||||
|
if (inbound.canSniffing()) data.sniffing = inbound.sniffing.toString();
|
||||||
|
|
||||||
await this.submit('/xui/inbound/add', data, inModal);
|
await this.submit('/xui/inbound/add', data, inModal);
|
||||||
},
|
},
|
||||||
async updateInbound(inbound, dbInbound) {
|
async updateInbound(inbound, dbInbound) {
|
||||||
@@ -368,15 +519,80 @@
|
|||||||
port: inbound.port,
|
port: inbound.port,
|
||||||
protocol: inbound.protocol,
|
protocol: inbound.protocol,
|
||||||
settings: inbound.settings.toString(),
|
settings: inbound.settings.toString(),
|
||||||
streamSettings: inbound.stream.toString(),
|
|
||||||
sniffing: inbound.canSniffing() ? inbound.sniffing.toString() : '{}',
|
|
||||||
};
|
};
|
||||||
|
if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString();
|
||||||
|
if (inbound.canSniffing()) data.sniffing = inbound.sniffing.toString();
|
||||||
|
|
||||||
await this.submit(`/xui/inbound/update/${dbInbound.id}`, data, inModal);
|
await this.submit(`/xui/inbound/update/${dbInbound.id}`, data, inModal);
|
||||||
},
|
},
|
||||||
resetTraffic(dbInbound) {
|
openAddClient(dbInboundId) {
|
||||||
|
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||||
|
clientModal.show({
|
||||||
|
title: '{{ i18n "pages.client.add"}}',
|
||||||
|
okText: '{{ i18n "pages.client.submitAdd"}}',
|
||||||
|
dbInbound: dbInbound,
|
||||||
|
confirm: async (inbound, dbInbound, index) => {
|
||||||
|
clientModal.loading();
|
||||||
|
await this.addClient(inbound, dbInbound);
|
||||||
|
clientModal.close();
|
||||||
|
},
|
||||||
|
isEdit: false
|
||||||
|
});
|
||||||
|
},
|
||||||
|
openAddBulkClient(dbInboundId) {
|
||||||
|
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||||
|
clientsBulkModal.show({
|
||||||
|
title: '{{ i18n "pages.client.bulk"}} ' + dbInbound.remark,
|
||||||
|
okText: '{{ i18n "pages.client.bulk"}}',
|
||||||
|
dbInbound: dbInbound,
|
||||||
|
confirm: async (inbound, dbInbound) => {
|
||||||
|
clientsBulkModal.loading();
|
||||||
|
await this.addClient(inbound, dbInbound);
|
||||||
|
clientsBulkModal.close();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
openEditClient(dbInboundId, client) {
|
||||||
|
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||||
|
clients = this.getInboundClients(dbInbound);
|
||||||
|
index = this.findIndexOfClient(clients, client);
|
||||||
|
clientModal.show({
|
||||||
|
title: '{{ i18n "pages.client.edit"}}',
|
||||||
|
okText: '{{ i18n "pages.client.submitEdit"}}',
|
||||||
|
dbInbound: dbInbound,
|
||||||
|
index: index,
|
||||||
|
confirm: async (inbound, dbInbound, index) => {
|
||||||
|
clientModal.loading();
|
||||||
|
await this.updateClient(inbound, dbInbound, index);
|
||||||
|
clientModal.close();
|
||||||
|
},
|
||||||
|
isEdit: true
|
||||||
|
});
|
||||||
|
},
|
||||||
|
findIndexOfClient(clients,client) {
|
||||||
|
firstKey = Object.keys(client)[0];
|
||||||
|
return clients.findIndex(c => c[firstKey] === client[firstKey]);
|
||||||
|
},
|
||||||
|
async addClient(inbound, dbInbound) {
|
||||||
|
const data = {
|
||||||
|
id: dbInbound.id,
|
||||||
|
settings: inbound.settings.toString(),
|
||||||
|
};
|
||||||
|
await this.submit('/xui/inbound/addClient/', data);
|
||||||
|
},
|
||||||
|
async updateClient(inbound, dbInbound, index) {
|
||||||
|
const data = {
|
||||||
|
id: dbInbound.id,
|
||||||
|
settings: inbound.settings.toString(),
|
||||||
|
};
|
||||||
|
await this.submit(`/xui/inbound/updateClient/${index}`, data);
|
||||||
|
},
|
||||||
|
resetTraffic(dbInboundId) {
|
||||||
|
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||||
this.$confirm({
|
this.$confirm({
|
||||||
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
|
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
|
||||||
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
|
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
|
||||||
|
class: siderDrawer.isDarkTheme ? darkClass : '',
|
||||||
okText: '{{ i18n "reset"}}',
|
okText: '{{ i18n "reset"}}',
|
||||||
cancelText: '{{ i18n "cancel"}}',
|
cancelText: '{{ i18n "cancel"}}',
|
||||||
onOk: () => {
|
onOk: () => {
|
||||||
@@ -387,16 +603,37 @@
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
delInbound(dbInbound) {
|
delInbound(dbInboundId) {
|
||||||
this.$confirm({
|
this.$confirm({
|
||||||
title: '{{ i18n "pages.inbounds.deleteInbound"}}',
|
title: '{{ i18n "pages.inbounds.deleteInbound"}}',
|
||||||
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
|
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
|
||||||
|
class: siderDrawer.isDarkTheme ? darkClass : '',
|
||||||
okText: '{{ i18n "delete"}}',
|
okText: '{{ i18n "delete"}}',
|
||||||
cancelText: '{{ i18n "cancel"}}',
|
cancelText: '{{ i18n "cancel"}}',
|
||||||
onOk: () => this.submit('/xui/inbound/del/' + dbInbound.id),
|
onOk: () => this.submit('/xui/inbound/del/' + dbInboundId),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getClients(protocol, clientSettings) {
|
delClient(dbInboundId,client) {
|
||||||
|
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||||
|
newDbInbound = new DBInbound(dbInbound);
|
||||||
|
inbound = newDbInbound.toInbound();
|
||||||
|
clients = this.getClients(dbInbound.protocol, inbound.settings);
|
||||||
|
index = this.findIndexOfClient(clients, client);
|
||||||
|
clients.splice(index, 1);
|
||||||
|
const data = {
|
||||||
|
id: dbInboundId,
|
||||||
|
settings: inbound.settings.toString(),
|
||||||
|
};
|
||||||
|
this.$confirm({
|
||||||
|
title: '{{ i18n "pages.inbounds.deleteInbound"}}',
|
||||||
|
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
|
||||||
|
class: siderDrawer.isDarkTheme ? darkClass : '',
|
||||||
|
okText: '{{ i18n "delete"}}',
|
||||||
|
cancelText: '{{ i18n "cancel"}}',
|
||||||
|
onOk: () => this.submit('/xui/inbound/delClient/' + client.email, data),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getClients(protocol, clientSettings) {
|
||||||
switch(protocol){
|
switch(protocol){
|
||||||
case Protocols.VMESS: return clientSettings.vmesses;
|
case Protocols.VMESS: return clientSettings.vmesses;
|
||||||
case Protocols.VLESS: return clientSettings.vlesses;
|
case Protocols.VLESS: return clientSettings.vlesses;
|
||||||
@@ -404,18 +641,29 @@
|
|||||||
default: return null;
|
default: return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
showQrcode(dbInbound, clientIndex) {
|
showQrcode(dbInbound, clientIndex) {
|
||||||
const link = dbInbound.genLink(clientIndex);
|
const link = dbInbound.genLink(clientIndex);
|
||||||
qrModal.show('{{ i18n "qrCode"}}', link, dbInbound);
|
qrModal.show('{{ i18n "qrCode"}}', link, dbInbound);
|
||||||
},
|
},
|
||||||
showInfo(dbInbound, index) {
|
showInfo(dbInbound, index) {
|
||||||
infoModal.show(dbInbound, index);
|
infoModal.show(dbInbound, index);
|
||||||
},
|
},
|
||||||
switchEnable(dbInbound) {
|
switchEnable(dbInboundId) {
|
||||||
this.submit(`/xui/inbound/update/${dbInbound.id}`, dbInbound);
|
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||||
|
this.submit(`/xui/inbound/update/${dbInboundId}`, dbInbound);
|
||||||
},
|
},
|
||||||
async submit(url, data, modal) {
|
async switchEnableClient(dbInboundId, client) {
|
||||||
const msg = await HttpUtil.postWithModal(url, data, modal);
|
this.loading()
|
||||||
|
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||||
|
inbound = dbInbound.toInbound();
|
||||||
|
clients = this.getClients(dbInbound.protocol, inbound.settings);
|
||||||
|
index = this.findIndexOfClient(clients, client);
|
||||||
|
clients[index].enable = ! clients[index].enable
|
||||||
|
await this.updateClient(inbound, dbInbound, index);
|
||||||
|
this.loading(false);
|
||||||
|
},
|
||||||
|
async submit(url, data) {
|
||||||
|
const msg = await HttpUtil.postWithModal(url, data);
|
||||||
if (msg.success) {
|
if (msg.success) {
|
||||||
await this.getDBInbounds();
|
await this.getDBInbounds();
|
||||||
}
|
}
|
||||||
@@ -429,34 +677,35 @@
|
|||||||
return dbInbound.toInbound().settings.trojans
|
return dbInbound.toInbound().settings.trojans
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
resetClientTraffic(client,inbound,event) {
|
resetClientTraffic(client,dbInboundId) {
|
||||||
this.$confirm({
|
this.$confirm({
|
||||||
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
|
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
|
||||||
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
|
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
|
||||||
|
class: siderDrawer.isDarkTheme ? darkClass : '',
|
||||||
okText: '{{ i18n "reset"}}',
|
okText: '{{ i18n "reset"}}',
|
||||||
cancelText: '{{ i18n "cancel"}}',
|
cancelText: '{{ i18n "cancel"}}',
|
||||||
onOk: () => {
|
onOk: () => this.submit('/xui/inbound/' + dbInboundId + '/resetClientTraffic/'+ client.email),
|
||||||
this.resetClTraffic(client,inbound,event);
|
})
|
||||||
},
|
},
|
||||||
|
resetAllTraffic() {
|
||||||
|
this.$confirm({
|
||||||
|
title: '{{ i18n "pages.inbounds.resetAllTrafficTitle"}}',
|
||||||
|
content: '{{ i18n "pages.inbounds.resetAllTrafficContent"}}',
|
||||||
|
class: siderDrawer.isDarkTheme ? darkClass : '',
|
||||||
|
okText: '{{ i18n "reset"}}',
|
||||||
|
cancelText: '{{ i18n "cancel"}}',
|
||||||
|
onOk: () => this.submit('/xui/inbound/resetAllTraffics'),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
async resetClTraffic(client,inbound,event) {
|
resetAllClientTraffics(dbInboundId) {
|
||||||
const msg = await HttpUtil.post('/xui/inbound/resetClientTraffic/'+ client.email);
|
this.$confirm({
|
||||||
if (!msg.success) {
|
title: '{{ i18n "pages.inbounds.resetAllClientTrafficTitle"}}',
|
||||||
return;
|
content: '{{ i18n "pages.inbounds.resetAllClientTrafficContent"}}',
|
||||||
}
|
class: siderDrawer.isDarkTheme ? darkClass : '',
|
||||||
clientStats = inbound.clientStats
|
okText: '{{ i18n "reset"}}',
|
||||||
if(clientStats.length > 0)
|
cancelText: '{{ i18n "cancel"}}',
|
||||||
{
|
onOk: () => this.submit('/xui/inbound/resetAllClientTraffics/' + dbInboundId),
|
||||||
for (const key in clientStats) {
|
})
|
||||||
if (Object.hasOwnProperty.call(clientStats, key)) {
|
|
||||||
if(clientStats[key]['email'] == client.email){
|
|
||||||
clientStats[key]['up'] = 0
|
|
||||||
clientStats[key]['down'] = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
isExpiry(dbInbound, index) {
|
isExpiry(dbInbound, index) {
|
||||||
return dbInbound.toInbound().isExpiry(index)
|
return dbInbound.toInbound().isExpiry(index)
|
||||||
@@ -476,6 +725,13 @@
|
|||||||
clientStats = dbInbound.clientStats.find(stats => stats.email === email)
|
clientStats = dbInbound.clientStats.find(stats => stats.email === email)
|
||||||
return clientStats ? clientStats.down + clientStats.up > clientStats.total : false
|
return clientStats ? clientStats.down + clientStats.up > clientStats.total : false
|
||||||
},
|
},
|
||||||
|
isClientEnabled(dbInbound, email) {
|
||||||
|
clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null
|
||||||
|
return clientStats ? clientStats['enable'] : true
|
||||||
|
},
|
||||||
|
isRemovable(dbInbound_id){
|
||||||
|
return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInbound_id)).length > 1
|
||||||
|
},
|
||||||
inboundLinks(dbInboundId) {
|
inboundLinks(dbInboundId) {
|
||||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||||
txtModal.show('{{ i18n "pages.inbounds.export"}}',dbInbound.genInboundLinks,dbInbound.remark);
|
txtModal.show('{{ i18n "pages.inbounds.export"}}',dbInbound.genInboundLinks,dbInbound.remark);
|
||||||
@@ -487,10 +743,6 @@
|
|||||||
}
|
}
|
||||||
txtModal.show('{{ i18n "pages.inbounds.export"}}',copyText,'All-Inbounds');
|
txtModal.show('{{ i18n "pages.inbounds.export"}}',copyText,'All-Inbounds');
|
||||||
},
|
},
|
||||||
isClientEnabled(dbInbound, email) {
|
|
||||||
clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null
|
|
||||||
return clientStats ? clientStats['enable'] : true
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
searchKey: debounce(function (newVal) {
|
searchKey: debounce(function (newVal) {
|
||||||
@@ -498,37 +750,30 @@
|
|||||||
}, 500)
|
}, 500)
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.getDefaultSettings();
|
||||||
this.getDBInbounds();
|
this.getDBInbounds();
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
total() {
|
total() {
|
||||||
let down = 0, up = 0;
|
let down = 0, up = 0;
|
||||||
let clients = 0, active = 0, deactive = 0;
|
let clients = 0, deactive = [], depleted = [], expiring = [];
|
||||||
this.dbInbounds.forEach(dbInbound => {
|
this.dbInbounds.forEach(dbInbound => {
|
||||||
down += dbInbound.down;
|
down += dbInbound.down;
|
||||||
up += dbInbound.up;
|
up += dbInbound.up;
|
||||||
inbound = dbInbound.toInbound();
|
if (this.clientCount[dbInbound.id]) {
|
||||||
clients = this.getClients(dbInbound.protocol, inbound.settings);
|
clients += this.clientCount[dbInbound.id].clients;
|
||||||
if(clients){
|
deactive = deactive.concat(this.clientCount[dbInbound.id].deactive);
|
||||||
if(dbInbound.enable){
|
depleted = depleted.concat(this.clientCount[dbInbound.id].depleted);
|
||||||
isClientEnable = false;
|
expiring = expiring.concat(this.clientCount[dbInbound.id].expiring);
|
||||||
clients.forEach(client => {
|
|
||||||
isClientEnable = client.email == "" ? true: this.isClientEnabled(dbInbound,client.email);
|
|
||||||
isClientEnable ? active++ : deactive++;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
deactive += clients.length;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dbInbound.enable ? active++ : deactive++;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
down: down,
|
down: down,
|
||||||
up: up,
|
up: up,
|
||||||
clients: active + deactive,
|
clients: clients,
|
||||||
active: active,
|
|
||||||
deactive: deactive,
|
deactive: deactive,
|
||||||
|
depleted: depleted,
|
||||||
|
expiring: expiring,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -541,5 +786,7 @@
|
|||||||
{{template "qrcodeModal"}}
|
{{template "qrcodeModal"}}
|
||||||
{{template "textModal"}}
|
{{template "textModal"}}
|
||||||
{{template "inboundInfoModal"}}
|
{{template "inboundInfoModal"}}
|
||||||
|
{{template "clientsModal"}}
|
||||||
|
{{template "clientsBulkModal"}}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -11,6 +11,10 @@
|
|||||||
.ant-col-sm-24 {
|
.ant-col-sm-24 {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-card-dark h2 {
|
||||||
|
color: hsla(0,0%,100%,.65);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<body>
|
<body>
|
||||||
<a-layout id="app" v-cloak>
|
<a-layout id="app" v-cloak>
|
||||||
@@ -27,14 +31,14 @@
|
|||||||
<a-col :span="12" style="text-align: center">
|
<a-col :span="12" style="text-align: center">
|
||||||
<a-progress type="dashboard" status="normal"
|
<a-progress type="dashboard" status="normal"
|
||||||
:stroke-color="status.cpu.color"
|
:stroke-color="status.cpu.color"
|
||||||
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||||
:percent="status.cpu.percent"></a-progress>
|
:percent="status.cpu.percent"></a-progress>
|
||||||
<div>CPU</div>
|
<div>CPU</div>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="12" style="text-align: center">
|
<a-col :span="12" style="text-align: center">
|
||||||
<a-progress type="dashboard" status="normal"
|
<a-progress type="dashboard" status="normal"
|
||||||
:stroke-color="status.mem.color"
|
:stroke-color="status.mem.color"
|
||||||
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||||
:percent="status.mem.percent"></a-progress>
|
:percent="status.mem.percent"></a-progress>
|
||||||
<div>
|
<div>
|
||||||
{{ i18n "pages.index.memory"}}: [[ sizeFormat(status.mem.current) ]] / [[ sizeFormat(status.mem.total) ]]
|
{{ i18n "pages.index.memory"}}: [[ sizeFormat(status.mem.current) ]] / [[ sizeFormat(status.mem.total) ]]
|
||||||
@@ -47,7 +51,7 @@
|
|||||||
<a-col :span="12" style="text-align: center">
|
<a-col :span="12" style="text-align: center">
|
||||||
<a-progress type="dashboard" status="normal"
|
<a-progress type="dashboard" status="normal"
|
||||||
:stroke-color="status.swap.color"
|
:stroke-color="status.swap.color"
|
||||||
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||||
:percent="status.swap.percent"></a-progress>
|
:percent="status.swap.percent"></a-progress>
|
||||||
<div>
|
<div>
|
||||||
Swap: [[ sizeFormat(status.swap.current) ]] / [[ sizeFormat(status.swap.total) ]]
|
Swap: [[ sizeFormat(status.swap.current) ]] / [[ sizeFormat(status.swap.total) ]]
|
||||||
@@ -56,7 +60,7 @@
|
|||||||
<a-col :span="12" style="text-align: center">
|
<a-col :span="12" style="text-align: center">
|
||||||
<a-progress type="dashboard" status="normal"
|
<a-progress type="dashboard" status="normal"
|
||||||
:stroke-color="status.disk.color"
|
:stroke-color="status.disk.color"
|
||||||
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||||
:percent="status.disk.percent"></a-progress>
|
:percent="status.disk.percent"></a-progress>
|
||||||
<div>
|
<div>
|
||||||
{{ i18n "pages.index.hard"}}: [[ sizeFormat(status.disk.current) ]] / [[ sizeFormat(status.disk.total) ]]
|
{{ i18n "pages.index.hard"}}: [[ sizeFormat(status.disk.current) ]] / [[ sizeFormat(status.disk.total) ]]
|
||||||
@@ -165,6 +169,13 @@
|
|||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
</a-card>
|
</a-card>
|
||||||
|
</a-col>
|
||||||
|
<a-col :sm="24" :md="12">
|
||||||
|
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
|
||||||
|
3x-ui: <a href="https://github.com/MHSanaei/3x-ui/releases" target="_blank"><a-tag color="green">v{{ .cur_ver }}</a-tag></a>
|
||||||
|
<a href="https://t.me/panel3xui" target="_blank"><a-tag color="green">Telegram</a-tag></a>
|
||||||
|
<a-tag color="blue" style="cursor: pointer;" @click="openLogs(20)">Log Reports</a-tag>
|
||||||
|
</a-card>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
</transition>
|
</transition>
|
||||||
@@ -172,7 +183,8 @@
|
|||||||
</a-layout>
|
</a-layout>
|
||||||
<a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}'
|
<a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}'
|
||||||
:closable="true" @ok="() => versionModal.visible = false"
|
:closable="true" @ok="() => versionModal.visible = false"
|
||||||
ok-text='{{ i18n "confirm" }}' cancel-text='{{ i18n "cancel"}}'>
|
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||||
|
footer="">
|
||||||
<h2>{{ i18n "pages.index.xraySwitchClick"}}</h2>
|
<h2>{{ i18n "pages.index.xraySwitchClick"}}</h2>
|
||||||
<h2>{{ i18n "pages.index.xraySwitchClickDesk"}}</h2>
|
<h2>{{ i18n "pages.index.xraySwitchClickDesk"}}</h2>
|
||||||
<template v-for="version, index in versionModal.versions">
|
<template v-for="version, index in versionModal.versions">
|
||||||
@@ -182,6 +194,36 @@
|
|||||||
</a-tag>
|
</a-tag>
|
||||||
</template>
|
</template>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
|
<a-modal id="log-modal" v-model="logModal.visible" title="X-UI logs"
|
||||||
|
:closable="true" @ok="() => logModal.visible = false" @cancel="() => logModal.visible = false"
|
||||||
|
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||||
|
width="800px"
|
||||||
|
footer="">
|
||||||
|
<a-form layout="inline">
|
||||||
|
<a-form-item label="Count">
|
||||||
|
<a-select v-model="logModal.rows"
|
||||||
|
style="width: 80px"
|
||||||
|
@change="openLogs(logModal.rows)"
|
||||||
|
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||||
|
<a-select-option value="10">10</a-select-option>
|
||||||
|
<a-select-option value="20">20</a-select-option>
|
||||||
|
<a-select-option value="50">50</a-select-option>
|
||||||
|
<a-select-option value="100">100</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item>
|
||||||
|
<button class="ant-btn ant-btn-primary" @click="openLogs(logModal.rows)"><a-icon type="sync"></a-icon> Reload</button>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item>
|
||||||
|
<a-button type="primary" style="margin-bottom: 10px;"
|
||||||
|
:href="'data:application/text;charset=utf-8,' + encodeURIComponent(logModal.logs)" download="x-ui.log">
|
||||||
|
{{ i18n "download" }} x-ui.log
|
||||||
|
</a-button>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
<a-input type="textarea" v-model="logModal.logs" disabled="true"
|
||||||
|
:autosize="{ minRows: 10, maxRows: 22}"></a-input>
|
||||||
|
</a-modal>
|
||||||
</a-layout>
|
</a-layout>
|
||||||
{{template "js" .}}
|
{{template "js" .}}
|
||||||
<script>
|
<script>
|
||||||
@@ -275,6 +317,20 @@
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const logModal = {
|
||||||
|
visible: false,
|
||||||
|
logs: '',
|
||||||
|
rows: 20,
|
||||||
|
show(logs, rows) {
|
||||||
|
this.visible = true;
|
||||||
|
this.rows = rows;
|
||||||
|
this.logs = logs.join("\n");
|
||||||
|
},
|
||||||
|
hide() {
|
||||||
|
this.visible = false;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const app = new Vue({
|
const app = new Vue({
|
||||||
delimiters: ['[[', ']]'],
|
delimiters: ['[[', ']]'],
|
||||||
el: '#app',
|
el: '#app',
|
||||||
@@ -282,6 +338,7 @@
|
|||||||
siderDrawer,
|
siderDrawer,
|
||||||
status: new Status(),
|
status: new Status(),
|
||||||
versionModal,
|
versionModal,
|
||||||
|
logModal,
|
||||||
spinning: false,
|
spinning: false,
|
||||||
loadingTip: '{{ i18n "loading"}}',
|
loadingTip: '{{ i18n "loading"}}',
|
||||||
},
|
},
|
||||||
@@ -313,6 +370,7 @@
|
|||||||
title: '{{ i18n "pages.index.xraySwitchVersionDialog"}}',
|
title: '{{ i18n "pages.index.xraySwitchVersionDialog"}}',
|
||||||
content: '{{ i18n "pages.index.xraySwitchVersionDialogDesc"}}' + ` ${version}?`,
|
content: '{{ i18n "pages.index.xraySwitchVersionDialogDesc"}}' + ` ${version}?`,
|
||||||
okText: '{{ i18n "confirm"}}',
|
okText: '{{ i18n "confirm"}}',
|
||||||
|
class: siderDrawer.isDarkTheme ? darkClass : '',
|
||||||
cancelText: '{{ i18n "cancel"}}',
|
cancelText: '{{ i18n "cancel"}}',
|
||||||
onOk: async () => {
|
onOk: async () => {
|
||||||
versionModal.hide();
|
versionModal.hide();
|
||||||
@@ -322,7 +380,7 @@
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
//here add stop xray function
|
//here add stop xray function
|
||||||
async stopXrayService() {
|
async stopXrayService() {
|
||||||
this.loading(true);
|
this.loading(true);
|
||||||
const msg = await HttpUtil.post('server/stopXrayService');
|
const msg = await HttpUtil.post('server/stopXrayService');
|
||||||
@@ -331,7 +389,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
//here add restart xray function
|
//here add restart xray function
|
||||||
async restartXrayService() {
|
async restartXrayService() {
|
||||||
this.loading(true);
|
this.loading(true);
|
||||||
const msg = await HttpUtil.post('server/restartXrayService');
|
const msg = await HttpUtil.post('server/restartXrayService');
|
||||||
@@ -340,6 +398,15 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async openLogs(rows){
|
||||||
|
this.loading(true);
|
||||||
|
const msg = await HttpUtil.post('server/logs/'+rows);
|
||||||
|
this.loading(false);
|
||||||
|
if (!msg.success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logModal.show(msg.obj,rows);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
:not(.ant-card-dark)>.ant-tabs-top-bar {
|
:not(.ant-card-dark)>.ant-tabs-top-bar {
|
||||||
background: white;
|
background: white;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -44,6 +44,8 @@
|
|||||||
<setting-list-item type="text" title='{{ i18n "pages.setting.publicKeyPath"}}' desc='{{ i18n "pages.setting.publicKeyPathDesc"}}' v-model="allSetting.webCertFile"></setting-list-item>
|
<setting-list-item type="text" title='{{ i18n "pages.setting.publicKeyPath"}}' desc='{{ i18n "pages.setting.publicKeyPathDesc"}}' v-model="allSetting.webCertFile"></setting-list-item>
|
||||||
<setting-list-item type="text" title='{{ i18n "pages.setting.privateKeyPath"}}' desc='{{ i18n "pages.setting.privateKeyPathDesc"}}' v-model="allSetting.webKeyFile"></setting-list-item>
|
<setting-list-item type="text" title='{{ i18n "pages.setting.privateKeyPath"}}' desc='{{ i18n "pages.setting.privateKeyPathDesc"}}' v-model="allSetting.webKeyFile"></setting-list-item>
|
||||||
<setting-list-item type="text" title='{{ i18n "pages.setting.panelUrlPath"}}' desc='{{ i18n "pages.setting.panelUrlPathDesc"}}' v-model="allSetting.webBasePath"></setting-list-item>
|
<setting-list-item type="text" title='{{ i18n "pages.setting.panelUrlPath"}}' desc='{{ i18n "pages.setting.panelUrlPathDesc"}}' v-model="allSetting.webBasePath"></setting-list-item>
|
||||||
|
<setting-list-item type="number" title='{{ i18n "pages.setting.expireTimeDiff" }}' desc='{{ i18n "pages.setting.expireTimeDiffDesc" }}' v-model="allSetting.expireDiff" :min="0"></setting-list-item>
|
||||||
|
<setting-list-item type="number" title='{{ i18n "pages.setting.trafficDiff" }}' desc='{{ i18n "pages.setting.trafficDiffDesc" }}' v-model="allSetting.trafficDiff" :min="0"></setting-list-item>
|
||||||
<a-list-item>
|
<a-list-item>
|
||||||
<a-row style="padding: 20px">
|
<a-row style="padding: 20px">
|
||||||
<a-col :lg="24" :xl="12">
|
<a-col :lg="24" :xl="12">
|
||||||
@@ -56,6 +58,7 @@
|
|||||||
ref="selectLang"
|
ref="selectLang"
|
||||||
v-model="lang"
|
v-model="lang"
|
||||||
@change="setLang(lang)"
|
@change="setLang(lang)"
|
||||||
|
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
>
|
>
|
||||||
<a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs">
|
<a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs">
|
||||||
@@ -87,13 +90,30 @@
|
|||||||
style="max-width: 300px"></a-input>
|
style="max-width: 300px"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<!-- <a-button type="primary" @click="updateUser">update</a-button>-->
|
<!-- <a-button type="primary" @click="updateUser">Revise</a-button>-->
|
||||||
<a-button type="primary" @click="updateUser">{{ i18n "confirm" }}</a-button>
|
<a-button type="primary" @click="updateUser">{{ i18n "confirm" }}</a-button>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane key="3" tab='{{ i18n "pages.setting.xrayConfiguration"}}'>
|
<a-tab-pane key="3" tab='{{ i18n "pages.setting.xrayConfiguration"}}'>
|
||||||
<a-list item-layout="horizontal" :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65);': 'background: white;'">
|
<a-list item-layout="horizontal" :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65);': 'background: white;'">
|
||||||
|
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigTorrent"}}' desc='{{ i18n "pages.setting.xrayConfigTorrentDesc"}}' v-model="torrentSettings"></setting-list-item>
|
||||||
|
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigPrivateIp"}}' desc='{{ i18n "pages.setting.xrayConfigPrivateIpDesc"}}' v-model="privateIpSettings"></setting-list-item>
|
||||||
|
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigIRIp"}}' desc='{{ i18n "pages.setting.xrayConfigIRIpDesc"}}' v-model="IRIpSettings"></setting-list-item>
|
||||||
|
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigIRdomain"}}' desc='{{ i18n "pages.setting.xrayConfigIRdomainDesc"}}' v-model="IRdomainSettings"></setting-list-item>
|
||||||
|
<a-divider>{{ i18n "pages.setting.advancedTemplate"}}</a-divider>
|
||||||
|
<a-collapse>
|
||||||
|
<a-collapse-panel header="{{ i18n "pages.setting.xrayConfigInbounds"}}">
|
||||||
|
<setting-list-item type="textarea" title='{{ i18n "pages.setting.xrayConfigInbounds"}}' desc='{{ i18n "pages.setting.xrayConfigInboundsDesc"}}' v-model ="inboundSettings"></setting-list-item>
|
||||||
|
</a-collapse-panel>
|
||||||
|
<a-collapse-panel header="{{ i18n "pages.setting.xrayConfigOutbounds"}}">
|
||||||
|
<setting-list-item type="textarea" title='{{ i18n "pages.setting.xrayConfigOutbounds"}}' desc='{{ i18n "pages.setting.xrayConfigOutboundsDesc"}}' v-model ="outboundSettings"></setting-list-item>
|
||||||
|
</a-collapse-panel>
|
||||||
|
<a-collapse-panel header="{{ i18n "pages.setting.xrayConfigRoutings"}}">
|
||||||
|
<setting-list-item type="textarea" title='{{ i18n "pages.setting.xrayConfigRoutings"}}' desc='{{ i18n "pages.setting.xrayConfigRoutingsDesc"}}' v-model ="routingRuleSettings"></setting-list-item>
|
||||||
|
</a-collapse-panel>
|
||||||
|
</a-collapse>
|
||||||
|
<a-divider>{{ i18n "pages.setting.completeTemplate"}}</a-divider>
|
||||||
<setting-list-item type="textarea" title='{{ i18n "pages.setting.xrayConfigTemplate"}}' desc='{{ i18n "pages.setting.xrayConfigTemplateDesc"}}' v-model="allSetting.xrayTemplateConfig"></setting-list-item>
|
<setting-list-item type="textarea" title='{{ i18n "pages.setting.xrayConfigTemplate"}}' desc='{{ i18n "pages.setting.xrayConfigTemplateDesc"}}' v-model="allSetting.xrayTemplateConfig"></setting-list-item>
|
||||||
</a-list>
|
</a-list>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
@@ -101,8 +121,10 @@
|
|||||||
<a-list item-layout="horizontal" :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65);': 'background: white;'">
|
<a-list item-layout="horizontal" :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65);': 'background: white;'">
|
||||||
<setting-list-item type="switch" title='{{ i18n "pages.setting.telegramBotEnable" }}' desc='{{ i18n "pages.setting.telegramBotEnableDesc" }}' v-model="allSetting.tgBotEnable"></setting-list-item>
|
<setting-list-item type="switch" title='{{ i18n "pages.setting.telegramBotEnable" }}' desc='{{ i18n "pages.setting.telegramBotEnableDesc" }}' v-model="allSetting.tgBotEnable"></setting-list-item>
|
||||||
<setting-list-item type="text" title='{{ i18n "pages.setting.telegramToken"}}' desc='{{ i18n "pages.setting.telegramTokenDesc"}}' v-model="allSetting.tgBotToken"></setting-list-item>
|
<setting-list-item type="text" title='{{ i18n "pages.setting.telegramToken"}}' desc='{{ i18n "pages.setting.telegramTokenDesc"}}' v-model="allSetting.tgBotToken"></setting-list-item>
|
||||||
<setting-list-item type="number" title='{{ i18n "pages.setting.telegramChatId"}}' desc='{{ i18n "pages.setting.telegramChatIdDesc"}}' v-model.number="allSetting.tgBotChatId"></setting-list-item>
|
<setting-list-item type="text" title='{{ i18n "pages.setting.telegramChatId"}}' desc='{{ i18n "pages.setting.telegramChatIdDesc"}}' v-model="allSetting.tgBotChatId"></setting-list-item>
|
||||||
<setting-list-item type="text" title='{{ i18n "pages.setting.telegramNotifyTime"}}' desc='{{ i18n "pages.setting.telegramNotifyTimeDesc"}}' v-model="allSetting.tgRunTime"></setting-list-item>
|
<setting-list-item type="text" title='{{ i18n "pages.setting.telegramNotifyTime"}}' desc='{{ i18n "pages.setting.telegramNotifyTimeDesc"}}' v-model="allSetting.tgRunTime"></setting-list-item>
|
||||||
|
<setting-list-item type="switch" title='{{ i18n "pages.setting.tgNotifyBackup" }}' desc='{{ i18n "pages.setting.tgNotifyBackupDesc" }}' v-model="allSetting.tgBotBackup"></setting-list-item>
|
||||||
|
<setting-list-item type="number" title='{{ i18n "pages.setting.tgNotifyCpu" }}' desc='{{ i18n "pages.setting.tgNotifyCpuDesc" }}' v-model="allSetting.tgCpu" :min="0" :max="100"></setting-list-item>
|
||||||
</a-list>
|
</a-list>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane key="5" tab='{{ i18n "pages.setting.otherSetting"}}'>
|
<a-tab-pane key="5" tab='{{ i18n "pages.setting.otherSetting"}}'>
|
||||||
@@ -189,6 +211,169 @@
|
|||||||
this.saveBtnDisable = this.oldAllSetting.equals(this.allSetting);
|
this.saveBtnDisable = this.oldAllSetting.equals(this.allSetting);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
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) },
|
||||||
|
},
|
||||||
|
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
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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
|
||||||
|
},
|
||||||
|
},
|
||||||
|
torrentSettings: {
|
||||||
|
get: function () {
|
||||||
|
torrentFilter = false
|
||||||
|
if(this.templateSettings != null){
|
||||||
|
this.templateSettings.routing.rules.forEach(routingRule => {
|
||||||
|
if(routingRule.hasOwnProperty("protocol")){
|
||||||
|
if (routingRule.protocol[0] === "bittorrent" && routingRule.outboundTag == "blocked"){
|
||||||
|
torrentFilter = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return torrentFilter
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
newTemplateSettings = JSON.parse(this.allSetting.xrayTemplateConfig);
|
||||||
|
if (newValue){
|
||||||
|
newTemplateSettings.routing.rules.push(JSON.parse("{\"outboundTag\": \"blocked\",\"protocol\": [\"bittorrent\"],\"type\": \"field\"}"))
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
newTemplateSettings.routing.rules = [];
|
||||||
|
this.templateSettings.routing.rules.forEach(routingRule => {
|
||||||
|
if (routingRule.hasOwnProperty('protocol')){
|
||||||
|
if (routingRule.protocol[0] === "bittorrent" && routingRule.outboundTag == "blocked"){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newTemplateSettings.routing.rules.push(routingRule);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.templateSettings = newTemplateSettings
|
||||||
|
},
|
||||||
|
},
|
||||||
|
privateIpSettings: {
|
||||||
|
get: function () {
|
||||||
|
localIpFilter = false
|
||||||
|
if(this.templateSettings != null){
|
||||||
|
this.templateSettings.routing.rules.forEach(routingRule => {
|
||||||
|
if(routingRule.hasOwnProperty("ip")){
|
||||||
|
if (routingRule.ip[0] === "geoip:private" && routingRule.outboundTag == "blocked"){
|
||||||
|
localIpFilter = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return localIpFilter
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
newTemplateSettings = JSON.parse(this.allSetting.xrayTemplateConfig);
|
||||||
|
if (newValue){
|
||||||
|
newTemplateSettings.routing.rules.push(JSON.parse("{\"outboundTag\": \"blocked\",\"ip\": [\"geoip:private\"],\"type\": \"field\"}"))
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
newTemplateSettings.routing.rules = [];
|
||||||
|
this.templateSettings.routing.rules.forEach(routingRule => {
|
||||||
|
if (routingRule.hasOwnProperty('ip')){
|
||||||
|
if (routingRule.ip[0] === "geoip:private" && routingRule.outboundTag == "blocked"){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newTemplateSettings.routing.rules.push(routingRule);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.templateSettings = newTemplateSettings
|
||||||
|
},
|
||||||
|
},
|
||||||
|
IRIpSettings: {
|
||||||
|
get: function () {
|
||||||
|
localIpFilter = false
|
||||||
|
if(this.templateSettings != null){
|
||||||
|
this.templateSettings.routing.rules.forEach(routingRule => {
|
||||||
|
if(routingRule.hasOwnProperty("ip")){
|
||||||
|
if (routingRule.ip[0] === "geoip:ir" && routingRule.outboundTag == "blocked"){
|
||||||
|
localIpFilter = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return localIpFilter
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
newTemplateSettings = JSON.parse(this.allSetting.xrayTemplateConfig);
|
||||||
|
if (newValue){
|
||||||
|
newTemplateSettings.routing.rules.push(JSON.parse("{\"outboundTag\": \"blocked\",\"ip\": [\"geoip:ir\"],\"type\": \"field\"}"))
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
newTemplateSettings.routing.rules = [];
|
||||||
|
this.templateSettings.routing.rules.forEach(routingRule => {
|
||||||
|
if (routingRule.hasOwnProperty('ip')){
|
||||||
|
if (routingRule.ip[0] === "geoip:ir" && routingRule.outboundTag == "blocked"){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newTemplateSettings.routing.rules.push(routingRule);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.templateSettings = newTemplateSettings
|
||||||
|
},
|
||||||
|
},
|
||||||
|
IRdomainSettings: {
|
||||||
|
get: function () {
|
||||||
|
localdomainFilter = false
|
||||||
|
if(this.templateSettings != null){
|
||||||
|
this.templateSettings.routing.rules.forEach(routingRule => {
|
||||||
|
if(routingRule.hasOwnProperty("domain")){
|
||||||
|
if ((routingRule.domain[0] === "regexp:.+.ir$" || routingRule.domain[0] === "ext:iran.dat:ir" || routingRule.domain[0] === "ext:iran.dat:other") && routingRule.outboundTag == "blocked") {
|
||||||
|
localdomainFilter = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return localdomainFilter
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
newTemplateSettings = JSON.parse(this.allSetting.xrayTemplateConfig);
|
||||||
|
if (newValue){
|
||||||
|
newTemplateSettings.routing.rules.push(JSON.parse("{\"outboundTag\": \"blocked\",\"domain\": [\"regexp:.+.ir$\", \"ext:iran.dat:ir\", \"ext:iran.dat:other\"],\"type\": \"field\"}"))
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
newTemplateSettings.routing.rules = [];
|
||||||
|
this.templateSettings.routing.rules.forEach(routingRule => {
|
||||||
|
if (routingRule.hasOwnProperty('domain')){
|
||||||
|
if ((routingRule.domain[0] === "regexp:.+.ir$" || routingRule.domain[0] === "ext:iran.dat:ir" || routingRule.domain[0] === "ext:iran.dat:other") && routingRule.outboundTag == "blocked"){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newTemplateSettings.routing.rules.push(routingRule);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.templateSettings = newTemplateSettings
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -154,15 +154,17 @@ func GetInboundClientIps(clientEmail string) (*model.InboundClientIps, error) {
|
|||||||
}
|
}
|
||||||
return InboundClientIps, nil
|
return InboundClientIps, nil
|
||||||
}
|
}
|
||||||
func addInboundClientIps(clientEmail string,ips []string) error {
|
func addInboundClientIps(clientEmail string, ips []string) error {
|
||||||
inboundClientIps := &model.InboundClientIps{}
|
inboundClientIps := &model.InboundClientIps{}
|
||||||
jsonIps, err := json.Marshal(ips)
|
jsonIps, err := json.Marshal(ips)
|
||||||
checkError(err)
|
checkError(err)
|
||||||
|
|
||||||
|
// Trim any leading/trailing whitespace from clientEmail
|
||||||
|
clientEmail = strings.TrimSpace(clientEmail)
|
||||||
|
|
||||||
inboundClientIps.ClientEmail = clientEmail
|
inboundClientIps.ClientEmail = clientEmail
|
||||||
inboundClientIps.Ips = string(jsonIps)
|
inboundClientIps.Ips = string(jsonIps)
|
||||||
|
|
||||||
|
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
tx := db.Begin()
|
tx := db.Begin()
|
||||||
|
|
||||||
@@ -247,47 +249,46 @@ func GetInboundByEmail(clientEmail string) (*model.Inbound, error) {
|
|||||||
return inbounds, nil
|
return inbounds, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func LimitDevice(){
|
func LimitDevice() {
|
||||||
|
var destIp, destPort, srcIp, srcPort string
|
||||||
|
|
||||||
localIp,err := LocalIP()
|
localIp,err := LocalIP()
|
||||||
checkError(err)
|
checkError(err)
|
||||||
|
|
||||||
c := cmd.NewCmd("bash","-c","ss --tcp | grep -E '" + IPsToRegex(localIp) + "'| awk '{if($1==\"ESTAB\") print $4,$5;}'","| sort | uniq -c | sort -nr | head")
|
c := cmd.NewCmd("bash","-c","ss --tcp | grep -E '" + IPsToRegex(localIp) + "'| awk '{if($1==\"ESTAB\") print $4,$5;}'","| sort | uniq -c | sort -nr | head")
|
||||||
|
|
||||||
<-c.Start()
|
<-c.Start()
|
||||||
if len(c.Status().Stdout) > 0 {
|
if len(c.Status().Stdout) > 0 {
|
||||||
ipRegx, _ := regexp.Compile(`[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+`)
|
ipRegx, _ := regexp.Compile(`[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+`)
|
||||||
portRegx, _ := regexp.Compile(`(?:(:))([0-9]..[^.][0-9]+)`)
|
portRegx, _ := regexp.Compile(`(?:(:))([0-9]..[^.][0-9]+)`)
|
||||||
|
|
||||||
for _, row := range c.Status().Stdout {
|
for _, row := range c.Status().Stdout {
|
||||||
|
|
||||||
data := strings.Split(row," ")
|
data := strings.Split(row," ")
|
||||||
|
|
||||||
destIp,destPort,srcIp,srcPort := "","","",""
|
if len(data) < 2 {
|
||||||
|
continue // Skip this row if it doesn't have at least two elements
|
||||||
|
}
|
||||||
|
|
||||||
|
destIp = string(ipRegx.FindString(data[0]))
|
||||||
|
destPort = portRegx.FindString(data[0])
|
||||||
|
destPort = strings.Replace(destPort,":","",-1)
|
||||||
|
|
||||||
destIp = string(ipRegx.FindString(data[0]))
|
srcIp = string(ipRegx.FindString(data[1]))
|
||||||
|
srcPort = portRegx.FindString(data[1])
|
||||||
|
srcPort = strings.Replace(srcPort,":","",-1)
|
||||||
|
|
||||||
destPort = portRegx.FindString(data[0])
|
if contains(disAllowedIps,srcIp){
|
||||||
destPort = strings.Replace(destPort,":","",-1)
|
dropCmd := cmd.NewCmd("bash","-c","ss -K dport = " + srcPort)
|
||||||
|
dropCmd.Start()
|
||||||
|
|
||||||
srcIp = string(ipRegx.FindString(data[1]))
|
|
||||||
|
|
||||||
srcPort = portRegx.FindString(data[1])
|
|
||||||
srcPort = strings.Replace(srcPort,":","",-1)
|
|
||||||
|
|
||||||
if(contains(disAllowedIps,srcIp)){
|
|
||||||
dropCmd := cmd.NewCmd("bash","-c","ss -K dport = " + srcPort)
|
|
||||||
dropCmd.Start()
|
|
||||||
|
|
||||||
logger.Debug("request droped : ",srcIp,srcPort,"to",destIp,destPort)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
logger.Debug("request droped : ",srcIp,srcPort,"to",destIp,destPort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func LocalIP() ([]string, error) {
|
func LocalIP() ([]string, error) {
|
||||||
// get machine ips
|
// get machine ips
|
||||||
|
|
||||||
|
|||||||
30
web/job/check_cpu_usage.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package job
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
"x-ui/web/service"
|
||||||
|
|
||||||
|
"github.com/shirou/gopsutil/v3/cpu"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CheckCpuJob struct {
|
||||||
|
tgbotService service.Tgbot
|
||||||
|
settingService service.SettingService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCheckCpuJob() *CheckCpuJob {
|
||||||
|
return new(CheckCpuJob)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Here run is a interface method of Job interface
|
||||||
|
func (j *CheckCpuJob) Run() {
|
||||||
|
threshold, _ := j.settingService.GetTgCpu()
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
j.tgbotService.SendMsgToTgbotAdmins(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,7 @@
|
|||||||
package job
|
package job
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
"x-ui/logger"
|
|
||||||
"x-ui/util/common"
|
|
||||||
"x-ui/web/service"
|
"x-ui/web/service"
|
||||||
|
|
||||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type LoginStatus byte
|
type LoginStatus byte
|
||||||
@@ -20,229 +12,18 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type StatsNotifyJob struct {
|
type StatsNotifyJob struct {
|
||||||
enable bool
|
xrayService service.XrayService
|
||||||
xrayService service.XrayService
|
tgbotService service.Tgbot
|
||||||
inboundService service.InboundService
|
|
||||||
settingService service.SettingService
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStatsNotifyJob() *StatsNotifyJob {
|
func NewStatsNotifyJob() *StatsNotifyJob {
|
||||||
return new(StatsNotifyJob)
|
return new(StatsNotifyJob)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *StatsNotifyJob) SendMsgToTgbot(msg string) {
|
|
||||||
//Telegram bot basic info
|
|
||||||
tgBottoken, err := j.settingService.GetTgBotToken()
|
|
||||||
if err != nil || tgBottoken == "" {
|
|
||||||
logger.Warning("sendMsgToTgbot failed,GetTgBotToken fail:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tgBotid, err := j.settingService.GetTgBotChatId()
|
|
||||||
if err != nil {
|
|
||||||
logger.Warning("sendMsgToTgbot failed,GetTgBotChatId fail:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
bot, err := tgbotapi.NewBotAPI(tgBottoken)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("get tgbot error:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
bot.Debug = true
|
|
||||||
fmt.Printf("Authorized on account %s", bot.Self.UserName)
|
|
||||||
info := tgbotapi.NewMessage(int64(tgBotid), msg)
|
|
||||||
//msg.ReplyToMessageID = int(tgBotid)
|
|
||||||
bot.Send(info)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Here run is a interface method of Job interface
|
// Here run is a interface method of Job interface
|
||||||
func (j *StatsNotifyJob) Run() {
|
func (j *StatsNotifyJob) Run() {
|
||||||
if !j.xrayService.IsXrayRunning() {
|
if !j.xrayService.IsXrayRunning() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var info string
|
j.tgbotService.SendReport()
|
||||||
//get hostname
|
|
||||||
name, err := os.Hostname()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("get hostname error:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
info = fmt.Sprintf("Hostname:%s\r\n", name)
|
|
||||||
//get ip address
|
|
||||||
var ip string
|
|
||||||
netInterfaces, err := net.Interfaces()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("net.Interfaces failed, err:", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < len(netInterfaces); i++ {
|
|
||||||
if (netInterfaces[i].Flags & net.FlagUp) != 0 {
|
|
||||||
addrs, _ := netInterfaces[i].Addrs()
|
|
||||||
|
|
||||||
for _, address := range addrs {
|
|
||||||
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
|
|
||||||
if ipnet.IP.To4() != nil {
|
|
||||||
ip = ipnet.IP.String()
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
ip = ipnet.IP.String()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
info += fmt.Sprintf("IP:%s\r\n \r\n", ip)
|
|
||||||
|
|
||||||
// get traffic
|
|
||||||
inbouds, err := j.inboundService.GetAllInbounds()
|
|
||||||
if err != nil {
|
|
||||||
logger.Warning("StatsNotifyJob run failed:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// NOTE:If there no any sessions here,need to notify here
|
|
||||||
// TODO:Sub-node push, automatic conversion format
|
|
||||||
for _, inbound := range inbouds {
|
|
||||||
info += fmt.Sprintf("Node name:%s\r\nPort:%d\r\nUpload↑:%s\r\nDownload↓:%s\r\nTotal:%s\r\n", inbound.Remark, inbound.Port, common.FormatTraffic(inbound.Up), common.FormatTraffic(inbound.Down), common.FormatTraffic((inbound.Up + inbound.Down)))
|
|
||||||
if inbound.ExpiryTime == 0 {
|
|
||||||
info += fmt.Sprintf("Expire date:unlimited\r\n \r\n")
|
|
||||||
} else {
|
|
||||||
info += fmt.Sprintf("Expire date:%s\r\n \r\n", time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
j.SendMsgToTgbot(info)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *StatsNotifyJob) UserLoginNotify(username string, ip string, time string, status LoginStatus) {
|
|
||||||
if username == "" || ip == "" || time == "" {
|
|
||||||
logger.Warning("UserLoginNotify failed,invalid info")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var msg string
|
|
||||||
// Get hostname
|
|
||||||
name, err := os.Hostname()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("get hostname error:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if status == LoginSuccess {
|
|
||||||
msg = fmt.Sprintf("Successfully logged-in to the panel\r\nHostname:%s\r\n", name)
|
|
||||||
} else if status == LoginFail {
|
|
||||||
msg = fmt.Sprintf("Login to the panel was unsuccessful\r\nHostname:%s\r\n", name)
|
|
||||||
}
|
|
||||||
msg += fmt.Sprintf("Time:%s\r\n", time)
|
|
||||||
msg += fmt.Sprintf("Username:%s\r\n", username)
|
|
||||||
msg += fmt.Sprintf("IP:%s\r\n", ip)
|
|
||||||
j.SendMsgToTgbot(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
var numericKeyboard = tgbotapi.NewInlineKeyboardMarkup(
|
|
||||||
tgbotapi.NewInlineKeyboardRow(
|
|
||||||
tgbotapi.NewInlineKeyboardButtonData("Get Usage", "get_usage"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
func (j *StatsNotifyJob) OnReceive() *StatsNotifyJob {
|
|
||||||
tgBottoken, err := j.settingService.GetTgBotToken()
|
|
||||||
if err != nil || tgBottoken == "" {
|
|
||||||
logger.Warning("sendMsgToTgbot failed,GetTgBotToken fail:", err)
|
|
||||||
return j
|
|
||||||
}
|
|
||||||
bot, err := tgbotapi.NewBotAPI(tgBottoken)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("get tgbot error:", err)
|
|
||||||
return j
|
|
||||||
}
|
|
||||||
bot.Debug = false
|
|
||||||
u := tgbotapi.NewUpdate(0)
|
|
||||||
u.Timeout = 10
|
|
||||||
|
|
||||||
updates := bot.GetUpdatesChan(u)
|
|
||||||
|
|
||||||
for update := range updates {
|
|
||||||
if update.Message == nil {
|
|
||||||
|
|
||||||
if update.CallbackQuery != nil {
|
|
||||||
// Respond to the callback query, telling Telegram to show the user
|
|
||||||
// a message with the data received.
|
|
||||||
callback := tgbotapi.NewCallback(update.CallbackQuery.ID, update.CallbackQuery.Data)
|
|
||||||
if _, err := bot.Request(callback); err != nil {
|
|
||||||
logger.Warning(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// And finally, send a message containing the data received.
|
|
||||||
msg := tgbotapi.NewMessage(update.CallbackQuery.Message.Chat.ID, "")
|
|
||||||
|
|
||||||
switch update.CallbackQuery.Data {
|
|
||||||
case "get_usage":
|
|
||||||
msg.Text = "for get your usage send command like this : \n <code>/usage uuid | id</code> \n example : <code>/usage fc3239ed-8f3b-4151-ff51-b183d5182142</code>"
|
|
||||||
msg.ParseMode = "HTML"
|
|
||||||
}
|
|
||||||
if _, err := bot.Send(msg); err != nil {
|
|
||||||
logger.Warning(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !update.Message.IsCommand() { // ignore any non-command Messages
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new MessageConfig. We don't have text yet,
|
|
||||||
// so we leave it empty.
|
|
||||||
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "")
|
|
||||||
|
|
||||||
// Extract the command from the Message.
|
|
||||||
switch update.Message.Command() {
|
|
||||||
case "help":
|
|
||||||
msg.Text = "What you need?"
|
|
||||||
msg.ReplyMarkup = numericKeyboard
|
|
||||||
case "start":
|
|
||||||
msg.Text = "Hi :) \n What you need?"
|
|
||||||
msg.ReplyMarkup = numericKeyboard
|
|
||||||
|
|
||||||
case "status":
|
|
||||||
msg.Text = "bot is ok."
|
|
||||||
|
|
||||||
case "usage":
|
|
||||||
msg.Text = j.getClientUsage(update.Message.CommandArguments())
|
|
||||||
default:
|
|
||||||
msg.Text = "I don't know that command, /help"
|
|
||||||
msg.ReplyMarkup = numericKeyboard
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := bot.Send(msg); err != nil {
|
|
||||||
logger.Warning(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return j
|
|
||||||
|
|
||||||
}
|
|
||||||
func (j *StatsNotifyJob) getClientUsage(id string) string {
|
|
||||||
traffic, err := j.inboundService.GetClientTrafficById(id)
|
|
||||||
if err != nil {
|
|
||||||
logger.Warning(err)
|
|
||||||
return "something wrong!"
|
|
||||||
}
|
|
||||||
expiryTime := ""
|
|
||||||
if traffic.ExpiryTime == 0 {
|
|
||||||
expiryTime = fmt.Sprintf("unlimited")
|
|
||||||
} else {
|
|
||||||
expiryTime = fmt.Sprintf("%s", time.Unix((traffic.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
|
|
||||||
}
|
|
||||||
total := ""
|
|
||||||
if traffic.Total == 0 {
|
|
||||||
total = fmt.Sprintf("unlimited")
|
|
||||||
} else {
|
|
||||||
total = fmt.Sprintf("%s", common.FormatTraffic((traffic.Total)))
|
|
||||||
}
|
|
||||||
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Download↑: %s\r\n🔽 Upload↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
|
|
||||||
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
|
|
||||||
total, expiryTime)
|
|
||||||
|
|
||||||
return output
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,5 +34,4 @@ func (j *XrayTrafficJob) Run() {
|
|||||||
logger.Warning("add client traffic failed:", err)
|
logger.Warning("add client traffic failed:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
"loglevel": "warning",
|
"loglevel": "warning",
|
||||||
"access": "./access.log"
|
"access": "./access.log"
|
||||||
},
|
},
|
||||||
|
|
||||||
"api": {
|
"api": {
|
||||||
"services": [
|
"services": [
|
||||||
"HandlerService",
|
"HandlerService",
|
||||||
@@ -47,6 +46,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"routing": {
|
"routing": {
|
||||||
|
"domainStrategy": "IPIfNonMatch",
|
||||||
"rules": [
|
"rules": [
|
||||||
{
|
{
|
||||||
"inboundTag": [
|
"inboundTag": [
|
||||||
@@ -56,10 +56,10 @@
|
|||||||
"type": "field"
|
"type": "field"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"outboundTag": "blocked",
|
||||||
"ip": [
|
"ip": [
|
||||||
"geoip:private"
|
"geoip:private"
|
||||||
],
|
],
|
||||||
"outboundTag": "blocked",
|
|
||||||
"type": "field"
|
"type": "field"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ func (s *InboundService) getClients(inbound *model.Inbound) ([]model.Client, err
|
|||||||
settings := map[string][]model.Client{}
|
settings := map[string][]model.Client{}
|
||||||
json.Unmarshal([]byte(inbound.Settings), &settings)
|
json.Unmarshal([]byte(inbound.Settings), &settings)
|
||||||
if settings == nil {
|
if settings == nil {
|
||||||
return nil, fmt.Errorf("Setting is null")
|
return nil, fmt.Errorf("setting is null")
|
||||||
}
|
}
|
||||||
|
|
||||||
clients := settings["clients"]
|
clients := settings["clients"]
|
||||||
@@ -125,11 +125,18 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, err
|
|||||||
return inbound, common.NewError("Duplicate email:", existEmail)
|
return inbound, common.NewError("Duplicate email:", existEmail)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clients, err := s.getClients(inbound)
|
||||||
|
if err != nil {
|
||||||
|
return inbound, err
|
||||||
|
}
|
||||||
|
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
|
|
||||||
err = db.Save(inbound).Error
|
err = db.Save(inbound).Error
|
||||||
if err == nil {
|
if err == nil {
|
||||||
s.UpdateClientStat(inbound.Id, inbound.Settings)
|
for _, client := range clients {
|
||||||
|
s.AddClientStat(inbound.Id, &client)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return inbound, err
|
return inbound, err
|
||||||
}
|
}
|
||||||
@@ -168,6 +175,24 @@ func (s *InboundService) AddInbounds(inbounds []*model.Inbound) error {
|
|||||||
|
|
||||||
func (s *InboundService) DelInbound(id int) error {
|
func (s *InboundService) DelInbound(id int) error {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
|
err := db.Where("inbound_id = ?", id).Delete(xray.ClientTraffic{}).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
inbound, err := s.GetInbound(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
clients, err := s.getClients(inbound)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, client := range clients {
|
||||||
|
err := s.DelClientIPs(db, client.Email)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
return db.Delete(model.Inbound{}, id).Error
|
return db.Delete(model.Inbound{}, id).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,11 +241,128 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
|
|||||||
oldInbound.Sniffing = inbound.Sniffing
|
oldInbound.Sniffing = inbound.Sniffing
|
||||||
oldInbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
|
oldInbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
|
||||||
|
|
||||||
s.UpdateClientStat(inbound.Id, inbound.Settings)
|
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
return inbound, db.Save(oldInbound).Error
|
return inbound, db.Save(oldInbound).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) AddInboundClient(inbound *model.Inbound) error {
|
||||||
|
existEmail, err := s.checkEmailExistForInbound(inbound)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if existEmail != "" {
|
||||||
|
return common.NewError("Duplicate email:", existEmail)
|
||||||
|
}
|
||||||
|
|
||||||
|
clients, err := s.getClients(inbound)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
oldInbound, err := s.GetInbound(inbound.Id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
oldClients, err := s.getClients(oldInbound)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
oldInbound.Settings = inbound.Settings
|
||||||
|
|
||||||
|
if len(clients[len(clients)-1].Email) > 0 {
|
||||||
|
s.AddClientStat(inbound.Id, &clients[len(clients)-1])
|
||||||
|
}
|
||||||
|
for i := len(oldClients); i < len(clients); i++ {
|
||||||
|
if len(clients[i].Email) > 0 {
|
||||||
|
s.AddClientStat(inbound.Id, &clients[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
db := database.GetDB()
|
||||||
|
return db.Save(oldInbound).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) DelInboundClient(inbound *model.Inbound, email string) error {
|
||||||
|
db := database.GetDB()
|
||||||
|
err := s.DelClientStat(db, email)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Delete stats Data Error")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
oldInbound, err := s.GetInbound(inbound.Id)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Load Old Data Error")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
oldInbound.Settings = inbound.Settings
|
||||||
|
|
||||||
|
err = s.DelClientIPs(db, email)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Error in delete client IPs")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.Save(oldInbound).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) UpdateInboundClient(inbound *model.Inbound, index int) error {
|
||||||
|
existEmail, err := s.checkEmailExistForInbound(inbound)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if existEmail != "" {
|
||||||
|
return common.NewError("Duplicate email:", existEmail)
|
||||||
|
}
|
||||||
|
|
||||||
|
clients, err := s.getClients(inbound)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
oldInbound, err := s.GetInbound(inbound.Id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
oldClients, err := s.getClients(oldInbound)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
oldInbound.Settings = inbound.Settings
|
||||||
|
|
||||||
|
db := database.GetDB()
|
||||||
|
|
||||||
|
if len(clients[index].Email) > 0 {
|
||||||
|
if len(oldClients[index].Email) > 0 {
|
||||||
|
err = s.UpdateClientStat(oldClients[index].Email, &clients[index])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = s.UpdateClientIPs(db, oldClients[index].Email, clients[index].Email)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
s.AddClientStat(inbound.Id, &clients[index])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = s.DelClientStat(db, oldClients[index].Email)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = s.DelClientIPs(db, oldClients[index].Email)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return db.Save(oldInbound).Error
|
||||||
|
}
|
||||||
|
|
||||||
func (s *InboundService) AddTraffic(traffics []*xray.Traffic) (err error) {
|
func (s *InboundService) AddTraffic(traffics []*xray.Traffic) (err error) {
|
||||||
if len(traffics) == 0 {
|
if len(traffics) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -252,11 +394,16 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
|
|||||||
if len(traffics) == 0 {
|
if len(traffics) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
db := database.GetDB()
|
|
||||||
dbInbound := db.Model(model.Inbound{})
|
|
||||||
|
|
||||||
|
traffics, err = s.adjustTraffics(traffics)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
db := database.GetDB()
|
||||||
db = db.Model(xray.ClientTraffic{})
|
db = db.Model(xray.ClientTraffic{})
|
||||||
tx := db.Begin()
|
tx := db.Begin()
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
@@ -264,7 +411,20 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
|
|||||||
tx.Commit()
|
tx.Commit()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
err = tx.Save(traffics).Error
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("AddClientTraffic update data ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) adjustTraffics(traffics []*xray.ClientTraffic) (full_traffics []*xray.ClientTraffic, err error) {
|
||||||
|
db := database.GetDB()
|
||||||
|
dbInbound := db.Model(model.Inbound{})
|
||||||
txInbound := dbInbound.Begin()
|
txInbound := dbInbound.Begin()
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
txInbound.Rollback()
|
txInbound.Rollback()
|
||||||
@@ -273,17 +433,20 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for _, traffic := range traffics {
|
for traffic_index, traffic := range traffics {
|
||||||
inbound := &model.Inbound{}
|
inbound := &model.Inbound{}
|
||||||
client := &xray.ClientTraffic{}
|
client_traffic := &xray.ClientTraffic{}
|
||||||
err := tx.Where("email = ?", traffic.Email).First(client).Error
|
err := db.Model(xray.ClientTraffic{}).Where("email = ?", traffic.Email).First(client_traffic).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == gorm.ErrRecordNotFound {
|
if err == gorm.ErrRecordNotFound {
|
||||||
logger.Warning(err, traffic.Email)
|
logger.Warning(err, traffic.Email)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
err = txInbound.Where("id=?", client.InboundId).First(inbound).Error
|
client_traffic.Up += traffic.Up
|
||||||
|
client_traffic.Down += traffic.Down
|
||||||
|
|
||||||
|
err = txInbound.Where("id=?", client_traffic.InboundId).First(inbound).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == gorm.ErrRecordNotFound {
|
if err == gorm.ErrRecordNotFound {
|
||||||
logger.Warning(err, traffic.Email)
|
logger.Warning(err, traffic.Email)
|
||||||
@@ -294,29 +457,35 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
|
|||||||
settings := map[string][]model.Client{}
|
settings := map[string][]model.Client{}
|
||||||
json.Unmarshal([]byte(inbound.Settings), &settings)
|
json.Unmarshal([]byte(inbound.Settings), &settings)
|
||||||
clients := settings["clients"]
|
clients := settings["clients"]
|
||||||
for _, client := range clients {
|
needUpdate := false
|
||||||
|
for client_index, client := range clients {
|
||||||
if traffic.Email == client.Email {
|
if traffic.Email == client.Email {
|
||||||
traffic.ExpiryTime = client.ExpiryTime
|
if client.ExpiryTime < 0 {
|
||||||
traffic.Total = client.TotalGB
|
clients[client_index].ExpiryTime = (time.Now().Unix() * 1000) - client.ExpiryTime
|
||||||
|
needUpdate = true
|
||||||
|
}
|
||||||
|
client_traffic.ExpiryTime = client.ExpiryTime
|
||||||
|
client_traffic.Total = client.TotalGB
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if tx.Where("inbound_id = ?", inbound.Id).Where("email = ?", traffic.Email).
|
|
||||||
UpdateColumns(map[string]interface{}{
|
if needUpdate {
|
||||||
"enable": true,
|
settings["clients"] = clients
|
||||||
"expiry_time": traffic.ExpiryTime,
|
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
|
||||||
"total": traffic.Total,
|
if err != nil {
|
||||||
"up": gorm.Expr("up + ?", traffic.Up),
|
return nil, err
|
||||||
"down": gorm.Expr("down + ?", traffic.Down)}).RowsAffected == 0 {
|
}
|
||||||
err = tx.Create(traffic).Error
|
|
||||||
}
|
err = txInbound.Where("id=?", inbound.Id).Update("settings", string(modifiedSettings)).Error
|
||||||
|
if err != nil {
|
||||||
if err != nil {
|
return nil, err
|
||||||
logger.Warning("AddClientTraffic update data ", err)
|
}
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
traffics[traffic_index] = client_traffic
|
||||||
}
|
}
|
||||||
return
|
return traffics, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *InboundService) DisableInvalidInbounds() (int64, error) {
|
func (s *InboundService) DisableInvalidInbounds() (int64, error) {
|
||||||
@@ -339,67 +508,89 @@ func (s *InboundService) DisableInvalidClients() (int64, error) {
|
|||||||
count := result.RowsAffected
|
count := result.RowsAffected
|
||||||
return count, err
|
return count, err
|
||||||
}
|
}
|
||||||
func (s *InboundService) UpdateClientStat(inboundId int, inboundSettings string) error {
|
func (s *InboundService) AddClientStat(inboundId int, client *model.Client) error {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
|
|
||||||
// get settings clients
|
clientTraffic := xray.ClientTraffic{}
|
||||||
settings := map[string][]model.Client{}
|
clientTraffic.InboundId = inboundId
|
||||||
json.Unmarshal([]byte(inboundSettings), &settings)
|
clientTraffic.Email = client.Email
|
||||||
clients := settings["clients"]
|
clientTraffic.Total = client.TotalGB
|
||||||
for _, client := range clients {
|
clientTraffic.ExpiryTime = client.ExpiryTime
|
||||||
result := db.Model(xray.ClientTraffic{}).
|
clientTraffic.Enable = true
|
||||||
Where("inbound_id = ? and email = ?", inboundId, client.Email).
|
clientTraffic.Up = 0
|
||||||
Updates(map[string]interface{}{"enable": true, "total": client.TotalGB, "expiry_time": client.ExpiryTime})
|
clientTraffic.Down = 0
|
||||||
if result.RowsAffected == 0 {
|
result := db.Create(&clientTraffic)
|
||||||
clientTraffic := xray.ClientTraffic{}
|
err := result.Error
|
||||||
clientTraffic.InboundId = inboundId
|
if err != nil {
|
||||||
clientTraffic.Email = client.Email
|
return err
|
||||||
clientTraffic.Total = client.TotalGB
|
|
||||||
clientTraffic.ExpiryTime = client.ExpiryTime
|
|
||||||
clientTraffic.Enable = true
|
|
||||||
clientTraffic.Up = 0
|
|
||||||
clientTraffic.Down = 0
|
|
||||||
db.Create(&clientTraffic)
|
|
||||||
}
|
|
||||||
err := result.Error
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
func (s *InboundService) UpdateClientStat(email string, client *model.Client) error {
|
||||||
|
db := database.GetDB()
|
||||||
|
|
||||||
|
result := db.Model(xray.ClientTraffic{}).
|
||||||
|
Where("email = ?", email).
|
||||||
|
Updates(map[string]interface{}{
|
||||||
|
"enable": true,
|
||||||
|
"email": client.Email,
|
||||||
|
"total": client.TotalGB,
|
||||||
|
"expiry_time": client.ExpiryTime})
|
||||||
|
err := result.Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) UpdateClientIPs(tx *gorm.DB, oldEmail string, newEmail string) error {
|
||||||
|
return tx.Model(model.InboundClientIps{}).Where("client_email = ?", oldEmail).Update("client_email", newEmail).Error
|
||||||
|
}
|
||||||
|
|
||||||
func (s *InboundService) DelClientStat(tx *gorm.DB, email string) error {
|
func (s *InboundService) DelClientStat(tx *gorm.DB, email string) error {
|
||||||
return tx.Where("email = ?", email).Delete(xray.ClientTraffic{}).Error
|
return tx.Where("email = ?", email).Delete(xray.ClientTraffic{}).Error
|
||||||
}
|
}
|
||||||
func (s *InboundService) GetInboundClientIps(clientEmail string) (string, error) {
|
|
||||||
db := database.GetDB()
|
func (s *InboundService) DelClientIPs(tx *gorm.DB, email string) error {
|
||||||
InboundClientIps := &model.InboundClientIps{}
|
logger.Warning(email)
|
||||||
err := db.Model(model.InboundClientIps{}).Where("client_email = ?", clientEmail).First(InboundClientIps).Error
|
return tx.Where("client_email = ?", email).Delete(model.InboundClientIps{}).Error
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return InboundClientIps.Ips, nil
|
|
||||||
}
|
}
|
||||||
func (s *InboundService) ClearClientIps(clientEmail string) (error) {
|
|
||||||
|
func (s *InboundService) ResetClientTraffic(id int, clientEmail string) error {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
|
|
||||||
result := db.Model(model.InboundClientIps{}).
|
result := db.Model(xray.ClientTraffic{}).
|
||||||
Where("client_email = ?", clientEmail).
|
Where("inbound_id = ? and email = ?", id, clientEmail).
|
||||||
Update("ips", "")
|
Updates(map[string]interface{}{"enable": true, "up": 0, "down": 0})
|
||||||
err := result.Error
|
|
||||||
|
|
||||||
|
err := result.Error
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (s *InboundService) ResetClientTraffic(clientEmail string) error {
|
|
||||||
|
func (s *InboundService) ResetAllClientTraffics(id int) error {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
|
|
||||||
result := db.Model(xray.ClientTraffic{}).
|
result := db.Model(xray.ClientTraffic{}).
|
||||||
Where("email = ?", clientEmail).
|
Where("inbound_id = ?", id).
|
||||||
|
Updates(map[string]interface{}{"enable": true, "up": 0, "down": 0})
|
||||||
|
|
||||||
|
err := result.Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) ResetAllTraffics() error {
|
||||||
|
db := database.GetDB()
|
||||||
|
|
||||||
|
result := db.Model(model.Inbound{}).
|
||||||
|
Where("user_id > ?", 0).
|
||||||
Updates(map[string]interface{}{"up": 0, "down": 0})
|
Updates(map[string]interface{}{"up": 0, "down": 0})
|
||||||
|
|
||||||
err := result.Error
|
err := result.Error
|
||||||
@@ -409,12 +600,57 @@ func (s *InboundService) ResetClientTraffic(clientEmail string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (s *InboundService) GetClientTrafficById(uuid string) (traffic *xray.ClientTraffic, err error) {
|
|
||||||
|
func (s *InboundService) GetClientTrafficTgBot(tguname string) ([]*xray.ClientTraffic, error) {
|
||||||
|
db := database.GetDB()
|
||||||
|
var inbounds []*model.Inbound
|
||||||
|
err := db.Model(model.Inbound{}).Where("settings like ?", fmt.Sprintf(`%%"tgId": "%s"%%`, tguname)).Find(&inbounds).Error
|
||||||
|
if err != nil && err != gorm.ErrRecordNotFound {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var emails []string
|
||||||
|
for _, inbound := range inbounds {
|
||||||
|
clients, err := s.getClients(inbound)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Unable to get clients from inbound")
|
||||||
|
}
|
||||||
|
for _, client := range clients {
|
||||||
|
if client.TgID == tguname {
|
||||||
|
emails = append(emails, client.Email)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var traffics []*xray.ClientTraffic
|
||||||
|
err = db.Model(xray.ClientTraffic{}).Where("email IN ?", emails).Find(&traffics).Error
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
logger.Warning(err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return traffics, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) GetClientTrafficByEmail(email string) (traffic []*xray.ClientTraffic, err error) {
|
||||||
|
db := database.GetDB()
|
||||||
|
var traffics []*xray.ClientTraffic
|
||||||
|
|
||||||
|
err = db.Model(xray.ClientTraffic{}).Where("email like ?", "%"+email+"%").Find(&traffics).Error
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
logger.Warning(err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return traffics, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) SearchClientTraffic(query string) (traffic *xray.ClientTraffic, err error) {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
inbound := &model.Inbound{}
|
inbound := &model.Inbound{}
|
||||||
traffic = &xray.ClientTraffic{}
|
traffic = &xray.ClientTraffic{}
|
||||||
|
|
||||||
err = db.Model(model.Inbound{}).Where("settings like ?", "%"+uuid+"%").First(inbound).Error
|
err = db.Model(model.Inbound{}).Where("settings like ?", "%\""+query+"\"%").First(inbound).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == gorm.ErrRecordNotFound {
|
if err == gorm.ErrRecordNotFound {
|
||||||
logger.Warning(err)
|
logger.Warning(err)
|
||||||
@@ -428,9 +664,17 @@ func (s *InboundService) GetClientTrafficById(uuid string) (traffic *xray.Client
|
|||||||
json.Unmarshal([]byte(inbound.Settings), &settings)
|
json.Unmarshal([]byte(inbound.Settings), &settings)
|
||||||
clients := settings["clients"]
|
clients := settings["clients"]
|
||||||
for _, client := range clients {
|
for _, client := range clients {
|
||||||
if uuid == client.ID {
|
if client.ID == query && client.Email != "" {
|
||||||
traffic.Email = client.Email
|
traffic.Email = client.Email
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
if client.Password == query && client.Email != "" {
|
||||||
|
traffic.Email = client.Email
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if traffic.Email == "" {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
err = db.Model(xray.ClientTraffic{}).Where("email = ?", traffic.Email).First(traffic).Error
|
err = db.Model(xray.ClientTraffic{}).Where("email = ?", traffic.Email).First(traffic).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -439,3 +683,36 @@ func (s *InboundService) GetClientTrafficById(uuid string) (traffic *xray.Client
|
|||||||
}
|
}
|
||||||
return traffic, err
|
return traffic, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) GetInboundClientIps(clientEmail string) (string, error) {
|
||||||
|
db := database.GetDB()
|
||||||
|
InboundClientIps := &model.InboundClientIps{}
|
||||||
|
err := db.Model(model.InboundClientIps{}).Where("client_email = ?", clientEmail).First(InboundClientIps).Error
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return InboundClientIps.Ips, nil
|
||||||
|
}
|
||||||
|
func (s *InboundService) ClearClientIps(clientEmail string) error {
|
||||||
|
db := database.GetDB()
|
||||||
|
|
||||||
|
result := db.Model(model.InboundClientIps{}).
|
||||||
|
Where("client_email = ?", clientEmail).
|
||||||
|
Update("ips", "")
|
||||||
|
err := result.Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) SearchInbounds(query string) ([]*model.Inbound, error) {
|
||||||
|
db := database.GetDB()
|
||||||
|
var inbounds []*model.Inbound
|
||||||
|
err := db.Model(model.Inbound{}).Preload("ClientStats").Where("remark like ?", "%"+query+"%").Find(&inbounds).Error
|
||||||
|
if err != nil && err != gorm.ErrRecordNotFound {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return inbounds, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import (
|
|||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"x-ui/logger"
|
"x-ui/logger"
|
||||||
"x-ui/util/sys"
|
"x-ui/util/sys"
|
||||||
@@ -200,24 +202,24 @@ func (s *ServerService) GetXrayVersions() ([]string, error) {
|
|||||||
|
|
||||||
func (s *ServerService) StopXrayService() (string error) {
|
func (s *ServerService) StopXrayService() (string error) {
|
||||||
|
|
||||||
err := s.xrayService.StopXray()
|
err := s.xrayService.StopXray()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("stop xray failed:", err)
|
logger.Error("stop xray failed:", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ServerService) RestartXrayService() (string error) {
|
func (s *ServerService) RestartXrayService() (string error) {
|
||||||
|
|
||||||
s.xrayService.StopXray()
|
s.xrayService.StopXray()
|
||||||
defer func() {
|
defer func() {
|
||||||
err := s.xrayService.RestartXray(true)
|
err := s.xrayService.RestartXray(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("start xray failed:", err)
|
logger.Error("start xray failed:", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -324,3 +326,26 @@ func (s *ServerService) UpdateXray(version string) error {
|
|||||||
return nil
|
return nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ServerService) GetLogs(count string) ([]string, error) {
|
||||||
|
// Define the journalctl command and its arguments
|
||||||
|
var cmdArgs []string
|
||||||
|
if runtime.GOOS == "linux" {
|
||||||
|
cmdArgs = []string{"journalctl", "-u", "x-ui", "--no-pager", "-n", count}
|
||||||
|
} else {
|
||||||
|
return []string{"Unsupported operating system"}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the command
|
||||||
|
cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
|
||||||
|
var out bytes.Buffer
|
||||||
|
cmd.Stdout = &out
|
||||||
|
err := cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(out.String(), "\n")
|
||||||
|
|
||||||
|
return lines, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,11 +28,15 @@ var defaultValueMap = map[string]string{
|
|||||||
"webKeyFile": "",
|
"webKeyFile": "",
|
||||||
"secret": random.Seq(32),
|
"secret": random.Seq(32),
|
||||||
"webBasePath": "/",
|
"webBasePath": "/",
|
||||||
|
"expireDiff": "0",
|
||||||
|
"trafficDiff": "0",
|
||||||
"timeLocation": "Asia/Tehran",
|
"timeLocation": "Asia/Tehran",
|
||||||
"tgBotEnable": "false",
|
"tgBotEnable": "false",
|
||||||
"tgBotToken": "",
|
"tgBotToken": "",
|
||||||
"tgBotChatId": "0",
|
"tgBotChatId": "",
|
||||||
"tgRunTime": "",
|
"tgRunTime": "@daily",
|
||||||
|
"tgBotBackup": "false",
|
||||||
|
"tgCpu": "0",
|
||||||
}
|
}
|
||||||
|
|
||||||
type SettingService struct {
|
type SettingService struct {
|
||||||
@@ -202,30 +206,46 @@ func (s *SettingService) SetTgBotToken(token string) error {
|
|||||||
return s.setString("tgBotToken", token)
|
return s.setString("tgBotToken", token)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SettingService) GetTgBotChatId() (int, error) {
|
func (s *SettingService) GetTgBotChatId() (string, error) {
|
||||||
return s.getInt("tgBotChatId")
|
return s.getString("tgBotChatId")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SettingService) SetTgBotChatId(chatId int) error {
|
func (s *SettingService) SetTgBotChatId(chatIds string) error {
|
||||||
return s.setInt("tgBotChatId", chatId)
|
return s.setString("tgBotChatId", chatIds)
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SettingService) SetTgbotenabled(value bool) error {
|
|
||||||
return s.setBool("tgBotEnable", value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SettingService) GetTgbotenabled() (bool, error) {
|
func (s *SettingService) GetTgbotenabled() (bool, error) {
|
||||||
return s.getBool("tgBotEnable")
|
return s.getBool("tgBotEnable")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SettingService) SetTgbotRuntime(time string) error {
|
func (s *SettingService) SetTgbotenabled(value bool) error {
|
||||||
return s.setString("tgRunTime", time)
|
return s.setBool("tgBotEnable", value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SettingService) GetTgbotRuntime() (string, error) {
|
func (s *SettingService) GetTgbotRuntime() (string, error) {
|
||||||
return s.getString("tgRunTime")
|
return s.getString("tgRunTime")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) SetTgbotRuntime(time string) error {
|
||||||
|
return s.setString("tgRunTime", time)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetTgBotBackup() (bool, error) {
|
||||||
|
return s.getBool("tgBotBackup")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) SetTgBotBackup(value bool) error {
|
||||||
|
return s.setBool("tgBotBackup", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetTgCpu() (int, error) {
|
||||||
|
return s.getInt("tgCpu")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) SetTgCpu(value int) error {
|
||||||
|
return s.setInt("tgCpu", value)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SettingService) GetPort() (int, error) {
|
func (s *SettingService) GetPort() (int, error) {
|
||||||
return s.getInt("webPort")
|
return s.getInt("webPort")
|
||||||
}
|
}
|
||||||
@@ -242,6 +262,22 @@ func (s *SettingService) GetKeyFile() (string, error) {
|
|||||||
return s.getString("webKeyFile")
|
return s.getString("webKeyFile")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetExpireDiff() (int, error) {
|
||||||
|
return s.getInt("expireDiff")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) SetExpireDiff(value int) error {
|
||||||
|
return s.setInt("expireDiff", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetTrafficDiff() (int, error) {
|
||||||
|
return s.getInt("trafficDiff")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) SetgetTrafficDiff(value int) error {
|
||||||
|
return s.setInt("trafficDiff", value)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SettingService) GetSecret() ([]byte, error) {
|
func (s *SettingService) GetSecret() ([]byte, error) {
|
||||||
secret, err := s.getString("secret")
|
secret, err := s.getString("secret")
|
||||||
if secret == defaultValueMap["secret"] {
|
if secret == defaultValueMap["secret"] {
|
||||||
|
|||||||
505
web/service/sub.go
Normal file
@@ -0,0 +1,505 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"x-ui/database"
|
||||||
|
"x-ui/database/model"
|
||||||
|
"x-ui/logger"
|
||||||
|
|
||||||
|
"github.com/goccy/go-json"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SubService struct {
|
||||||
|
address string
|
||||||
|
inboundService InboundService
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SubService) GetSubs(subId string, host string) ([]string, error) {
|
||||||
|
s.address = host
|
||||||
|
var result []string
|
||||||
|
inbounds, err := s.getInboundsBySubId(subId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, inbound := range inbounds {
|
||||||
|
clients, err := s.inboundService.getClients(inbound)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("SubService - GetSub: Unable to get clients from inbound")
|
||||||
|
}
|
||||||
|
if clients == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, client := range clients {
|
||||||
|
if client.SubID == subId {
|
||||||
|
link := s.getLink(inbound, client.Email)
|
||||||
|
result = append(result, link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
|
||||||
|
db := database.GetDB()
|
||||||
|
var inbounds []*model.Inbound
|
||||||
|
err := db.Model(model.Inbound{}).Where("settings like ?", fmt.Sprintf(`%%"subId": "%s"%%`, subId)).Find(&inbounds).Error
|
||||||
|
if err != nil && err != gorm.ErrRecordNotFound {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return inbounds, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SubService) getLink(inbound *model.Inbound, email string) string {
|
||||||
|
switch inbound.Protocol {
|
||||||
|
case "vmess":
|
||||||
|
return s.genVmessLink(inbound, email)
|
||||||
|
case "vless":
|
||||||
|
return s.genVlessLink(inbound, email)
|
||||||
|
case "trojan":
|
||||||
|
return s.genTrojanLink(inbound, email)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
||||||
|
address := s.address
|
||||||
|
if inbound.Protocol != model.VMess {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var stream map[string]interface{}
|
||||||
|
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
|
||||||
|
network, _ := stream["network"].(string)
|
||||||
|
typeStr := "none"
|
||||||
|
host := ""
|
||||||
|
path := ""
|
||||||
|
sni := ""
|
||||||
|
fp := ""
|
||||||
|
var alpn []string
|
||||||
|
allowInsecure := false
|
||||||
|
switch network {
|
||||||
|
case "tcp":
|
||||||
|
tcp, _ := stream["tcpSettings"].(map[string]interface{})
|
||||||
|
header, _ := tcp["header"].(map[string]interface{})
|
||||||
|
typeStr, _ = header["type"].(string)
|
||||||
|
if typeStr == "http" {
|
||||||
|
request := header["request"].(map[string]interface{})
|
||||||
|
requestPath, _ := request["path"].([]interface{})
|
||||||
|
path = requestPath[0].(string)
|
||||||
|
headers, _ := request["headers"].(map[string]interface{})
|
||||||
|
host = searchHost(headers)
|
||||||
|
}
|
||||||
|
case "kcp":
|
||||||
|
kcp, _ := stream["kcpSettings"].(map[string]interface{})
|
||||||
|
header, _ := kcp["header"].(map[string]interface{})
|
||||||
|
typeStr, _ = header["type"].(string)
|
||||||
|
path, _ = kcp["seed"].(string)
|
||||||
|
case "ws":
|
||||||
|
ws, _ := stream["wsSettings"].(map[string]interface{})
|
||||||
|
path = ws["path"].(string)
|
||||||
|
headers, _ := ws["headers"].(map[string]interface{})
|
||||||
|
host = searchHost(headers)
|
||||||
|
case "http":
|
||||||
|
network = "h2"
|
||||||
|
http, _ := stream["httpSettings"].(map[string]interface{})
|
||||||
|
path, _ = http["path"].(string)
|
||||||
|
host = searchHost(http)
|
||||||
|
case "quic":
|
||||||
|
quic, _ := stream["quicSettings"].(map[string]interface{})
|
||||||
|
header := quic["header"].(map[string]interface{})
|
||||||
|
typeStr, _ = header["type"].(string)
|
||||||
|
host, _ = quic["security"].(string)
|
||||||
|
path, _ = quic["key"].(string)
|
||||||
|
case "grpc":
|
||||||
|
grpc, _ := stream["grpcSettings"].(map[string]interface{})
|
||||||
|
path = grpc["serviceName"].(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
security, _ := stream["security"].(string)
|
||||||
|
if security == "tls" {
|
||||||
|
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
|
||||||
|
alpns, _ := tlsSetting["alpn"].([]interface{})
|
||||||
|
for _, a := range alpns {
|
||||||
|
alpn = append(alpn, a.(string))
|
||||||
|
}
|
||||||
|
tlsSettings, _ := searchKey(tlsSetting, "settings")
|
||||||
|
if tlsSetting != nil {
|
||||||
|
if sniValue, ok := searchKey(tlsSettings, "serverName"); ok {
|
||||||
|
sni, _ = sniValue.(string)
|
||||||
|
}
|
||||||
|
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
|
||||||
|
fp, _ = fpValue.(string)
|
||||||
|
}
|
||||||
|
if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
|
||||||
|
allowInsecure, _ = insecure.(bool)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
serverName, _ := tlsSetting["serverName"].(string)
|
||||||
|
if serverName != "" {
|
||||||
|
address = serverName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clients, _ := s.inboundService.getClients(inbound)
|
||||||
|
clientIndex := -1
|
||||||
|
for i, client := range clients {
|
||||||
|
if client.Email == email {
|
||||||
|
clientIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
obj := map[string]interface{}{
|
||||||
|
"v": "2",
|
||||||
|
"ps": email,
|
||||||
|
"add": address,
|
||||||
|
"port": inbound.Port,
|
||||||
|
"id": clients[clientIndex].ID,
|
||||||
|
"aid": clients[clientIndex].AlterIds,
|
||||||
|
"net": network,
|
||||||
|
"type": typeStr,
|
||||||
|
"host": host,
|
||||||
|
"path": path,
|
||||||
|
"tls": security,
|
||||||
|
"sni": sni,
|
||||||
|
"fp": fp,
|
||||||
|
"alpn": strings.Join(alpn, ","),
|
||||||
|
"allowInsecure": allowInsecure,
|
||||||
|
}
|
||||||
|
jsonStr, _ := json.MarshalIndent(obj, "", " ")
|
||||||
|
return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||||
|
address := s.address
|
||||||
|
if inbound.Protocol != model.VLESS {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var stream map[string]interface{}
|
||||||
|
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
|
||||||
|
clients, _ := s.inboundService.getClients(inbound)
|
||||||
|
clientIndex := -1
|
||||||
|
for i, client := range clients {
|
||||||
|
if client.Email == email {
|
||||||
|
clientIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uuid := clients[clientIndex].ID
|
||||||
|
port := inbound.Port
|
||||||
|
streamNetwork := stream["network"].(string)
|
||||||
|
params := make(map[string]string)
|
||||||
|
params["type"] = streamNetwork
|
||||||
|
|
||||||
|
switch streamNetwork {
|
||||||
|
case "tcp":
|
||||||
|
tcp, _ := stream["tcpSettings"].(map[string]interface{})
|
||||||
|
header, _ := tcp["header"].(map[string]interface{})
|
||||||
|
typeStr, _ := header["type"].(string)
|
||||||
|
if typeStr == "http" {
|
||||||
|
request := header["request"].(map[string]interface{})
|
||||||
|
requestPath, _ := request["path"].([]interface{})
|
||||||
|
params["path"] = requestPath[0].(string)
|
||||||
|
headers, _ := request["headers"].(map[string]interface{})
|
||||||
|
params["host"] = searchHost(headers)
|
||||||
|
params["headerType"] = "http"
|
||||||
|
}
|
||||||
|
case "kcp":
|
||||||
|
kcp, _ := stream["kcpSettings"].(map[string]interface{})
|
||||||
|
header, _ := kcp["header"].(map[string]interface{})
|
||||||
|
params["headerType"] = header["type"].(string)
|
||||||
|
params["seed"] = kcp["seed"].(string)
|
||||||
|
case "ws":
|
||||||
|
ws, _ := stream["wsSettings"].(map[string]interface{})
|
||||||
|
params["path"] = ws["path"].(string)
|
||||||
|
headers, _ := ws["headers"].(map[string]interface{})
|
||||||
|
params["host"] = searchHost(headers)
|
||||||
|
case "http":
|
||||||
|
http, _ := stream["httpSettings"].(map[string]interface{})
|
||||||
|
params["path"] = http["path"].(string)
|
||||||
|
params["host"] = searchHost(http)
|
||||||
|
case "quic":
|
||||||
|
quic, _ := stream["quicSettings"].(map[string]interface{})
|
||||||
|
params["quicSecurity"] = quic["security"].(string)
|
||||||
|
params["key"] = quic["key"].(string)
|
||||||
|
header := quic["header"].(map[string]interface{})
|
||||||
|
params["headerType"] = header["type"].(string)
|
||||||
|
case "grpc":
|
||||||
|
grpc, _ := stream["grpcSettings"].(map[string]interface{})
|
||||||
|
params["serviceName"] = grpc["serviceName"].(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
security, _ := stream["security"].(string)
|
||||||
|
if security == "tls" {
|
||||||
|
params["security"] = "tls"
|
||||||
|
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
|
||||||
|
alpns, _ := tlsSetting["alpn"].([]interface{})
|
||||||
|
var alpn []string
|
||||||
|
for _, a := range alpns {
|
||||||
|
alpn = append(alpn, a.(string))
|
||||||
|
}
|
||||||
|
if len(alpn) > 0 {
|
||||||
|
params["alpn"] = strings.Join(alpn, ",")
|
||||||
|
}
|
||||||
|
tlsSettings, _ := searchKey(tlsSetting, "settings")
|
||||||
|
if tlsSetting != nil {
|
||||||
|
if sniValue, ok := searchKey(tlsSettings, "serverName"); ok {
|
||||||
|
params["sni"], _ = sniValue.(string)
|
||||||
|
}
|
||||||
|
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
|
||||||
|
params["fp"], _ = fpValue.(string)
|
||||||
|
}
|
||||||
|
if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
|
||||||
|
if insecure.(bool) {
|
||||||
|
params["allowInsecure"] = "1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
|
||||||
|
params["flow"] = clients[clientIndex].Flow
|
||||||
|
}
|
||||||
|
|
||||||
|
serverName, _ := tlsSetting["serverName"].(string)
|
||||||
|
if serverName != "" {
|
||||||
|
address = serverName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if security == "xtls" {
|
||||||
|
params["security"] = "xtls"
|
||||||
|
xtlsSetting, _ := stream["xtlsSettings"].(map[string]interface{})
|
||||||
|
alpns, _ := xtlsSetting["alpn"].([]interface{})
|
||||||
|
var alpn []string
|
||||||
|
for _, a := range alpns {
|
||||||
|
alpn = append(alpn, a.(string))
|
||||||
|
}
|
||||||
|
if len(alpn) > 0 {
|
||||||
|
params["alpn"] = strings.Join(alpn, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
xtlsSettings, _ := searchKey(xtlsSetting, "settings")
|
||||||
|
if xtlsSetting != nil {
|
||||||
|
if sniValue, ok := searchKey(xtlsSettings, "serverName"); ok {
|
||||||
|
params["sni"], _ = sniValue.(string)
|
||||||
|
}
|
||||||
|
if fpValue, ok := searchKey(xtlsSettings, "fingerprint"); ok {
|
||||||
|
params["fp"], _ = fpValue.(string)
|
||||||
|
}
|
||||||
|
if insecure, ok := searchKey(xtlsSettings, "allowInsecure"); ok {
|
||||||
|
if insecure.(bool) {
|
||||||
|
params["allowInsecure"] = "1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
|
||||||
|
params["flow"] = clients[clientIndex].Flow
|
||||||
|
}
|
||||||
|
|
||||||
|
serverName, _ := xtlsSetting["serverName"].(string)
|
||||||
|
if serverName != "" {
|
||||||
|
address = serverName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
link := fmt.Sprintf("vless://%s@%s:%d", uuid, address, port)
|
||||||
|
url, _ := url.Parse(link)
|
||||||
|
q := url.Query()
|
||||||
|
|
||||||
|
for k, v := range params {
|
||||||
|
q.Add(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the new query values on the URL
|
||||||
|
url.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
url.Fragment = email
|
||||||
|
return url.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string {
|
||||||
|
address := s.address
|
||||||
|
if inbound.Protocol != model.Trojan {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var stream map[string]interface{}
|
||||||
|
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
|
||||||
|
clients, _ := s.inboundService.getClients(inbound)
|
||||||
|
clientIndex := -1
|
||||||
|
for i, client := range clients {
|
||||||
|
if client.Email == email {
|
||||||
|
clientIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
password := clients[clientIndex].Password
|
||||||
|
port := inbound.Port
|
||||||
|
streamNetwork := stream["network"].(string)
|
||||||
|
params := make(map[string]string)
|
||||||
|
params["type"] = streamNetwork
|
||||||
|
|
||||||
|
switch streamNetwork {
|
||||||
|
case "tcp":
|
||||||
|
tcp, _ := stream["tcpSettings"].(map[string]interface{})
|
||||||
|
header, _ := tcp["header"].(map[string]interface{})
|
||||||
|
typeStr, _ := header["type"].(string)
|
||||||
|
if typeStr == "http" {
|
||||||
|
request := header["request"].(map[string]interface{})
|
||||||
|
requestPath, _ := request["path"].([]interface{})
|
||||||
|
params["path"] = requestPath[0].(string)
|
||||||
|
headers, _ := request["headers"].(map[string]interface{})
|
||||||
|
params["host"] = searchHost(headers)
|
||||||
|
params["headerType"] = "http"
|
||||||
|
}
|
||||||
|
case "kcp":
|
||||||
|
kcp, _ := stream["kcpSettings"].(map[string]interface{})
|
||||||
|
header, _ := kcp["header"].(map[string]interface{})
|
||||||
|
params["headerType"] = header["type"].(string)
|
||||||
|
params["seed"] = kcp["seed"].(string)
|
||||||
|
case "ws":
|
||||||
|
ws, _ := stream["wsSettings"].(map[string]interface{})
|
||||||
|
params["path"] = ws["path"].(string)
|
||||||
|
headers, _ := ws["headers"].(map[string]interface{})
|
||||||
|
params["host"] = searchHost(headers)
|
||||||
|
case "http":
|
||||||
|
http, _ := stream["httpSettings"].(map[string]interface{})
|
||||||
|
params["path"] = http["path"].(string)
|
||||||
|
params["host"] = searchHost(http)
|
||||||
|
case "quic":
|
||||||
|
quic, _ := stream["quicSettings"].(map[string]interface{})
|
||||||
|
params["quicSecurity"] = quic["security"].(string)
|
||||||
|
params["key"] = quic["key"].(string)
|
||||||
|
header := quic["header"].(map[string]interface{})
|
||||||
|
params["headerType"] = header["type"].(string)
|
||||||
|
case "grpc":
|
||||||
|
grpc, _ := stream["grpcSettings"].(map[string]interface{})
|
||||||
|
params["serviceName"] = grpc["serviceName"].(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
security, _ := stream["security"].(string)
|
||||||
|
if security == "tls" {
|
||||||
|
params["security"] = "tls"
|
||||||
|
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
|
||||||
|
alpns, _ := tlsSetting["alpn"].([]interface{})
|
||||||
|
var alpn []string
|
||||||
|
for _, a := range alpns {
|
||||||
|
alpn = append(alpn, a.(string))
|
||||||
|
}
|
||||||
|
if len(alpn) > 0 {
|
||||||
|
params["alpn"] = strings.Join(alpn, ",")
|
||||||
|
}
|
||||||
|
tlsSettings, _ := searchKey(tlsSetting, "settings")
|
||||||
|
if tlsSetting != nil {
|
||||||
|
if sniValue, ok := searchKey(tlsSettings, "serverName"); ok {
|
||||||
|
params["sni"], _ = sniValue.(string)
|
||||||
|
}
|
||||||
|
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
|
||||||
|
params["fp"], _ = fpValue.(string)
|
||||||
|
}
|
||||||
|
if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
|
||||||
|
if insecure.(bool) {
|
||||||
|
params["allowInsecure"] = "1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
serverName, _ := tlsSetting["serverName"].(string)
|
||||||
|
if serverName != "" {
|
||||||
|
address = serverName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if security == "xtls" {
|
||||||
|
params["security"] = "xtls"
|
||||||
|
xtlsSetting, _ := stream["xtlsSettings"].(map[string]interface{})
|
||||||
|
alpns, _ := xtlsSetting["alpn"].([]interface{})
|
||||||
|
var alpn []string
|
||||||
|
for _, a := range alpns {
|
||||||
|
alpn = append(alpn, a.(string))
|
||||||
|
}
|
||||||
|
if len(alpn) > 0 {
|
||||||
|
params["alpn"] = strings.Join(alpn, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
xtlsSettings, _ := searchKey(xtlsSetting, "settings")
|
||||||
|
if xtlsSetting != nil {
|
||||||
|
if sniValue, ok := searchKey(xtlsSettings, "serverName"); ok {
|
||||||
|
params["sni"], _ = sniValue.(string)
|
||||||
|
}
|
||||||
|
if fpValue, ok := searchKey(xtlsSettings, "fingerprint"); ok {
|
||||||
|
params["fp"], _ = fpValue.(string)
|
||||||
|
}
|
||||||
|
if insecure, ok := searchKey(xtlsSettings, "allowInsecure"); ok {
|
||||||
|
if insecure.(bool) {
|
||||||
|
params["allowInsecure"] = "1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
|
||||||
|
params["flow"] = clients[clientIndex].Flow
|
||||||
|
}
|
||||||
|
|
||||||
|
serverName, _ := xtlsSetting["serverName"].(string)
|
||||||
|
if serverName != "" {
|
||||||
|
address = serverName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
link := fmt.Sprintf("trojan://%s@%s:%d", password, address, port)
|
||||||
|
|
||||||
|
url, _ := url.Parse(link)
|
||||||
|
q := url.Query()
|
||||||
|
|
||||||
|
for k, v := range params {
|
||||||
|
q.Add(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the new query values on the URL
|
||||||
|
url.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
url.Fragment = email
|
||||||
|
return url.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func searchKey(data interface{}, key string) (interface{}, bool) {
|
||||||
|
switch val := data.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
for k, v := range val {
|
||||||
|
if k == key {
|
||||||
|
return v, true
|
||||||
|
}
|
||||||
|
if result, ok := searchKey(v, key); ok {
|
||||||
|
return result, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case []interface{}:
|
||||||
|
for _, v := range val {
|
||||||
|
if result, ok := searchKey(v, key); ok {
|
||||||
|
return result, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func searchHost(headers interface{}) string {
|
||||||
|
data, _ := headers.(map[string]interface{})
|
||||||
|
for k, v := range data {
|
||||||
|
if strings.EqualFold(k, "host") {
|
||||||
|
switch v.(type) {
|
||||||
|
case []interface{}:
|
||||||
|
hosts, _ := v.([]interface{})
|
||||||
|
return hosts[0].(string)
|
||||||
|
case interface{}:
|
||||||
|
return v.(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
612
web/service/tgbot.go
Normal file
@@ -0,0 +1,612 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"x-ui/config"
|
||||||
|
"x-ui/database/model"
|
||||||
|
"x-ui/logger"
|
||||||
|
"x-ui/util/common"
|
||||||
|
"x-ui/xray"
|
||||||
|
|
||||||
|
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
var bot *tgbotapi.BotAPI
|
||||||
|
var adminIds []int64
|
||||||
|
var isRunning bool
|
||||||
|
|
||||||
|
type LoginStatus byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
LoginSuccess LoginStatus = 1
|
||||||
|
LoginFail LoginStatus = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
type Tgbot struct {
|
||||||
|
inboundService InboundService
|
||||||
|
settingService SettingService
|
||||||
|
serverService ServerService
|
||||||
|
lastStatus *Status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) NewTgbot() *Tgbot {
|
||||||
|
return new(Tgbot)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) Start() error {
|
||||||
|
tgBottoken, err := t.settingService.GetTgBotToken()
|
||||||
|
if err != nil || tgBottoken == "" {
|
||||||
|
logger.Warning("Get TgBotToken failed:", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tgBotid, err := t.settingService.GetTgBotChatId()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("Get GetTgBotChatId failed:", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, adminId := range strings.Split(tgBotid, ",") {
|
||||||
|
id, err := strconv.Atoi(adminId)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("Failed to get IDs from GetTgBotChatId:", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
adminIds = append(adminIds, int64(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
bot, err = tgbotapi.NewBotAPI(tgBottoken)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Get tgbot's api error:", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bot.Debug = false
|
||||||
|
|
||||||
|
// listen for TG bot income messages
|
||||||
|
if !isRunning {
|
||||||
|
logger.Info("Starting Telegram receiver ...")
|
||||||
|
go t.OnReceive()
|
||||||
|
isRunning = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) IsRunnging() bool {
|
||||||
|
return isRunning
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) Stop() {
|
||||||
|
bot.StopReceivingUpdates()
|
||||||
|
logger.Info("Stop Telegram receiver ...")
|
||||||
|
isRunning = false
|
||||||
|
adminIds = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) OnReceive() {
|
||||||
|
u := tgbotapi.NewUpdate(0)
|
||||||
|
u.Timeout = 10
|
||||||
|
|
||||||
|
updates := bot.GetUpdatesChan(u)
|
||||||
|
|
||||||
|
for update := range updates {
|
||||||
|
tgId := update.FromChat().ID
|
||||||
|
chatId := update.FromChat().ChatConfig().ChatID
|
||||||
|
isAdmin := checkAdmin(tgId)
|
||||||
|
if update.Message == nil {
|
||||||
|
if update.CallbackQuery != nil {
|
||||||
|
t.asnwerCallback(update.CallbackQuery, isAdmin)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if update.Message.IsCommand() {
|
||||||
|
t.answerCommand(update.Message, chatId, isAdmin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) answerCommand(message *tgbotapi.Message, chatId int64, isAdmin bool) {
|
||||||
|
msg := ""
|
||||||
|
// Extract the command from the Message.
|
||||||
|
switch message.Command() {
|
||||||
|
case "help":
|
||||||
|
msg = "This bot is providing you some specefic data from the server.\n\n Please choose:"
|
||||||
|
case "start":
|
||||||
|
msg = "Hello <i>" + message.From.FirstName + "</i> 👋"
|
||||||
|
if isAdmin {
|
||||||
|
hostname, _ := os.Hostname()
|
||||||
|
msg += "\nWelcome to <b>" + hostname + "</b> management bot"
|
||||||
|
}
|
||||||
|
msg += "\n\nI can do some magics for you, please choose:"
|
||||||
|
case "status":
|
||||||
|
msg = "bot is ok ✅"
|
||||||
|
case "usage":
|
||||||
|
if len(message.CommandArguments()) > 1 {
|
||||||
|
if isAdmin {
|
||||||
|
t.searchClient(chatId, message.CommandArguments())
|
||||||
|
} else {
|
||||||
|
t.searchForClient(chatId, message.CommandArguments())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
msg = "❗Please provide a text for search!"
|
||||||
|
}
|
||||||
|
case "inbound":
|
||||||
|
if isAdmin {
|
||||||
|
t.searchInbound(chatId, message.CommandArguments())
|
||||||
|
} else {
|
||||||
|
msg = "❗ Unknown command"
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
msg = "❗ Unknown command"
|
||||||
|
}
|
||||||
|
t.SendAnswer(chatId, msg, isAdmin)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) asnwerCallback(callbackQuery *tgbotapi.CallbackQuery, isAdmin bool) {
|
||||||
|
// Respond to the callback query, telling Telegram to show the user
|
||||||
|
// a message with the data received.
|
||||||
|
callback := tgbotapi.NewCallback(callbackQuery.ID, callbackQuery.Data)
|
||||||
|
if _, err := bot.Request(callback); err != nil {
|
||||||
|
logger.Warning(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch callbackQuery.Data {
|
||||||
|
case "get_usage":
|
||||||
|
t.SendMsgToTgbot(callbackQuery.From.ID, t.getServerUsage())
|
||||||
|
case "inbounds":
|
||||||
|
t.SendMsgToTgbot(callbackQuery.From.ID, t.getInboundUsages())
|
||||||
|
case "deplete_soon":
|
||||||
|
t.SendMsgToTgbot(callbackQuery.From.ID, t.getExhausted())
|
||||||
|
case "get_backup":
|
||||||
|
t.sendBackup(callbackQuery.From.ID)
|
||||||
|
case "client_traffic":
|
||||||
|
t.getClientUsage(callbackQuery.From.ID, callbackQuery.From.UserName)
|
||||||
|
case "client_commands":
|
||||||
|
t.SendMsgToTgbot(callbackQuery.From.ID, "To search for statistics, just use folowing command:\r\n \r\n<code>/usage [UID|Passowrd]</code>\r\n \r\nUse UID for vmess/vless and Password for Trojan.")
|
||||||
|
case "commands":
|
||||||
|
t.SendMsgToTgbot(callbackQuery.From.ID, "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>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkAdmin(tgId int64) bool {
|
||||||
|
for _, adminId := range adminIds {
|
||||||
|
if adminId == tgId {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) {
|
||||||
|
var numericKeyboard = tgbotapi.NewInlineKeyboardMarkup(
|
||||||
|
tgbotapi.NewInlineKeyboardRow(
|
||||||
|
tgbotapi.NewInlineKeyboardButtonData("Server Usage", "get_usage"),
|
||||||
|
tgbotapi.NewInlineKeyboardButtonData("Get DB Backup", "get_backup"),
|
||||||
|
),
|
||||||
|
tgbotapi.NewInlineKeyboardRow(
|
||||||
|
tgbotapi.NewInlineKeyboardButtonData("Get Inbounds", "inbounds"),
|
||||||
|
tgbotapi.NewInlineKeyboardButtonData("Deplete soon", "deplete_soon"),
|
||||||
|
),
|
||||||
|
tgbotapi.NewInlineKeyboardRow(
|
||||||
|
tgbotapi.NewInlineKeyboardButtonData("Commands", "commands"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
var numericKeyboardClient = tgbotapi.NewInlineKeyboardMarkup(
|
||||||
|
tgbotapi.NewInlineKeyboardRow(
|
||||||
|
tgbotapi.NewInlineKeyboardButtonData("Get Usage", "client_traffic"),
|
||||||
|
tgbotapi.NewInlineKeyboardButtonData("Commands", "client_commands"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
msgConfig := tgbotapi.NewMessage(chatId, msg)
|
||||||
|
msgConfig.ParseMode = "HTML"
|
||||||
|
if isAdmin {
|
||||||
|
msgConfig.ReplyMarkup = numericKeyboard
|
||||||
|
} else {
|
||||||
|
msgConfig.ReplyMarkup = numericKeyboardClient
|
||||||
|
}
|
||||||
|
_, err := bot.Send(msgConfig)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("Error sending telegram message :", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) SendMsgToTgbot(tgid int64, msg string) {
|
||||||
|
var allMessages []string
|
||||||
|
limit := 2000
|
||||||
|
// paging message if it is big
|
||||||
|
if len(msg) > limit {
|
||||||
|
messages := strings.Split(msg, "\r\n \r\n")
|
||||||
|
lastIndex := -1
|
||||||
|
for _, message := range messages {
|
||||||
|
if (len(allMessages) == 0) || (len(allMessages[lastIndex])+len(message) > limit) {
|
||||||
|
allMessages = append(allMessages, message)
|
||||||
|
lastIndex++
|
||||||
|
} else {
|
||||||
|
allMessages[lastIndex] += "\r\n \r\n" + message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
allMessages = append(allMessages, msg)
|
||||||
|
}
|
||||||
|
for _, message := range allMessages {
|
||||||
|
info := tgbotapi.NewMessage(tgid, message)
|
||||||
|
info.ParseMode = "HTML"
|
||||||
|
_, err := bot.Send(info)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("Error sending telegram message :", err)
|
||||||
|
}
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) SendMsgToTgbotAdmins(msg string) {
|
||||||
|
for _, adminId := range adminIds {
|
||||||
|
t.SendMsgToTgbot(adminId, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) SendReport() {
|
||||||
|
runTime, err := t.settingService.GetTgbotRuntime()
|
||||||
|
if err == nil && len(runTime) > 0 {
|
||||||
|
t.SendMsgToTgbotAdmins("🕰 Scheduled reports: " + runTime + "\r\nDate-Time: " + time.Now().Format("2006-01-02 15:04:05"))
|
||||||
|
}
|
||||||
|
info := t.getServerUsage()
|
||||||
|
t.SendMsgToTgbotAdmins(info)
|
||||||
|
exhausted := t.getExhausted()
|
||||||
|
t.SendMsgToTgbotAdmins(exhausted)
|
||||||
|
backupEnable, err := t.settingService.GetTgBotBackup()
|
||||||
|
if err == nil && backupEnable {
|
||||||
|
for _, adminId := range adminIds {
|
||||||
|
t.sendBackup(int64(adminId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) getServerUsage() string {
|
||||||
|
var info string
|
||||||
|
//get hostname
|
||||||
|
name, err := os.Hostname()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("get hostname error:", err)
|
||||||
|
name = ""
|
||||||
|
}
|
||||||
|
info = fmt.Sprintf("💻 Hostname: %s\r\n", name)
|
||||||
|
info += fmt.Sprintf("🚀X-UI Version: %s\r\n", config.GetVersion())
|
||||||
|
//get ip address
|
||||||
|
var ip string
|
||||||
|
var ipv6 string
|
||||||
|
netInterfaces, err := net.Interfaces()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("net.Interfaces failed, err:", err.Error())
|
||||||
|
info += "🌐 IP: Unknown\r\n \r\n"
|
||||||
|
} else {
|
||||||
|
for i := 0; i < len(netInterfaces); i++ {
|
||||||
|
if (netInterfaces[i].Flags & net.FlagUp) != 0 {
|
||||||
|
addrs, _ := netInterfaces[i].Addrs()
|
||||||
|
|
||||||
|
for _, address := range addrs {
|
||||||
|
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
|
||||||
|
if ipnet.IP.To4() != nil {
|
||||||
|
ip += ipnet.IP.String() + " "
|
||||||
|
} else if ipnet.IP.To16() != nil && !ipnet.IP.IsLinkLocalUnicast() {
|
||||||
|
ipv6 += ipnet.IP.String() + " "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info += fmt.Sprintf("🌐IP: %s\r\n🌐IPv6: %s\r\n", ip, ipv6)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get latest status of server
|
||||||
|
t.lastStatus = t.serverService.GetStatus(t.lastStatus)
|
||||||
|
info += fmt.Sprintf("🔌Server Uptime: %d days\r\n", int(t.lastStatus.Uptime/86400))
|
||||||
|
info += fmt.Sprintf("📈Server Load: %.1f, %.1f, %.1f\r\n", t.lastStatus.Loads[0], t.lastStatus.Loads[1], t.lastStatus.Loads[2])
|
||||||
|
info += fmt.Sprintf("📋Server Memory: %s/%s\r\n", common.FormatTraffic(int64(t.lastStatus.Mem.Current)), common.FormatTraffic(int64(t.lastStatus.Mem.Total)))
|
||||||
|
info += fmt.Sprintf("🔹TcpCount: %d\r\n", t.lastStatus.TcpCount)
|
||||||
|
info += fmt.Sprintf("🔸UdpCount: %d\r\n", t.lastStatus.UdpCount)
|
||||||
|
info += fmt.Sprintf("🚦Traffic: %s (↑%s,↓%s)\r\n", common.FormatTraffic(int64(t.lastStatus.NetTraffic.Sent+t.lastStatus.NetTraffic.Recv)), common.FormatTraffic(int64(t.lastStatus.NetTraffic.Sent)), common.FormatTraffic(int64(t.lastStatus.NetTraffic.Recv)))
|
||||||
|
info += fmt.Sprintf("ℹXray status: %s", t.lastStatus.Xray.State)
|
||||||
|
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) UserLoginNotify(username string, ip string, time string, status LoginStatus) {
|
||||||
|
if username == "" || ip == "" || time == "" {
|
||||||
|
logger.Warning("UserLoginNotify failed,invalid info")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var msg string
|
||||||
|
// Get hostname
|
||||||
|
name, err := os.Hostname()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("get hostname error:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if status == LoginSuccess {
|
||||||
|
msg = fmt.Sprintf("✅ Successfully logged-in to the panel\r\nHostname:%s\r\n", name)
|
||||||
|
} else if status == LoginFail {
|
||||||
|
msg = fmt.Sprintf("❗ Login to the panel was unsuccessful\r\nHostname:%s\r\n", name)
|
||||||
|
}
|
||||||
|
msg += fmt.Sprintf("⏰ Time:%s\r\n", time)
|
||||||
|
msg += fmt.Sprintf("🆔 Username:%s\r\n", username)
|
||||||
|
msg += fmt.Sprintf("🌐 IP:%s\r\n", ip)
|
||||||
|
t.SendMsgToTgbotAdmins(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) getInboundUsages() string {
|
||||||
|
info := ""
|
||||||
|
// get traffic
|
||||||
|
inbouds, err := t.inboundService.GetAllInbounds()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("GetAllInbounds run failed:", err)
|
||||||
|
info += "❌ Failed to get inbounds"
|
||||||
|
} else {
|
||||||
|
// NOTE:If there no any sessions here,need to notify here
|
||||||
|
// TODO:Sub-node push, automatic conversion format
|
||||||
|
for _, inbound := range inbouds {
|
||||||
|
info += fmt.Sprintf("📍Inbound:%s\r\nPort:%d\r\n", inbound.Remark, inbound.Port)
|
||||||
|
info += fmt.Sprintf("Traffic: %s (↑%s,↓%s)\r\n", common.FormatTraffic((inbound.Up + inbound.Down)), common.FormatTraffic(inbound.Up), common.FormatTraffic(inbound.Down))
|
||||||
|
if inbound.ExpiryTime == 0 {
|
||||||
|
info += "Expire date: ♾ Unlimited\r\n \r\n"
|
||||||
|
} else {
|
||||||
|
info += fmt.Sprintf("Expire date:%s\r\n \r\n", time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) getClientUsage(chatId int64, tgUserName string) {
|
||||||
|
if len(tgUserName) == 0 {
|
||||||
|
msg := "Your configuration is not found!\nYou should configure your telegram username and ask Admin to add it to your configuration."
|
||||||
|
t.SendMsgToTgbot(chatId, msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserName)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning(err)
|
||||||
|
msg := "❌ Something went wrong!"
|
||||||
|
t.SendMsgToTgbot(chatId, msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(traffics) == 0 {
|
||||||
|
msg := "Your configuration is not found!\nPlease ask your Admin to use your telegram username in your configuration(s).\n\nYour username: <b>@" + tgUserName + "</b>"
|
||||||
|
t.SendMsgToTgbot(chatId, msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, traffic := range traffics {
|
||||||
|
expiryTime := ""
|
||||||
|
if traffic.ExpiryTime == 0 {
|
||||||
|
expiryTime = "♾Unlimited"
|
||||||
|
} else if traffic.ExpiryTime < 0 {
|
||||||
|
expiryTime = fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000)
|
||||||
|
} else {
|
||||||
|
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
|
||||||
|
}
|
||||||
|
total := ""
|
||||||
|
if traffic.Total == 0 {
|
||||||
|
total = "♾Unlimited"
|
||||||
|
} else {
|
||||||
|
total = common.FormatTraffic((traffic.Total))
|
||||||
|
}
|
||||||
|
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
|
||||||
|
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
|
||||||
|
total, expiryTime)
|
||||||
|
t.SendMsgToTgbot(chatId, output)
|
||||||
|
}
|
||||||
|
t.SendAnswer(chatId, "Please choose:", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) searchClient(chatId int64, email string) {
|
||||||
|
traffics, err := t.inboundService.GetClientTrafficByEmail(email)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning(err)
|
||||||
|
msg := "❌ Something went wrong!"
|
||||||
|
t.SendMsgToTgbot(chatId, msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(traffics) == 0 {
|
||||||
|
msg := "No result!"
|
||||||
|
t.SendMsgToTgbot(chatId, msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, traffic := range traffics {
|
||||||
|
expiryTime := ""
|
||||||
|
if traffic.ExpiryTime == 0 {
|
||||||
|
expiryTime = "♾Unlimited"
|
||||||
|
} else if traffic.ExpiryTime < 0 {
|
||||||
|
expiryTime = fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000)
|
||||||
|
} else {
|
||||||
|
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
|
||||||
|
}
|
||||||
|
total := ""
|
||||||
|
if traffic.Total == 0 {
|
||||||
|
total = "♾Unlimited"
|
||||||
|
} else {
|
||||||
|
total = common.FormatTraffic((traffic.Total))
|
||||||
|
}
|
||||||
|
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
|
||||||
|
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
|
||||||
|
total, expiryTime)
|
||||||
|
t.SendMsgToTgbot(chatId, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) searchInbound(chatId int64, remark string) {
|
||||||
|
inbouds, err := t.inboundService.SearchInbounds(remark)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning(err)
|
||||||
|
msg := "❌ Something went wrong!"
|
||||||
|
t.SendMsgToTgbot(chatId, msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, inbound := range inbouds {
|
||||||
|
info := ""
|
||||||
|
info += fmt.Sprintf("📍Inbound:%s\r\nPort:%d\r\n", inbound.Remark, inbound.Port)
|
||||||
|
info += fmt.Sprintf("Traffic: %s (↑%s,↓%s)\r\n", common.FormatTraffic((inbound.Up + inbound.Down)), common.FormatTraffic(inbound.Up), common.FormatTraffic(inbound.Down))
|
||||||
|
if inbound.ExpiryTime == 0 {
|
||||||
|
info += "Expire date: ♾ Unlimited\r\n \r\n"
|
||||||
|
} else {
|
||||||
|
info += fmt.Sprintf("Expire date:%s\r\n \r\n", time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
|
||||||
|
}
|
||||||
|
t.SendMsgToTgbot(chatId, info)
|
||||||
|
for _, traffic := range inbound.ClientStats {
|
||||||
|
expiryTime := ""
|
||||||
|
if traffic.ExpiryTime == 0 {
|
||||||
|
expiryTime = "♾Unlimited"
|
||||||
|
} else if traffic.ExpiryTime < 0 {
|
||||||
|
expiryTime = fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000)
|
||||||
|
} else {
|
||||||
|
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
|
||||||
|
}
|
||||||
|
total := ""
|
||||||
|
if traffic.Total == 0 {
|
||||||
|
total = "♾Unlimited"
|
||||||
|
} else {
|
||||||
|
total = common.FormatTraffic((traffic.Total))
|
||||||
|
}
|
||||||
|
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
|
||||||
|
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
|
||||||
|
total, expiryTime)
|
||||||
|
t.SendMsgToTgbot(chatId, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) searchForClient(chatId int64, query string) {
|
||||||
|
traffic, err := t.inboundService.SearchClientTraffic(query)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning(err)
|
||||||
|
msg := "❌ Something went wrong!"
|
||||||
|
t.SendMsgToTgbot(chatId, msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if traffic == nil {
|
||||||
|
msg := "No result!"
|
||||||
|
t.SendMsgToTgbot(chatId, msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
expiryTime := ""
|
||||||
|
if traffic.ExpiryTime == 0 {
|
||||||
|
expiryTime = "♾Unlimited"
|
||||||
|
} else if traffic.ExpiryTime < 0 {
|
||||||
|
expiryTime = fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000)
|
||||||
|
} else {
|
||||||
|
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
|
||||||
|
}
|
||||||
|
total := ""
|
||||||
|
if traffic.Total == 0 {
|
||||||
|
total = "♾Unlimited"
|
||||||
|
} else {
|
||||||
|
total = common.FormatTraffic((traffic.Total))
|
||||||
|
}
|
||||||
|
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
|
||||||
|
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
|
||||||
|
total, expiryTime)
|
||||||
|
t.SendMsgToTgbot(chatId, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) getExhausted() string {
|
||||||
|
trDiff := int64(0)
|
||||||
|
exDiff := int64(0)
|
||||||
|
now := time.Now().Unix() * 1000
|
||||||
|
var exhaustedInbounds []model.Inbound
|
||||||
|
var exhaustedClients []xray.ClientTraffic
|
||||||
|
var disabledInbounds []model.Inbound
|
||||||
|
var disabledClients []xray.ClientTraffic
|
||||||
|
output := ""
|
||||||
|
TrafficThreshold, err := t.settingService.GetTrafficDiff()
|
||||||
|
if err == nil && TrafficThreshold > 0 {
|
||||||
|
trDiff = int64(TrafficThreshold) * 1073741824
|
||||||
|
}
|
||||||
|
ExpireThreshold, err := t.settingService.GetExpireDiff()
|
||||||
|
if err == nil && ExpireThreshold > 0 {
|
||||||
|
exDiff = int64(ExpireThreshold) * 86400000
|
||||||
|
}
|
||||||
|
inbounds, err := t.inboundService.GetAllInbounds()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("Unable to load Inbounds", err)
|
||||||
|
}
|
||||||
|
for _, inbound := range inbounds {
|
||||||
|
if inbound.Enable {
|
||||||
|
if (inbound.ExpiryTime > 0 && (inbound.ExpiryTime-now < exDiff)) ||
|
||||||
|
(inbound.Total > 0 && (inbound.Total-(inbound.Up+inbound.Down) < trDiff)) {
|
||||||
|
exhaustedInbounds = append(exhaustedInbounds, *inbound)
|
||||||
|
}
|
||||||
|
if len(inbound.ClientStats) > 0 {
|
||||||
|
for _, client := range inbound.ClientStats {
|
||||||
|
if client.Enable {
|
||||||
|
if (client.ExpiryTime > 0 && (client.ExpiryTime-now < exDiff)) ||
|
||||||
|
(client.Total > 0 && (client.Total-(client.Up+client.Down) < trDiff)) {
|
||||||
|
exhaustedClients = append(exhaustedClients, client)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
disabledClients = append(disabledClients, client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
disabledInbounds = append(disabledInbounds, *inbound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output += fmt.Sprintf("Exhausted Inbounds count:\r\n🛑 Disabled: %d\r\n🔜 Deplete soon: %d\r\n \r\n", len(disabledInbounds), len(exhaustedInbounds))
|
||||||
|
if len(exhaustedInbounds) > 0 {
|
||||||
|
output += "Exhausted Inbounds:\r\n"
|
||||||
|
for _, inbound := range exhaustedInbounds {
|
||||||
|
output += fmt.Sprintf("📍Inbound:%s\r\nPort:%d\r\nTraffic: %s (↑%s,↓%s)\r\n", inbound.Remark, inbound.Port, common.FormatTraffic((inbound.Up + inbound.Down)), common.FormatTraffic(inbound.Up), common.FormatTraffic(inbound.Down))
|
||||||
|
if inbound.ExpiryTime == 0 {
|
||||||
|
output += "Expire date: ♾Unlimited\r\n \r\n"
|
||||||
|
} else {
|
||||||
|
output += fmt.Sprintf("Expire date:%s\r\n \r\n", time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output += fmt.Sprintf("Exhausted Clients count:\r\n🛑 Exhausted: %d\r\n🔜 Deplete soon: %d\r\n \r\n", len(disabledClients), len(exhaustedClients))
|
||||||
|
if len(exhaustedClients) > 0 {
|
||||||
|
output += "Exhausted Clients:\r\n"
|
||||||
|
for _, traffic := range exhaustedClients {
|
||||||
|
expiryTime := ""
|
||||||
|
if traffic.ExpiryTime == 0 {
|
||||||
|
expiryTime = "♾Unlimited"
|
||||||
|
} else if traffic.ExpiryTime < 0 {
|
||||||
|
expiryTime += fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000)
|
||||||
|
} else {
|
||||||
|
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
|
||||||
|
}
|
||||||
|
total := ""
|
||||||
|
if traffic.Total == 0 {
|
||||||
|
total = "♾Unlimited"
|
||||||
|
} else {
|
||||||
|
total = common.FormatTraffic((traffic.Total))
|
||||||
|
}
|
||||||
|
output += fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire date: %s\r\n \r\n",
|
||||||
|
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
|
||||||
|
total, expiryTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) sendBackup(chatId int64) {
|
||||||
|
sendingTime := time.Now().Format("2006-01-02 15:04:05")
|
||||||
|
t.SendMsgToTgbot(chatId, "Backup time: "+sendingTime)
|
||||||
|
file := tgbotapi.FilePath(config.GetDBPath())
|
||||||
|
msg := tgbotapi.NewDocument(chatId, file)
|
||||||
|
_, err := bot.Send(msg)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("Error in uploading backup: ", err)
|
||||||
|
}
|
||||||
|
file = tgbotapi.FilePath(xray.GetConfigPath())
|
||||||
|
msg = tgbotapi.NewDocument(chatId, file)
|
||||||
|
_, err = bot.Send(msg)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("Error in uploading config.json: ", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -84,15 +84,16 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
|||||||
clients, ok := settings["clients"].([]interface{})
|
clients, ok := settings["clients"].([]interface{})
|
||||||
if ok {
|
if ok {
|
||||||
// check users active or not
|
// check users active or not
|
||||||
|
|
||||||
clientStats := inbound.ClientStats
|
clientStats := inbound.ClientStats
|
||||||
for _, clientTraffic := range clientStats {
|
for _, clientTraffic := range clientStats {
|
||||||
|
|
||||||
|
indexDecrease := 0
|
||||||
for index, client := range clients {
|
for index, client := range clients {
|
||||||
c := client.(map[string]interface{})
|
c := client.(map[string]interface{})
|
||||||
if c["email"] == clientTraffic.Email {
|
if c["email"] == clientTraffic.Email {
|
||||||
if !clientTraffic.Enable {
|
if !clientTraffic.Enable {
|
||||||
clients = RemoveIndex(clients, index)
|
clients = RemoveIndex(clients, index-indexDecrease)
|
||||||
|
indexDecrease++
|
||||||
logger.Info("Remove Inbound User", c["email"], "due the expire or traffic limit")
|
logger.Info("Remove Inbound User", c["email"], "due the expire or traffic limit")
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -101,7 +102,27 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
settings["clients"] = clients
|
|
||||||
|
// clear client config for additional parameters
|
||||||
|
var final_clients []interface{}
|
||||||
|
for _, client := range clients {
|
||||||
|
|
||||||
|
c := client.(map[string]interface{})
|
||||||
|
|
||||||
|
if c["enable"] != nil {
|
||||||
|
if enable, ok := c["enable"].(bool); ok && !enable {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for key := range c {
|
||||||
|
if key != "email" && key != "id" && key != "password" && key != "flow" && key != "alterId" {
|
||||||
|
delete(c, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final_clients = append(final_clients, interface{}(c))
|
||||||
|
}
|
||||||
|
|
||||||
|
settings["clients"] = final_clients
|
||||||
modifiedSettings, err := json.Marshal(settings)
|
modifiedSettings, err := json.Marshal(settings)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -160,5 +181,5 @@ func (s *XrayService) SetToNeedRestart() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *XrayService) IsNeedRestartAndSetFalse() bool {
|
func (s *XrayService) IsNeedRestartAndSetFalse() bool {
|
||||||
return isNeedXrayRestart.CAS(true, false)
|
return isNeedXrayRestart.CompareAndSwap(true, false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"username" = "username"
|
"username" = "Username"
|
||||||
"password" = "password"
|
"password" = "Password"
|
||||||
"login" = "Login"
|
"login" = "Login"
|
||||||
"confirm" = "Confirm"
|
"confirm" = "Confirm"
|
||||||
"cancel" = "Cancel"
|
"cancel" = "Cancel"
|
||||||
@@ -10,6 +10,8 @@
|
|||||||
"remark" = "Remark"
|
"remark" = "Remark"
|
||||||
"enable" = "Enable"
|
"enable" = "Enable"
|
||||||
"protocol" = "Protocol"
|
"protocol" = "Protocol"
|
||||||
|
"search" = "Search"
|
||||||
|
|
||||||
"loading" = "Loading"
|
"loading" = "Loading"
|
||||||
"second" = "Second"
|
"second" = "Second"
|
||||||
"minute" = "Minute"
|
"minute" = "Minute"
|
||||||
@@ -20,6 +22,7 @@
|
|||||||
"unlimited" = "Unlimited"
|
"unlimited" = "Unlimited"
|
||||||
"none" = "None"
|
"none" = "None"
|
||||||
"qrCode" = "QR Code"
|
"qrCode" = "QR Code"
|
||||||
|
"info" = "More information"
|
||||||
"edit" = "Edit"
|
"edit" = "Edit"
|
||||||
"delete" = "Delete"
|
"delete" = "Delete"
|
||||||
"reset" = "Reset"
|
"reset" = "Reset"
|
||||||
@@ -30,21 +33,21 @@
|
|||||||
"host" = "Host"
|
"host" = "Host"
|
||||||
"path" = "Path"
|
"path" = "Path"
|
||||||
"camouflage" = "Camouflage"
|
"camouflage" = "Camouflage"
|
||||||
|
"status" = "Status"
|
||||||
"enabled" = "Enabled"
|
"enabled" = "Enabled"
|
||||||
"disabled" = "Disabled"
|
"disabled" = "Disabled"
|
||||||
"domainName" = "Domain Name"
|
"depleted" = "Depleted"
|
||||||
|
"depletingSoon" = "Depleting soon"
|
||||||
|
"domainName" = "Domain name"
|
||||||
"additional" = "Alter"
|
"additional" = "Alter"
|
||||||
"monitor" = "Listen IP"
|
"monitor" = "Listen IP"
|
||||||
"certificate" = "Certificate"
|
"certificate" = "Certificat"
|
||||||
"fail" = "Fail"
|
"fail" = "Fail"
|
||||||
"success" = "Success"
|
"success" = " Success"
|
||||||
"getVersion" = "Get Version"
|
"getVersion" = "Get version"
|
||||||
"install" = "Install"
|
"install" = "Install"
|
||||||
"used" = "Used"
|
|
||||||
"clients" = "Clients"
|
"clients" = "Clients"
|
||||||
"search" = "Search"
|
|
||||||
"usage" = "Usage"
|
"usage" = "Usage"
|
||||||
"info" = "Details"
|
|
||||||
|
|
||||||
[menu]
|
[menu]
|
||||||
"dashboard" = "System Status"
|
"dashboard" = "System Status"
|
||||||
@@ -61,18 +64,17 @@
|
|||||||
"invalidFormData" = "Input Data Format Is Invalid"
|
"invalidFormData" = "Input Data Format Is Invalid"
|
||||||
"emptyUsername" = "Please Enter Username"
|
"emptyUsername" = "Please Enter Username"
|
||||||
"emptyPassword" = "Please Enter Password"
|
"emptyPassword" = "Please Enter Password"
|
||||||
"wrongUsernameOrPassword" = "invalid username or password"
|
"wrongUsernameOrPassword" = "Invalid username or password"
|
||||||
"successLogin" = "Login"
|
"successLogin" = "Login"
|
||||||
|
|
||||||
|
|
||||||
[pages.index]
|
[pages.index]
|
||||||
"title" = "System Status"
|
"title" = "System Status"
|
||||||
"memory" = "Memory"
|
"memory" = "Memory"
|
||||||
"hard" = "Hard Disk"
|
"hard" = "Hard Disk"
|
||||||
"xrayStatus" = "Xray Status"
|
"xrayStatus" = "Xray Status"
|
||||||
"xraySwitch" = "Switch Version"
|
|
||||||
"restartXray" = "Restart"
|
|
||||||
"stopXray" = "Stop"
|
"stopXray" = "Stop"
|
||||||
|
"restartXray" = "Restart"
|
||||||
|
"xraySwitch" = "Switch Version"
|
||||||
"xraySwitchClick" = "Click on the version you want to switch"
|
"xraySwitchClick" = "Click on the version you want to switch"
|
||||||
"xraySwitchClickDesk" = "Please choose carefully, older versions may have incompatible configurations"
|
"xraySwitchClickDesk" = "Please choose carefully, older versions may have incompatible configurations"
|
||||||
"operationHours" = "Operation Hours"
|
"operationHours" = "Operation Hours"
|
||||||
@@ -84,16 +86,15 @@
|
|||||||
"downSpeed" = "Total download speed for all network cards"
|
"downSpeed" = "Total download speed for all network cards"
|
||||||
"totalSent" = "Total upload traffic of all network cards since system startup"
|
"totalSent" = "Total upload traffic of all network cards since system startup"
|
||||||
"totalReceive" = "Total download traffic of all network cards since system startup"
|
"totalReceive" = "Total download traffic of all network cards since system startup"
|
||||||
"xraySwitchVersionDialog" = "switch xray version"
|
"xraySwitchVersionDialog" = "Switch xray version"
|
||||||
"xraySwitchVersionDialogDesc" = "whether to switch the xray version to"
|
"xraySwitchVersionDialogDesc" = "Whether to switch the xray version to"
|
||||||
"dontRefreshh" = "Installation is in progress, please do not refresh this page"
|
"dontRefreshh" = "Installation is in progress, please do not refresh this page"
|
||||||
|
|
||||||
[pages.inbounds]
|
[pages.inbounds]
|
||||||
"export" = "Export"
|
|
||||||
"title" = "Inbounds"
|
"title" = "Inbounds"
|
||||||
"totalDownUp" = "Total Uploads/Downloads"
|
"totalDownUp" = "Total uploads/downloads"
|
||||||
"totalUsage" = "Total Usage"
|
"totalUsage" = "Total usage"
|
||||||
"inboundCount" = "Number Of Inbound"
|
"inboundCount" = "Number of inbound"
|
||||||
"operate" = "Actions"
|
"operate" = "Actions"
|
||||||
"enable" = "Enable"
|
"enable" = "Enable"
|
||||||
"remark" = "Remark"
|
"remark" = "Remark"
|
||||||
@@ -102,11 +103,11 @@
|
|||||||
"traffic" = "Traffic"
|
"traffic" = "Traffic"
|
||||||
"details" = "Details"
|
"details" = "Details"
|
||||||
"transportConfig" = "Transport"
|
"transportConfig" = "Transport"
|
||||||
"expireDate" = "Expire Date"
|
"expireDate" = "Expire date"
|
||||||
"resetTraffic" = "Reset Traffic"
|
"resetTraffic" = "Reset traffic"
|
||||||
"addInbound" = "Add Inbound"
|
"addInbound" = "Add Inbound"
|
||||||
"addTo" = "Add To"
|
"addTo" = "Add To"
|
||||||
"revise" = "Save"
|
"revise" = "Revise"
|
||||||
"modifyInbound" = "Modify InBound"
|
"modifyInbound" = "Modify InBound"
|
||||||
"deleteInbound" = "Delete Inbound"
|
"deleteInbound" = "Delete Inbound"
|
||||||
"deleteInboundContent" = "Are you sure you want to delete inbound?"
|
"deleteInboundContent" = "Are you sure you want to delete inbound?"
|
||||||
@@ -115,44 +116,81 @@
|
|||||||
"address" = "Address"
|
"address" = "Address"
|
||||||
"network" = "Network"
|
"network" = "Network"
|
||||||
"destinationPort" = "Destination port"
|
"destinationPort" = "Destination port"
|
||||||
"targetAddress" = "Target Address"
|
"targetAddress" = "Target address"
|
||||||
"disableInsecureEncryption" = "Disable insecure encryption"
|
"disableInsecureEncryption" = "Disable insecure encryption"
|
||||||
"monitorDesc" = "Leave blank by default"
|
"monitorDesc" = "Leave blank by default"
|
||||||
"meansNoLimit" = "Means No Limit"
|
"meansNoLimit" = "Means no limit"
|
||||||
"totalFlow" = "Total Traffic"
|
"totalFlow" = "Total flow"
|
||||||
"leaveBlankToNeverExpire" = "Leave blank to never expire"
|
"leaveBlankToNeverExpire" = "Leave blank to never expire"
|
||||||
"noRecommendKeepDefault" = "There are no special requirements to keep the default"
|
"noRecommendKeepDefault" = "There are no special requirements to keep the default"
|
||||||
"certificatePath" = "Certificate File Path"
|
"certificatePath" = "Certificate file path"
|
||||||
"certificateContent" = "Certificate File Content"
|
"certificateContent" = "Certificate file content"
|
||||||
"publicKeyPath" = "Public Key Path"
|
"publicKeyPath" = "Public key path"
|
||||||
"publicKeyContent" = "public Key Content"
|
"publicKeyContent" = "Public key content"
|
||||||
"keyPath" = "Private key Path"
|
"keyPath" = "Private Key path"
|
||||||
"keyContent" = "Private Key Content"
|
"keyContent" = "Private Key content"
|
||||||
|
"clickOnQRcode" = "Click on QR Code to Copy"
|
||||||
"client" = "Client"
|
"client" = "Client"
|
||||||
"uid" = "UID"
|
"export" = "Export links"
|
||||||
|
"Clone" = "Clone"
|
||||||
|
"cloneInbound" = "Create"
|
||||||
|
"cloneInboundContent" = "All items of this inbound except Port, Listening IP, Clients will be applied to the clone"
|
||||||
|
"cloneInboundOk" = "Creating a clone from"
|
||||||
|
"resetAllTraffic" = "Reset All Inbounds Traffic"
|
||||||
|
"resetAllTrafficTitle" = "Reset all inbounds traffic"
|
||||||
|
"resetAllTrafficContent" = "Are you sure to reset all inbounds traffic ?"
|
||||||
|
"resetAllTrafficOkText" = "Confirm"
|
||||||
|
"resetAllTrafficCancelText" = "Cancel"
|
||||||
|
"IPLimit" = "IP Limit"
|
||||||
|
"IPLimitDesc" = "disable inbound if more than entered count (0 for disable limit ip)"
|
||||||
|
"resetAllClientTraffics" = "Reset Clients Traffic"
|
||||||
|
"resetAllClientTrafficTitle" = "Reset all clients traffic"
|
||||||
|
"resetAllClientTrafficContent" = "Are you sure to reset all traffics of this inbound's clients ?"
|
||||||
|
"Email" = "Email"
|
||||||
|
"EmailDesc" = "The Email Must Be Completely Unique"
|
||||||
|
"IPLimitlog" = "IP Log"
|
||||||
|
"IPLimitlogDesc" = "IPs history Log (before enabling inbound after it has been disabled by IP limit, you should clear the log)"
|
||||||
|
"IPLimitlogclear" = "Clear The Log"
|
||||||
|
"setDefaultCert" = "Set cert from panel"
|
||||||
|
"XTLSdec" = "Xray core needs to be 1.7.5 and below"
|
||||||
|
"Realitydec" = "Xray core needs to be 1.8.0 and above"
|
||||||
|
|
||||||
|
[pages.client]
|
||||||
|
"add" = "Add client"
|
||||||
|
"edit" = "Edit client"
|
||||||
|
"submitAdd" = "Add client"
|
||||||
|
"submitEdit" = "Save changes"
|
||||||
|
"clientCount" = "Number of clients"
|
||||||
|
"bulk" = "Add bulk"
|
||||||
|
"method" = "Method"
|
||||||
|
"first" = "First"
|
||||||
|
"last" = "Last"
|
||||||
|
"prefix" = "Prefix"
|
||||||
|
"postfix" = "postfix"
|
||||||
|
"delayedStart" = "Start after first use"
|
||||||
|
"expireDays" = "Expire days"
|
||||||
|
"days" = "day(s)"
|
||||||
|
|
||||||
[pages.inbounds.toasts]
|
[pages.inbounds.toasts]
|
||||||
"obtain" = "Obtain"
|
"obtain" = "Obtain"
|
||||||
|
|
||||||
[pages.inbounds.stream.general]
|
[pages.inbounds.stream.general]
|
||||||
"requestHeader" = "Request Header"
|
"requestHeader" = "Request header"
|
||||||
"name" = "Name"
|
"name" = "Name"
|
||||||
"value" = "Value"
|
"value" = "Value"
|
||||||
|
|
||||||
[pages.inbounds.stream.tcp]
|
[pages.inbounds.stream.tcp]
|
||||||
"requestVersion" = "Request Version"
|
"requestVersion" = "Request version"
|
||||||
"requestMethod" = "Request Method"
|
"requestMethod" = "Request method"
|
||||||
"requestPath" = "Request Path"
|
"requestPath" = "Request path"
|
||||||
"responseVersion" = "Response Version"
|
"responseVersion" = "Response version"
|
||||||
"responseStatus" = "Response Status"
|
"responseStatus" = "Response status"
|
||||||
"responseStatusDescription" = "Response Status Description"
|
"responseStatusDescription" = "Response status description"
|
||||||
"responseHeader" = "Response Header"
|
"responseHeader" = "Response header"
|
||||||
|
|
||||||
[pages.inbounds.stream.quic]
|
[pages.inbounds.stream.quic]
|
||||||
"encryption" = "Encryption"
|
"encryption" = "Encryption"
|
||||||
|
|
||||||
|
|
||||||
[pages.setting]
|
[pages.setting]
|
||||||
"title" = "Setting"
|
"title" = "Setting"
|
||||||
"save" = "Save"
|
"save" = "Save"
|
||||||
@@ -169,7 +207,7 @@
|
|||||||
"panelPortDesc" = "Restart the panel to take effect"
|
"panelPortDesc" = "Restart the panel to take effect"
|
||||||
"publicKeyPath" = "Panel certificate public key file path"
|
"publicKeyPath" = "Panel certificate public key file path"
|
||||||
"publicKeyPathDesc" = "Fill in an absolute path starting with '/', restart the panel to take effect"
|
"publicKeyPathDesc" = "Fill in an absolute path starting with '/', restart the panel to take effect"
|
||||||
"privateKeyPath" = "Panel certificate key file path"
|
"privateKeyPath" = "Panel certificate private key file path"
|
||||||
"privateKeyPathDesc" = "Fill in an absolute path starting with '/', restart the panel to take effect"
|
"privateKeyPathDesc" = "Fill in an absolute path starting with '/', restart the panel to take effect"
|
||||||
"panelUrlPath" = "panel url root path"
|
"panelUrlPath" = "panel url root path"
|
||||||
"panelUrlPathDesc" = "Must start with '/' and end with '/', restart the panel to take effect"
|
"panelUrlPathDesc" = "Must start with '/' and end with '/', restart the panel to take effect"
|
||||||
@@ -177,22 +215,46 @@
|
|||||||
"currentPassword" = "Current Password"
|
"currentPassword" = "Current Password"
|
||||||
"newUsername" = "New Username"
|
"newUsername" = "New Username"
|
||||||
"newPassword" = "New Password"
|
"newPassword" = "New Password"
|
||||||
"xrayConfigTemplate" = "xray Configuration Template"
|
"advancedTemplate" = "Advanced template parts"
|
||||||
"xrayConfigTemplateDesc" = "Generate the final xray configuration file based on this template, restart the panel to take effect"
|
"completeTemplate" = "Complete template of Xray configuration"
|
||||||
|
"xrayConfigTemplate" = "Xray Configuration Template"
|
||||||
|
"xrayConfigTemplateDesc" = "Generate the final xray configuration file based on this template, restart the panel to take effect."
|
||||||
|
"xrayConfigTorrent" = "Ban bittorrent usage"
|
||||||
|
"xrayConfigTorrentDesc" = "Change the configuration temlate to avoid using bittorrent by users, restart the panel to take effect"
|
||||||
|
"xrayConfigPrivateIp" = "Ban private IP ranges to connect"
|
||||||
|
"xrayConfigPrivateIpDesc" = "Change the configuration temlate to avoid connecting with private IP ranges, restart the panel to take effect"
|
||||||
|
"xrayConfigIRIp" = "Ban Iran IP ranges to connect"
|
||||||
|
"xrayConfigIRIpDesc" = "Change the configuration temlate to avoid connecting with Iran IP ranges, restart the panel to take effect"
|
||||||
|
"xrayConfigIRdomain" = "Ban IR domains to connect"
|
||||||
|
"xrayConfigIRdomainDesc" = "Change the configuration temlate to avoid connecting with IR domains, restart the panel to take effect"
|
||||||
|
"xrayConfigInbounds" = "Configuration of Inbounds"
|
||||||
|
"xrayConfigInboundsDesc" = "Change the configuration temlate to accept special clients, restart the panel to take effect"
|
||||||
|
"xrayConfigOutbounds" = "Configuration of Outbounds"
|
||||||
|
"xrayConfigOutboundsDesc" = "Change the configuration temlate to define outgoing ways for this server, restart the panel to take effect"
|
||||||
|
"xrayConfigRoutings" = "Configuration of Routing rules"
|
||||||
|
"xrayConfigRoutingsDesc" = "Change the configuration temlate to define Routing rules for this server, restart the panel to take effect"
|
||||||
"telegramBotEnable" = "Enable telegram bot"
|
"telegramBotEnable" = "Enable telegram bot"
|
||||||
"telegramBotEnableDesc" = "Restart the panel to take effect"
|
"telegramBotEnableDesc" = "Restart the panel to take effect"
|
||||||
"telegramToken" = "Telegram Token"
|
"telegramToken" = "Telegram Token"
|
||||||
"telegramTokenDesc" = "Restart the panel to take effect"
|
"telegramTokenDesc" = "Restart the panel to take effect"
|
||||||
"telegramChatId" = "Telegram ChatId"
|
"telegramChatId" = "Telegram Admin ChatIds"
|
||||||
"telegramChatIdDesc" = "Restart the panel to take effect"
|
"telegramChatIdDesc" = "Multi chatIDs separated by comma. Restart the panel to take effect"
|
||||||
"telegramNotifyTime" = "Telegram bot notification time"
|
"telegramNotifyTime" = "Telegram bot notification time"
|
||||||
"telegramNotifyTimeDesc" = "Using Crontab timing format, restart the panel to take effect"
|
"telegramNotifyTimeDesc" = "Using Crontab timing format. Restart the panel to take effect"
|
||||||
|
"tgNotifyBackup" = "Database backup"
|
||||||
|
"tgNotifyBackupDesc" = "Sending database backup file with report notification. Restart the panel to take effect"
|
||||||
|
"expireTimeDiff" = "Exhaustion time threshold"
|
||||||
|
"expireTimeDiffDesc" = "Detect exhaustion before expiration (unit:day)"
|
||||||
|
"trafficDiff" = "Exhaustion traffic threshold"
|
||||||
|
"trafficDiffDesc" = "Detect exhaustion before finishing traffic (unit:GB)"
|
||||||
|
"tgNotifyCpu" = "CPU percentage alert threshold"
|
||||||
|
"tgNotifyCpuDesc" = "This telegram bot will send you a notification if CPU usage is more than this percentage (unit:%)"
|
||||||
"timeZonee" = "Time Zone"
|
"timeZonee" = "Time Zone"
|
||||||
"timeZoneDesc" = "The scheduled task runs according to the time in the time zone, and restarts the panel to take effect"
|
"timeZoneDesc" = "The scheduled task runs according to the time in the time zone, and restarts the panel to take effect"
|
||||||
|
|
||||||
[pages.setting.toasts]
|
[pages.setting.toasts]
|
||||||
"modifySetting" = "modify setting"
|
"modifySetting" = "Modify setting"
|
||||||
"getSetting" = "get setting"
|
"getSetting" = "Get setting"
|
||||||
"modifyUser" = "modify user"
|
"modifyUser" = "Modify user"
|
||||||
"originalUserPassIncorrect" = "The original user name or original password is incorrect"
|
"originalUserPassIncorrect" = "The original user name or original password is incorrect"
|
||||||
"userPassMustBeNotEmpty" = "New username and new password cannot be empty"
|
"userPassMustBeNotEmpty" = "New username and new password cannot be empty"
|
||||||
@@ -10,6 +10,8 @@
|
|||||||
"remark" = "نام"
|
"remark" = "نام"
|
||||||
"enable" = "فعال"
|
"enable" = "فعال"
|
||||||
"protocol" = "پروتکل"
|
"protocol" = "پروتکل"
|
||||||
|
"search" = "جستجو"
|
||||||
|
|
||||||
"loading" = "در حال بروزرسانی..."
|
"loading" = "در حال بروزرسانی..."
|
||||||
"second" = "ثانیه"
|
"second" = "ثانیه"
|
||||||
"minute" = "دقیقه"
|
"minute" = "دقیقه"
|
||||||
@@ -20,6 +22,7 @@
|
|||||||
"unlimited" = "نامحدود"
|
"unlimited" = "نامحدود"
|
||||||
"none" = "هیچ"
|
"none" = "هیچ"
|
||||||
"qrCode" = "QR کد"
|
"qrCode" = "QR کد"
|
||||||
|
"info" = "اطلاعات بیشتر"
|
||||||
"edit" = "ویرایش"
|
"edit" = "ویرایش"
|
||||||
"delete" = "حذف"
|
"delete" = "حذف"
|
||||||
"reset" = "ریست"
|
"reset" = "ریست"
|
||||||
@@ -30,21 +33,21 @@
|
|||||||
"host" = "آدرس"
|
"host" = "آدرس"
|
||||||
"path" = "مسیر"
|
"path" = "مسیر"
|
||||||
"camouflage" = "استتار"
|
"camouflage" = "استتار"
|
||||||
"enabled" = "فعال شد"
|
"status" = "وضعیت"
|
||||||
"disabled" = "غیرفعال شد"
|
"enabled" = "فعال"
|
||||||
|
"disabled" = "غیرفعال"
|
||||||
|
"depleted" = "منقضی"
|
||||||
|
"depletingSoon" = "در حال انقضا"
|
||||||
"domainName" = "آدرس دامنه"
|
"domainName" = "آدرس دامنه"
|
||||||
"additional" = "آی دی جایگزین"
|
"additional" = "آی دی جایگزین"
|
||||||
"monitor" = "آی پی اتصال"
|
"monitor" = "آی پی اتصال"
|
||||||
"certificate" = "سرتیفیکیت"
|
"certificate" = "گواهی دیجیتال"
|
||||||
"fail" = "خطا"
|
"fail" = "خطا"
|
||||||
"success" = " موفق"
|
"success" = " موفق"
|
||||||
"getVersion" = "دریافت ورژن"
|
"getVersion" = "دریافت ورژن"
|
||||||
"install" = "نصب"
|
"install" = "نصب"
|
||||||
"used" = "استفاده شده"
|
|
||||||
"clients" = "کاربران"
|
"clients" = "کاربران"
|
||||||
"search" = "جستجو"
|
|
||||||
"usage" = "استفاده"
|
"usage" = "استفاده"
|
||||||
"info" = "جزئیات"
|
|
||||||
|
|
||||||
[menu]
|
[menu]
|
||||||
"dashboard" = "وضعیت سیستم"
|
"dashboard" = "وضعیت سیستم"
|
||||||
@@ -64,33 +67,30 @@
|
|||||||
"wrongUsernameOrPassword" = "نام کاربری و رمز عبور اشتباه میباشد"
|
"wrongUsernameOrPassword" = "نام کاربری و رمز عبور اشتباه میباشد"
|
||||||
"successLogin" = "خوش آمدید"
|
"successLogin" = "خوش آمدید"
|
||||||
|
|
||||||
|
|
||||||
[pages.index]
|
[pages.index]
|
||||||
"title" = "وضعیت سیستم"
|
"title" = "وضعیت سیستم"
|
||||||
"memory" = "حافظه رم"
|
"memory" = "حافظه رم"
|
||||||
"hard" = "حافظه دیسک"
|
"hard" = "حافظه دیسک"
|
||||||
"xrayStatus" = "وضعیت Xray"
|
"xrayStatus" = "وضعیت Xray"
|
||||||
"xraySwitch" = "تغییر ورژن"
|
|
||||||
"restartXray" = "راه اندازی مجدد"
|
|
||||||
"stopXray" = "توقف"
|
"stopXray" = "توقف"
|
||||||
|
"restartXray" = "شروع مجدد"
|
||||||
|
"xraySwitch" = "تغییر ورژن"
|
||||||
"xraySwitchClick" = "ورژن مورد نظر را انتخاب کنید"
|
"xraySwitchClick" = "ورژن مورد نظر را انتخاب کنید"
|
||||||
"xraySwitchClickDesk" = "لطفا با دقت انتخاب کنید ، در صورت انتخاب اشتباه امکان قطعی سیستم وجود دارد ."
|
"xraySwitchClickDesk" = "لطفا با دقت انتخاب کنید ، در صورت انتخاب اشتباه امکان قطعی سیستم وجود دارد ."
|
||||||
"operationHours" = "ساعت فعال"
|
"operationHours" = "مدت فعالیت"
|
||||||
"operationHoursDesc" = "ساعت فعال بعد از شروع سیستم"
|
"operationHoursDesc" = "مدت فعالیت سیستم بعد از روشن شدن"
|
||||||
"systemLoad" = "سرعت لود سیستم"
|
"systemLoad" = "بار روی سیستم"
|
||||||
"connectionCount" = "تعداد کانکشن ها"
|
"connectionCount" = "تعداد کانکشن ها"
|
||||||
"connectionCountDesc" = "تعداد کانکشن ها برای کل شبکه"
|
"connectionCountDesc" = "تعداد کانکشن ها برای کل شبکه"
|
||||||
"upSpeed" = "سرعت آپلود در حال حاضر سیستم"
|
"upSpeed" = "سرعت آپلود در حال حاضر سیستم"
|
||||||
"downSpeed" = "سرعت دانلود در حال حاضر سیستم"
|
"downSpeed" = "سرعت دانلود در حال حاضر سیستم"
|
||||||
"totalSent" = "جمع کل ترافیک آپلود مصرفی"
|
"totalSent" = "جمع کل ترافیک آپلود مصرفی"
|
||||||
"totalReceive" = "جمع کل ترافیک دانلود مصرفی"
|
"totalReceive" = "جمع کل ترافیک دانلود مصرفی"
|
||||||
"xraySwitchVersionDialog" = "تغییر ورژن Xray"
|
"xraySwitchVersionDialog" = "تغییر ورژن"
|
||||||
"xraySwitchVersionDialogDesc" = "آیا از تغییر ورژن مطمئن هستین"
|
"xraySwitchVersionDialogDesc" = "آیا از تغییر ورژن مطمئن هستین"
|
||||||
"dontRefreshh" = "در حال نصب ، لطفا رفرش نکنید "
|
"dontRefreshh" = "در حال نصب ، لطفا رفرش نکنید "
|
||||||
|
|
||||||
|
|
||||||
[pages.inbounds]
|
[pages.inbounds]
|
||||||
"export" = "استخراج لینکها"
|
|
||||||
"title" = "کاربران"
|
"title" = "کاربران"
|
||||||
"totalDownUp" = "جمع آپلود/دانلود"
|
"totalDownUp" = "جمع آپلود/دانلود"
|
||||||
"totalUsage" = "جمع کل"
|
"totalUsage" = "جمع کل"
|
||||||
@@ -107,7 +107,7 @@
|
|||||||
"resetTraffic" = "ریست ترافیک"
|
"resetTraffic" = "ریست ترافیک"
|
||||||
"addInbound" = "اضافه کردن سرویس"
|
"addInbound" = "اضافه کردن سرویس"
|
||||||
"addTo" = "اضافه کردن"
|
"addTo" = "اضافه کردن"
|
||||||
"revise" = "ذخیره"
|
"revise" = "ویرایش"
|
||||||
"modifyInbound" = "ویرایش سرویس"
|
"modifyInbound" = "ویرایش سرویس"
|
||||||
"deleteInbound" = "حذف سرویس"
|
"deleteInbound" = "حذف سرویس"
|
||||||
"deleteInboundContent" = "آیا مطمئن به حذف سرویس هستید ؟"
|
"deleteInboundContent" = "آیا مطمئن به حذف سرویس هستید ؟"
|
||||||
@@ -125,12 +125,49 @@
|
|||||||
"noRecommendKeepDefault" = "توصیه می شود به عنوان پیش فرض حفظ شود"
|
"noRecommendKeepDefault" = "توصیه می شود به عنوان پیش فرض حفظ شود"
|
||||||
"certificatePath" = "مسیر فایل گواهی"
|
"certificatePath" = "مسیر فایل گواهی"
|
||||||
"certificateContent" = "محتوای فایل گواهی"
|
"certificateContent" = "محتوای فایل گواهی"
|
||||||
"publicKeyPath" = "مسیر فایل Certificate.crt"
|
"publicKeyPath" = "مسیر کلید عمومی"
|
||||||
"publicKeyContent" = "محتوای Certificate.crt"
|
"publicKeyContent" = "محتوای کلید عمومی"
|
||||||
"keyPath" = "مسیر فایل Private.key"
|
"keyPath" = "مسیر کلید خصوصی"
|
||||||
"keyContent" = "محتوای Private.key"
|
"keyContent" = "محتوای کلید خصوصی"
|
||||||
|
"clickOnQRcode" = "برای کپی بر روی کد تصویری کلیک کنید"
|
||||||
"client" = "کاربر"
|
"client" = "کاربر"
|
||||||
"uid" = "UID"
|
"export" = "استخراج لینکها"
|
||||||
|
"Clone" = "شبیه سازی"
|
||||||
|
"cloneInbound" = "ایجاد"
|
||||||
|
"cloneInboundContent" = "همه موارد این ورودی بجز پورت ، ای پی و کلاینت ها شبیه سازی خواهند شد"
|
||||||
|
"cloneInboundOk" = "ساختن شبیه ساز"
|
||||||
|
"resetAllTraffic" = "ریست ترافیک کل سرویس ها"
|
||||||
|
"resetAllTrafficTitle" = "ریست ترافیک کل سرویس ها"
|
||||||
|
"resetAllTrafficContent" = "آیا مطمئن هستید که میخواهید تمام ترافیک سرویس ها را ریست کنید؟"
|
||||||
|
"resetAllClientTraffics" = "ریست ترافیک کاربران"
|
||||||
|
"resetAllClientTrafficTitle" = "ریست ترافیک کل کاربران"
|
||||||
|
"resetAllClientTrafficContent" = "آیا مطمئن هستید که میخواهید تمام ترافیک کاربران این سرویس را ریست کنید؟"
|
||||||
|
"IPLimit" = "محدودیت ای پی"
|
||||||
|
"IPLimitDesc" = "غیرفعال کردن ورودی در صورت بیش از تعداد وارد شده (0 برای غیرفعال کردن محدودیت ای پی )"
|
||||||
|
"Email" = "ایمیل"
|
||||||
|
"EmailDesc" = "ایمیل باید کاملا منحصر به فرد باشد"
|
||||||
|
"IPLimitlog" = "گزارش ها"
|
||||||
|
"IPLimitlogDesc" = "گزارش سابقه ای پی (قبل از فعال کردن ورودی پس از غیرفعال شدن توسط محدودیت ای پی، باید گزارش را پاک کنید)"
|
||||||
|
"IPLimitlogclear" = "پاک کردن گزارش ها"
|
||||||
|
"setDefaultCert" = "استفاده از گواهی پنل"
|
||||||
|
"XTLSdec" = "هسته Xray باید 1.7.5 و کمتر باشد"
|
||||||
|
"Realitydec" = "هسته Xray باید 1.8.0 و بالاتر باشد"
|
||||||
|
|
||||||
|
[pages.client]
|
||||||
|
"add" = "کاربر جدید"
|
||||||
|
"edit" = "ویرایش کاربر"
|
||||||
|
"submitAdd" = "اضافه کردن"
|
||||||
|
"submitEdit" = "ذخیره تغییرات"
|
||||||
|
"clientCount" = "تعداد کاربران"
|
||||||
|
"bulk" = "انبوه سازی"
|
||||||
|
"method" = "روش"
|
||||||
|
"first" = "از"
|
||||||
|
"last" = "تا"
|
||||||
|
"prefix" = "پیشوند"
|
||||||
|
"postfix" = "پسوند"
|
||||||
|
"delayedStart" = "شروع بعد از اولین استفاده"
|
||||||
|
"expireDays" = "روزهای اعتبار"
|
||||||
|
"days" = "(روز)"
|
||||||
|
|
||||||
[pages.inbounds.toasts]
|
[pages.inbounds.toasts]
|
||||||
"obtain" = "Obtain"
|
"obtain" = "Obtain"
|
||||||
@@ -152,7 +189,6 @@
|
|||||||
[pages.inbounds.stream.quic]
|
[pages.inbounds.stream.quic]
|
||||||
"encryption" = "رمزنگاری"
|
"encryption" = "رمزنگاری"
|
||||||
|
|
||||||
|
|
||||||
[pages.setting]
|
[pages.setting]
|
||||||
"title" = "تنظیمات"
|
"title" = "تنظیمات"
|
||||||
"save" = "ذخیره"
|
"save" = "ذخیره"
|
||||||
@@ -167,9 +203,9 @@
|
|||||||
"panelListeningIPDesc" = "برای استفاده از تمام IP ها به طور پیش فرض خالی بگذارید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
"panelListeningIPDesc" = "برای استفاده از تمام IP ها به طور پیش فرض خالی بگذارید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||||
"panelPort" = "پورت پنل"
|
"panelPort" = "پورت پنل"
|
||||||
"panelPortDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
"panelPortDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||||
"publicKeyPath" = "مسیر فایل پنل Certificate.crt"
|
"publicKeyPath" = "مسیر فایل گواهی کلید عمومی پنل"
|
||||||
"publicKeyPathDesc" = "باید یک مسیر مطلق باشد که با / شروع می شود . پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
"publicKeyPathDesc" = "باید یک مسیر مطلق باشد که با / شروع می شود . پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||||
"privateKeyPath" = "مسیر فایل پنل private.key"
|
"privateKeyPath" = "مسیر فایل گواهی کلید خصوصی پنل"
|
||||||
"privateKeyPathDesc" = "باید یک مسیر مطلق باشد که با / شروع می شود . پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
"privateKeyPathDesc" = "باید یک مسیر مطلق باشد که با / شروع می شود . پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||||
"panelUrlPath" = "آدرس روت پنل"
|
"panelUrlPath" = "آدرس روت پنل"
|
||||||
"panelUrlPathDesc" = "باید با '/' شروع شود و با '/' تمام شود. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
"panelUrlPathDesc" = "باید با '/' شروع شود و با '/' تمام شود. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||||
@@ -177,16 +213,40 @@
|
|||||||
"currentPassword" = "رمز عبور فعلی"
|
"currentPassword" = "رمز عبور فعلی"
|
||||||
"newUsername" = "نام کاربری جدید"
|
"newUsername" = "نام کاربری جدید"
|
||||||
"newPassword" = "رمز عبور جدید"
|
"newPassword" = "رمز عبور جدید"
|
||||||
"xrayConfigTemplate" = "تنظیمات قالب Xray"
|
"advancedTemplate" = "بخش های پیشرفته الگو"
|
||||||
"xrayConfigTemplateDesc" = "فایل پیکربندی xray نهایی را بر اساس این الگو ایجاد کنید. لطفاً این را تغییر ندهید مگر اینکه دقیقاً بدانید که چه کاری انجام می دهید! پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
"completeTemplate" = "الگوی کامل تنظیمات ایکس ری"
|
||||||
|
"xrayConfigTemplate" = "تنظیمات الگو ایکس ری"
|
||||||
|
"xrayConfigTemplateDesc" = "فایل پیکربندی ایکس ری نهایی بر اساس این الگو ایجاد میشود. لطفاً این را تغییر ندهید مگر اینکه دقیقاً بدانید که چه کاری انجام می دهید! پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||||
|
"xrayConfigTorrent" = "فیلتر کردن بیت تورنت"
|
||||||
|
"xrayConfigTorrentDesc" = "الگوی تنظیمات را برای فیلتر کردن پروتکل بیت تورنت برای کاربران تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||||
|
"xrayConfigPrivateIp" = "جلوگیری از اتصال آی پی های نامعتبر"
|
||||||
|
"xrayConfigPrivateIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آی پی های نامعتبر و بسته های سرگردان تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||||
|
"xrayConfigIRIp" = "جلوگیری از اتصال آی پی های ایران"
|
||||||
|
"xrayConfigIRIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آی پی های ایران تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||||
|
"xrayConfigIRdomain" = "جلوگیری از اتصال دامنه های ایران"
|
||||||
|
"xrayConfigIRdomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های ایران تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||||
|
"xrayConfigInbounds" = "تنظیمات ورودی"
|
||||||
|
"xrayConfigInboundsDesc" = "میتوانید الگوی تنظیمات را برای ورودی های خاص تنظیم نمایید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||||
|
"xrayConfigOutbounds" = "تنظیمات خروجی"
|
||||||
|
"xrayConfigOutboundsDesc" = "میتوانید الگوی تنظیمات را برای خروجی اینترنت تنظیم نمایید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||||
|
"xrayConfigRoutings" = "تنظیمات قوانین مسیریابی"
|
||||||
|
"xrayConfigRoutingsDesc" = "میتوانید الگوی تنظیمات را برای مسیریابی تنظیم نمایید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||||
"telegramBotEnable" = "فعالسازی ربات تلگرام"
|
"telegramBotEnable" = "فعالسازی ربات تلگرام"
|
||||||
"telegramBotEnableDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
"telegramBotEnableDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||||
"telegramToken" = "توکن تلگرام"
|
"telegramToken" = "توکن تلگرام"
|
||||||
"telegramTokenDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
"telegramTokenDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||||
"telegramChatId" = "آی دی تلگرام مدیریت . از ربات @getidsbot آی دی خود را دریافت کنید"
|
"telegramChatId" = "آی دی تلگرام مدیریت"
|
||||||
"telegramChatIdDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
"telegramChatIdDesc" = "با استفاده از کاما میتونید چند آی دی را از هم جدا کنید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||||
"telegramNotifyTime" = "مدت زمان نوتیفیکیشن ربات تلگرام"
|
"telegramNotifyTime" = "مدت زمان نوتیفیکیشن ربات تلگرام"
|
||||||
"telegramNotifyTimeDesc" = "از فرمت زمان بندی Crontab استفاده کنید . پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
"telegramNotifyTimeDesc" = "از فرمت زمان بندی لینوکس استفاده کنید . پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||||
|
"tgNotifyBackup" = "پشتیبان گیری از پایگاه داده"
|
||||||
|
"tgNotifyBackupDesc" = "ارسال کپی فایل پایگاه داده به همراه گزارش دوره ای"
|
||||||
|
"expireTimeDiff" = "آستانه زمان باقی مانده"
|
||||||
|
"expireTimeDiffDesc" = "فاصله زمانی هشدار تا رسیدن به زمان انقضا (واحد: روز)"
|
||||||
|
"trafficDiff" = "آستانه ترافیک باقی مانده"
|
||||||
|
"trafficDiffDesc" = "فاصله زمانی هشدار تا رسیدن به اتمام ترافیک (واحد: گیگابایت)"
|
||||||
|
"tgNotifyCpu" = "آستانه هشدار درصد پردازنده"
|
||||||
|
"tgNotifyCpuDesc" = "این ربات تلگرام در صورت استفاده پردازنده بیشتر از این درصد برای شما پیام ارسال می کند.(واحد: درصد)"
|
||||||
"timeZonee" = "منظقه زمانی"
|
"timeZonee" = "منظقه زمانی"
|
||||||
"timeZoneDesc" = "وظایف برنامه ریزی شده بر اساس این منطقه زمانی اجرا می شوند. پنل را مجدداً راه اندازی می کند تا اعمال شود"
|
"timeZoneDesc" = "وظایف برنامه ریزی شده بر اساس این منطقه زمانی اجرا می شوند. پنل را مجدداً راه اندازی می کند تا اعمال شود"
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
"remark" = "备注"
|
"remark" = "备注"
|
||||||
"enable" = "启用"
|
"enable" = "启用"
|
||||||
"protocol" = "协议"
|
"protocol" = "协议"
|
||||||
|
"search" = "搜尋"
|
||||||
|
|
||||||
"loading" = "加载中"
|
"loading" = "加载中"
|
||||||
"second" = "秒"
|
"second" = "秒"
|
||||||
"minute" = "分钟"
|
"minute" = "分钟"
|
||||||
@@ -20,6 +22,7 @@
|
|||||||
"unlimited" = "无限制"
|
"unlimited" = "无限制"
|
||||||
"none" = "无"
|
"none" = "无"
|
||||||
"qrCode" = "二维码"
|
"qrCode" = "二维码"
|
||||||
|
"info" = "更多信息"
|
||||||
"edit" = "编辑"
|
"edit" = "编辑"
|
||||||
"delete" = "删除"
|
"delete" = "删除"
|
||||||
"reset" = "重置"
|
"reset" = "重置"
|
||||||
@@ -30,8 +33,11 @@
|
|||||||
"host" = "主持人"
|
"host" = "主持人"
|
||||||
"path" = "小路"
|
"path" = "小路"
|
||||||
"camouflage" = "伪装"
|
"camouflage" = "伪装"
|
||||||
|
"status" = "状态"
|
||||||
"enabled" = "开启"
|
"enabled" = "开启"
|
||||||
"disabled" = "关闭"
|
"disabled" = "关闭"
|
||||||
|
"depleted" = "耗尽"
|
||||||
|
"depletingSoon" = "即将耗尽"
|
||||||
"domainName" = "域名"
|
"domainName" = "域名"
|
||||||
"additional" = "额外"
|
"additional" = "额外"
|
||||||
"monitor" = "监听"
|
"monitor" = "监听"
|
||||||
@@ -40,11 +46,8 @@
|
|||||||
"success" = "成功"
|
"success" = "成功"
|
||||||
"getVersion" = "获取版本"
|
"getVersion" = "获取版本"
|
||||||
"install" = "安装"
|
"install" = "安装"
|
||||||
"used" = "用过的"
|
|
||||||
"clients" = "客户端"
|
"clients" = "客户端"
|
||||||
"search" = "搜索"
|
|
||||||
"usage" = "用法"
|
"usage" = "用法"
|
||||||
"info" = "细节"
|
|
||||||
|
|
||||||
[menu]
|
[menu]
|
||||||
"dashboard" = "系统状态"
|
"dashboard" = "系统状态"
|
||||||
@@ -69,9 +72,9 @@
|
|||||||
"memory" = "内存"
|
"memory" = "内存"
|
||||||
"hard" = "硬盘"
|
"hard" = "硬盘"
|
||||||
"xrayStatus" = "xray 状态"
|
"xrayStatus" = "xray 状态"
|
||||||
"xraySwitch" = "切换版本"
|
|
||||||
"restartXray" = "重新开始"
|
|
||||||
"stopXray" = "停止"
|
"stopXray" = "停止"
|
||||||
|
"restartXray" = "重启"
|
||||||
|
"xraySwitch" = "切换版本"
|
||||||
"xraySwitchClick" = "点击你想切换的版本"
|
"xraySwitchClick" = "点击你想切换的版本"
|
||||||
"xraySwitchClickDesk" = "请谨慎选择,旧版本可能配置不兼容"
|
"xraySwitchClickDesk" = "请谨慎选择,旧版本可能配置不兼容"
|
||||||
"operationHours" = "运行时间"
|
"operationHours" = "运行时间"
|
||||||
@@ -87,9 +90,7 @@
|
|||||||
"xraySwitchVersionDialogDesc" = "是否切换 xray 版本至"
|
"xraySwitchVersionDialogDesc" = "是否切换 xray 版本至"
|
||||||
"dontRefreshh" = "安装中,请不要刷新此页面"
|
"dontRefreshh" = "安装中,请不要刷新此页面"
|
||||||
|
|
||||||
|
|
||||||
[pages.inbounds]
|
[pages.inbounds]
|
||||||
"export" = "导出链接"
|
|
||||||
"title" = "入站列表"
|
"title" = "入站列表"
|
||||||
"totalDownUp" = "总上传 / 下载"
|
"totalDownUp" = "总上传 / 下载"
|
||||||
"totalUsage" = "总用量"
|
"totalUsage" = "总用量"
|
||||||
@@ -128,9 +129,45 @@
|
|||||||
"publicKeyContent" = "公钥内容"
|
"publicKeyContent" = "公钥内容"
|
||||||
"keyPath" = "密钥文件路径"
|
"keyPath" = "密钥文件路径"
|
||||||
"keyContent" = "密钥内容"
|
"keyContent" = "密钥内容"
|
||||||
|
"clickOnQRcode" = "点击二维码复制"
|
||||||
"client" = "客户"
|
"client" = "客户"
|
||||||
"uid" = "UID"
|
"export" = "导出链接"
|
||||||
|
"Clone" = "克隆"
|
||||||
|
"cloneInbound" = "创造"
|
||||||
|
"cloneInboundContent" = "此入站的所有项目除 Port、Listening IP、Clients 将应用于克隆"
|
||||||
|
"cloneInboundOk" = "从创建克隆"
|
||||||
|
"resetAllTraffic" = "重置所有入站流量"
|
||||||
|
"resetAllTrafficTitle" = "重置所有入站流量"
|
||||||
|
"resetAllTrafficContent" = "您确定要重置所有入站流量吗?"
|
||||||
|
"resetAllClientTraffics" = "重置客户端流量"
|
||||||
|
"resetAllClientTrafficTitle" = "重置所有客户端流量"
|
||||||
|
"resetAllClientTrafficContent" = "您确定要重置此入站客户端的所有流量吗?"
|
||||||
|
"IPLimit" = "IP限制"
|
||||||
|
"IPLimitDesc" = "如果超过输入的计数则禁用入站(0 表示禁用限制 ip)"
|
||||||
|
"Email" = "电子邮件"
|
||||||
|
"EmailDesc" = "电子邮件必须完全唯"
|
||||||
|
"IPLimitlog" = "IP日志"
|
||||||
|
"IPLimitlogDesc" = "IP 历史日志 (通过IP限制禁用inbound之前,需要清空日志)"
|
||||||
|
"IPLimitlogclear" = "清除日志"
|
||||||
|
"setDefaultCert" = "从面板设置证书"
|
||||||
|
"XTLSdec" = "Xray核心需要1.7.5及以下版本"
|
||||||
|
"Realitydec" = "Xray核心需要1.8.0及以上版本"
|
||||||
|
|
||||||
|
[pages.client]
|
||||||
|
"add" = "添加客户端"
|
||||||
|
"edit" = "编辑客户"
|
||||||
|
"submitAdd" = "添加客户端"
|
||||||
|
"submitEdit" = "保存修改"
|
||||||
|
"clientCount" = "客户数量"
|
||||||
|
"bulk" = "批量创建"
|
||||||
|
"method" = "方法"
|
||||||
|
"first" = "第一"
|
||||||
|
"last" = "最后"
|
||||||
|
"prefix" = "前缀"
|
||||||
|
"postfix" = "后缀"
|
||||||
|
"delayedStart" = "首次使用后开始"
|
||||||
|
"expireDays" = "过期天数"
|
||||||
|
"days" = "天"
|
||||||
|
|
||||||
[pages.inbounds.toasts]
|
[pages.inbounds.toasts]
|
||||||
"obtain" = "获取"
|
"obtain" = "获取"
|
||||||
@@ -152,7 +189,6 @@
|
|||||||
[pages.inbounds.stream.quic]
|
[pages.inbounds.stream.quic]
|
||||||
"encryption" = "加密"
|
"encryption" = "加密"
|
||||||
|
|
||||||
|
|
||||||
[pages.setting]
|
[pages.setting]
|
||||||
"title" = "设置"
|
"title" = "设置"
|
||||||
"save" = "保存配置"
|
"save" = "保存配置"
|
||||||
@@ -177,16 +213,40 @@
|
|||||||
"currentPassword" = "原密码"
|
"currentPassword" = "原密码"
|
||||||
"newUsername" = "新用户名"
|
"newUsername" = "新用户名"
|
||||||
"newPassword" = "新密码"
|
"newPassword" = "新密码"
|
||||||
"xrayConfigTemplate" = "xray 配置模版"
|
"advancedTemplate" = "高级模板部件"
|
||||||
"xrayConfigTemplateDesc" = "以该模版为基础生成最终的 xray 配置文件,重启面板生效"
|
"completeTemplate" = "Xray 配置的完整模板"
|
||||||
|
"xrayConfigTemplate" = "xray 配置模板"
|
||||||
|
"xrayConfigTemplateDesc" = "以该模型为基础生成最终的xray配置文件,重新启动面板生成效率"
|
||||||
|
"xrayConfigTorrent" = "禁止使用 bittorrent"
|
||||||
|
"xrayConfigTorrentDesc" = "更改配置模板避免用户使用bittorrent,重启面板生效"
|
||||||
|
"xrayConfigPrivateIp" = "禁止私人 ip 范围连接"
|
||||||
|
"xrayConfigPrivateIpDesc" = "更改配置模板以避免连接私有 IP 范围,重启面板生效"
|
||||||
|
"xrayConfigIRIp" = "禁止伊朗 IP 范围连接"
|
||||||
|
"xrayConfigIRIpDesc" = "修改配置模板避免连接伊朗IP范围,重启面板生效"
|
||||||
|
"xrayConfigIRdomain" = "禁止伊朗域连接"
|
||||||
|
"xrayConfigIRdomainDesc" = "修改配置模板避免连接伊朗域名,重启面板生效"
|
||||||
|
"xrayConfigInbounds" = "入站配置"
|
||||||
|
"xrayConfigInboundsDesc" = "更改配置模板接受特殊客户端,重启面板生效"
|
||||||
|
"xrayConfigOutbounds" = "出站配置"
|
||||||
|
"xrayConfigOutboundsDesc" = "更改配置模板定义此服务器的传出方式,重启面板生效"
|
||||||
|
"xrayConfigRoutings" = "路由规则配置"
|
||||||
|
"xrayConfigRoutingsDesc" = "更改配置模板为该服务器定义路由规则,重启面板生效"
|
||||||
"telegramBotEnable" = "启用电报机器人"
|
"telegramBotEnable" = "启用电报机器人"
|
||||||
"telegramBotEnableDesc" = "重启面板生效"
|
"telegramBotEnableDesc" = "重启面板生效"
|
||||||
"telegramToken" = "电报机器人TOKEN"
|
"telegramToken" = "电报机器人TOKEN"
|
||||||
"telegramTokenDesc" = "重启面板生效"
|
"telegramTokenDesc" = "重启面板生效"
|
||||||
"telegramChatId" = "电报机器人ChatId"
|
"telegramChatId" = "以逗号分隔的多个 chatID 重启面板生效"
|
||||||
"telegramChatIdDesc" = "重启面板生效"
|
"telegramChatIdDesc" = "重启面板生效"
|
||||||
"telegramNotifyTime" = "电报机器人通知时间"
|
"telegramNotifyTime" = "电报机器人通知时间"
|
||||||
"telegramNotifyTimeDesc" = "采用Crontab定时格式,重启面板生效"
|
"telegramNotifyTimeDesc" = "采用Crontab定时格式,重启面板生效"
|
||||||
|
"tgNotifyBackup" = "数据库备份"
|
||||||
|
"tgNotifyBackupDesc" = "正在发送数据库备份文件和报告通知。重启面板生效"
|
||||||
|
"expireTimeDiff" = "耗尽时间阈值"
|
||||||
|
"expireTimeDiffDesc" = "到期前检测耗尽(单位:天)"
|
||||||
|
"trafficDiff" = "耗尽流量阈值"
|
||||||
|
"trafficDiffDesc" = "完成流量前检测耗尽(单位:GB)"
|
||||||
|
"tgNotifyCpu" = "CPU 百分比警报阈值"
|
||||||
|
"tgNotifyCpuDesc" = "如果 CPU 使用率超过此百分比(单位:%),此 talegram bot 将向您发送通知"
|
||||||
"timeZonee" = "时区"
|
"timeZonee" = "时区"
|
||||||
"timeZoneDesc" = "定时任务按照该时区的时间运行,重启面板生效"
|
"timeZoneDesc" = "定时任务按照该时区的时间运行,重启面板生效"
|
||||||
|
|
||||||
|
|||||||
32
web/web.go
@@ -21,11 +21,11 @@ import (
|
|||||||
"x-ui/web/network"
|
"x-ui/web/network"
|
||||||
"x-ui/web/service"
|
"x-ui/web/service"
|
||||||
|
|
||||||
"github.com/pelletier/go-toml/v2"
|
|
||||||
"github.com/gin-contrib/sessions"
|
"github.com/gin-contrib/sessions"
|
||||||
"github.com/gin-contrib/sessions/cookie"
|
"github.com/gin-contrib/sessions/cookie"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||||
|
"github.com/pelletier/go-toml/v2"
|
||||||
"github.com/robfig/cron/v3"
|
"github.com/robfig/cron/v3"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
)
|
)
|
||||||
@@ -33,6 +33,9 @@ import (
|
|||||||
//go:embed assets/*
|
//go:embed assets/*
|
||||||
var assetsFS embed.FS
|
var assetsFS embed.FS
|
||||||
|
|
||||||
|
//go:embed assets/favicon.ico
|
||||||
|
var favicon []byte
|
||||||
|
|
||||||
//go:embed html/*
|
//go:embed html/*
|
||||||
var htmlFS embed.FS
|
var htmlFS embed.FS
|
||||||
|
|
||||||
@@ -85,10 +88,11 @@ type Server struct {
|
|||||||
server *controller.ServerController
|
server *controller.ServerController
|
||||||
xui *controller.XUIController
|
xui *controller.XUIController
|
||||||
api *controller.APIController
|
api *controller.APIController
|
||||||
|
sub *controller.SUBController
|
||||||
|
|
||||||
xrayService service.XrayService
|
xrayService service.XrayService
|
||||||
settingService service.SettingService
|
settingService service.SettingService
|
||||||
inboundService service.InboundService
|
tgbotService service.Tgbot
|
||||||
|
|
||||||
cron *cron.Cron
|
cron *cron.Cron
|
||||||
|
|
||||||
@@ -157,6 +161,11 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
|||||||
|
|
||||||
engine := gin.Default()
|
engine := gin.Default()
|
||||||
|
|
||||||
|
// Add favicon
|
||||||
|
engine.GET("/favicon.ico", func(c *gin.Context) {
|
||||||
|
c.Data(200, "image/x-icon", favicon)
|
||||||
|
})
|
||||||
|
|
||||||
secret, err := s.settingService.GetSecret()
|
secret, err := s.settingService.GetSecret()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -208,6 +217,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
|||||||
s.server = controller.NewServerController(g)
|
s.server = controller.NewServerController(g)
|
||||||
s.xui = controller.NewXUIController(g)
|
s.xui = controller.NewXUIController(g)
|
||||||
s.api = controller.NewAPIController(g)
|
s.api = controller.NewAPIController(g)
|
||||||
|
s.sub = controller.NewSUBController(g)
|
||||||
|
|
||||||
return engine, nil
|
return engine, nil
|
||||||
}
|
}
|
||||||
@@ -328,8 +338,13 @@ func (s *Server) startTask() {
|
|||||||
logger.Warning("Add NewStatsNotifyJob error", err)
|
logger.Warning("Add NewStatsNotifyJob error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// listen for TG bot income messages
|
|
||||||
go job.NewStatsNotifyJob().OnReceive()
|
// 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 {
|
} else {
|
||||||
s.cron.Remove(entry)
|
s.cron.Remove(entry)
|
||||||
}
|
}
|
||||||
@@ -406,6 +421,12 @@ func (s *Server) Start() (err error) {
|
|||||||
s.httpServer.Serve(listener)
|
s.httpServer.Serve(listener)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
isTgbotenabled, err := s.settingService.GetTgbotenabled()
|
||||||
|
if (err == nil) && (isTgbotenabled) {
|
||||||
|
tgBot := s.tgbotService.NewTgbot()
|
||||||
|
tgBot.Start()
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -415,6 +436,9 @@ func (s *Server) Stop() error {
|
|||||||
if s.cron != nil {
|
if s.cron != nil {
|
||||||
s.cron.Stop()
|
s.cron.Stop()
|
||||||
}
|
}
|
||||||
|
if s.tgbotService.IsRunnging() {
|
||||||
|
s.tgbotService.Stop()
|
||||||
|
}
|
||||||
var err1 error
|
var err1 error
|
||||||
var err2 error
|
var err2 error
|
||||||
if s.httpServer != nil {
|
if s.httpServer != nil {
|
||||||
|
|||||||
232
x-ui.sh
@@ -454,6 +454,64 @@ ssl_cert_issue() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open_ports() {
|
||||||
|
|
||||||
|
# Check if the firewall is inactive
|
||||||
|
if sudo ufw status | grep -q "Status: active"; then
|
||||||
|
echo "firewall is already active"
|
||||||
|
else
|
||||||
|
# Open the necessary ports
|
||||||
|
sudo ufw allow ssh
|
||||||
|
sudo ufw allow http
|
||||||
|
sudo ufw allow https
|
||||||
|
sudo ufw allow 2053/tcp
|
||||||
|
|
||||||
|
# Enable the firewall
|
||||||
|
sudo ufw --force enable
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prompt the user to enter a list of ports
|
||||||
|
read -p "Enter the ports you want to open (e.g. 80,443,2053 or range 400-500): " ports
|
||||||
|
|
||||||
|
# Check if the input is valid
|
||||||
|
if ! [[ $ports =~ ^([0-9]+|[0-9]+-[0-9]+)(,([0-9]+|[0-9]+-[0-9]+))*$ ]]; then
|
||||||
|
echo "Error: Invalid input. Please enter a comma-separated list of ports or a range of ports (e.g. 80,443,2053 or 400-500)." >&2; exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Open the specified ports using ufw
|
||||||
|
IFS=',' read -ra PORT_LIST <<< "$ports"
|
||||||
|
for port in "${PORT_LIST[@]}"; do
|
||||||
|
if [[ $port == *-* ]]; then
|
||||||
|
# Split the range into start and end ports
|
||||||
|
start_port=$(echo $port | cut -d'-' -f1)
|
||||||
|
end_port=$(echo $port | cut -d'-' -f2)
|
||||||
|
# Loop through the range and open each port
|
||||||
|
for ((i=start_port; i<=end_port; i++)); do
|
||||||
|
sudo ufw allow $i
|
||||||
|
done
|
||||||
|
else
|
||||||
|
sudo ufw allow "$port"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Confirm that the ports are open
|
||||||
|
sudo ufw status | grep $ports
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
update_geo(){
|
||||||
|
systemctl stop x-ui
|
||||||
|
cd /usr/local/x-ui/bin
|
||||||
|
rm -f geoip.dat geosite.dat iran.dat
|
||||||
|
wget -N https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
|
||||||
|
wget -N https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
|
||||||
|
wget -N https://github.com/bootmortis/iran-hosted-domains/releases/latest/download/iran.dat
|
||||||
|
systemctl start x-ui
|
||||||
|
echo -e "${green}Geosite and Geoip have been updated successfully!${plain}"
|
||||||
|
before_show_menu
|
||||||
|
}
|
||||||
|
|
||||||
install_acme() {
|
install_acme() {
|
||||||
cd ~
|
cd ~
|
||||||
LOGI "install acme..."
|
LOGI "install acme..."
|
||||||
@@ -490,14 +548,7 @@ ssl_cert_issue_standalone() {
|
|||||||
else
|
else
|
||||||
LOGI "install socat succeed..."
|
LOGI "install socat succeed..."
|
||||||
fi
|
fi
|
||||||
#creat a directory for install cert
|
|
||||||
certPath=/root/cert
|
|
||||||
if [ ! -d "$certPath" ]; then
|
|
||||||
mkdir $certPath
|
|
||||||
else
|
|
||||||
rm -rf $certPath
|
|
||||||
mkdir $certPath
|
|
||||||
fi
|
|
||||||
#get the domain here,and we need verify it
|
#get the domain here,and we need verify it
|
||||||
local domain=""
|
local domain=""
|
||||||
read -p "please input your domain:" domain
|
read -p "please input your domain:" domain
|
||||||
@@ -512,6 +563,16 @@ ssl_cert_issue_standalone() {
|
|||||||
else
|
else
|
||||||
LOGI "your domain is ready for issuing cert now..."
|
LOGI "your domain is ready for issuing cert now..."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
#create a directory for install cert
|
||||||
|
certPath="/root/cert/${domain}"
|
||||||
|
if [ ! -d "$certPath" ]; then
|
||||||
|
mkdir -p "$certPath"
|
||||||
|
else
|
||||||
|
rm -rf "$certPath"
|
||||||
|
mkdir -p "$certPath"
|
||||||
|
fi
|
||||||
|
|
||||||
#get needed port here
|
#get needed port here
|
||||||
local WebPort=80
|
local WebPort=80
|
||||||
read -p "please choose which port do you use,default will be 80 port:" WebPort
|
read -p "please choose which port do you use,default will be 80 port:" WebPort
|
||||||
@@ -531,9 +592,9 @@ ssl_cert_issue_standalone() {
|
|||||||
LOGE "issue certs succeed,installing certs..."
|
LOGE "issue certs succeed,installing certs..."
|
||||||
fi
|
fi
|
||||||
#install cert
|
#install cert
|
||||||
~/.acme.sh/acme.sh --installcert -d ${domain} --ca-file /root/cert/ca.cer \
|
~/.acme.sh/acme.sh --installcert -d ${domain} \
|
||||||
--cert-file /root/cert/${domain}.cer --key-file /root/cert/${domain}.key \
|
--key-file /root/cert/${domain}/privkey.pem \
|
||||||
--fullchain-file /root/cert/fullchain.cer
|
--fullchain-file /root/cert/${domain}/fullchain.pem
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
LOGE "install certs failed,exit"
|
LOGE "install certs failed,exit"
|
||||||
@@ -542,17 +603,18 @@ ssl_cert_issue_standalone() {
|
|||||||
else
|
else
|
||||||
LOGI "install certs succeed,enable auto renew..."
|
LOGI "install certs succeed,enable auto renew..."
|
||||||
fi
|
fi
|
||||||
~/.acme.sh/acme.sh --upgrade --auto-upgrade
|
|
||||||
if [ $? -ne 0 ]; then
|
~/.acme.sh/acme.sh --upgrade --auto-upgrade
|
||||||
LOGE "auto renew failed,certs details:"
|
if [ $? -ne 0 ]; then
|
||||||
ls -lah cert
|
LOGE "auto renew failed, certs details:"
|
||||||
chmod 755 $certPath
|
ls -lah cert/*
|
||||||
exit 1
|
chmod 755 $certPath/*
|
||||||
else
|
exit 1
|
||||||
LOGI "auto renew succeed,certs details:"
|
else
|
||||||
ls -lah cert
|
LOGI "auto renew succeed, certs details:"
|
||||||
chmod 755 $certPath
|
ls -lah cert/*
|
||||||
fi
|
chmod 755 $certPath/*
|
||||||
|
fi
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -573,13 +635,7 @@ ssl_cert_issue_by_cloudflare() {
|
|||||||
CF_Domain=""
|
CF_Domain=""
|
||||||
CF_GlobalKey=""
|
CF_GlobalKey=""
|
||||||
CF_AccountEmail=""
|
CF_AccountEmail=""
|
||||||
certPath=/root/cert
|
|
||||||
if [ ! -d "$certPath" ]; then
|
|
||||||
mkdir $certPath
|
|
||||||
else
|
|
||||||
rm -rf $certPath
|
|
||||||
mkdir $certPath
|
|
||||||
fi
|
|
||||||
LOGD "please input your domain:"
|
LOGD "please input your domain:"
|
||||||
read -p "Input your domain here:" CF_Domain
|
read -p "Input your domain here:" CF_Domain
|
||||||
LOGD "your domain is:${CF_Domain},check it..."
|
LOGD "your domain is:${CF_Domain},check it..."
|
||||||
@@ -593,6 +649,16 @@ ssl_cert_issue_by_cloudflare() {
|
|||||||
else
|
else
|
||||||
LOGI "your domain is ready for issuing cert now..."
|
LOGI "your domain is ready for issuing cert now..."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
#create a directory for install cert
|
||||||
|
certPath="/root/cert/${CF_Domain}"
|
||||||
|
if [ ! -d "$certPath" ]; then
|
||||||
|
mkdir -p "$certPath"
|
||||||
|
else
|
||||||
|
rm -rf "$certPath"
|
||||||
|
mkdir -p "$certPath"
|
||||||
|
fi
|
||||||
|
|
||||||
LOGD "please inout your cloudflare global API key:"
|
LOGD "please inout your cloudflare global API key:"
|
||||||
read -p "Input your key here:" CF_GlobalKey
|
read -p "Input your key here:" CF_GlobalKey
|
||||||
LOGD "your cloudflare global API key is:${CF_GlobalKey}"
|
LOGD "your cloudflare global API key is:${CF_GlobalKey}"
|
||||||
@@ -611,34 +677,72 @@ ssl_cert_issue_by_cloudflare() {
|
|||||||
LOGE "issue cert failed,exit"
|
LOGE "issue cert failed,exit"
|
||||||
rm -rf ~/.acme.sh/${CF_Domain}
|
rm -rf ~/.acme.sh/${CF_Domain}
|
||||||
exit 1
|
exit 1
|
||||||
else
|
else
|
||||||
LOGI "Certificate issued Successfully, Installing..."
|
LOGI "Certificate issued Successfully, Installing..."
|
||||||
fi
|
fi
|
||||||
~/.acme.sh/acme.sh --installcert -d ${CF_Domain} -d *.${CF_Domain} --ca-file /root/cert/ca.cer \
|
~/.acme.sh/acme.sh --installcert -d ${CF_Domain} -d *.${CF_Domain} \
|
||||||
--cert-file /root/cert/${CF_Domain}.cer --key-file /root/cert/${CF_Domain}.key \
|
--key-file /root/cert/${CF_Domain}/privkey.pem \
|
||||||
--fullchain-file /root/cert/fullchain.cer
|
--fullchain-file /root/cert/${CF_Domain}/fullchain.pem
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
LOGE "install cert failed,exit"
|
if [ $? -ne 0 ]; then
|
||||||
rm -rf ~/.acme.sh/${CF_Domain}
|
LOGE "install cert failed,exit"
|
||||||
exit 1
|
rm -rf ~/.acme.sh/${CF_Domain}
|
||||||
else
|
exit 1
|
||||||
LOGI "Certificate installed Successfully,Turning on automatic updates..."
|
else
|
||||||
fi
|
LOGI "Certificate installed Successfully,Turning on automatic updates..."
|
||||||
~/.acme.sh/acme.sh --upgrade --auto-upgrade
|
fi
|
||||||
if [ $? -ne 0 ]; then
|
~/.acme.sh/acme.sh --upgrade --auto-upgrade
|
||||||
LOGE "Auto update setup Failed, script exiting..."
|
if [ $? -ne 0 ]; then
|
||||||
ls -lah cert
|
LOGE "auto renew failed, certs details:"
|
||||||
chmod 755 $certPath
|
ls -lah cert/*
|
||||||
exit 1
|
chmod 755 $certPath/*
|
||||||
else
|
exit 1
|
||||||
LOGI "The certificate is installed and auto-renewal is turned on, Specific information is as follows"
|
else
|
||||||
ls -lah cert
|
LOGI "auto renew succeed, certs details:"
|
||||||
chmod 755 $certPath
|
ls -lah cert/*
|
||||||
fi
|
chmod 755 $certPath/*
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
show_menu
|
show_menu
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
google_recaptcha() {
|
||||||
|
curl -O https://raw.githubusercontent.com/jinwyp/one_click_script/master/install_kernel.sh && chmod +x ./install_kernel.sh && ./install_kernel.sh
|
||||||
|
echo ""
|
||||||
|
before_show_menu
|
||||||
|
}
|
||||||
|
|
||||||
|
run_speedtest() {
|
||||||
|
# Check if Speedtest is already installed
|
||||||
|
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://install.speedtest.net/app/cli/install.rpm.sh | sudo bash
|
||||||
|
sudo dnf install -y speedtest
|
||||||
|
elif command -v yum &> /dev/null; then
|
||||||
|
sudo yum install -y curl
|
||||||
|
curl -s https://install.speedtest.net/app/cli/install.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://install.speedtest.net/app/cli/install.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://install.speedtest.net/app/cli/install.deb.sh | sudo bash
|
||||||
|
sudo apt install -y speedtest
|
||||||
|
else
|
||||||
|
echo "Error: Package manager not found. You may need to install Speedtest manually."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run Speedtest
|
||||||
|
speedtest
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
show_usage() {
|
show_usage() {
|
||||||
echo "x-ui control menu usages: "
|
echo "x-ui control menu usages: "
|
||||||
@@ -681,10 +785,14 @@ show_menu() {
|
|||||||
${green}14.${plain} Disabel x-ui On System Startup
|
${green}14.${plain} Disabel x-ui On System Startup
|
||||||
————————————————
|
————————————————
|
||||||
${green}15.${plain} Enable BBR
|
${green}15.${plain} Enable BBR
|
||||||
${green}16.${plain} Issuse Certs
|
${green}16.${plain} Apply for an SSL Certificate
|
||||||
|
${green}17.${plain} Update Geo Files
|
||||||
|
${green}18.${plain} Active Firewall and open ports
|
||||||
|
${green}19.${plain} Fixing Google reCAPTCHA
|
||||||
|
${green}20.${plain} Speedtest by Ookla
|
||||||
"
|
"
|
||||||
show_status
|
show_status
|
||||||
echo && read -p "Please enter your selection [0-16]: " num
|
echo && read -p "Please enter your selection [0-20]: " num
|
||||||
|
|
||||||
case "${num}" in
|
case "${num}" in
|
||||||
0)
|
0)
|
||||||
@@ -738,8 +846,20 @@ show_menu() {
|
|||||||
16)
|
16)
|
||||||
ssl_cert_issue
|
ssl_cert_issue
|
||||||
;;
|
;;
|
||||||
|
17)
|
||||||
|
update_geo
|
||||||
|
;;
|
||||||
|
18)
|
||||||
|
open_ports
|
||||||
|
;;
|
||||||
|
19)
|
||||||
|
google_recaptcha
|
||||||
|
;;
|
||||||
|
20)
|
||||||
|
run_speedtest
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
LOGE "Please enter the correct number [0-16]"
|
LOGE "Please enter the correct number [0-20]"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|||||||