Compare commits

...

64 Commits
0.4.3 ... 0.5.1

Author SHA1 Message Date
Alireza Ahmadi
838bdaf314 v.0.5.1 2023-04-12 11:08:44 +02:00
Alireza Ahmadi
f6f0cf3657 Merge pull request #188 from alireza0/dependabot/go_modules/gorm.io/gorm-1.25.0
Bump gorm.io/gorm from 1.24.6 to 1.25.0
2023-04-12 10:56:29 +02:00
dependabot[bot]
dd5e9a97a6 Bump gorm.io/gorm from 1.24.6 to 1.25.0
Bumps [gorm.io/gorm](https://github.com/go-gorm/gorm) from 1.24.6 to 1.25.0.
- [Release notes](https://github.com/go-gorm/gorm/releases)
- [Commits](https://github.com/go-gorm/gorm/compare/v1.24.6...v1.25.0)

---
updated-dependencies:
- dependency-name: gorm.io/gorm
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-12 08:55:25 +00:00
Alireza Ahmadi
edfbb91dff Merge pull request #189 from alireza0/dependabot/go_modules/gorm.io/driver/sqlite-1.5.0
Bump gorm.io/driver/sqlite from 1.4.4 to 1.5.0
2023-04-12 10:54:29 +02:00
dependabot[bot]
eb239b33af Bump gorm.io/driver/sqlite from 1.4.4 to 1.5.0
Bumps [gorm.io/driver/sqlite](https://github.com/go-gorm/sqlite) from 1.4.4 to 1.5.0.
- [Release notes](https://github.com/go-gorm/sqlite/releases)
- [Commits](https://github.com/go-gorm/sqlite/compare/v1.4.4...v1.5.0)

---
updated-dependencies:
- dependency-name: gorm.io/driver/sqlite
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-11 22:04:50 +00:00
Alireza Ahmadi
4b9940fb06 Fix #161 2023-04-11 14:25:06 +02:00
Alireza Ahmadi
98d5e960a4 v0.5.1 2023-04-11 05:13:04 +02:00
Alireza Ahmadi
61fd029e28 Merge pull request #183 from masoud-hidden/main
Fix some problems.
2023-04-11 04:27:31 +02:00
Masoud Hidden
2be3df59be Fix vless inbounds problem "decryption" set to null 2023-04-11 05:18:36 +03:30
Masoud Hidden
7cf192e179 Fix basePath in database backup 2023-04-11 05:16:42 +03:30
Alireza Ahmadi
3a002c886d Fix basePath in subscription link 2023-04-11 03:32:37 +02:00
Alireza Ahmadi
4b05c333ca Merge pull request #182 from alireza0/dependabot/go_modules/github.com/goccy/go-json-0.10.2
Bump github.com/goccy/go-json from 0.10.0 to 0.10.2
2023-04-11 03:15:40 +02:00
Alireza Ahmadi
bfc5b37bb1 Fix fallback alpn #156 2023-04-11 03:15:13 +02:00
Alireza Ahmadi
07089455b5 Fix traffic exhaustion detection 2023-04-11 03:14:00 +02:00
dependabot[bot]
7add969312 Bump github.com/goccy/go-json from 0.10.0 to 0.10.2
Bumps [github.com/goccy/go-json](https://github.com/goccy/go-json) from 0.10.0 to 0.10.2.
- [Release notes](https://github.com/goccy/go-json/releases)
- [Changelog](https://github.com/goccy/go-json/blob/master/CHANGELOG.md)
- [Commits](https://github.com/goccy/go-json/compare/v0.10.0...v0.10.2)

---
updated-dependencies:
- dependency-name: github.com/goccy/go-json
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-10 22:09:26 +00:00
Alireza Ahmadi
501f778c9e [feature] backup from panel #151 2023-04-10 12:27:32 +02:00
Alireza Ahmadi
ac52340c51 #176 2023-04-10 09:53:59 +02:00
Alireza Ahmadi
f5f90d81a3 [Feature] default cert from settings #155 2023-04-09 12:58:11 +02:00
Alireza Ahmadi
e6c23caa1d Merge pull request #135 from alireza0/dependabot/go_modules/github.com/shirou/gopsutil/v3-3.23.3
Bump github.com/shirou/gopsutil/v3 from 3.23.2 to 3.23.3
2023-04-09 12:09:07 +02:00
Alireza Ahmadi
bf3a64c77d Merge pull request #159 from alireza0/dependabot/go_modules/golang.org/x/text-0.9.0
Bump golang.org/x/text from 0.8.0 to 0.9.0
2023-04-09 12:08:58 +02:00
Alireza Ahmadi
e2e88d8652 Merge pull request #168 from forkeer/main
Fix /dev/fd/63: line 76 in install.sh
2023-04-09 12:08:41 +02:00
Alireza Ahmadi
91b453fff7 Fix #156 2023-04-09 12:06:38 +02:00
Alireza Ahmadi
9e955df76a Fix #164 2023-04-09 11:45:02 +02:00
Leonardo
58ca5f7a51 Update install.sh
Fix /dev/fd/63: line 76: apt: command not found in fedora
2023-04-08 15:31:59 +03:30
dependabot[bot]
4f469f9ad5 Bump golang.org/x/text from 0.8.0 to 0.9.0
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.8.0 to 0.9.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.8.0...v0.9.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-06 22:04:29 +00:00
Alireza Ahmadi
1969120c94 Fix reset all inbound bug 2023-04-05 18:37:09 +02:00
Alireza Ahmadi
cdbf742b66 [tgbot] fix bug #148 2023-04-05 18:36:47 +02:00
Alireza Ahmadi
02a4f0c7d4 v0.5.0 with ALPN h3 2023-04-05 11:53:01 +02:00
Alireza Ahmadi
e0a64a8257 [bug] fix sub link hostname #140 2023-04-05 09:44:57 +02:00
Alireza Ahmadi
38db94f507 [bug] fix time calculation #143 2023-04-05 09:36:23 +02:00
Alireza Ahmadi
d0ce67a8f0 [bug] fix traffic calculation errors #142 2023-04-05 09:35:10 +02:00
Alireza Ahmadi
fcc3f67181 Update README.md 2023-04-05 00:11:15 +02:00
Alireza Ahmadi
357630b077 v0.5.0 2023-04-05 00:05:16 +02:00
Alireza Ahmadi
1246b54ffa better translate from @MHSanaei 2023-04-04 01:11:09 +02:00
Alireza Ahmadi
2e3119ab9b [feature] Reset all clients/inbounds 2023-04-04 00:37:09 +02:00
dependabot[bot]
b834ac5d93 Bump github.com/shirou/gopsutil/v3 from 3.23.2 to 3.23.3
Bumps [github.com/shirou/gopsutil/v3](https://github.com/shirou/gopsutil) from 3.23.2 to 3.23.3.
- [Release notes](https://github.com/shirou/gopsutil/releases)
- [Commits](https://github.com/shirou/gopsutil/compare/v3.23.2...v3.23.3)

---
updated-dependencies:
- dependency-name: github.com/shirou/gopsutil/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-03 22:28:37 +00:00
Alireza Ahmadi
92df5659c9 Merge pull request #134 from alireza0:Delayed-Start
Delayed-Start
2023-04-03 22:16:44 +02:00
Alireza Ahmadi
f85bd39a0a [delayStart] traslate 2023-04-03 19:12:57 +02:00
Alireza Ahmadi
67fe79a7eb [delayStart] Intiate 2023-04-03 18:33:26 +02:00
Alireza Ahmadi
5cd3ee10de Merge pull request #132 from alireza0:Adapt-new-changes
Adapt-new-changes
2023-04-02 15:10:40 +02:00
Alireza Ahmadi
e3e7b0f736 Add favicon #125 2023-04-02 15:01:56 +02:00
Alireza Ahmadi
19280cdfbd General exhaustion alert #100 2023-04-02 12:28:48 +02:00
Alireza Ahmadi
17a483266e [log view] add refresh,count,download #50 2023-04-02 12:13:49 +02:00
Alireza Ahmadi
1e7d2010a8 [darkMode] fix datetime color #50 2023-04-02 11:21:35 +02:00
Alireza Ahmadi
97a0e2cae9 Adapt bulk modal 2023-03-29 12:25:29 +02:00
Alireza Ahmadi
e9c3c330f2 Merge pull request #119 from alireza0:tgbot-separation
[tgbot] adjust tgbot usage to new method
2023-03-29 12:22:38 +02:00
Alireza Ahmadi
294894eca0 [tgbot] adjust tgbot usage to new method 2023-03-29 12:12:27 +02:00
Alireza Ahmadi
f3df93a950 [sub] back to merge 2023-03-29 01:07:09 +02:00
Alireza Ahmadi
1780f06242 Remove additional data in config.json 2023-03-29 00:18:51 +02:00
Alireza Ahmadi
50671bfce3 Merge branch 'main' of https://github.com/alireza0/x-ui 2023-03-29 00:18:01 +02:00
Alireza Ahmadi
7332258dce [tgbot] better reply 2023-03-29 00:17:52 +02:00
Alireza Ahmadi
d9523cfd37 Merge pull request #117 from alireza0:client-disable-enable
[client] add disable-enable feature
2023-03-29 00:14:13 +02:00
Alireza Ahmadi
47f75f6382 [client] add disable-enable feature 2023-03-29 00:13:37 +02:00
Alireza Ahmadi
a6f2dc577a Merge pull request #116 from alireza0/Subscription
[sub] fix conflicts
2023-03-29 00:03:23 +02:00
Alireza Ahmadi
36ddde3491 Merge branch 'main' into Subscription 2023-03-29 00:03:16 +02:00
Alireza Ahmadi
f1a87c7d92 [sub] fix conflicts 2023-03-28 23:58:45 +02:00
Alireza Ahmadi
48f7b88ede Merge pull request #115 from alireza0/revert-114-Subscription
Revert "Subscription"
2023-03-28 23:49:34 +02:00
Alireza Ahmadi
b7048cdab8 Revert "Subscription" 2023-03-28 23:49:23 +02:00
Alireza Ahmadi
df2ef6cdde Merge pull request #114 from alireza0:Subscription
Subscription
2023-03-28 23:44:10 +02:00
Alireza Ahmadi
efd1b06593 [sub] frontend and adaptation 2023-03-28 23:42:14 +02:00
Alireza Ahmadi
28050aba23 revert client traffic change 2023-03-28 23:09:51 +02:00
Alireza Ahmadi
27de7b030b fix initiation of expirytime 2023-03-27 15:19:53 +02:00
Alireza Ahmadi
9ae49ed562 [sub] add backend 2023-03-27 15:06:22 +02:00
Alireza Ahmadi
cee117e1e8 [sub] prepare database 2023-03-27 15:03:55 +02:00
44 changed files with 1552 additions and 273 deletions

View File

@@ -8,7 +8,7 @@ on:
jobs:
linuxamd64build:
name: build x-ui amd64 version
runs-on: ubuntu-18.04
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- name: Set up Go
@@ -46,7 +46,7 @@ jobs:
prerelease: true
linuxarm64build:
name: build x-ui arm64 version
runs-on: ubuntu-18.04
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- name: Set up Go
@@ -86,7 +86,7 @@ jobs:
prerelease: true
linuxs390xbuild:
name: build x-ui s390x version
runs-on: ubuntu-18.04
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- name: Set up Go

View File

@@ -18,6 +18,8 @@ xray panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese)**
| REST API | :heavy_check_mark: |
| Telegram BOT (admin + clients) | :heavy_check_mark: |
| Backup database using Telegram BOT | :heavy_check_mark: |
| Subscription link | :heavy_check_mark: |
| Calculate expire date on first usage | :heavy_check_mark: |
**If you think this project is helpful to you, you may wish to give a** :star2:
@@ -31,15 +33,43 @@ xray panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese)**
- Support for configuring more transport configurations
- Traffic statistics, limit traffic, limit expiration time
- Customizable xray configuration templates
- Support subscription ( multi ) link
- Detect users which are expiring or exceed traffic limit soon
- Support https access panel (self-provided domain name + ssl certificate)
- Support one-click SSL certificate application and automatic renewal
- For more advanced configuration items, please refer to the panel
## API routes
- `/login` with `PUSH` user data: `{username: '', password: ''}` for login
- `/xui/API/inbounds` base for following actions:
| Method | Path | Action |
| ------------- | ------------- | ------------- |
| GET | "/" | Get all inbounds |
| GET | "/get/:id" | Get inbound with inbound.id |
| POST | "/add" | Add inbound |
| POST | "/del/:id" | Delete Inbound |
| POST | "/update/:id" | Update Inbound |
| POST | "/addClient/" | Add Client to inbound |
| POST | "/delClient/:email" | Delete Client |
| POST | "/updateClient/:index" | Update Client |
| POST | "/:id/resetClientTraffic/:email" | Reset Client's Traffic |
| POST | "/resetAllTraffics" | Reset traffics of all inbounds |
| POST | "/resetAllClientTraffics/:id" | Reset traffics of all clients in an inbound |
# Screenshot from Inbouds page
![inbounds](./media/inbounds.png)
![Dark inbounds](./media/inbounds-dark.png)
## suggestion system
- CentOS 8+
- Ubuntu 20+
- Debian 8+
- Fedora 36+
# Install & Upgrade to latest version
```
@@ -47,9 +77,9 @@ bash <(curl -Ls https://raw.githubusercontent.com/alireza0/x-ui/master/install.s
```
## Install custom version
To install your desired version you can add the version to the end of install command. Example for ver `0.4.0`:
To install your desired version you can add the version to the end of install command. Example for ver `0.5.1`:
```
bash <(curl -Ls https://raw.githubusercontent.com/alireza0/x-ui/master/install.sh) 0.4.0
bash <(curl -Ls https://raw.githubusercontent.com/alireza0/x-ui/master/install.sh) 0.5.1
```
## Manual install & upgrade
@@ -100,6 +130,8 @@ docker build -t x-ui .
```
## SSL certificate application
<details>
<summary>Click for details</summary>
### Cloudflare
@@ -114,8 +146,11 @@ ln -s /snap/bin/certbot /usr/bin/certbot
certbot certonly --standalone --register-unsafely-without-email --non-interactive --agree-tos -d <Your Domain Name>
```
</details>
## Tg robot use
<details>
<summary>Click for details</summary>
> This feature and tutorial are provided by [FranzKafkaYu](https://github.com/FranzKafkaYu)
@@ -133,6 +168,7 @@ Set the robot-related parameters in the panel background, including:
Reference syntax:
- 30 * * * * * //Notify at the 30s of each point
- 0 */10 * * * * //Notify at the first second of each 10 minutes
- @hourly // hourly notification
- @daily // Daily notification (00:00 in the morning)
- @every 8h // notify every 8 hours
@@ -143,26 +179,19 @@ Reference syntax:
- Login notification
- CPU threshold notification
- 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
- Menu based bot
- Search client by email ( only admin )
- Check all inbounds
- Check server status
- Check Exhausted users
- Check depleted users
- Receive backup by request and in periodic reports
</details>
## suggestion system
- CentOS 7+
- Ubuntu 16+
- Debian 8+
- Fedora 36+
# common problem
# Common problem
<details>
<summary>Click for details</summary>
## Migrating from v2-ui
First install the latest version of x-ui on the server where v2-ui is installed, and then use the following command to migrate, which will migrate the native v2-ui `All inbound account data` to x-ui`Panel settings and username passwords are not migrated`
@@ -192,7 +221,6 @@ find this in config :
},
```
**the final output is like :**
```json
"policy": {
@@ -211,10 +239,10 @@ find this in config :
"routing": {
```
restart panel
</details>
# a special thanks to
- [HexaSoftwareTech](https://github.com/HexaSoftwareTech/)
- [FranzKafkaYu](https://github.com/FranzKafkaYu)
- [MHSanaei](https://github.com/MHSanaei)
## Stargazers over time

View File

@@ -1 +1 @@
0.4.3
0.5.1

View File

@@ -74,4 +74,7 @@ type Client struct {
Email string `json:"email"`
TotalGB int64 `json:"totalGB" form:"totalGB"`
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
Enable bool `json:"enable" form:"enable"`
TgID string `json:"tgId" form:"tgId"`
SubID string `json:"subId" form:"subId"`
}

11
go.mod
View File

@@ -7,17 +7,18 @@ require (
github.com/gin-contrib/sessions v0.0.4
github.com/gin-gonic/gin v1.9.0
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
github.com/goccy/go-json v0.10.2
github.com/nicksnyder/go-i18n/v2 v2.2.1
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pelletier/go-toml/v2 v2.0.7
github.com/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
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
gorm.io/driver/sqlite v1.4.4
gorm.io/gorm v1.24.6
gorm.io/driver/sqlite v1.5.0
gorm.io/gorm v1.25.0
)
require (
@@ -29,7 +30,6 @@ require (
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.11.2 // indirect
github.com/goccy/go-json v0.10.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
@@ -46,6 +46,7 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pires/go-proxyproto v0.6.2 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/shoenig/go-m1cpu v0.1.4 // indirect
github.com/tklauser/go-sysconf v0.3.11 // indirect
github.com/tklauser/numcpus v0.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect

28
go.sum
View File

@@ -44,8 +44,8 @@ github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVL
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
@@ -70,7 +70,6 @@ github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/z
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@@ -132,8 +131,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/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/shirou/gopsutil/v3 v3.23.2 h1:PAWSuiAszn7IhPMBtXsbSCafej7PqUOvY6YywlQUExU=
github.com/shirou/gopsutil/v3 v3.23.2/go.mod h1:gv0aQw33GLo3pG8SiWKiQrbDzbRY1K80RyZJ7V4Th1M=
github.com/shirou/gopsutil/v3 v3.23.3 h1:Syt5vVZXUDXPEXpIBt5ziWsJ4LdSAAxF4l/xZeQgSEE=
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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -205,7 +208,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-20220811171246-fbc7d0a398ab/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/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -215,8 +217,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.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.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
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/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=
@@ -244,11 +246,11 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.4.4 h1:gIufGoR0dQzjkyqDyYSCvsYR6fba1Gw5YKDqKeChxFc=
gorm.io/driver/sqlite v1.4.4/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
gorm.io/gorm v1.24.6 h1:wy98aq9oFEetsc4CAbKD2SoBCdMzsbSIvSUUFJuHi5s=
gorm.io/gorm v1.24.6/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/driver/sqlite v1.5.0 h1:zKYbzRCpBrT1bNijRnxLDJWPjVfImGEn0lSnUY5gZ+c=
gorm.io/driver/sqlite v1.5.0/go.mod h1:kDMDfntV9u/vuMmz8APHtHF0b4nyBB7sfCieC6G8k8I=
gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.0 h1:+KtYtb2roDz14EQe4bla8CbQlmb9dN3VejSai3lprfU=
gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gvisor.dev/gvisor v0.0.0-20220901235040-6ca97ef2ce1c h1:m5lcgWnL3OElQNVyp3qcncItJ2c0sQlSGjYK2+nJTA4=
lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@@ -8,7 +8,7 @@ plain='\033[0m'
cur_dir=$(pwd)
# check root
[[ $EUID -ne 0 ]] && echo -e "${red}Fatal error${plain} Please run this script with root privilege \n " && exit 1
[[ $EUID -ne 0 ]] && echo -e "${red}Fatal error: ${plain} Please run this script with root privilege \n " && exit 1
# Check OS and set release variable
if [[ -f /etc/os-release ]]; then
@@ -47,17 +47,17 @@ os_version=""
os_version=$(grep -i version_id /etc/os-release | cut -d \" -f2 | cut -d . -f1)
if [[ "${release}" == "centos" ]]; then
if [[ ${os_version} -lt 7 ]]; then
echo -e "${red} Please use CentOS 7 or higher ${plain}\n" && exit 1
if [[ ${os_version} -lt 8 ]]; then
echo -e "${red} Please use CentOS 8 or higher ${plain}\n" && exit 1
fi
elif [[ "${release}" == "ubuntu" ]]; then
if [[ ${os_version} -lt 18 ]]; then
echo -e "${red}please use Ubuntu 18 or higher version${plain}\n" && exit 1
if [[ ${os_version} -lt 20 ]]; then
echo -e "${red}please use Ubuntu 20 or higher version! ${plain}\n" && exit 1
fi
elif [[ "${release}" == "fedora" ]]; then
if [[ ${os_version} -lt 36 ]]; then
echo -e "${red}please use Fedora 36 or higher version${plain}\n" && exit 1
echo -e "${red}please use Fedora 36 or higher version! ${plain}\n" && exit 1
fi
elif [[ "${release}" == "debian" ]]; then
@@ -70,7 +70,7 @@ fi
install_base() {
if [[ "${release}" == "centos" ]]; then
if [[ "${release}" == "centos" ]] || [[ "${release}" == "fedora" ]] ; then
yum install wget curl tar -y
else
apt install wget curl tar -y

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 324 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 316 KiB

View File

@@ -156,6 +156,12 @@
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-sub,
.ant-layout-header,
@@ -174,6 +180,7 @@
.ant-card-dark:hover {
border-color: #e8e8e8;
box-shadow: 0 2px 8px rgba(255,255,255,.15);
}
.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-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;
}
.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);
background-color: #1a212a;
}
.ant-card-dark .ant-input,
.ant-card-dark .ant-input-number,
.ant-card-dark .ant-input-number-handler-wrap,
.ant-card-dark .ant-calendar-input,
.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);
background-color: #2e3b52;
}
@@ -239,6 +251,12 @@
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-body,
.ant-card-dark .ant-modal-header,
@@ -280,6 +298,12 @@
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 {
color: #3c9ae8;
background: #111d2c;
@@ -334,6 +358,29 @@
color: hsla(0,0%,100%,.65);
background-color: #073763;
border-color: #1890ff;
text-shadow: 0 -1px 0 rgba(0,0,0,.12);
box-shadow: 0 2px 0 rgba(0,0,0,.045);
text-shadow: 0 -1px 0 rgba(255,255,255,.12);
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -170,13 +170,13 @@ class AllSetting {
this.webCertFile = "";
this.webKeyFile = "";
this.webBasePath = "/";
this.expireDiff = "";
this.trafficDiff = "";
this.tgBotEnable = false;
this.tgBotToken = "";
this.tgBotChatId = "";
this.tgRunTime = "@daily";
this.tgBotBackup = false;
this.tgExpireDiff = "";
this.tgTrafficDiff = "";
this.tgCpu = "";
this.xrayTemplateConfig = "";

View File

@@ -93,6 +93,7 @@ const UTLS_FINGERPRINT = {
};
const ALPN_OPTION = {
H3: "h3",
H2: "h2",
HTTP1: "http/1.1",
};
@@ -477,7 +478,7 @@ class TlsStreamSettings extends XrayCommonClass {
maxVersion = TLS_VERSION_OPTION.TLS12,
cipherSuites = '',
certificates=[new TlsStreamSettings.Cert()],
alpn=[''],
alpn=[],
settings=[new TlsStreamSettings.Settings()]) {
super();
this.server = serverName;
@@ -919,16 +920,16 @@ class Inbound extends XrayCommonClass {
isExpiry(index) {
switch (this.protocol) {
case Protocols.VMESS:
if(this.settings.vmesses[index]._expiryTime != null)
return this.settings.vmesses[index]._expiryTime < new Date().getTime();
if(this.settings.vmesses[index].expiryTime > 0)
return this.settings.vmesses[index].expiryTime < new Date().getTime();
return false
case Protocols.VLESS:
if(this.settings.vlesses[index]._expiryTime != null)
return this.settings.vlesses[index]._expiryTime < new Date().getTime();
if(this.settings.vlesses[index].expiryTime > 0)
return this.settings.vlesses[index].expiryTime < new Date().getTime();
return false
case Protocols.TROJAN:
if(this.settings.trojans[index]._expiryTime != null)
return this.settings.trojans[index]._expiryTime < new Date().getTime();
if(this.settings.trojans[index].expiryTime > 0)
return this.settings.trojans[index].expiryTime < new Date().getTime();
return false
default:
return false;
@@ -1431,13 +1432,16 @@ Inbound.VmessSettings = class extends Inbound.Settings {
}
};
Inbound.VmessSettings.Vmess = class extends XrayCommonClass {
constructor(id=RandomUtil.randomUUID(), alterId=0, email=RandomUtil.randomText(), totalGB=0, expiryTime='') {
constructor(id=RandomUtil.randomUUID(), alterId=0, email=RandomUtil.randomText(), totalGB=0, expiryTime=0, enable=true, tgId='', subId='') {
super();
this.id = id;
this.alterId = alterId;
this.email = email;
this.totalGB = totalGB;
this.expiryTime = expiryTime;
this.enable = enable;
this.tgId = tgId;
this.subId = subId;
}
static fromJson(json={}) {
@@ -1447,13 +1451,18 @@ Inbound.VmessSettings.Vmess = class extends XrayCommonClass {
json.email,
json.totalGB,
json.expiryTime,
json.enable,
json.tgId,
json.subId,
);
}
get _expiryTime() {
if (this.expiryTime === 0 || this.expiryTime === "") {
return null;
}
if (this.expiryTime < 0){
return this.expiryTime / -86400000;
}
return moment(this.expiryTime);
}
@@ -1506,22 +1515,23 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
toJson() {
return {
clients: Inbound.VLESSSettings.toJsonArray(this.vlesses),
decryption: this.decryption,
decryption: 'none',
fallbacks: Inbound.VLESSSettings.toJsonArray(this.fallbacks),
};
}
};
Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
constructor(id=RandomUtil.randomUUID(), flow='', email=RandomUtil.randomText(), totalGB=0, expiryTime='') {
constructor(id=RandomUtil.randomUUID(), flow='', email=RandomUtil.randomText(), totalGB=0, expiryTime=0, enable=true, tgId='', subId='') {
super();
this.id = id;
this.flow = flow;
this.email = email;
this.totalGB = totalGB;
this.expiryTime = expiryTime;
this.enable = enable;
this.tgId = tgId;
this.subId = subId;
}
static fromJson(json={}) {
@@ -1531,6 +1541,9 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
json.email,
json.totalGB,
json.expiryTime,
json.enable,
json.tgId,
json.subId,
);
}
@@ -1538,6 +1551,9 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
if (this.expiryTime === 0 || this.expiryTime === "") {
return null;
}
if (this.expiryTime < 0){
return this.expiryTime / -86400000;
}
return moment(this.expiryTime);
}
@@ -1627,13 +1643,16 @@ Inbound.TrojanSettings = class extends Inbound.Settings {
}
};
Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
constructor(password=RandomUtil.randomSeq(10), flow='', email=RandomUtil.randomText(), totalGB=0, expiryTime='') {
constructor(password=RandomUtil.randomSeq(10), flow='', email=RandomUtil.randomText(), totalGB=0, expiryTime=0, enable=true, tgId='', subId='') {
super();
this.password = password;
this.flow = flow;
this.email = email;
this.totalGB = totalGB;
this.expiryTime = expiryTime;
this.enable = enable;
this.tgId = tgId;
this.subId = subId;
}
toJson() {
@@ -1643,6 +1662,9 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
email: this.email,
totalGB: this.totalGB,
expiryTime: this.expiryTime,
enable: this.enable,
tgId: this.tgId,
subId: this.subId,
};
}
@@ -1653,7 +1675,9 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
json.email,
json.totalGB,
json.expiryTime,
json.enable,
json.tgId,
json.subId,
);
}
@@ -1661,6 +1685,9 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
if (this.expiryTime === 0 || this.expiryTime === "") {
return null;
}
if (this.expiryTime < 0){
return this.expiryTime / -86400000;
}
return moment(this.expiryTime);
}

View File

@@ -1,14 +1,10 @@
package controller
import (
"github.com/gin-gonic/gin"
)
import "github.com/gin-gonic/gin"
type APIController struct {
BaseController
inboundController *InboundController
settingController *SettingController
}
func NewAPIController(g *gin.RouterGroup) *APIController {
@@ -26,6 +22,12 @@ func (a *APIController) initRouter(g *gin.RouterGroup) {
g.POST("/add", a.addInbound)
g.POST("/del/:id", a.delInbound)
g.POST("/update/:id", a.updateInbound)
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)
}
@@ -45,3 +47,21 @@ func (a *APIController) delInbound(c *gin.Context) {
func (a *APIController) updateInbound(c *gin.Context) {
a.inboundController.updateInbound(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)
}

View File

@@ -35,6 +35,8 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
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)
}
@@ -208,3 +210,27 @@ func (a *InboundController) resetClientTraffic(c *gin.Context) {
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)
}

View File

@@ -38,7 +38,9 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
g.POST("/stopXrayService", a.stopXrayService)
g.POST("/restartXrayService", a.restartXrayService)
g.POST("/installXray/:version", a.installXray)
g.POST("/logs", a.getLogs)
g.POST("/logs/:count", a.getLogs)
g.POST("/getConfigJson", a.getConfigJson)
g.GET("/getDb", a.getDb)
}
func (a *ServerController) refreshStatus() {
@@ -109,10 +111,34 @@ func (a *ServerController) restartXrayService(c *gin.Context) {
}
func (a *ServerController) getLogs(c *gin.Context) {
logs, err := a.serverService.GetLogs()
count := c.Param("count")
logs, err := a.serverService.GetLogs(count)
if err != nil {
jsonMsg(c, I18n(c, "getLogs"), err)
return
}
jsonObj(c, logs, nil)
}
func (a *ServerController) getConfigJson(c *gin.Context) {
configJson, err := a.serverService.GetConfigJson()
if err != nil {
jsonMsg(c, I18n(c, "getLogs"), err)
return
}
jsonObj(c, configJson, nil)
}
func (a *ServerController) getDb(c *gin.Context) {
db, err := a.serverService.GetDb()
if err != nil {
jsonMsg(c, I18n(c, "getLogs"), err)
return
}
// Set the headers for the response
c.Header("Content-Type", "application/octet-stream")
c.Header("Content-Disposition", "attachment; filename=xui.db")
// Write the file contents to the response
c.Writer.Write(db)
}

View File

@@ -33,6 +33,7 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) {
g = g.Group("/setting")
g.POST("/all", a.getAllSetting)
g.POST("/defaultSettings", a.getDefaultSettings)
g.POST("/update", a.updateSetting)
g.POST("/updateUser", a.updateUser)
g.POST("/restartPanel", a.restartPanel)
@@ -47,6 +48,36 @@ func (a *SettingController) getAllSetting(c *gin.Context) {
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) {
allSetting := &entity.AllSetting{}
err := c.ShouldBind(allSetting)

42
web/controller/sub.go Normal file
View 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)))
}
}

View File

@@ -32,13 +32,13 @@ type AllSetting struct {
WebCertFile string `json:"webCertFile" form:"webCertFile"`
WebKeyFile string `json:"webKeyFile" form:"webKeyFile"`
WebBasePath string `json:"webBasePath" form:"webBasePath"`
ExpireDiff int `json:"expireDiff" form:"expireDiff"`
TrafficDiff int `json:"trafficDiff" form:"trafficDiff"`
TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"`
TgBotToken string `json:"tgBotToken" form:"tgBotToken"`
TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"`
TgRunTime string `json:"tgRunTime" form:"tgRunTime"`
TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"`
TgExpireDiff int `json:"tgExpireDiff" form:"tgExpireDiff"`
TgTrafficDiff int `json:"tgTrafficDiff" form:"tgTrafficDiff"`
TgCpu int `json:"tgCpu" form:"tgCpu"`
XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"`
TimeLocation string `json:"timeLocation" form:"timeLocation"`

View File

@@ -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/element-ui@2.15.0/theme-chalk/display.css">
<link rel="stylesheet" href="{{ .base_path }}assets/css/custom.css?{{ .cur_ver }}">
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
<style>
[v-cloak] {
display: none;

View File

@@ -10,8 +10,7 @@
<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">Random+Prefix+Num@Telegram Username</a-select-option>
<a-select-option :value="5">Prefix+Num+Postfix [ BE CAREFUL! ]</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">
@@ -27,15 +26,19 @@
<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" v-if="clientsBulkModal.emailMethod == 4">tg_uname</span>
<span slot="label" v-else>{{ i18n "pages.client.postfix" }}</span>
<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)
@@ -48,7 +51,13 @@
</span>
<a-input-number v-model="clientsBulkModal.totalGB" :min="0"></a-input-number>
</a-form-item>
<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>
@@ -83,6 +92,9 @@
lastNum: 1,
emailPrefix: "",
emailPostfix: "",
subId: "",
tgId: "",
delayedStart: false,
ok() {
method=clientsBulkModal.emailMethod;
if(method>1){
@@ -94,11 +106,13 @@
}
prefix = (method>0 && clientsBulkModal.emailPrefix.length>0) ? clientsBulkModal.emailPrefix : "";
useNum=(method>1);
postfix = (method>2 && clientsBulkModal.emailPostfix.length>0) ? (method == 4 ? "@" : "") + clientsBulkModal.emailPostfix : "";
postfix = (method>2 && clientsBulkModal.emailPostfix.length>0) ? clientsBulkModal.emailPostfix : "";
for (let i = start; i < end; i++) {
newClient = clientsBulkModal.newClient(clientsBulkModal.dbInbound.protocol);
if(method==5) newClient.email = "";
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);
@@ -112,16 +126,18 @@
this.confirm = confirm;
this.quantity = 1;
this.totalGB = 0;
this.expiryTime = '';
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){
@@ -156,6 +172,12 @@
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>

View File

@@ -18,6 +18,7 @@
clientStats: [],
index: null,
isExpired: false,
delayedStart: false,
ok() {
ObjectUtil.execute(clientModal.confirm, clientModal.inbound, clientModal.dbInbound, clientModal.index);
},
@@ -31,8 +32,13 @@
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;
@@ -81,7 +87,7 @@
},
get isTrafficExhausted() {
if(!clientStats) return false
if(clientStats.total == 0) return false
if(clientStats.total <= 0) return false
if(clientStats.up + clientStats.down < clientStats.total) return false
return true
},
@@ -90,10 +96,16 @@
},
get statsColor() {
if(!clientStats) return 'blue'
if(clientStats.total === 0) 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) {

View File

@@ -5,16 +5,19 @@
</template>
<a-form-item>
<span slot="label">
Email
<span>{{ i18n "pages.inbounds.Email" }}</span>
<a-tooltip>
<template slot="title">
The email must be completely unique
<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"></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"></a-input>
</a-form-item>
@@ -24,6 +27,12 @@
<a-form-item label='{{ i18n "additional" }} ID' v-if="inbound.protocol === Protocols.VMESS">
<a-input type="number" v-model.number="client.alterId"></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 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>
@@ -56,7 +65,13 @@
</a-tag>
</template>
</a-form-item>
<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>

View File

@@ -5,10 +5,10 @@
<a-form layout="inline">
<a-form-item>
<span slot="label">
Email
<span>{{ i18n "pages.inbounds.Email" }}</span>
<a-tooltip>
<template slot="title">
The email must be completely unique
<span>{{ i18n "pages.inbounds.EmailDesc" }}</span>
</template>
<a-icon @click="getNewEmail(client)" type="sync"> </a-icon>
</a-tooltip>

View File

@@ -5,10 +5,10 @@
<a-form layout="inline">
<a-form-item>
<span slot="label">
Email
<span>{{ i18n "pages.inbounds.Email" }}</span>
<a-tooltip>
<template slot="title">
The email must be completely unique
<span>{{ i18n "pages.inbounds.EmailDesc" }}</span>
</template>
<a-icon type="sync" @click="getNewEmail(client)"></a-icon>
</a-tooltip>

View File

@@ -5,10 +5,10 @@
<a-form layout="inline">
<a-form-item>
<span slot="label">
Email
<span>{{ i18n "pages.inbounds.Email" }}</span>
<a-tooltip>
<template slot="title">
The email must be completely unique
<span>{{ i18n "pages.inbounds.EmailDesc" }}</span>
</template>
<a-icon type="sync" @click="getNewEmail(client)"></a-icon>
</a-tooltip>

View File

@@ -61,6 +61,7 @@
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
<a-input v-model.trim="inbound.stream.tls.certs[0].keyFile" style="width:300px;"></a-input>
</a-form-item>
<a-button @click="setDefaultCertData">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
</template>
<template v-else>
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>

View File

@@ -21,9 +21,12 @@
<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 slot="client" slot-scope="text, client">
[[ 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 slot="traffic" slot-scope="text, client">
<a-tag color="blue">[[ sizeFormat(getUpStats(record, client.email)) ]] / [[ sizeFormat(getDownStats(record, client.email)) ]]</a-tag>
@@ -34,11 +37,12 @@
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
</template>
<template slot="expiryTime" slot-scope="text, client, index">
<template v-if="client._expiryTime > 0">
<template v-if="client.expiryTime > 0">
<a-tag :color="isExpiry(record, index)? 'red' : 'blue'">
[[ DateUtil.formatMillis(client._expiryTime) ]]
</a-tag>
</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>
</template>
{{end}}

View File

@@ -63,9 +63,21 @@
<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>
<td>{{ i18n "status" }}</td>
<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>
</table>
<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>
<td>
<a-tag v-if="infoModal.clientStats" :color="statsColor(infoModal.clientStats)">
@@ -84,12 +96,19 @@
[[ DateUtil.formatMillis(infoModal.clientSettings.expiryTime) ]]
</a-tag>
</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>
</td>
<td>
<a-tag v-if="isEnable" color="blue">{{ i18n "enabled" }}</a-tag>
<a-tag v-else color="red">{{ i18n "disabled" }}</a-tag>
</td>
</tr>
</table>
<table v-if="infoModal.clientSettings.subId + infoModal.clientSettings.tgId" style="margin-bottom: 10px;">
<tr v-if="infoModal.clientSettings.subId">
<td>Subscription link</td>
<td><a :href="[[ subBase + infoModal.clientSettings.subId ]]" target="_blank">[[ subBase + infoModal.clientSettings.subId ]]</a></td>
</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>
@@ -160,13 +179,12 @@
</div>
</a-modal>
<script>
const infoModal = {
visible: false,
inbound: new Inbound(),
dbInbound: new DBInbound(),
settings: null,
clientSettings: new Inbound.Settings(),
clientSettings: null,
clientStats: [],
upStats: 0,
downStats: 0,
@@ -209,12 +227,24 @@
get inbound() {
return this.infoModal.inbound;
},
get isEnable() {
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:"") + basePath + "sub/";
},
get tgBase() {
return "https://t.me/"
},
},
methods: {
copyTextToClipboard(elmentId,content) {

View File

@@ -96,6 +96,10 @@
clientStats = this.dbInbound.clientStats ? this.dbInbound.clientStats.find(stats => stats.email === email) : null
return clientStats ? clientStats['enable'] : true
},
setDefaultCertData(){
inModal.inbound.stream.tls.certs[0].certFile = app.defaultCert;
inModal.inbound.stream.tls.certs[0].keyFile = app.defaultKey;
},
getNewEmail(client) {
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
var string = '';

View File

@@ -41,8 +41,24 @@
<a-col :xs="24" :sm="24" :lg="12">
{{ i18n "clients" }}:
<a-tag color="green">[[ total.clients ]]</a-tag>
<a-tag color="blue">{{ i18n "enabled" }} [[ total.active ]]</a-tag>
<a-tag color="red">{{ i18n "disabled" }} [[ total.deactive ]]</a-tag>
<a-popover title="{{ i18n "disabled" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
<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-row>
</a-card>
@@ -52,6 +68,7 @@
<div slot="title">
<a-button type="primary" icon="plus" @click="openAddInbound">{{ i18n "pages.inbounds.addInbound" }}</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>
<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"
@@ -63,7 +80,7 @@
<template slot="action" slot-scope="text, dbInbound">
<a-dropdown :trigger="['click']">
<a-icon @click="e => e.preventDefault()" type="menu"></a-icon>
<a-menu slot="overlay" @click="a => clickAction(a, dbInbound)" :theme="siderDrawer.theme" style="border: 1px solid rgba(255, 255, 255, 0.65);">
<a-menu slot="overlay" @click="a => clickAction(a, dbInbound)" :theme="siderDrawer.theme">
<a-menu-item v-if="dbInbound.isSS" key="qrcode">
<a-icon type="qrcode"></a-icon>
{{ i18n "qrCode" }}
@@ -74,13 +91,17 @@
</a-menu-item>
<template v-if="dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess">
<a-menu-item key="addClient">
<a-icon type="user"></a-icon>
<a-icon type="user-add"></a-icon>
{{ i18n "pages.client.add"}}
</a-menu-item>
<a-menu-item key="addBulkClient">
<a-icon type="team"></a-icon>
<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-icon type="export"></a-icon>
{{ i18n "pages.inbounds.export"}}
@@ -95,6 +116,9 @@
<a-menu-item key="resetTraffic">
<a-icon type="retweet"></a-icon> {{ i18n "pages.inbounds.resetTraffic" }}
</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">
<span style="color: #FF4D4F">
<a-icon type="delete"></a-icon> {{ i18n "delete"}}
@@ -104,7 +128,35 @@
</a-dropdown>
</template>
<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>
</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 slot="traffic" slot-scope="text, dbInbound">
<a-tag color="blue">[[ sizeFormat(dbInbound.up) ]] / [[ sizeFormat(dbInbound.down) ]]</a-tag>
@@ -114,14 +166,6 @@
</template>
<a-tag v-else color="green">{{ i18n "unlimited" }}</a-tag>
</template>
<template slot="stream" slot-scope="text, dbInbound">
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
<a-tag color="green">[[ dbInbound.toInbound().stream.network ]]</a-tag>
<a-tag v-if="dbInbound.toInbound().stream.isTls" color="blue">tls</a-tag>
<a-tag v-if="dbInbound.toInbound().stream.isXTls" color="blue">xtls</a-tag>
</template>
<template v-else>{{ i18n "none" }}</template>
</template>
<template slot="enable" slot-scope="text, dbInbound">
<a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound.id)"></a-switch>
</template>
@@ -184,28 +228,28 @@
}, {
title: '{{ i18n "pages.inbounds.remark" }}',
align: 'center',
width: 60,
width: 50,
dataIndex: "remark",
}, {
title: '{{ i18n "pages.inbounds.protocol" }}',
align: 'center',
width: 40,
scopedSlots: { customRender: 'protocol' },
}, {
title: '{{ i18n "pages.inbounds.port" }}',
align: 'center',
dataIndex: "port",
width: 40,
}, {
title: '{{ i18n "pages.inbounds.protocol" }}',
align: 'left',
width: 70,
scopedSlots: { customRender: 'protocol' },
}, {
title: '{{ i18n "clients" }}',
align: 'left',
width: 50,
scopedSlots: { customRender: 'clients' },
}, {
title: '{{ i18n "pages.inbounds.traffic" }}↑|↓',
align: 'center',
width: 120,
scopedSlots: { customRender: 'traffic' },
}, {
title: '{{ i18n "pages.inbounds.transportConfig" }}',
align: 'center',
width: 60,
scopedSlots: { customRender: 'stream' },
}, {
title: '{{ i18n "pages.inbounds.expireDate" }}',
align: 'center',
@@ -214,7 +258,8 @@
}];
const innerColumns = [
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 80, scopedSlots: { customRender: 'actions' } },
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } },
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 30, scopedSlots: { customRender: 'enable' } },
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 70, scopedSlots: { customRender: 'traffic' } },
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
@@ -222,7 +267,8 @@
];
const innerTrojanColumns = [
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 80, scopedSlots: { customRender: 'actions' } },
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } },
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 30, scopedSlots: { customRender: 'enable' } },
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 70, scopedSlots: { customRender: 'traffic' } },
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
@@ -239,6 +285,11 @@
dbInbounds: [],
searchKey: '',
searchedInbounds: [],
expireDiff: 0,
trafficDiff: 0,
defaultCert: '',
defaultKey: '',
clientCount: {},
},
methods: {
loading(spinning=true) {
@@ -254,17 +305,66 @@
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) {
this.inbounds.splice(0);
this.dbInbounds.splice(0);
this.searchedInbounds.splice(0);
for (const inbound of dbInbounds) {
const dbInbound = new DBInbound(inbound);
this.inbounds.push(dbInbound.toInbound());
to_inbound = dbInbound.toInbound()
this.inbounds.push(to_inbound);
this.dbInbounds.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) {
if (ObjectUtil.isEmpty(key)) {
this.searchedInbounds = this.dbInbounds.slice();
@@ -311,6 +411,12 @@
case "resetTraffic":
this.resetTraffic(dbInbound.id);
break;
case "resetClients":
this.resetAllClientTraffics(dbInbound.id);
break;
case "clone":
this.openCloneInbound(dbInbound);
break;
case "delete":
this.delInbound(dbInbound.id);
break;
@@ -345,6 +451,39 @@
},
isEdit: true
});
},
openCloneInbound(dbInbound) {
this.$confirm({
title: '{{ i18n "pages.inbounds.cloneInbound"}} ' + dbInbound.remark,
content: '{{ i18n "pages.inbounds.cloneInboundContent"}}',
okText: '{{ i18n "pages.inbounds.revise"}}',
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);
},
async addInbound(inbound, dbInbound) {
const data = {
@@ -437,7 +576,7 @@
id: dbInbound.id,
settings: inbound.settings.toString(),
};
await this.submit('/xui/inbound/addClient', data);
await this.submit('/xui/inbound/addClient/', data);
},
async updateClient(inbound, dbInbound, index) {
const data = {
@@ -511,6 +650,16 @@
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
this.submit(`/xui/inbound/update/${dbInboundId}`, dbInbound);
},
async switchEnableClient(dbInboundId, client) {
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) {
@@ -536,6 +685,26 @@
onOk: () => this.submit('/xui/inbound/' + dbInboundId + '/resetClientTraffic/'+ client.email),
})
},
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'),
});
},
resetAllClientTraffics(dbInboundId) {
this.$confirm({
title: '{{ i18n "pages.inbounds.resetAllClientTrafficTitle"}}',
content: '{{ i18n "pages.inbounds.resetAllClientTrafficContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '',
okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/resetAllClientTraffics/' + dbInboundId),
})
},
isExpiry(dbInbound, index) {
return dbInbound.toInbound().isExpiry(index)
},
@@ -579,37 +748,30 @@
}, 500)
},
mounted() {
this.getDefaultSettings();
this.getDBInbounds();
},
computed: {
total() {
let down = 0, up = 0;
let clients = 0, active = 0, deactive = 0;
let clients = 0, deactive = [], depleted = [], expiring = [];
this.dbInbounds.forEach(dbInbound => {
down += dbInbound.down;
up += dbInbound.up;
inbound = dbInbound.toInbound();
clients = this.getClients(dbInbound.protocol, inbound.settings);
if(clients){
if(dbInbound.enable){
isClientEnable = false;
clients.forEach(client => {
isClientEnable = client.email == "" ? true: this.isClientEnabled(dbInbound,client.email);
isClientEnable ? active++ : deactive++;
});
} else {
deactive += clients.length;
}
} else {
dbInbound.enable ? active++ : deactive++;
if (this.clientCount[dbInbound.id]) {
clients += this.clientCount[dbInbound.id].clients;
deactive = deactive.concat(this.clientCount[dbInbound.id].deactive);
depleted = depleted.concat(this.clientCount[dbInbound.id].depleted);
expiring = expiring.concat(this.clientCount[dbInbound.id].expiring);
}
});
return {
down: down,
up: up,
clients: active + deactive,
active: active,
clients: clients,
deactive: deactive,
depleted: depleted,
expiring: expiring,
};
}
},

View File

@@ -76,24 +76,12 @@
<a-row>
<a-col :sm="24" :md="12">
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
{{ i18n "pages.index.xrayStatus" }}:
<a-tag :color="status.xray.color">[[ status.xray.state ]]</a-tag>
<a-tooltip v-if="status.xray.state === State.Error">
<template slot="title">
<p v-for="line in status.xray.errorMsg.split('\n')">[[ line ]]</p>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
<a-tag color="green" style="cursor: pointer;" @click="openSelectV2rayVersion">[[ status.xray.version ]]</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="stopXrayService">{{ i18n "pages.index.stopXray" }}</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="restartXrayService">{{ i18n "pages.index.restartXray" }}</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="openSelectV2rayVersion">{{ i18n "pages.index.xraySwitch" }}</a-tag>
x-ui: <a-tag color="green">{{ .cur_ver }}</a-tag>
xray: <a-tag color="green" style="cursor: pointer;" @click="openSelectV2rayVersion">[[ status.xray.version ]]</a-tag>
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
x-ui: <a-tag color="green">{{ .cur_ver }}</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="openLogs">Logs</a-tag>
{{ i18n "pages.index.operationHours" }}:
<a-tag color="green">[[ formatSecond(status.uptime) ]]</a-tag>
<a-tooltip>
@@ -104,6 +92,29 @@
</a-tooltip>
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
{{ i18n "pages.index.xrayStatus" }}:
<a-tag :color="status.xray.color">[[ status.xray.state ]]</a-tag>
<a-tooltip v-if="status.xray.state === State.Error">
<template slot="title">
<p v-for="line in status.xray.errorMsg.split('\n')">[[ line ]]</p>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
<a-tag color="blue" style="cursor: pointer;" @click="stopXrayService">{{ i18n "pages.index.stopXray" }}</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="restartXrayService">{{ i18n "pages.index.restartXray" }}</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="openSelectV2rayVersion">{{ i18n "pages.index.xraySwitch" }}</a-tag>
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
{{ i18n "menu.link" }}:
<a-tag color="blue" style="cursor: pointer;" @click="openLogs(20)">Logs</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="openConfig">Config</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="getBackup">Backup</a-tag>
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
{{ i18n "pages.index.systemLoad" }}: [[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
@@ -194,14 +205,34 @@
:class="siderDrawer.isDarkTheme ? darkClass : ''"
width="800px"
footer="">
<table style="margin: 0px; width: 100%; background-color: black; color: hsla(0,0%,100%,.65);">
<tr v-for="log , index in logModal.logs">
<td style="vertical-align: top;">[[ index ]]</td><td>[[ log ]]</td>
</tr>
</table>
<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>
{{template "js" .}}
{{template "textModal"}}
<script>
const State = {
@@ -296,9 +327,11 @@
const logModal = {
visible: false,
logs: '',
show(logs) {
rows: 20,
show(logs, rows) {
this.visible = true;
this.logs = logs;
this.rows = rows;
this.logs = logs.join("\n");
},
hide() {
this.visible = false;
@@ -372,14 +405,26 @@
return;
}
},
async openLogs(){
async openLogs(rows){
this.loading(true);
const msg = await HttpUtil.post('server/logs');
const msg = await HttpUtil.post('server/logs/'+rows);
this.loading(false);
if (!msg.success) {
return;
}
logModal.show(msg.obj);
logModal.show(msg.obj,rows);
},
async openConfig(){
this.loading(true);
const msg = await HttpUtil.post('server/getConfigJson');
this.loading(false);
if (!msg.success) {
return;
}
txtModal.show('config.json',JSON.stringify(msg.obj, null, 2),'config.json');
},
getBackup(){
window.location = basePath + 'server/getDb';
}
},
async mounted() {

View File

@@ -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.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="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-row style="padding: 20px">
<a-col :lg="24" :xl="12">
@@ -120,8 +122,6 @@
<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="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.tgNotifyExpireTimeDiff" }}' desc='{{ i18n "pages.setting.tgNotifyExpireTimeDiffDesc" }}' v-model="allSetting.tgExpireDiff" :min="0"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.setting.tgNotifyTrafficDiff" }}' desc='{{ i18n "pages.setting.tgNotifyTrafficDiffDesc" }}' v-model="allSetting.tgTrafficDiff" :min="0"></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-tab-pane>

View File

@@ -366,11 +366,16 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
if len(traffics) == 0 {
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{})
tx := db.Begin()
defer func() {
if err != nil {
tx.Rollback()
@@ -378,7 +383,20 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
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()
defer func() {
if err != nil {
txInbound.Rollback()
@@ -387,50 +405,68 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
}
}()
for _, traffic := range traffics {
for traffic_index, traffic := range traffics {
inbound := &model.Inbound{}
client := &xray.ClientTraffic{}
err := tx.Where("email = ?", traffic.Email).First(client).Error
client_traffic := &xray.ClientTraffic{}
err := db.Model(xray.ClientTraffic{}).Where("email = ?", traffic.Email).First(client_traffic).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
logger.Warning(err, traffic.Email)
}
continue
}
client_traffic.Up += traffic.Up
client_traffic.Down += traffic.Down
err = txInbound.Where("id=?", client.InboundId).First(inbound).Error
err = txInbound.Where("id=?", client_traffic.InboundId).First(inbound).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
logger.Warning(err, traffic.Email)
}
continue
}
// get settings clients
settings := map[string][]model.Client{}
json.Unmarshal([]byte(inbound.Settings), &settings)
clients := settings["clients"]
for _, client := range clients {
if traffic.Email == client.Email {
traffic.ExpiryTime = client.ExpiryTime
traffic.Total = client.TotalGB
// get clients
clients, err := s.getClients(inbound)
needUpdate := false
if err == nil {
for client_index, client := range clients {
if traffic.Email == client.Email {
if client.ExpiryTime < 0 {
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 = ? and email = ?", inbound.Id, traffic.Email).
UpdateColumns(map[string]interface{}{
"enable": true,
"expiry_time": traffic.ExpiryTime,
"total": traffic.Total,
"up": gorm.Expr("up + ?", traffic.Up),
"down": gorm.Expr("down + ?", traffic.Down)}).RowsAffected == 0 {
err = tx.Create(traffic).Error
if needUpdate {
settings := map[string]interface{}{}
json.Unmarshal([]byte(inbound.Settings), &settings)
// Convert clients to []interface to update clients in settings
var clientsInterface []interface{}
for _, c := range clients {
clientsInterface = append(clientsInterface, interface{}(c))
}
settings["clients"] = clientsInterface
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return nil, err
}
err = txInbound.Where("id=?", inbound.Id).Update("settings", string(modifiedSettings)).Error
if err != nil {
return nil, err
}
}
if err != nil {
logger.Warning("AddClientTraffic update data ", err)
continue
}
traffics[traffic_index] = client_traffic
}
return
return traffics, nil
}
func (s *InboundService) DisableInvalidInbounds() (int64, error) {
@@ -505,11 +541,58 @@ func (s *InboundService) ResetClientTraffic(id int, clientEmail string) error {
}
return nil
}
func (s *InboundService) GetClientTrafficTgBot(tguname string) (traffic []*xray.ClientTraffic, err error) {
db := database.GetDB()
var traffics []*xray.ClientTraffic
err = db.Model(xray.ClientTraffic{}).Where("email like ?", "%@"+tguname).Find(&traffics).Error
func (s *InboundService) ResetAllClientTraffics(id int) error {
db := database.GetDB()
result := db.Model(xray.ClientTraffic{}).
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})
err := result.Error
if err != nil {
return err
}
return nil
}
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)

View File

@@ -13,6 +13,7 @@ import (
"runtime"
"strings"
"time"
"x-ui/config"
"x-ui/logger"
"x-ui/util/sys"
"x-ui/xray"
@@ -327,11 +328,11 @@ func (s *ServerService) UpdateXray(version string) error {
}
func (s *ServerService) GetLogs() ([]string, error) {
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", "100"}
cmdArgs = []string{"journalctl", "-u", "x-ui", "--no-pager", "-n", count}
} else {
return []string{"Unsupported operating system"}, nil
}
@@ -349,3 +350,43 @@ func (s *ServerService) GetLogs() ([]string, error) {
return lines, nil
}
func (s *ServerService) GetConfigJson() (interface{}, error) {
// Open the file for reading
file, err := os.Open(xray.GetConfigPath())
if err != nil {
return nil, err
}
defer file.Close()
// Read the file contents
fileContents, err := io.ReadAll(file)
if err != nil {
return nil, err
}
var jsonData interface{}
err = json.Unmarshal(fileContents, &jsonData)
if err != nil {
return nil, err
}
return jsonData, nil
}
func (s *ServerService) GetDb() ([]byte, error) {
// Open the file for reading
file, err := os.Open(config.GetDBPath())
if err != nil {
return nil, err
}
defer file.Close()
// Read the file contents
fileContents, err := io.ReadAll(file)
if err != nil {
return nil, err
}
return fileContents, nil
}

View File

@@ -28,14 +28,14 @@ var defaultValueMap = map[string]string{
"webKeyFile": "",
"secret": random.Seq(32),
"webBasePath": "/",
"expireDiff": "0",
"trafficDiff": "0",
"timeLocation": "Asia/Tehran",
"tgBotEnable": "false",
"tgBotToken": "",
"tgBotChatId": "",
"tgRunTime": "@daily",
"tgBotBackup": "false",
"tgExpireDiff": "0",
"tgTrafficDiff": "0",
"tgCpu": "0",
}
@@ -238,22 +238,6 @@ func (s *SettingService) SetTgBotBackup(value bool) error {
return s.setBool("tgBotBackup", value)
}
func (s *SettingService) GetTgExpireDiff() (int, error) {
return s.getInt("tgExpireDiff")
}
func (s *SettingService) SetTgExpireDiff(value int) error {
return s.setInt("tgExpireDiff", value)
}
func (s *SettingService) GetTgTrafficDiff() (int, error) {
return s.getInt("tgTrafficDiff")
}
func (s *SettingService) SetTgTrafficDiff(value int) error {
return s.setInt("tgTrafficDiff", value)
}
func (s *SettingService) GetTgCpu() (int, error) {
return s.getInt("tgCpu")
}
@@ -278,6 +262,22 @@ func (s *SettingService) GetKeyFile() (string, error) {
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) {
secret, err := s.getString("secret")
if secret == defaultValueMap["secret"] {

505
web/service/sub.go Normal file
View 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 ""
}

View File

@@ -160,14 +160,14 @@ func (t *Tgbot) asnwerCallback(callbackQuery *tgbotapi.CallbackQuery, isAdmin bo
t.SendMsgToTgbot(callbackQuery.From.ID, t.getServerUsage())
case "inbounds":
t.SendMsgToTgbot(callbackQuery.From.ID, t.getInboundUsages())
case "exhausted_soon":
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 and vless and Password for Trojan.")
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>")
}
@@ -190,7 +190,7 @@ func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) {
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("Get Inbounds", "inbounds"),
tgbotapi.NewInlineKeyboardButtonData("Exhausted soon", "exhausted_soon"),
tgbotapi.NewInlineKeyboardButtonData("Deplete soon", "deplete_soon"),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("Commands", "commands"),
@@ -363,6 +363,11 @@ func (t *Tgbot) getInboundUsages() string {
}
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)
@@ -373,11 +378,14 @@ func (t *Tgbot) getClientUsage(chatId int64, tgUserName string) {
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")
}
@@ -412,6 +420,8 @@ func (t *Tgbot) searchClient(chatId int64, email string) {
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")
}
@@ -450,6 +460,8 @@ func (t *Tgbot) searchInbound(chatId int64, remark string) {
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")
}
@@ -483,6 +495,8 @@ func (t *Tgbot) searchForClient(chatId int64, query string) {
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")
}
@@ -507,13 +521,13 @@ func (t *Tgbot) getExhausted() string {
var disabledInbounds []model.Inbound
var disabledClients []xray.ClientTraffic
output := ""
TrafficThreshold, err := t.settingService.GetTgTrafficDiff()
TrafficThreshold, err := t.settingService.GetTrafficDiff()
if err == nil && TrafficThreshold > 0 {
trDiff = int64(TrafficThreshold) * 1073741824
}
ExpireThreshold, err := t.settingService.GetTgExpireDiff()
ExpireThreshold, err := t.settingService.GetExpireDiff()
if err == nil && ExpireThreshold > 0 {
exDiff = int64(ExpireThreshold) * 84600000
exDiff = int64(ExpireThreshold) * 86400000
}
inbounds, err := t.inboundService.GetAllInbounds()
if err != nil {
@@ -522,14 +536,14 @@ func (t *Tgbot) getExhausted() string {
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)) {
(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)) {
(client.Total > 0 && (client.Total-(client.Up+client.Down) < trDiff)) {
exhaustedClients = append(exhaustedClients, client)
}
} else {
@@ -541,7 +555,7 @@ func (t *Tgbot) getExhausted() string {
disabledInbounds = append(disabledInbounds, *inbound)
}
}
output += fmt.Sprintf("Exhausted Inbounds count:\r\n🛑 Disabled: %d\r\n🔜 Exhaust soon: %d\r\n \r\n", len(disabledInbounds), len(exhaustedInbounds))
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 {
@@ -553,13 +567,15 @@ func (t *Tgbot) getExhausted() string {
}
}
}
output += fmt.Sprintf("Exhausted Clients count:\r\n🛑 Disabled: %d\r\n🔜 Exhaust soon: %d\r\n \r\n", len(disabledClients), len(exhaustedClients))
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")
}

View File

@@ -84,15 +84,16 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
clients, ok := settings["clients"].([]interface{})
if ok {
// check users active or not
clientStats := inbound.ClientStats
for _, clientTraffic := range clientStats {
indexDecrease := 0
for index, client := range clients {
c := client.(map[string]interface{})
if c["email"] == clientTraffic.Email {
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")
}
@@ -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)
if err != nil {
return nil, err

View File

@@ -33,8 +33,11 @@
"host" = "Host"
"path" = "Path"
"camouflage" = "Camouflage"
"status" = "Status"
"enabled" = "Enabled"
"disabled" = "Disabled"
"depleted" = "Depleted"
"depletingSoon" = "Depleting soon"
"domainName" = "Domain name"
"additional" = "Alter"
"monitor" = "Listen IP"
@@ -128,7 +131,19 @@
"keyContent" = "Key content"
"clickOnQRcode" = "Click on QR Code to Copy"
"client" = "Client"
"export" = "Export links"
"export" = "Export Links"
"Clone" = "Clone"
"cloneInbound" = "Create"
"cloneInboundContent" = "All items of this inbound except Port, Listening IP, Clients will be applied to the clone"
"resetAllTraffic" = "Reset All Inbounds Traffic"
"resetAllTrafficTitle" = "Reset all inbounds traffic"
"resetAllTrafficContent" = "Are you sure to reset all inbounds traffic ?"
"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"
"setDefaultCert" = "Set cert from panel"
[pages.client]
"add" = "Add client"
@@ -142,6 +157,9 @@
"last" = "Last"
"prefix" = "Prefix"
"postfix" = "postfix"
"delayedStart" = "Start after first use"
"expireDays" = "Expire days"
"days" = "day(s)"
[pages.inbounds.toasts]
"obtain" = "Obtain"
@@ -179,7 +197,7 @@
"panelPortDesc" = "Restart the panel to take effect"
"publicKeyPath" = "Panel certificate public key file path"
"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"
"panelUrlPath" = "panel url root path"
"panelUrlPathDesc" = "Must start with '/' and end with '/', restart the panel to take effect"
@@ -211,10 +229,10 @@
"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"
"tgNotifyExpireTimeDiff" = "Remained time threshold"
"tgNotifyExpireTimeDiffDesc" = "This telegram bot will send you a notification before expiration (unit:day)"
"tgNotifyTrafficDiff" = "Remained traffic threshold"
"tgNotifyTrafficDiffDesc" = "This telegram bot will send you a notification before finishing traffic (unit:GB)"
"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"

View File

@@ -33,12 +33,15 @@
"host" = "آدرس"
"path" = "مسیر"
"camouflage" = "استتار"
"status" = "وضعیت"
"enabled" = "فعال"
"disabled" = "غیرفعال"
"depleted" = "منقضی"
"depletingSoon" = "در حال انقضا"
"domainName" = "آدرس دامنه"
"additional" = "آی دی جایگزین"
"monitor" = "آی پی اتصال"
"certificate" = "سرتیفیکیت"
"certificate" = "گواهی دیجیتال"
"fail" = "خطا"
"success" = " موفق"
"getVersion" = "دریافت ورژن"
@@ -122,13 +125,25 @@
"noRecommendKeepDefault" = "توصیه می شود به عنوان پیش فرض حفظ شود"
"certificatePath" = "مسیر فایل گواهی"
"certificateContent" = "محتوای فایل گواهی"
"publicKeyPath" = "مسیر فایل Certificate.crt"
"publicKeyContent" = "محتوای Certificate.crt"
"keyPath" = "مسیر فایل Private.key"
"keyContent" = "محتوای Private.key"
"publicKeyPath" = "مسیر کلید عمومی"
"publicKeyContent" = "محتوای کلید عمومی"
"keyPath" = "مسیر کلید خصوصی"
"keyContent" = "محتوای کلید خصوصی"
"clickOnQRcode" = "برای کپی بر روی کد تصویری کلیک کنید"
"client" = "کاربر"
"export" = "استخراج لینک‌ها"
"Clone" = "شبیه سازی"
"cloneInbound" = "ایجاد"
"cloneInboundContent" = "همه موارد این ورودی بجز پورت ، ای پی و کلاینت ها شبیه سازی خواهند شد"
"resetAllTraffic" = "ریست ترافیک کل سرویس ها"
"resetAllTrafficTitle" = "ریست ترافیک کل سرویس ها"
"resetAllTrafficContent" = "آیا مطمئن هستید که میخواهید تمام ترافیک سرویس ها را ریست کنید؟"
"resetAllClientTraffics" = "ریست ترافیک کاربران"
"resetAllClientTrafficTitle" = "ریست ترافیک کل کاربران"
"resetAllClientTrafficContent" = "آیا مطمئن هستید که میخواهید تمام ترافیک کاربران این سرویس را ریست کنید؟"
"Email" = "ایمیل"
"EmailDesc" = "ایمیل باید کاملا منحصر به فرد باشد"
"setDefaultCert" = "استفاده از گواهی پنل"
[pages.client]
"add" = "کاربر جدید"
@@ -142,6 +157,9 @@
"last" = "تا"
"prefix" = "پیشوند"
"postfix" = "پسوند"
"delayedStart" = "شروع بعد از اولین استفاده"
"expireDays" = "روزهای اعتبار"
"days" = "(روز)"
[pages.inbounds.toasts]
"obtain" = "Obtain"
@@ -177,9 +195,9 @@
"panelListeningIPDesc" = "برای استفاده از تمام IP ها به طور پیش فرض خالی بگذارید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"panelPort" = "پورت پنل"
"panelPortDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
"publicKeyPath" = "مسیر فایل پنل Certificate.crt"
"publicKeyPath" = "مسیر فایل گواهی کلید عمومی پنل"
"publicKeyPathDesc" = "باید یک مسیر مطلق باشد که با / شروع می شود . پنل را مجدداً راه اندازی کنید تا اعمال شود"
"privateKeyPath" = "مسیر فایل پنل private.key"
"privateKeyPath" = "مسیر فایل گواهی کلید خصوصی پنل"
"privateKeyPathDesc" = "باید یک مسیر مطلق باشد که با / شروع می شود . پنل را مجدداً راه اندازی کنید تا اعمال شود"
"panelUrlPath" = "آدرس روت پنل"
"panelUrlPathDesc" = "باید با '/' شروع شود و با '/' تمام شود. پنل را مجدداً راه اندازی کنید تا اعمال شود"
@@ -208,13 +226,13 @@
"telegramChatId" = "آی دی تلگرام مدیریت"
"telegramChatIdDesc" = "با استفاده از کاما میتونید چند آی دی را از هم جدا کنید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"telegramNotifyTime" = "مدت زمان نوتیفیکیشن ربات تلگرام"
"telegramNotifyTimeDesc" = "از فرمت زمان بندی Crontab استفاده کنید . پنل را مجدداً راه اندازی کنید تا اعمال شود"
"telegramNotifyTimeDesc" = "از فرمت زمان بندی لینوکس استفاده کنید . پنل را مجدداً راه اندازی کنید تا اعمال شود"
"tgNotifyBackup" = "پشتیبان گیری از پایگاه داده"
"tgNotifyBackupDesc" = "ارسال کپی فایل پایگاه داده به همراه گزارش دوره ای"
"tgNotifyExpireTimeDiff" = "آستانه زمان باقی مانده"
"tgNotifyExpireTimeDiffDesc" = "این ربات تلگرام قبل از انقضا برای شما پیام ارسال می کند (واحد: روز)"
"tgNotifyTrafficDiff" = "آستانه ترافیک باقی مانده"
"tgNotifyTrafficDiffDesc" = "این ربات تلگرام قبل از اتمام ترافیک برای شما پیام ارسال می کند (واحد: گیگابایت)"
"expireTimeDiff" = "آستانه زمان باقی مانده"
"expireTimeDiffDesc" = "فاصله زمانی هشدار تا رسیدن به زمان انقضا (واحد: روز)"
"trafficDiff" = "آستانه ترافیک باقی مانده"
"trafficDiffDesc" = "فاصله زمانی هشدار تا رسیدن به اتمام ترافیک (واحد: گیگابایت)"
"tgNotifyCpu" = "آستانه هشدار درصد پردازنده"
"tgNotifyCpuDesc" = "این ربات تلگرام در صورت استفاده پردازنده بیشتر از این درصد برای شما پیام ارسال می کند.(واحد: درصد)"
"timeZonee" = "منظقه زمانی"

View File

@@ -33,8 +33,11 @@
"host" = "主持人"
"path" = "小路"
"camouflage" = "伪装"
"status" = "状态"
"enabled" = "开启"
"disabled" = "关闭"
"depleted" = "耗尽"
"depletingSoon" = "即将耗尽"
"domainName" = "域名"
"additional" = "额外"
"monitor" = "监听"
@@ -129,6 +132,18 @@
"clickOnQRcode" = "点击二维码复制"
"client" = "客户"
"export" = "导出链接"
"Clone" = "克隆"
"cloneInbound" = "创造"
"cloneInboundContent" = "此入站的所有项目除 Port、Listening IP、Clients 将应用于克隆"
"resetAllTraffic" = "重置所有入站流量"
"resetAllTrafficTitle" = "重置所有入站流量"
"resetAllTrafficContent" = "您确定要重置所有入站流量吗?"
"resetAllClientTraffics" = "重置客户端流量"
"resetAllClientTrafficTitle" = "重置所有客户端流量"
"resetAllClientTrafficContent" = "您确定要重置此入站客户端的所有流量吗?"
"Email" = "电子邮件"
"EmailDesc" = "电子邮件必须完全唯"
"setDefaultCert" = "从面板设置证书"
[pages.client]
"add" = "添加客户端"
@@ -142,6 +157,9 @@
"last" = "最后"
"prefix" = "前缀"
"postfix" = "后缀"
"delayedStart" = "首次使用后开始"
"expireDays" = "过期天数"
"days" = "天"
[pages.inbounds.toasts]
"obtain" = "获取"
@@ -211,10 +229,10 @@
"telegramNotifyTimeDesc" = "采用Crontab定时格式,重启面板生效"
"tgNotifyBackup" = "数据库备份"
"tgNotifyBackupDesc" = "正在发送数据库备份文件和报告通知。重启面板生效"
"tgNotifyExpireTimeDiff" = "剩余时间阈值"
"tgNotifyExpireTimeDiffDesc" = "这个 talegram bot 会在到期前给你发送通知(单位:天)"
"tgNotifyTrafficDiff" = "剩余流量阈值"
"tgNotifyTrafficDiffDesc" = "这个 talegram bot 会在流量结束前给你发送通知单位GB"
"expireTimeDiff" = "耗尽时间阈值"
"expireTimeDiffDesc" = "到期前检测耗尽(单位:天)"
"trafficDiff" = "耗尽流量阈值"
"trafficDiffDesc" = "完成流量前检测耗尽单位GB"
"tgNotifyCpu" = "CPU 百分比警报阈值"
"tgNotifyCpuDesc" = "如果 CPU 使用率超过此百分比(单位:%),此 talegram bot 将向您发送通知"
"timeZonee" = "时区"

View File

@@ -33,6 +33,9 @@ import (
//go:embed assets/*
var assetsFS embed.FS
//go:embed assets/favicon.ico
var favicon []byte
//go:embed html/*
var htmlFS embed.FS
@@ -85,6 +88,7 @@ type Server struct {
server *controller.ServerController
xui *controller.XUIController
api *controller.APIController
sub *controller.SUBController
xrayService service.XrayService
settingService service.SettingService
@@ -157,6 +161,11 @@ func (s *Server) initRouter() (*gin.Engine, error) {
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()
if err != nil {
return nil, err
@@ -208,6 +217,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
s.server = controller.NewServerController(g)
s.xui = controller.NewXUIController(g)
s.api = controller.NewAPIController(g)
s.sub = controller.NewSUBController(g)
return engine, nil
}

10
x-ui.sh
View File

@@ -39,17 +39,17 @@ os_version=""
os_version=$(grep -i version_id /etc/os-release | cut -d \" -f2 | cut -d . -f1)
if [[ "${release}" == "centos" ]]; then
if [[ ${os_version} -lt 7 ]]; then
echo -e "${red} Please use CentOS 7 or higher ${plain}\n" && exit 1
if [[ ${os_version} -lt 8 ]]; then
echo -e "${red} Please use CentOS 8 or higher ${plain}\n" && exit 1
fi
elif [[ "${release}" == "ubuntu" ]]; then
if [[ ${os_version} -lt 18 ]]; then
echo -e "${red}please use Ubuntu 18 or higher version${plain}\n" && exit 1
if [[ ${os_version} -lt 20 ]]; then
echo -e "${red}please use Ubuntu 20 or higher version! ${plain}\n" && exit 1
fi
elif [[ "${release}" == "fedora" ]]; then
if [[ ${os_version} -lt 36 ]]; then
echo -e "${red}please use Fedora 36 or higher version${plain}\n" && exit 1
echo -e "${red}please use Fedora 36 or higher version! ${plain}\n" && exit 1
fi
elif [[ "${release}" == "debian" ]]; then