Compare commits

...

305 Commits

Author SHA1 Message Date
Alireza Ahmadi
453594ee9e v1.1.0 2023-04-25 19:46:51 +02:00
Alireza Ahmadi
a2d8bec80a [bug] vision-udp443 only for client 2023-04-25 19:30:10 +02:00
Alireza Ahmadi
b0ca8a8e6c Translation 2023-04-25 18:16:33 +02:00
Alireza Ahmadi
99c9d777c0 add hostname to page title 2023-04-25 17:38:58 +02:00
Alireza Ahmadi
97d109c900 [api] support for delete depleted clients 2023-04-25 16:48:18 +02:00
Alireza Ahmadi
35ad91a9e0 [feature] delete depleted clients 2023-04-25 16:24:13 +02:00
Alireza Ahmadi
8bc16b020b [migration] add fix for omitted traffics 2023-04-25 16:22:42 +02:00
Alireza Ahmadi
6db9bd0ad9 fix switch enable disable client 2023-04-25 13:38:26 +02:00
Alireza Ahmadi
5baa397d1c [feature] reset traffics of all client 2023-04-25 12:26:56 +02:00
Alireza Ahmadi
76e1243da3 [feature] add general action menu 2023-04-25 12:22:50 +02:00
Alireza Ahmadi
7b7a0f9aa7 [sub] fix bug in http link without host 2023-04-25 10:44:39 +02:00
Alireza Ahmadi
134e2236a6 [feature] add login session timeout 2023-04-25 08:41:11 +02:00
Alireza Ahmadi
aab672976e update by client id #218 2023-04-24 20:30:01 +02:00
Alireza Ahmadi
f7aee9fe18 v1.0.2 2023-04-24 17:52:33 +02:00
Alireza Ahmadi
a19b58675b Add migration to install script 2023-04-24 11:16:28 +02:00
Alireza Ahmadi
7a229b27f5 Better client delete + api #218 2023-04-24 11:10:59 +02:00
Alireza Ahmadi
abf68446e5 Optimize database #258 2023-04-24 10:40:37 +02:00
Alireza Ahmadi
b4b7ec565e simplify API: del client #218 2023-04-22 11:21:44 +02:00
Alireza Ahmadi
4b1b920bf4 Add database migration 2023-04-22 11:19:22 +02:00
Alireza Ahmadi
11bff57f23 [feature] add getClientTraffics api 2023-04-20 19:16:06 +02:00
Alireza Ahmadi
4838b0f6f0 v1.0.1 2023-04-20 14:30:58 +02:00
Alireza Ahmadi
bb8807129a [client] add reset traffic in edit #244 2023-04-20 13:59:32 +02:00
Alireza Ahmadi
cb050bfe34 [sub] fix userinfo header #217 2023-04-20 12:53:50 +02:00
Alireza Ahmadi
76a996cc2d main.go enhancements 2023-04-20 10:55:58 +02:00
Alireza Ahmadi
7fec1dd5cf [reality] fix fingerprint issue #236 2023-04-20 10:50:20 +02:00
Alireza Ahmadi
a24e9089b6 small visual enhancements #237 2023-04-20 10:48:57 +02:00
Alireza Ahmadi
b6474eaef6 fix setting numbers #237 2023-04-20 10:47:24 +02:00
Alireza Ahmadi
76f90b0a46 fix enabletgbot cli 2023-04-20 10:45:21 +02:00
Alireza Ahmadi
42abffdaef [docker] fix build for amd64 2023-04-20 10:36:46 +02:00
Alireza Ahmadi
73d518dfe3 v1.0.0 2023-04-19 02:35:29 +02:00
Alireza Ahmadi
4592a8d201 typos 2023-04-19 02:02:49 +02:00
Alireza Ahmadi
6ab99326ce [docker] add initial files #221 2023-04-19 01:58:36 +02:00
Alireza Ahmadi
6c5b098e1d [reality] optimize links 2023-04-19 00:53:07 +02:00
Alireza Ahmadi
fe670ae885 [sub] reality link 2023-04-19 00:43:19 +02:00
Alireza Ahmadi
f4f1d477b3 Optimize client settings 2023-04-19 00:42:54 +02:00
Alireza Ahmadi
7455a2baa3 update packages 2023-04-18 16:49:05 +02:00
Alireza Ahmadi
4c5576c38b [reality] fix trojan link issue + sub 2023-04-18 16:31:04 +02:00
Alireza Ahmadi
18ee201412 [sub] add subscription-userinfo #217 2023-04-18 12:18:33 +02:00
Alireza Ahmadi
772a1b341e v1.0.0_beta1 2023-04-18 09:34:16 +02:00
Alireza Ahmadi
5923c765a9 Merge pull request #227 from alireza0:Reality
Reality
2023-04-18 09:32:12 +02:00
Alireza Ahmadi
d7074cc3c9 Update dockerfile and actions 2023-04-18 09:30:53 +02:00
Alireza Ahmadi
f4f8c0d6df simplify API calls #218 2023-04-18 01:22:33 +02:00
Alireza Ahmadi
0c755be648 add flow in bulk creation #213 2023-04-16 19:24:39 +02:00
Alireza Ahmadi
c8dc4a96b4 [Reality] make it pretty 2023-04-16 19:21:35 +02:00
Alireza Ahmadi
8d9f6ccc11 [Reality] initiate 2023-04-15 09:54:58 +02:00
Alireza Ahmadi
3d7a8e372e Merge pull request #205 from hamid-gh98/main
Support set db and bin folder path from env
2023-04-14 14:51:28 +02:00
Hamidreza Ghavami
2fe4f1ad07 update README.md 2023-04-14 16:56:35 +04:30
Hamidreza Ghavami
04095b6ec4 update README.md 2023-04-14 16:54:41 +04:30
Hamidreza Ghavami
6d404e4b27 update get paths functions 2023-04-14 04:44:04 +04:30
Hamidreza Ghavami
f9f8431dd2 update v2-ui.db path 2023-04-14 04:40:46 +04:30
Hamidreza Ghavami
f996b9ee2b update en lang 2023-04-14 04:39:35 +04:30
Hamidreza Ghavami
a3e1ee1f35 update db config path 2023-04-14 04:36:33 +04:30
Alireza Ahmadi
e16391ad05 Merge pull request #203 from lk29/main 2023-04-13 23:41:14 +02:00
lk29
48b543becb Update tgbot.go syntax fix ..Passowrd 2023-04-14 01:53:47 +05:00
Alireza Ahmadi
80f052697c v0.5.2 2023-04-13 16:59:01 +02:00
Alireza Ahmadi
fb15248030 [migration] Remove Orphaned Traffics 2023-04-13 16:31:35 +02:00
Alireza Ahmadi
6a18ba48aa remove traffics of edited emails 2023-04-13 16:30:39 +02:00
Alireza Ahmadi
c13b7b2a18 [backup] fix db name in we request 2023-04-13 14:13:04 +02:00
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
Alireza Ahmadi
8f0a701d9e v0.4.3 2023-03-26 13:38:32 +02:00
Alireza Ahmadi
921a72b5cf v0.4.3 2023-03-24 12:52:00 +01:00
Alireza Ahmadi
9e902fc82f Add version and log 2023-03-24 12:24:12 +01:00
Alireza Ahmadi
6ed8ff8e05 [infoModal] better display 2023-03-24 12:23:23 +01:00
Alireza Ahmadi
f333ad23d9 Merge pull request #92 from alireza0/dependabot/go_modules/google.golang.org/grpc-1.54.0
Bump google.golang.org/grpc from 1.53.0 to 1.54.0
2023-03-24 12:06:58 +01:00
Alireza Ahmadi
3f49aa5541 Merge pull request #89 from alireza0/TLS-enhancements
TLS-enhancements
2023-03-24 12:06:33 +01:00
Alireza Ahmadi
0f487dc10f [tgbot] fix admins input 2023-03-24 08:43:09 +01:00
dependabot[bot]
f3c0edac07 Bump google.golang.org/grpc from 1.53.0 to 1.54.0
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.53.0 to 1.54.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.53.0...v1.54.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-23 22:07:30 +00:00
Alireza Ahmadi
394c857bb6 [tgbot] fix exhausted report 2023-03-23 22:55:23 +01:00
Alireza Ahmadi
6080709fc7 Enable fallback in xtls as well 2023-03-23 20:13:21 +01:00
Alireza Ahmadi
17c47f0711 Remove ugly-bugy qrCode footer 2023-03-23 18:29:12 +01:00
Alireza Ahmadi
dac234357a Fix input-group dark theme 2023-03-23 12:53:36 +01:00
Alireza Ahmadi
2d15073a90 Fix vmess link 2023-03-23 11:16:48 +01:00
Alireza Ahmadi
1b2e165a65 Fix TLS and ALPN 2023-03-23 10:03:09 +01:00
Alireza Ahmadi
e1f8e8efd2 Add allowInsecure option 2023-03-23 10:02:18 +01:00
Alireza Ahmadi
738c7064a9 Default TLS version change to 1.0-1.2 2023-03-23 10:01:28 +01:00
Alireza Ahmadi
c24483ad44 [tgbot] fix client search #85 2023-03-23 09:59:34 +01:00
Alireza Ahmadi
046f071378 [bug] Fix Transport column in search table 2023-03-21 15:03:26 +01:00
Alireza Ahmadi
d40a16e29d Update README 2023-03-20 13:50:39 +01:00
Alireza Ahmadi
2f1f28cc0b v0.4.2 2023-03-20 12:03:52 +01:00
Alireza Ahmadi
70ff51d629 [Bulk client] add option: without random email #72 2023-03-20 11:50:44 +01:00
Alireza Ahmadi
7472c31c34 [TGBOT] add seach inbound #75 2023-03-20 11:18:47 +01:00
Alireza Ahmadi
04a8d5c177 [DarkMode] better dark theme based on antdv2 2023-03-20 10:50:31 +01:00
Alireza Ahmadi
7b2bd4247d TGBOT: Add xray config to backup #75 2023-03-20 10:22:41 +01:00
Alireza Ahmadi
5aee1c886b Merge branch 'main' of https://github.com/alireza0/x-ui 2023-03-19 20:19:04 +01:00
Alireza Ahmadi
7a9a30a038 Dark-Mode: Better contrast 2023-03-19 19:59:35 +01:00
Alireza Ahmadi
eb17cec289 Remove chat answer 2023-03-19 19:07:40 +01:00
Alireza Ahmadi
22e107c746 Update install.sh
Fix centos7+ message
2023-03-19 11:56:42 +01:00
Alireza Ahmadi
36a93e2959 Merge pull request #77 from forkeer/main
Fedora OS Support
2023-03-19 11:56:13 +01:00
Alireza Ahmadi
073247f054 Fix Link bug 2023-03-19 11:51:08 +01:00
Leonardo
96c8932961 Merge branch 'alireza0:main' into main 2023-03-19 00:14:23 +03:30
Alireza Ahmadi
3d4b87d72f v0.4.1 2023-03-18 17:26:39 +01:00
Leonardo
5fac7df174 Merge branch 'alireza0:main' into main 2023-03-18 13:45:41 +03:30
Leonardo
8bba9f3c25 Update install.sh
Fix s390x
2023-03-18 13:43:54 +03:30
Leonardo
268e8c7eb1 Update x-ui.sh
Fix Debian 8 Support
2023-03-18 13:39:54 +03:30
Leonardo
14e8f6cbfa Update x-ui.sh
Fix OS Version
2023-03-18 13:38:37 +03:30
Leonardo
7f0ba495db Update install.sh
Fix OS Version
2023-03-18 13:36:01 +03:30
Alireza Ahmadi
af07a9bf15 Fix error handling in client update 2023-03-17 18:57:20 +01:00
Alireza Ahmadi
6b8ca0c321 v0.4.1 2023-03-17 15:15:06 +01:00
Alireza Ahmadi
48554ab497 Dark-Mode: Media css fix 2023-03-17 12:36:55 +01:00
Alireza Ahmadi
dddebf02ef Add translation xray functions 2023-03-17 11:09:04 +01:00
Alireza Ahmadi
0c469848dc Merge pull request #63 from lk29/main-add-1
add Xray stop & Xray restart into web
2023-03-17 10:58:02 +01:00
Alireza Ahmadi
cb85d4f83f Merge pull request #70 from alireza0/dependabot/github_actions/actions/setup-go-4
Bump actions/setup-go from 3 to 4
2023-03-17 10:57:30 +01:00
Alireza Ahmadi
df29570dc1 Dark-Mode: CSS fixes #50 2023-03-17 10:49:44 +01:00
Alireza Ahmadi
7e9c336a17 Merge branch 'main' of https://github.com/alireza0/x-ui 2023-03-16 20:48:29 +01:00
Alireza Ahmadi
585f43498c TGBot: Add usage search for user #67 2023-03-16 20:48:21 +01:00
Alireza Ahmadi
2a7a09d12f Merge pull request #71 from alireza0:Dark-Mode
Add Dark-Mode Theme
2023-03-16 16:12:17 +01:00
Alireza Ahmadi
98759e964c Add theme to modal forms #50 2023-03-16 15:31:05 +01:00
Alireza Ahmadi
9ae3d62afb Remove access.log in default config 2023-03-16 11:22:28 +01:00
Alireza Ahmadi
24b718dca3 Init dark mode for main pages #50 2023-03-16 11:06:04 +01:00
dependabot[bot]
d0c4d0a941 Bump actions/setup-go from 3 to 4
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 3 to 4.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-15 22:07:17 +00:00
Leonardo
f829213dd1 go-version
go-version update to stable
2023-03-14 14:17:52 +03:30
Leonardo
4d5a73512e Update README.md
Fedora 36+
2023-03-14 14:01:52 +03:30
Leonardo
6dccc5b891 Update install.sh
Fedora OS Fix
2023-03-14 14:00:14 +03:30
Leonardo
f642830dc8 Update x-ui.sh
Fix Fedora
2023-03-14 13:56:56 +03:30
root
073b2ee8a8 add Xray stop & Xray restart 2023-03-13 22:32:24 +03:00
Alireza Ahmadi
8bcfa0b101 Merge branch 'main' of https://github.com/alireza0/x-ui 2023-03-13 13:19:56 +01:00
Alireza Ahmadi
cd9e903ec3 Delete db directory 2023-03-13 13:19:45 +01:00
Alireza Ahmadi
b9fecaa918 Merge branch 'main' of https://github.com/alireza0/x-ui 2023-03-13 13:19:18 +01:00
Alireza Ahmadi
eb516575ec Remove default docker db 2023-03-13 13:19:12 +01:00
Alireza Ahmadi
aeb5c9acaa Merge pull request #62 from alireza0/revert-59-main
Revert "Added Fedora OS Support"
2023-03-13 11:36:55 +01:00
Alireza Ahmadi
68a7b38117 Revert "Added Fedora OS Support" 2023-03-13 11:36:37 +01:00
Alireza Ahmadi
e084518f4f Merge pull request #59 from forkeer/main
Added Fedora OS Support
2023-03-13 11:23:49 +01:00
Leonardo
e6460d1d00 Update README.md
Added Fedora OS
2023-03-11 15:29:43 +03:30
Leonardo
0218af8b4f Update install.sh
Fix Fedora OS
2023-03-11 15:28:56 +03:30
Leonardo
552bc6c1f6 Update x-ui.sh
Fix Fedora OS
2023-03-11 15:27:26 +03:30
Alireza Ahmadi
3001107282 v0.4.0 2023-03-11 01:09:08 +01:00
Alireza Ahmadi
ed6f45f341 Merge pull request #37 from alireza0/dependabot/go_modules/gorm.io/gorm-1.24.6
Bump gorm.io/gorm from 1.24.5 to 1.24.6
2023-03-10 23:53:21 +01:00
Alireza Ahmadi
83853a963e Merge pull request #38 from alireza0/dependabot/go_modules/github.com/shirou/gopsutil/v3-3.23.2
Bump github.com/shirou/gopsutil/v3 from 3.23.1 to 3.23.2
2023-03-10 23:52:33 +01:00
dependabot[bot]
2774de744c Bump gorm.io/gorm from 1.24.5 to 1.24.6
Bumps [gorm.io/gorm](https://github.com/go-gorm/gorm) from 1.24.5 to 1.24.6.
- [Release notes](https://github.com/go-gorm/gorm/releases)
- [Commits](https://github.com/go-gorm/gorm/compare/v1.24.5...v1.24.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-10 22:51:46 +00:00
dependabot[bot]
7306be3cb6 Bump github.com/shirou/gopsutil/v3 from 3.23.1 to 3.23.2
Bumps [github.com/shirou/gopsutil/v3](https://github.com/shirou/gopsutil) from 3.23.1 to 3.23.2.
- [Release notes](https://github.com/shirou/gopsutil/releases)
- [Commits](https://github.com/shirou/gopsutil/compare/v3.23.1...v3.23.2)

---
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-03-10 22:51:46 +00:00
Alireza Ahmadi
b2d16b87bb Merge pull request #53 from alireza0/dependabot/go_modules/github.com/xtls/xray-core-1.8.0
Bump github.com/xtls/xray-core from 1.7.5 to 1.8.0
2023-03-10 23:50:55 +01:00
Alireza Ahmadi
4ab1135d89 InfoModal for all other protocols #54 2023-03-10 23:45:31 +01:00
Alireza Ahmadi
8a6782273e Add icons to tgbot #34 2023-03-10 21:49:35 +01:00
Alireza Ahmadi
b2075e9548 Copy the code by clicking on QR #41 2023-03-10 20:36:00 +01:00
Alireza Ahmadi
8efa776cd7 Remark only email when exist 2023-03-10 20:34:51 +01:00
Alireza Ahmadi
b641951759 Init + Translate #40 2023-03-10 20:27:11 +01:00
Alireza Ahmadi
f539cbec45 add user title in bulk add #40 2023-03-10 19:33:56 +01:00
Alireza Ahmadi
645b1535ac Revert "add user title in bulk add #40"
This reverts commit 3729ac5e3e.
2023-03-10 19:31:40 +01:00
Alireza Ahmadi
3729ac5e3e add user title in bulk add #40 2023-03-10 19:30:26 +01:00
Alireza Ahmadi
f736f069a0 Fix tgbot: Admin can only search for client 2023-03-10 15:10:17 +01:00
Alireza Ahmadi
cd3d42c7ee Fix labels in ISSUE template 2023-03-10 14:48:57 +01:00
Alireza Ahmadi
88cc4aed19 Merge pull request #55 from alireza0:34-telegram-new-features
34-telegram-new-features
2023-03-10 14:22:21 +01:00
Alireza Ahmadi
2ae1455a1f Conditional stream and sniffing 2023-03-10 14:18:58 +01:00
Alireza Ahmadi
c20d13ea96 Merge branch 'main' of https://github.com/alireza0/x-ui 2023-03-10 14:16:20 +01:00
Alireza Ahmadi
998d861d0a Add followRedirect to Dokodemo 2023-03-10 14:12:24 +01:00
Alireza Ahmadi
e1c76fdc7a Add CPU load alarm #34 2023-03-10 14:11:31 +01:00
dependabot[bot]
6749d63222 Bump github.com/xtls/xray-core from 1.7.5 to 1.8.0
Bumps [github.com/xtls/xray-core](https://github.com/xtls/xray-core) from 1.7.5 to 1.8.0.
- [Release notes](https://github.com/xtls/xray-core/releases)
- [Commits](https://github.com/xtls/xray-core/compare/v1.7.5...v1.8.0)

---
updated-dependencies:
- dependency-name: github.com/xtls/xray-core
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-09 22:06:07 +00:00
Alireza Ahmadi
546b66a61c Fix initiate DB #34 2023-03-09 22:40:34 +01:00
Alireza Ahmadi
2e2f066c4b Telegram complete reports + Frontend #34 2023-03-09 22:01:08 +01:00
Alireza Ahmadi
5bac00b419 tgbot admin & client separation #34 2023-03-09 15:49:28 +01:00
Alireza Ahmadi
d07a2119ce Fix docker command #47 2023-03-07 14:52:20 +01:00
Alireza Ahmadi
2951b5f94c Support multi telegramId for admins #34 2023-03-06 21:42:02 +01:00
Alireza Ahmadi
a9c25cddb3 Update issue templates 2023-03-05 14:41:13 +01:00
Alireza Ahmadi
edc911c2f3 Move TgBot to service + fix pannel restart bug #34 2023-03-03 17:22:37 +01:00
Alireza Ahmadi
c053991a37 Update README.md 2023-03-03 02:08:38 +01:00
Alireza Ahmadi
73803af14c Merge branch 'main' of https://github.com/alireza0/x-ui 2023-03-02 21:49:53 +01:00
Alireza Ahmadi
6dc823e215 Improved Add Client process alireza0/x-ui#35 2023-03-02 21:48:56 +01:00
Alireza Ahmadi
5b1eaf9cc3 Update issue templates 2023-03-02 21:27:53 +01:00
Alireza Ahmadi
2f611684a2 v0.3.2 2023-03-02 17:48:42 +01:00
Alireza Ahmadi
0471bbbdb1 Merge pull request #23 from alireza0/dependabot/go_modules/github.com/gin-gonic/gin-1.9.0
Bump github.com/gin-gonic/gin from 1.8.2 to 1.9.0
2023-03-02 17:44:30 +01:00
Alireza Ahmadi
0ced0fcdc3 Merge pull request #32 from alireza0/dependabot/go_modules/github.com/pelletier/go-toml/v2-2.0.7
Bump github.com/pelletier/go-toml/v2 from 2.0.6 to 2.0.7
2023-03-02 17:44:14 +01:00
Alireza Ahmadi
33f354c71c Update backend Client model 2023-03-02 12:54:38 +01:00
Alireza Ahmadi
f87dad254b New feature: Bulk add #28 (plus adjust methods) 2023-03-02 12:50:59 +01:00
Alireza Ahmadi
340b54d45f Add Export feature #28 2023-03-02 09:32:42 +01:00
Alireza Ahmadi
1288427cce Show clients counter #30 2023-03-01 23:11:51 +01:00
Alireza Ahmadi
52d3956203 Support stats in Windows OS 2023-03-01 21:50:23 +01:00
Alireza Ahmadi
e13540901f Stupid bug detecting traffic email #33 2023-03-01 21:38:07 +01:00
Alireza Ahmadi
4f91375cb2 Small fixes 2023-03-01 19:52:21 +01:00
Alireza Ahmadi
9e398aaa65 Optimize Client-traffic-reset 2023-03-01 17:08:28 +01:00
Alireza Ahmadi
ae45f9fef2 Fallback limit only for TCP+TLS 2023-03-01 14:50:06 +01:00
Alireza Ahmadi
698af18d25 Fix add client #26 2023-03-01 14:01:09 +01:00
Alireza Ahmadi
afd369993a Fix Bug in deleting the client alireza0/x-ui#20 2023-03-01 13:57:48 +01:00
Alireza Ahmadi
40bb9f9864 Simplify method resetClientTraffic 2023-03-01 13:55:09 +01:00
dependabot[bot]
1f669aa61d Bump github.com/pelletier/go-toml/v2 from 2.0.6 to 2.0.7
Bumps [github.com/pelletier/go-toml/v2](https://github.com/pelletier/go-toml) from 2.0.6 to 2.0.7.
- [Release notes](https://github.com/pelletier/go-toml/releases)
- [Changelog](https://github.com/pelletier/go-toml/blob/v2/.goreleaser.yaml)
- [Commits](https://github.com/pelletier/go-toml/compare/v2.0.6...v2.0.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-28 22:38:39 +00:00
Alireza Ahmadi
a776bdbb87 Merge branch 'main' of https://github.com/alireza0/x-ui 2023-02-28 10:25:43 +01:00
Alireza Ahmadi
9a0055d5d5 Rollback useless utls feature 2023-02-28 10:25:27 +01:00
Alireza Ahmadi
19c13625a1 Update README.md 2023-02-26 14:44:53 +01:00
Alireza Ahmadi
e1428ff0dc Fix editing a searched client is buggy alireza0/x-ui#24 2023-02-24 14:46:14 +01:00
Alireza Ahmadi
4843c93608 Fix Vless & Trojan fingerprint problem 2023-02-24 13:07:17 +01:00
Alireza Ahmadi
757c2a76f8 Minimize call API for Client 2023-02-24 00:05:53 +01:00
dependabot[bot]
95c777f0ac Bump github.com/gin-gonic/gin from 1.8.2 to 1.9.0
Bumps [github.com/gin-gonic/gin](https://github.com/gin-gonic/gin) from 1.8.2 to 1.9.0.
- [Release notes](https://github.com/gin-gonic/gin/releases)
- [Changelog](https://github.com/gin-gonic/gin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/gin-gonic/gin/compare/v1.8.2...v1.9.0)

---
updated-dependencies:
- dependency-name: github.com/gin-gonic/gin
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-23 22:33:47 +00:00
Alireza Ahmadi
fc4f1a4fd3 Fix typos in README 2023-02-23 17:44:16 +01:00
Alireza Ahmadi
c7be137f6d Update screenshot 2023-02-23 17:20:57 +01:00
Alireza Ahmadi
8a07474999 0.3.1 2023-02-23 11:33:42 +01:00
Alireza Ahmadi
7ec68574d0 Fix Bug: Auto restart xray after client update 2023-02-23 11:33:14 +01:00
Alireza Ahmadi
414669fa1b Fix bugs 2023-02-22 23:15:52 +01:00
Alireza Ahmadi
76f774fd6c 0.3.0 2023-02-22 20:38:01 +01:00
Alireza Ahmadi
b82ce31ec1 Change menu to icons for client actions 2023-02-22 20:34:49 +01:00
Alireza Ahmadi
9bbd612327 Fix double release creation 2023-02-22 19:51:01 +01:00
Alireza Ahmadi
a5922a8620 v.0.3.0 2023-02-22 19:44:45 +01:00
Alireza Ahmadi
35e4b96e9e Fix traffic update in db 2023-02-22 18:34:02 +01:00
Alireza Ahmadi
ceb7a7ac14 Fix Trouble resetting traffic alireza0/x-ui#18 2023-02-22 17:33:05 +01:00
Alireza Ahmadi
92dc390e58 Merge pull request #16 from alireza0/dependabot/go_modules/github.com/BurntSushi/toml-1.2.1
Bump github.com/BurntSushi/toml from 1.0.0 to 1.2.1
2023-02-22 15:16:15 +01:00
Alireza Ahmadi
a6edd1c631 Merge pull request #17 from alireza0/dependabot/go_modules/github.com/gin-contrib/sessions-0.0.5
Bump github.com/gin-contrib/sessions from 0.0.3 to 0.0.5
2023-02-22 15:16:01 +01:00
Alireza Ahmadi
146b4d7873 Separate client module 2023-02-22 15:13:45 +01:00
dependabot[bot]
f3dac6395a Bump github.com/gin-contrib/sessions from 0.0.3 to 0.0.5
Bumps [github.com/gin-contrib/sessions](https://github.com/gin-contrib/sessions) from 0.0.3 to 0.0.5.
- [Release notes](https://github.com/gin-contrib/sessions/releases)
- [Changelog](https://github.com/gin-contrib/sessions/blob/master/.goreleaser.yaml)
- [Commits](https://github.com/gin-contrib/sessions/compare/v0.0.3...v0.0.5)

---
updated-dependencies:
- dependency-name: github.com/gin-contrib/sessions
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-20 22:30:00 +00:00
dependabot[bot]
b48707923c Bump github.com/BurntSushi/toml from 1.0.0 to 1.2.1
Bumps [github.com/BurntSushi/toml](https://github.com/BurntSushi/toml) from 1.0.0 to 1.2.1.
- [Release notes](https://github.com/BurntSushi/toml/releases)
- [Commits](https://github.com/BurntSushi/toml/compare/v1.0.0...v1.2.1)

---
updated-dependencies:
- dependency-name: github.com/BurntSushi/toml
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-20 22:29:46 +00:00
Alireza Ahmadi
40ac7d4115 Fix problems after updating gorm to 1.24 2023-02-20 12:00:40 +01:00
Alireza Ahmadi
3e550d7e13 v0.2.4 2023-02-19 01:41:24 +01:00
Alireza Ahmadi
2e12cd6f6e Translate none 2023-02-19 01:41:02 +01:00
Alireza Ahmadi
57f538bf6b Some upgrades 2023-02-19 01:40:36 +01:00
Alireza Ahmadi
7d23e8cf9c Merge branch 'main' of https://github.com/alireza0/x-ui 2023-02-19 01:20:54 +01:00
Alireza Ahmadi
4120024e04 Merge pull request #8 from alireza0/dependabot/go_modules/gorm.io/driver/sqlite-1.4.4
Bump gorm.io/driver/sqlite from 1.1.4 to 1.4.4
2023-02-19 01:19:19 +01:00
Alireza Ahmadi
fedb0610e0 Merge pull request #3 from alireza0:dependabot/github_actions/actions/setup-go-3
Bump actions/setup-go from 2 to 3
2023-02-19 01:18:35 +01:00
Alireza Ahmadi
9ed41cf427 Merge pull request #4 from alireza0:dependabot/github_actions/actions/checkout-3
Bump actions/checkout from 2 to 3
2023-02-19 01:18:15 +01:00
dependabot[bot]
8a66a42a4e Bump gorm.io/driver/sqlite from 1.1.4 to 1.4.4
Bumps [gorm.io/driver/sqlite](https://github.com/go-gorm/sqlite) from 1.1.4 to 1.4.4.
- [Release notes](https://github.com/go-gorm/sqlite/releases)
- [Commits](https://github.com/go-gorm/sqlite/compare/v1.1.4...v1.4.4)

---
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-02-19 00:18:10 +00:00
Alireza Ahmadi
ac98fce996 Merge pull request #6 from alireza0:dependabot/github_actions/docker/setup-qemu-action-2
Bump docker/setup-qemu-action from 1 to 2
2023-02-19 01:17:57 +01:00
Alireza Ahmadi
2aacd1111c Merge pull request #7 from alireza0:dependabot/go_modules/golang.org/x/text-0.7.0
Bump golang.org/x/text from 0.6.0 to 0.7.0
2023-02-19 01:17:38 +01:00
Alireza Ahmadi
bcf3fab524 Merge pull request #9 from alireza0:dependabot/go_modules/github.com/shirou/gopsutil-3.21.11incompatible
Bump github.com/shirou/gopsutil from 3.21.3+incompatible to 3.21.11+incompatible
2023-02-19 01:16:59 +01:00
Alireza Ahmadi
76919a1cc5 Merge pull request #10 from alireza0:dependabot/go_modules/gorm.io/gorm-1.24.5
Bump gorm.io/gorm from 1.21.9 to 1.24.5
2023-02-19 01:16:35 +01:00
dependabot[bot]
cef6e13441 Bump github.com/shirou/gopsutil
Bumps [github.com/shirou/gopsutil](https://github.com/shirou/gopsutil) from 3.21.3+incompatible to 3.21.11+incompatible.
- [Release notes](https://github.com/shirou/gopsutil/releases)
- [Commits](https://github.com/shirou/gopsutil/compare/v3.21.3...v3.21.11)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-19 00:11:30 +00:00
dependabot[bot]
0c8b58841f Bump gorm.io/gorm from 1.21.9 to 1.24.5
Bumps [gorm.io/gorm](https://github.com/go-gorm/gorm) from 1.21.9 to 1.24.5.
- [Release notes](https://github.com/go-gorm/gorm/releases)
- [Commits](https://github.com/go-gorm/gorm/compare/v1.21.9...v1.24.5)

---
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-02-19 00:11:29 +00:00
dependabot[bot]
c8043bf9e2 Bump golang.org/x/text from 0.6.0 to 0.7.0
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.6.0 to 0.7.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.6.0...v0.7.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-02-19 00:11:28 +00:00
Alireza Ahmadi
e9b7ba8b11 Merge pull request #11 from alireza0:dependabot/go_modules/github.com/gin-gonic/gin-1.8.2
Bump github.com/gin-gonic/gin from 1.7.1 to 1.8.2
2023-02-19 01:10:21 +01:00
Alireza Ahmadi
5bee682d39 tls options 2023-02-19 00:38:54 +01:00
Alireza Ahmadi
ef0dd10fe7 Simplify codes with fix small view problems 2023-02-18 16:50:40 +01:00
Alireza Ahmadi
9ea9d4d4dd Remove case sensitivity in search 2023-02-18 16:47:16 +01:00
Alireza Ahmadi
d2e313a588 Fix bug: Edit inbound 2023-02-18 12:41:26 +01:00
Alireza Ahmadi
35de896e5e Fix formats 2023-02-16 11:12:09 +01:00
dependabot[bot]
1c00da7537 Bump docker/setup-qemu-action from 1 to 2
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 1 to 2.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v1...v2)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-16 08:40:51 +00:00
Alireza Ahmadi
1700e85d06 Update docker.yml 2023-02-16 09:40:01 +01:00
Alireza Ahmadi
931e949442 docker workflow 2023-02-15 23:04:58 +01:00
Alireza Ahmadi
8bca416142 Ready for new version 2023-02-15 21:29:01 +01:00
dependabot[bot]
5407e2330b Bump github.com/gin-gonic/gin from 1.7.1 to 1.8.2
Bumps [github.com/gin-gonic/gin](https://github.com/gin-gonic/gin) from 1.7.1 to 1.8.2.
- [Release notes](https://github.com/gin-gonic/gin/releases)
- [Changelog](https://github.com/gin-gonic/gin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/gin-gonic/gin/compare/v1.7.1...v1.8.2)

---
updated-dependencies:
- dependency-name: github.com/gin-gonic/gin
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-15 19:54:15 +00:00
dependabot[bot]
cb2c2b1f5c Bump actions/checkout from 2 to 3
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-15 19:53:33 +00:00
dependabot[bot]
f131f2512b Bump actions/setup-go from 2 to 3
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 2 to 3.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-15 19:53:30 +00:00
Alireza Ahmadi
adf2b96f38 small github related changes 2023-02-15 20:52:01 +01:00
Alireza Ahmadi
e47ea983bd New xray settings options 2023-02-15 20:51:25 +01:00
Alireza Ahmadi
b62f747e88 Default timezone change to Tehran 2023-02-15 16:35:19 +01:00
Alireza Ahmadi
784e6e24e2 Enhanced search 2023-02-15 12:33:34 +01:00
Alireza Ahmadi
4af4aadd14 infoModal add more info 2023-02-15 12:33:13 +01:00
Alireza Ahmadi
9b3415865a SMall change in view and Readme 2023-02-15 12:21:18 +01:00
Alireza Ahmadi
8b6ea9e0d5 initiate email field for new client 2023-02-12 12:43:38 +01:00
Alireza Ahmadi
2a6f678b53 version increase to 0.2.2 2023-02-12 01:35:36 +01:00
Alireza Ahmadi
7225f00c2b revert github builder to ubuntu 18.04 2023-02-12 01:26:26 +01:00
Alireza Ahmadi
85db586d95 revert github builder to ubuntu 18.04 2023-02-12 01:24:35 +01:00
Alireza Ahmadi
2d7663f971 Merge pull request #2 from alireza0:fix-remark
Add user's email to remark if exists
Fix false reports
Fix some translations
Fix dockerfile
2023-02-11 17:50:01 +01:00
Alireza Ahmadi
c935014ba9 Increse version 2023-02-11 17:07:28 +01:00
Alireza Ahmadi
2c61b5a426 Fix dockerfile 2023-02-11 17:07:00 +01:00
Alireza Ahmadi
a5cfe4fc1e Fix in translations 2023-02-11 17:05:58 +01:00
Alireza Ahmadi
3cc3b3b04d Fix false reports 2023-02-11 17:05:35 +01:00
Alireza Ahmadi
8e550bc308 Add user's email to remark if exists 2023-02-11 14:48:02 +01:00
89 changed files with 6946 additions and 2411 deletions

38
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -0,0 +1,10 @@
---
name: Question template
about: Ask if it is not clear that it is a bug
title: ''
labels: question
assignees: ''
---

10
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"

View File

@@ -1,49 +1,55 @@
name: Docker Image CI
on:
push:
branches: [ master ]
tags:
- "*"
workflow_dispatch:
inputs:
project:
description: 'Project'
required: true
default: ''
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
submodules: true
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: |
alireza7/x-ui
ghcr.io/alireza0/x-ui
tags: |
type=ref,event=branch
type=ref,event=tag
type=pep440,pattern={{version}}
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Docker Hub login
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKER_HUB_TOKEN }}
run: |
echo "${DOCKERHUB_TOKEN}" | docker login --username ${DOCKERHUB_USERNAME} --password-stdin
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
id: buildx
uses: crazy-max/ghaction-docker-buildx@v1
with:
buildx-version: latest
uses: docker/setup-buildx-action@v2
- name: Build Dockerfile
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }}
DOCKERHUB_REPO: ${{ secrets.DOCKER_HUB_REPOSITORY }}
run: |
docker buildx build \
--platform=linux/amd64,linux/arm64 \
--output "type=image,push=true" \
--file ./Dockerfile ./ \
--tag $(echo "${DOCKERHUB_USERNAME}" | tr '[:upper:]' '[:lower:]')/${{ github.event.inputs.project }}:latest
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
push: true
platforms: linux/amd64,linux/arm64/v8
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -4,32 +4,17 @@ on:
tags:
- "*"
workflow_dispatch:
jobs:
release:
runs-on: ubuntu-22.04
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
steps:
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GAYHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
draft: true
prerelease: true
linuxamd64build:
name: build x-ui amd64 version
needs: release
runs-on: ubuntu-22.04
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v4
with:
go-version: '1.20'
go-version: 'stable'
- name: build linux amd64 version
run: |
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o xui-release -v main.go
@@ -41,7 +26,7 @@ jobs:
mv xui-release x-ui
mkdir bin
cd bin
wget https://github.com/xtls/xray-core/releases/latest/download/Xray-linux-64.zip
wget https://github.com/XTLS/Xray-core/releases/download/v1.8.1/Xray-linux-64.zip
unzip Xray-linux-64.zip
rm -f Xray-linux-64.zip geoip.dat geosite.dat
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
@@ -52,22 +37,20 @@ jobs:
- name: package
run: tar -zcvf x-ui-linux-amd64.tar.gz x-ui
- name: upload
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GAYHUB_TOKEN }}
uses: svenstaro/upload-release-action@2.5.0
with:
upload_url: ${{ needs.release.outputs.upload_url }}
asset_path: x-ui-linux-amd64.tar.gz
repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref }}
file: x-ui-linux-amd64.tar.gz
asset_name: x-ui-linux-amd64.tar.gz
asset_content_type: application/gzip
prerelease: true
linuxarm64build:
name: build x-ui arm64 version
needs: release
runs-on: ubuntu-22.04
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v4
with:
go-version: '1.20'
- name: build linux arm64 version
@@ -83,7 +66,7 @@ jobs:
mv xui-release x-ui
mkdir bin
cd bin
wget https://github.com/xtls/xray-core/releases/latest/download/Xray-linux-arm64-v8a.zip
wget https://github.com/xtls/xray-core/releases/download/v1.8.1/Xray-linux-arm64-v8a.zip
unzip Xray-linux-arm64-v8a.zip
rm -f Xray-linux-arm64-v8a.zip geoip.dat geosite.dat
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
@@ -94,22 +77,20 @@ jobs:
- name: package
run: tar -zcvf x-ui-linux-arm64.tar.gz x-ui
- name: upload
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GAYHUB_TOKEN }}
uses: svenstaro/upload-release-action@2.5.0
with:
upload_url: ${{ needs.release.outputs.upload_url }}
asset_path: x-ui-linux-arm64.tar.gz
repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref }}
file: x-ui-linux-arm64.tar.gz
asset_name: x-ui-linux-arm64.tar.gz
asset_content_type: application/gzip
prerelease: true
linuxs390xbuild:
name: build x-ui s390x version
needs: release
runs-on: ubuntu-22.04
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v4
with:
go-version: '1.20'
- name: build linux s390x version
@@ -125,7 +106,7 @@ jobs:
mv xui-release x-ui
mkdir bin
cd bin
wget https://github.com/xtls/xray-core/releases/latest/download/Xray-linux-s390x.zip
wget https://github.com/xtls/xray-core/releases/download/v1.8.1/Xray-linux-s390x.zip
unzip Xray-linux-s390x.zip
rm -f Xray-linux-s390x.zip geoip.dat geosite.dat
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
@@ -136,11 +117,10 @@ jobs:
- name: package
run: tar -zcvf x-ui-linux-s390x.tar.gz x-ui
- name: upload
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GAYHUB_TOKEN }}
uses: svenstaro/upload-release-action@2.5.0
with:
upload_url: ${{ needs.release.outputs.upload_url }}
asset_path: x-ui-linux-s390x.tar.gz
repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref }}
file: x-ui-linux-s390x.tar.gz
asset_name: x-ui-linux-s390x.tar.gz
asset_content_type: application/gzip
prerelease: true

20
DockerInitFiles.sh Executable file
View File

@@ -0,0 +1,20 @@
#!/bin/sh
if [ $1 == "amd64" ]; then
ARCH="64";
FNAME="amd64";
elif [ $1 == "arm64" ]; then
ARCH="arm64-v8a"
FNAME="arm64";
else
ARCH="64";
FNAME="amd64";
fi
mkdir -p build/bin
cd build/bin
wget "https://github.com/XTLS/Xray-core/releases/download/v1.8.1/Xray-linux-${ARCH}.zip"
unzip "Xray-linux-${ARCH}.zip"
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat
mv xray "xray-linux-${FNAME}"
wget "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat"
wget "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat"
cd ../../

View File

@@ -1,16 +1,18 @@
FROM golang:1.20-alpine AS builder
WORKDIR /app
ENV CGO_ENABLED 1
RUN apk add gcc && apk --no-cache --update add build-base
ARG TARGETARCH
RUN apk --no-cache --update add build-base gcc wget unzip
COPY . .
RUN go build main.go
RUN env CGO_ENABLED=1 go build -o build/x-ui main.go
RUN ./DockerInitFiles.sh "$TARGETARCH"
FROM alpine
LABEL org.opencontainers.image.authors="alireza7@gmail.com"
RUN apk add ca-certificates tzdata
ENV TZ=Asia/Tehran
WORKDIR /app
COPY --from=builder /app/main /app/x-ui
COPY ./bin/. /app/bin/.
RUN apk add ca-certificates tzdata
COPY --from=builder /app/build/ /app/
VOLUME [ "/etc/x-ui" ]
CMD [ "./x-ui" ]

165
README.md
View File

@@ -1,43 +1,99 @@
# x-ui
> **Disclaimer: This project is only for personal learning and communication, please do not use it for illegal purposes, please do not use it in a production environment**
![](https://img.shields.io/github/v/release/alireza0/x-ui.svg)
![](https://img.shields.io/docker/pulls/alireza7/x-ui.svg)
[![Go Report Card](https://goreportcard.com/badge/github.com/alireza0/x-ui)](https://goreportcard.com/report/github.com/alireza0/x-ui)
[![Downloads](https://img.shields.io/github/downloads/alireza0/x-ui/total.svg)](https://img.shields.io/github/downloads/alireza0/x-ui/total.svg)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html)
> **Disclaimer: This project is only for personal learning and communication, please do not use it for illegal purposes, please do not use it in a production environment**
xray panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese)**
| Features | Enable? |
| ------------- |:-------------:|
| Multi-lang | :heavy_check_mark: |
| Search in deep | :heavy_check_mark: |
| Inbound Multi User | :heavy_check_mark: |
| Features | Enable? |
| ------------------------------------ | :----------------: |
| Multi-lang | :heavy_check_mark: |
| Dark/Light Theme | :heavy_check_mark: |
| Search in deep | :heavy_check_mark: |
| Inbound Multi User | :heavy_check_mark: |
| Multi User Traffic & Expiration time | :heavy_check_mark: |
| REST API | :heavy_check_mark: |
| Telegram BOT | :heavy_check_mark: |
| REST API | :heavy_check_mark: |
| Telegram BOT (admin + clients) | :heavy_check_mark: |
| Backup database using Telegram BOT | :heavy_check_mark: |
| Subscription link + userInfo | :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:
**If you think this project is helpful to you, you may wish to give a** :star2:
# Features
- System Status Monitoring
- Search within all inbounds and clients
- Support Dark/Light theme UI
- Support multi-user multi-protocol, web page visualization operation
- Supported protocols: vmess, vless, trojan, shadowsocks, dokodemo-door, socks, http
- 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
# Inbouds picture
## 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` | `"/:id/delClient/:clientId"` | Delete Client by UID/Password as clientId |
| `POST` | `"/updateClient/:clientId"` | Update Client by UID/Password as clientId |
| `POST` | `"/:id/resetClientTraffic/:email"` | Reset Client's Traffic |
| `POST` | `"/resetAllTraffics"` | Reset traffics of all inbounds |
| `POST` | `"/resetAllClientTraffics/:id"` | Reset inbound clients traffics (-1: all) |
| `POST` | `"/delDepletedClients/:id"` | Delete inbound depleted clients (-1: all) |
# Environment Variables
| Variable | Type | Default |
| -------------- | :--------------------------------------------: | :------------ |
| XUI_LOG_LEVEL | `"debug"` \| `"info"` \| `"warn"` \| `"error"` | `"info"` |
| XUI_DEBUG | `boolean` | `false` |
| XUI_BIN_FOLDER | `string` | `"bin"` |
| XUI_DB_FOLDER | `string` | `"/etc/x-ui"` |
# Screenshot from Inbouds page
![inbounds](./media/inbounds.png)
![Dark inbounds](./media/inbounds-dark.png)
# Install & Upgrade
## suggestion system
- CentOS 8+
- Ubuntu 20+
- Debian 10+
- Fedora 36+
# Install & Upgrade to latest version
```
bash <(curl -Ls https://raw.githubusercontent.com/alireza0/x-ui/master/install.sh)
```
## Install custom version
To install your desired version you can add the version to the end of install command. Example for ver `0.5.2`:
```
bash <(curl -Ls https://raw.githubusercontent.com/alireza0/x-ui/master/install.sh) 0.5.2
```
## Manual install & upgrade
1. First download the latest compressed package from https://github.com/alireza0/x-ui/releases , generally choose Architecture `amd64`
@@ -60,8 +116,6 @@ systemctl restart x-ui
## Install using docker
> This docker tutorial and docker image are provided by [alireza0](https://github.com/alireza0)
1. install docker
```shell
@@ -72,11 +126,13 @@ curl -fsSL https://get.docker.com | sh
```shell
mkdir x-ui && cd x-ui
docker run -itd --network=host \
docker run -itd \
-p 54321:54321 -p 443:443 -p 80:80 \
-e XRAY_VMESS_AEAD_FORCED=false \
-v $PWD/db/:/etc/x-ui/ \
-v $PWD/cert/:/root/cert/ \
--name x-ui --restart=unless-stopped \
alireza0/x-ui:latest
alireza7/x-ui:latest
```
> Build your own image
@@ -87,9 +143,8 @@ docker build -t x-ui .
## SSL certificate application
### Cloudflare
> This feature and tutorial are provided by [FranzKafkaYu](https://github.com/FranzKafkaYu)
<details>
<summary>Click for details</summary>
### Certbot
@@ -101,42 +156,52 @@ ln -s /snap/bin/certbot /usr/bin/certbot
certbot certonly --standalone --register-unsafely-without-email --non-interactive --agree-tos -d <Your Domain Name>
```
## Tg robot use (under development, temporarily unavailable)
</details>
> This feature and tutorial are provided by [FranzKafkaYu](https://github.com/FranzKafkaYu)
## Tg robot use
<details>
<summary>Click for details</summary>
X-UI supports daily traffic notification, panel login reminder and other functions through the Tg robot. To use the Tg robot, you need to apply for the specific application tutorial. You can refer to the [blog](https://coderfan.net/how-to-use-telegram-bot-to-alarm-you-when-someone-login-into-your-vps.html)
Set the robot-related parameters in the panel background, including:
- Tg Robot Token
- Tg Robot ChatId
- Tg robot Token
- Tg robot ChatId
- Tg robot cycle runtime, in crontab syntax
- Tg robot Expiration threshold
- Tg robot Traffic threshold
- Tg robot Enable send backup in cycle runtime
- Tg robot Enable CPU usage alarm threshold
Reference syntax:
- 30 * * * * * //Notify at the 30s of each point
- 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
- TG notification content:
- Node traffic usage
- Panel login reminder
- Node expiration reminder
- Traffic warning reminder
### Telegram Bot Features
More features are planned...
- Report periodic
- Login notification
- CPU threshold notification
- Threshold for Expiration time and Traffic to report in advance
- 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 depleted users
- Receive backup by request and in periodic reports
</details>
# Common problem
## suggestion system
- CentOS 7+
- Ubuntu 16+
- Debian 8+
# 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`
@@ -149,14 +214,17 @@ x-ui v2-ui
# T-Shoots:
**If you ygrade from an old version or other forks, for enable traffic for users you should do :**
**If you upgrade from an old version or other forks, for enable traffic for users you should do :**
find this in config :
``` json
find this in config :
```json
"policy": {
"system": {
```
**and add this just after ` "policy": {` :**
**and add this just after ` "policy": {` :**
```json
"levels": {
"0": {
@@ -166,8 +234,8 @@ find this in config :
},
```
**the final output is like :**
```json
"policy": {
"levels": {
@@ -184,8 +252,15 @@ find this in config :
},
"routing": {
```
restart panel
restart panel
</details>
# a special thanks to
- [HexaSoftwareTech](https://github.com/HexaSoftwareTech/)
- [MHSanaei](https://github.com/MHSanaei)
## Stargazers over time

View File

@@ -45,6 +45,22 @@ func IsDebug() bool {
return os.Getenv("XUI_DEBUG") == "true"
}
func GetDBPath() string {
return fmt.Sprintf("/etc/%s/%s.db", GetName(), GetName())
func GetBinFolderPath() string {
binFolderPath := os.Getenv("XUI_BIN_FOLDER")
if binFolderPath == "" {
binFolderPath = "bin"
}
return binFolderPath
}
func GetDBFolderPath() string {
dbFolderPath := os.Getenv("XUI_DB_FOLDER")
if dbFolderPath == "" {
dbFolderPath = "/etc/x-ui"
}
return dbFolderPath
}
func GetDBPath() string {
return fmt.Sprintf("%s/%s.db", GetDBFolderPath(), GetName())
}

View File

@@ -1 +1 @@
0.2.0
1.1.0

View File

@@ -68,9 +68,13 @@ type Setting struct {
type Client struct {
ID string `json:"id"`
Password string `json:"password"`
Flow string `json:"flow"`
AlterIds uint16 `json:"alterId"`
Email string `json:"email"`
Security string `json:"security"`
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"`
}

Binary file not shown.

View File

@@ -7,4 +7,6 @@ services:
- $PWD/db/:/etc/x-ui/
- $PWD/cert/:/root/cert/
restart: unless-stopped
network_mode: host
ports:
- 54321:54321
- 443:443

74
go.mod
View File

@@ -3,50 +3,60 @@ module x-ui
go 1.20
require (
github.com/BurntSushi/toml v1.0.0
github.com/Workiva/go-datastructures v1.0.53
github.com/gin-contrib/sessions v0.0.3
github.com/gin-gonic/gin v1.7.1
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.21.3+incompatible
github.com/xtls/xray-core v1.7.5
github.com/shirou/gopsutil/v3 v3.23.3
github.com/xtls/xray-core v1.8.1
go.uber.org/atomic v1.10.0
golang.org/x/text v0.6.0
google.golang.org/grpc v1.53.0
gorm.io/driver/sqlite v1.1.4
gorm.io/gorm v1.21.9
golang.org/x/text v0.9.0
google.golang.org/grpc v1.54.0
gorm.io/driver/sqlite v1.5.0
gorm.io/gorm v1.25.0
)
require (
github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46 // indirect
github.com/BurntSushi/toml v1.2.1 // indirect
github.com/bytedance/sonic v1.8.7 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-ole/go-ole v1.2.5 // indirect
github.com/go-playground/locales v0.13.0 // indirect
github.com/go-playground/universal-translator v0.17.0 // indirect
github.com/go-playground/validator/v10 v10.4.1 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.12.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
github.com/gorilla/sessions v1.1.3 // indirect
github.com/gorilla/sessions v1.2.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.2 // indirect
github.com/json-iterator/go v1.1.9 // indirect
github.com/leodido/go-urn v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.12 // indirect
github.com/mattn/go-sqlite3 v1.14.5 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.3 // indirect
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect
github.com/mattn/go-isatty v0.0.18 // indirect
github.com/mattn/go-sqlite3 v1.14.16 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/pires/go-proxyproto v0.6.2 // indirect
github.com/tklauser/go-sysconf v0.3.5 // indirect
github.com/tklauser/numcpus v0.2.2 // indirect
github.com/ugorji/go/codec v1.1.7 // indirect
golang.org/x/crypto v0.5.0 // indirect
golang.org/x/net v0.5.0 // indirect
golang.org/x/sys v0.5.0 // indirect
google.golang.org/genproto v0.0.0-20230202175211-008b39050e57 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pires/go-proxyproto v0.7.0 // indirect
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect
github.com/shoenig/go-m1cpu v0.1.5 // indirect
github.com/tklauser/go-sysconf v0.3.11 // indirect
github.com/tklauser/numcpus v0.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.8.0 // indirect
golang.org/x/net v0.9.0 // indirect
golang.org/x/sys v0.7.0 // indirect
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

527
go.sum
View File

@@ -1,458 +1,281 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU=
github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46 h1:5sXbqlSomvdjlRbWyNqkPsJ3Fg+tQZCbgeX1VGljbQY=
github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/Workiva/go-datastructures v1.0.53 h1:J6Y/52yX10Xc5JjXmGtWoSSxs3mZnGSaq37xZZh7Yig=
github.com/Workiva/go-datastructures v1.0.53/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/antonlindstrom/pgstore v0.0.0-20200229204646-b08ebf1105e0/go.mod h1:2Ti6VUHVxpC0VSmTZzEvpzysnaGAfGBOoMIz5ykPyyw=
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA=
github.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/bytedance/sonic v1.8.7 h1:d3sry5vGgVq/OpgozRUNP6xBsSo0mtNdwliApw+SAMQ=
github.com/bytedance/sonic v1.8.7/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165 h1:BS21ZUJ/B5X2UVUbczfmdWH7GapPWAhxcMsDnjJTU1E=
github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 h1:Mn26/9ZMNWSw9C9ERFA1PUxfmGpolnw2v0bKOREu5ew=
github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
github.com/gin-contrib/sessions v0.0.3 h1:PoBXki+44XdJdlgDqDrY5nDVe3Wk7wDV/UCOuLP6fBI=
github.com/gin-contrib/sessions v0.0.3/go.mod h1:8C/J6cad3Il1mWYYgtw0w+hqasmpvy25mPkXdOgeB9I=
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 h1:y7y0Oa6UawqTFPCDw9JG6pdKt4F9pAhHv0B7FMGaGD0=
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
github.com/gaukas/godicttls v0.0.3 h1:YNDIf0d9adcxOijiLrEzpfZGAkNwLRzPaG6OjU7EITk=
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
github.com/gin-contrib/sessions v0.0.4 h1:gq4fNa1Zmp564iHP5G6EBuktilEos8VKhe2sza1KMgo=
github.com/gin-contrib/sessions v0.0.4/go.mod h1:pQ3sIyviBBGcxgyR8mkeJuXbeV3h3NYmhJADQTq5+Vo=
github.com/gin-contrib/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfmF+0jE=
github.com/gin-contrib/sessions v0.0.5/go.mod h1:vYAuaUPqie3WUSsft6HUlCjlwwoJQs97miaG2+7neKY=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
github.com/gin-gonic/gin v1.7.1 h1:qC89GU3p8TvKWMAVhEpmpB2CIb1hnqt2UdKZaP93mS8=
github.com/gin-gonic/gin v1.7.1/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8=
github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k=
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-ole/go-ole v1.2.5 h1:t4MGB5xEDZvXI+0rMjjsfBsD7yAgp/s9ZDkL1JndXwY=
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=
github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=
github.com/go-playground/validator/v10 v10.12.0 h1:E4gtWgxWxp8YSxExrQFv5BpCahla0PVF2oTTEYaWQGI=
github.com/go-playground/validator/v10 v10.12.0/go.mod h1:hCAPuzYvKdP33pxWa+2+6AIKXEKqjIUyqsNCtbsSJrA=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
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.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/google/pprof v0.0.0-20230406165453-00490a63f317 h1:hFhpt7CTmR3DX+b4R19ydQFtofxT0Sv3QsKNMVQYTMQ=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU=
github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
github.com/h12w/go-socks5 v0.0.0-20200522160539-76189e178364/go.mod h1:eDJQioIyy4Yn3MVivT7rv/39gAJTrA7lgmYr8EW950c=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
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.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI=
github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
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=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/lucas-clemente/quic-go v0.20.0 h1:FSU3YN5VnLafHR27Ejs1r1CYMS7XMyIVDzRewkDLNBw=
github.com/lucas-clemente/quic-go v0.20.0/go.mod h1:fZq/HUDIM+mW6X6wtzORjC0E/WDBMKe5Hf9bgjISwLk=
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc=
github.com/marten-seemann/qtls-go1-15 v0.1.4 h1:RehYMOyRW8hPVEja1KBVsFVNSm35Jj9Mvs5yNoZZ28A=
github.com/marten-seemann/qtls-go1-15 v0.1.4/go.mod h1:GyFwywLKkRt+6mfU99csTEY1joMZz5vmB1WNZH3P81I=
github.com/marten-seemann/qtls-go1-16 v0.1.3 h1:XEZ1xGorVy9u+lJq+WXNE+hiqRYLNvJGYmwfwKQN2gU=
github.com/marten-seemann/qtls-go1-16 v0.1.3/go.mod h1:gNpI2Ol+lRS3WwSOtIUUtRwZEQMXjYK+dQSBFbethAk=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/leodido/go-urn v1.2.3 h1:6BE2vPT0lqoz3fmOesHZiaiFh7889ssCo2GMvLCfiuA=
github.com/leodido/go-urn v1.2.3/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a h1:N9zuLhTvBSRt0gWSiJswwQ2HqDmtX/ZCDJURnKUt1Ik=
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-sqlite3 v1.14.5 h1:1IdxlwTNazvbKJQSxoJ5/9ECbEeaTTyeU7sEAZ5KKTQ=
github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc=
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/miekg/dns v1.1.53 h1:ZBkuHr5dxHtB1caEOlZTLPo7D3L3TWckgUUs/RHfDxw=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/nicksnyder/go-i18n/v2 v2.2.1 h1:aOzRCdwsJuoExfZhoiXHy4bjruwCMdt5otbYojM/PaA=
github.com/nicksnyder/go-i18n/v2 v2.2.1/go.mod h1:fF2++lPHlo+/kPaj3nB0uxtPwzlPm+BlgwGX7MkeGj0=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM=
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml/v2 v2.0.7 h1:muncTPStnKRos5dpVKULv2FVd4bMOhNePj9CjgDb8Us=
github.com/pelletier/go-toml/v2 v2.0.7/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/pires/go-proxyproto v0.5.0 h1:A4Jv4ZCaV3AFJeGh5mGwkz4iuWUYMlQ7IoO/GTuSuLo=
github.com/pires/go-proxyproto v0.5.0/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY=
github.com/pires/go-proxyproto v0.6.2 h1:KAZ7UteSOt6urjme6ZldyFm4wDe/z0ZUP0Yv0Dos0d8=
github.com/pires/go-proxyproto v0.6.2/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
github.com/refraction-networking/utls v0.0.0-20201210053706-2179f286686b h1:lzo71oHzQEz0fKMSjR0BpVzuh2hOHvJTxnN3Rnikmtg=
github.com/refraction-networking/utls v0.0.0-20201210053706-2179f286686b/go.mod h1:tz9gX959MEFfFN5whTIocCLUG57WiILqtdVxI8c6Wj0=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3gMACTjAbMZBjXAqTOzOwFaj2Ld6cjeQ7Rig=
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
github.com/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U=
github.com/quic-go/qtls-go1-20 v0.2.2 h1:WLOPx6OY/hxtTxKV1Zrq20FtXtDEkeY00CGQm8GEa3E=
github.com/quic-go/quic-go v0.33.0 h1:ItNoTDN/Fm/zBlq769lLJc8ECe9gYaW40veHCCco7y0=
github.com/refraction-networking/utls v1.3.2 h1:o+AkWB57mkcoW36ET7uJ002CpBWHu0KPxi6vzxvPnv8=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/seiflotfy/cuckoofilter v0.0.0-20201222105146-bc6005554a0c h1:pqy40B3MQWYrza7YZXOXgl0Nf0QGFqrOC0BKae1UNAA=
github.com/seiflotfy/cuckoofilter v0.0.0-20201222105146-bc6005554a0c/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shirou/gopsutil v3.21.3+incompatible h1:uenXGGa8ESCQq+dbgtl916dmg6PSAz2cXov0uORQ9v8=
github.com/shirou/gopsutil v3.21.3+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/sagernet/sing v0.2.3 h1:V50MvZ4c3Iij2lYFWPlzL1PyipwSzjGeN9x+Ox89vpk=
github.com/sagernet/sing-shadowsocks v0.2.1 h1:FvdLQOqpvxHBJUcUe4fvgiYP2XLLwH5i1DtXQviVEPw=
github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c h1:vK2wyt9aWYHHvNLWniwijBu/n4pySypiKRhN32u/JGo=
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U=
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/go-m1cpu v0.1.5 h1:LF57Z/Fpb/WdGLjt2HZilNnmZOxg/q2bSKTQhgbrLrQ=
github.com/shoenig/go-m1cpu v0.1.5/go.mod h1:Wwvst4LR89UxjeFtLRMrpgRiyY4xPsejnVZym39dbAQ=
github.com/shoenig/test v0.6.3 h1:GVXWJFk9PiOjN0KoJ7VrJGH6uLPnqxR7/fe3HUPfE0c=
github.com/shoenig/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/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tinylib/msgp v1.1.5/go.mod h1:eQsjooMTnV42mHu917E26IogZ2930nFyBQdofk10Udg=
github.com/tklauser/go-sysconf v0.3.5 h1:uu3Xl4nkLzQfXNsWn15rPc/HQCJKObbt1dKJeWp3vU4=
github.com/tklauser/go-sysconf v0.3.5/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI=
github.com/tklauser/numcpus v0.2.2 h1:oyhllyrScuYI6g+h/zUvNXNp1wy7x8qQy3t/piefldA=
github.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZFu0T9wgjM=
github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM=
github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms=
github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4=
github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31/go.mod h1:onvgF043R+lC5RZ8IT9rBXDaEDnpnw/Cl+HFiw+v/7Q=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
github.com/xtls/go v0.0.0-20201118062508-3632bf3b7499 h1:QHESTXtfgc1ABV+ArlbPVqUx9Ht5I0dDkYhxYoXFxNo=
github.com/xtls/go v0.0.0-20201118062508-3632bf3b7499/go.mod h1:5TB2+k58gx4A4g2Nf5miSHNDF6CuAzHKpWBooLAshTs=
github.com/xtls/xray-core v1.4.2 h1:D0Le+Qy9L/eY5LbUQfrk7WJ8wbODpQSW/ZRCg+BRe7c=
github.com/xtls/xray-core v1.4.2/go.mod h1:DmL/9rOCliev/a6HciWEvSJVEhUF6C0EpD3clW8v0pc=
github.com/xtls/xray-core v1.7.5 h1:Ukr3hXnOG2ciViQL7kfYRl9S3GVej2dkV7DzabmoLL4=
github.com/xtls/xray-core v1.7.5/go.mod h1:Mx1QzIDvSk4eZ8hKa3AYsSPfyZJNQXWVXTJxJRJ98wI=
github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU=
github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI=
github.com/xtls/reality v0.0.0-20230331223127-176a94313eda h1:psRJD2RrZbnI0OWyHvXfgYCPqlRM5q5SPDcjDoDBWhE=
github.com/xtls/xray-core v1.8.1 h1:iSTTqXj82ZdwC1ah+eV331X4JTcnrDz+WuKuB/EB3P4=
github.com/xtls/xray-core v1.8.1/go.mod h1:AXxSso0MZwUE4NhRocCfHCg73BtJ+T2dSpQVo1Cg9VM=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
go.starlark.net v0.0.0-20210312235212-74c10e2c17dc h1:pVkptfeOTFfx+zXZo7HEHN3d5LmhatBFvHdm/f2QnpY=
go.starlark.net v0.0.0-20210312235212-74c10e2c17dc/go.mod h1:t3mmBBPzAVvK0L0n1drDmrQsJ8FoIx4INCqVMTr/Zo0=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 h1:Ss6D3hLXTM0KobyBYEAygXzFfGcjnmfEJOBgSbemCtg=
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210330230544-e57232859fb2/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201231184435-2d18734c6014/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
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 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
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-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20230202175211-008b39050e57 h1:vArvWooPH749rNHpBGgVl+U9B9dATjiEhJzcWGlovNs=
google.golang.org/genproto v0.0.0-20230202175211-008b39050e57/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.38.0 h1:/9BgsAsa5nWe26HqOlvlgJnqBuktYOLCgjCPqsa56W0=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag=
google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c h1:grhR+C34yXImVGp7EzNk+DTIk+323eIUWOmEevy6bDo=
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.1.4 h1:PDzwYE+sI6De2+mxAneV9Xs11+ZyKV6oxD3wDGkaNvM=
gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw=
gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
gorm.io/gorm v1.21.9 h1:INieZtn4P2Pw6xPJ8MzT0G4WUOsHq3RhfuDF1M6GW0E=
gorm.io/gorm v1.21.9/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
h12.io/socks v1.0.2/go.mod h1:AIhxy1jOId/XCz9BO+EIgNL2rQiPTBNnOfnVnQ+3Eck=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.5.0 h1:zKYbzRCpBrT1bNijRnxLDJWPjVfImGEn0lSnUY5gZ+c=
gorm.io/driver/sqlite v1.5.0/go.mod h1:kDMDfntV9u/vuMmz8APHtHF0b4nyBB7sfCieC6G8k8I=
gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/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,26 +8,20 @@ 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
if [[ -f /etc/redhat-release ]]; then
release="centos"
elif cat /etc/issue | grep -Eqi "debian"; then
release="debian"
elif cat /etc/issue | grep -Eqi "ubuntu"; then
release="ubuntu"
elif cat /etc/issue | grep -Eqi "centos|red hat|redhat"; then
release="centos"
elif cat /proc/version | grep -Eqi "debian"; then
release="debian"
elif cat /proc/version | grep -Eqi "ubuntu"; then
release="ubuntu"
elif cat /proc/version | grep -Eqi "centos|red hat|redhat"; then
release="centos"
# Check OS and set release variable
if [[ -f /etc/os-release ]]; then
source /etc/os-release
release=$ID
elif [[ -f /usr/lib/os-release ]]; then
source /usr/lib/os-release
release=$ID
else
echo -e "${red} Check system OS failed, please contact the author! ${plain}\n" && exit 1
echo "Failed to check the system OS, please contact the author!" >&2
exit 1
fi
echo "The OS release is: $release"
arch=$(arch)
@@ -39,42 +33,44 @@ elif [[ $arch == "s390x" ]]; then
arch="s390x"
else
arch="amd64"
echo -e "${red} Fail to check system arch, will use default arch: ${arch}${plain}"
echo -e "${red} Failed to check system arch, will use default arch: ${arch}${plain}"
fi
echo "arch: ${arch}"
if [ $(getconf WORD_BIT) != '32' ] && [ $(getconf LONG_BIT) != '64' ]; then
echo "x-ui dosen't support 32-bit(x86) system, please use 64 bit operating system(x86_64) instead,if there is something wrong, plz let me know"
echo "x-ui dosen't support 32-bit(x86) system, please use 64 bit operating system(x86_64) instead, if there is something wrong, please get in touch with me!"
exit -1
fi
os_version=""
os_version=$(grep -i version_id /etc/os-release | cut -d \" -f2 | cut -d . -f1)
# os version
if [[ -f /etc/os-release ]]; then
os_version=$(awk -F'[= ."]' '/VERSION_ID/{print $3}' /etc/os-release)
fi
if [[ -z "$os_version" && -f /etc/lsb-release ]]; then
os_version=$(awk -F'[= ."]+' '/DISTRIB_RELEASE/{print $2}' /etc/lsb-release)
fi
if [[ x"${release}" == x"centos" ]]; then
if [[ ${os_version} -le 6 ]]; then
echo -e "${red} Please use CentOS 7 or higher version ${plain}\n" && exit 1
fi
elif [[ x"${release}" == x"ubuntu" ]]; then
if [[ ${os_version} -lt 16 ]]; then
echo -e "${red} Please use Ubuntu 16 or higher version ${plain}\n" && exit 1
fi
elif [[ x"${release}" == x"debian" ]]; then
if [[ "${release}" == "centos" ]]; then
if [[ ${os_version} -lt 8 ]]; then
echo -e "${red} Please use Debian 8 or higher version ${plain}\n" && exit 1
echo -e "${red} Please use CentOS 8 or higher ${plain}\n" && exit 1
fi
elif [[ "${release}" == "ubuntu" ]]; then
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
fi
elif [[ "${release}" == "debian" ]]; then
if [[ ${os_version} -lt 10 ]]; then
echo -e "${red} Please use Debian 10 or higher ${plain}\n" && exit 1
fi
else
echo -e "${red}Failed to check the OS version, please contact the author!${plain}" && exit 1
fi
install_base() {
if [[ x"${release}" == x"centos" ]]; then
if [[ "${release}" == "centos" ]] || [[ "${release}" == "fedora" ]] ; then
yum install wget curl tar -y
else
apt install wget curl tar -y
@@ -83,6 +79,7 @@ install_base() {
#This function will be called when user installed x-ui out of sercurity
config_after_install() {
/usr/local/x-ui/x-ui migrate
echo -e "${yellow}Install/update finished! For security it's recommended to modify panel settings ${plain}"
read -p "Do you want to continue with the modification [y/n]? ": config_confirm
if [[ x"${config_confirm}" == x"y" || x"${config_confirm}" == x"Y" ]]; then

44
main.go
View File

@@ -51,8 +51,8 @@ func runWebServer() {
}
sigCh := make(chan os.Signal, 1)
//信号量捕获处理
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGKILL)
// Trap shutdown signals
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM)
for {
sig := <-sigCh
@@ -97,7 +97,7 @@ func showSetting(show bool) {
settingService := service.SettingService{}
port, err := settingService.GetPort()
if err != nil {
fmt.Println("get current port fialed,error info:", err)
fmt.Println("get current port failed,error info:", err)
}
userService := service.UserService{}
userModel, err := userService.GetFirstUser()
@@ -109,7 +109,7 @@ func showSetting(show bool) {
if (username == "") || (userpasswd == "") {
fmt.Println("current username or password is empty")
}
fmt.Println("current pannel settings as follows:")
fmt.Println("current panel settings as follows:")
fmt.Println("username:", username)
fmt.Println("userpasswd:", userpasswd)
fmt.Println("port:", port)
@@ -133,10 +133,9 @@ func updateTgbotEnableSts(status bool) {
logger.Infof("SetTgbotenabled[%v] success", status)
}
}
return
}
func updateTgbotSetting(tgBotToken string, tgBotChatid int, tgBotRuntime string) {
func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime string) {
err := database.InitDB(config.GetDBPath())
if err != nil {
fmt.Println(err)
@@ -165,7 +164,7 @@ func updateTgbotSetting(tgBotToken string, tgBotChatid int, tgBotRuntime string)
}
}
if tgBotChatid != 0 {
if tgBotChatid != "" {
err := settingService.SetTgBotChatId(tgBotChatid)
if err != nil {
fmt.Println(err)
@@ -204,6 +203,19 @@ func updateSetting(port int, username string, password string) {
}
}
func migrateDb() {
inboundService := service.InboundService{}
err := database.InitDB(config.GetDBPath())
if err != nil {
log.Fatal(err)
}
fmt.Println("Start migrating database...")
inboundService.MigrationRequirements()
inboundService.RemoveOrphanedTraffics()
fmt.Println("Migration done!")
}
func main() {
if len(os.Args) < 2 {
runWebServer()
@@ -217,14 +229,14 @@ func main() {
v2uiCmd := flag.NewFlagSet("v2-ui", flag.ExitOnError)
var dbPath string
v2uiCmd.StringVar(&dbPath, "db", "/etc/v2-ui/v2-ui.db", "set v2-ui db file path")
v2uiCmd.StringVar(&dbPath, "db", fmt.Sprintf("%s/v2-ui.db", config.GetDBFolderPath()), "set v2-ui db file path")
settingCmd := flag.NewFlagSet("setting", flag.ExitOnError)
var port int
var username string
var password string
var tgbottoken string
var tgbotchatid int
var tgbotchatid string
var enabletgbot bool
var tgbotRuntime string
var reset bool
@@ -234,9 +246,9 @@ func main() {
settingCmd.IntVar(&port, "port", 0, "set panel port")
settingCmd.StringVar(&username, "username", "", "set login username")
settingCmd.StringVar(&password, "password", "", "set login password")
settingCmd.StringVar(&tgbottoken, "tgbottoken", "", "set telegrame bot token")
settingCmd.StringVar(&tgbotRuntime, "tgbotRuntime", "", "set telegrame bot cron time")
settingCmd.IntVar(&tgbotchatid, "tgbotchatid", 0, "set telegrame bot chat id")
settingCmd.StringVar(&tgbottoken, "tgbottoken", "", "set telegram bot token")
settingCmd.StringVar(&tgbotRuntime, "tgbotRuntime", "", "set telegram bot cron time")
settingCmd.StringVar(&tgbotchatid, "tgbotchatid", "", "set telegram bot chat id")
settingCmd.BoolVar(&enabletgbot, "enabletgbot", false, "enable telegram bot notify")
oldUsage := flag.Usage
@@ -246,6 +258,7 @@ func main() {
fmt.Println("Commands:")
fmt.Println(" run run web panel")
fmt.Println(" v2-ui migrate form v2-ui")
fmt.Println(" migrate migrate form other/old x-ui")
fmt.Println(" setting set settings")
}
@@ -263,6 +276,8 @@ func main() {
return
}
runWebServer()
case "migrate":
migrateDb()
case "v2-ui":
err := v2uiCmd.Parse(os.Args[2:])
if err != nil {
@@ -287,9 +302,12 @@ func main() {
if show {
showSetting(show)
}
if (tgbottoken != "") || (tgbotchatid != 0) || (tgbotRuntime != "") {
if (tgbottoken != "") || (tgbotchatid != "") || (tgbotRuntime != "") {
updateTgbotSetting(tgbottoken, tgbotchatid, tgbotRuntime)
}
if enabletgbot {
updateTgbotEnableSts(enabletgbot)
}
default:
fmt.Println("except 'run' or 'v2-ui' or 'setting' subcommands")
fmt.Println()

BIN
media/inbounds-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 258 KiB

After

Width:  |  Height:  |  Size: 316 KiB

View File

@@ -4,5 +4,5 @@ import (
_ "unsafe"
)
//go:linkname HostProc github.com/shirou/gopsutil/internal/common.HostProc
//go:linkname HostProc github.com/shirou/gopsutil/v3/internal/common.HostProc
func HostProc(combineWith ...string) string

View File

@@ -1,9 +1,10 @@
//go:build darwin
// +build darwin
package sys
import (
"github.com/shirou/gopsutil/net"
"github.com/shirou/gopsutil/v3/net"
)
func GetTCPCount() (int, error) {

View File

@@ -1,3 +1,4 @@
//go:build linux
// +build linux
package sys

24
util/sys/sys_windows.go Normal file
View File

@@ -0,0 +1,24 @@
//go:build windows
// +build windows
package sys
import (
"github.com/shirou/gopsutil/v3/net"
)
func GetTCPCount() (int, error) {
stats, err := net.Connections("tcp")
if err != nil {
return 0, err
}
return len(stats), nil
}
func GetUDPCount() (int, error) {
stats, err := net.Connections("udp")
if err != nil {
return 0, err
}
return len(stats), nil
}

File diff suppressed because one or more lines are too long

View File

@@ -1,152 +1,391 @@
#app {
height: 100%;
}
.ant-space {
width: 100%;
}
.ant-layout-sider-zero-width-trigger {
display: none;
}
.ant-card {
border-radius: 30px;
}
.ant-card-hoverable {
cursor: auto;
}
.ant-card+.ant-card {
margin-top: 20px;
}
.drawer-handle {
position: absolute;
top: 72px;
width: 41px;
height: 40px;
cursor: pointer;
z-index: 0;
text-align: center;
line-height: 40px;
font-size: 16px;
display: flex;
justify-content: center;
align-items: center;
background: #fff;
right: -40px;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
border-radius: 0 4px 4px 0;
}
@media (min-width: 769px) {
.drawer-handle {
display: none;
}
}
.fade-in-enter, .fade-in-leave-active, .fade-in-linear-enter, .fade-in-linear-leave, .fade-in-linear-leave-active, .fade-in-linear-enter, .fade-in-linear-leave, .fade-in-linear-leave-active {
opacity: 0
}
.fade-in-linear-enter-active, .fade-in-linear-leave-active {
-webkit-transition: opacity .2s linear;
transition: opacity .2s linear
}
.fade-in-linear-enter-active, .fade-in-linear-leave-active {
-webkit-transition: opacity .2s linear;
transition: opacity .2s linear
}
.fade-in-enter-active, .fade-in-leave-active {
-webkit-transition: all .3s cubic-bezier(.55, 0, .1, 1);
transition: all .3s cubic-bezier(.55, 0, .1, 1)
}
.zoom-in-center-enter-active, .zoom-in-center-leave-active {
-webkit-transition: all .3s cubic-bezier(.55, 0, .1, 1);
transition: all .3s cubic-bezier(.55, 0, .1, 1)
}
.zoom-in-center-enter, .zoom-in-center-leave-active {
opacity: 0;
-webkit-transform: scaleX(0);
transform: scaleX(0)
}
.zoom-in-top-enter-active, .zoom-in-top-leave-active {
opacity: 1;
-webkit-transform: scaleY(1);
transform: scaleY(1);
-webkit-transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1);
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
-webkit-transform-origin: center top;
transform-origin: center top
}
.zoom-in-top-enter, .zoom-in-top-leave-active {
opacity: 0;
-webkit-transform: scaleY(0);
transform: scaleY(0)
}
.zoom-in-bottom-enter-active, .zoom-in-bottom-leave-active {
opacity: 1;
-webkit-transform: scaleY(1);
transform: scaleY(1);
-webkit-transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1);
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
-webkit-transform-origin: center bottom;
transform-origin: center bottom
}
.zoom-in-bottom-enter, .zoom-in-bottom-leave-active {
opacity: 0;
-webkit-transform: scaleY(0);
transform: scaleY(0)
}
.zoom-in-left-enter-active, .zoom-in-left-leave-active {
opacity: 1;
-webkit-transform: scale(1, 1);
transform: scale(1, 1);
-webkit-transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1);
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
-webkit-transform-origin: top left;
transform-origin: top left
}
.zoom-in-left-enter, .zoom-in-left-leave-active {
opacity: 0;
-webkit-transform: scale(.45, .45);
transform: scale(.45, .45)
}
.list-enter-active, .list-leave-active {
-webkit-transition: all .3s;
transition: all .3s
}
.list-enter, .list-leave-active {
opacity: 0;
-webkit-transform: translateY(-30px);
transform: translateY(-30px)
}
.ant-progress-inner {
background-color: #EBEEF5;
}
.deactive-client .ant-collapse-header{
color:rgb(255, 255, 255) !important;
background-color: rgb(255, 127, 127);
#app {
height: 100%;
}
.ant-space {
width: 100%;
}
.ant-layout-sider-zero-width-trigger {
display: none;
}
.ant-card {
border-radius: 30px;
}
.ant-card-hoverable {
cursor: auto;
}
.ant-card+.ant-card {
margin-top: 20px;
}
.drawer-handle {
position: absolute;
top: 72px;
width: 41px;
height: 40px;
cursor: pointer;
z-index: 0;
text-align: center;
line-height: 40px;
font-size: 16px;
display: flex;
justify-content: center;
align-items: center;
background: #fff;
right: -40px;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
border-radius: 0 4px 4px 0;
}
@media (min-width: 769px) {
.drawer-handle {
display: none;
}
}
.fade-in-enter, .fade-in-leave-active, .fade-in-linear-enter, .fade-in-linear-leave, .fade-in-linear-leave-active, .fade-in-linear-enter, .fade-in-linear-leave, .fade-in-linear-leave-active {
opacity: 0
}
.fade-in-linear-enter-active, .fade-in-linear-leave-active {
-webkit-transition: opacity .2s linear;
transition: opacity .2s linear
}
.fade-in-linear-enter-active, .fade-in-linear-leave-active {
-webkit-transition: opacity .2s linear;
transition: opacity .2s linear
}
.fade-in-enter-active, .fade-in-leave-active {
-webkit-transition: all .3s cubic-bezier(.55, 0, .1, 1);
transition: all .3s cubic-bezier(.55, 0, .1, 1)
}
.zoom-in-center-enter-active, .zoom-in-center-leave-active {
-webkit-transition: all .3s cubic-bezier(.55, 0, .1, 1);
transition: all .3s cubic-bezier(.55, 0, .1, 1)
}
.zoom-in-center-enter, .zoom-in-center-leave-active {
opacity: 0;
-webkit-transform: scaleX(0);
transform: scaleX(0)
}
.zoom-in-top-enter-active, .zoom-in-top-leave-active {
opacity: 1;
-webkit-transform: scaleY(1);
transform: scaleY(1);
-webkit-transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1);
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
-webkit-transform-origin: center top;
transform-origin: center top
}
.zoom-in-top-enter, .zoom-in-top-leave-active {
opacity: 0;
-webkit-transform: scaleY(0);
transform: scaleY(0)
}
.zoom-in-bottom-enter-active, .zoom-in-bottom-leave-active {
opacity: 1;
-webkit-transform: scaleY(1);
transform: scaleY(1);
-webkit-transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1);
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
-webkit-transform-origin: center bottom;
transform-origin: center bottom
}
.zoom-in-bottom-enter, .zoom-in-bottom-leave-active {
opacity: 0;
-webkit-transform: scaleY(0);
transform: scaleY(0)
}
.zoom-in-left-enter-active, .zoom-in-left-leave-active {
opacity: 1;
-webkit-transform: scale(1, 1);
transform: scale(1, 1);
-webkit-transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1);
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
-webkit-transform-origin: top left;
transform-origin: top left
}
.zoom-in-left-enter, .zoom-in-left-leave-active {
opacity: 0;
-webkit-transform: scale(.45, .45);
transform: scale(.45, .45)
}
.list-enter-active, .list-leave-active {
-webkit-transition: all .3s;
transition: all .3s
}
.list-enter, .list-leave-active {
opacity: 0;
-webkit-transform: translateY(-30px);
transform: translateY(-30px)
}
.ant-progress-inner {
background-color: #EBEEF5;
}
.deactive-client .ant-collapse-header{
color:rgb(255, 255, 255) !important;
background-color: rgb(255, 127, 127);
}
.ant-table-tbody>tr>td,
.ant-table-thead>tr>th{
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,
.ant-layout-sider-dark,
.ant-layout-sider-zero-width-trigger,
.ant-dropdown-menu-dark,.ant-dropdown-menu-dark .ant-dropdown-menu,
.ant-menu-dark.ant-menu-horizontal>.ant-menu-item,.ant-menu-dark.ant-menu-horizontal>.ant-menu-submenu {
background:#161b22
}
.ant-card-dark {
color: hsla(0,0%,100%,.65);
background-color: #1a212a;
border-color:rgba(0,0,0,.09);
}
.ant-card-dark:hover {
border-color: #e8e8e8;
box-shadow: 0 2px 8px rgba(255,255,255,.15);
}
.ant-card-dark .ant-table-thead th {
color: hsla(0,0%,100%,.65);
background-color: #161b22;
}
.ant-card-dark .ant-table-tbody tr td,
.ant-card-dark .ant-modal-title {
color: hsla(0,0%,100%,.65);
}
.ant-card-dark .ant-collapse-content,
.ant-card-dark .ant-calendar,
.ant-card-dark .ant-table-placeholder,
.ant-card-dark .ant-input-group-addon {
color: hsla(0,0%,100%,.65);
background-color: #262f3d;
}
.ant-card-dark .ant-list-item-meta-title,
.ant-card-dark .ant-list-item-meta-description,
.ant-card-dark .ant-form-item-label>label,
.ant-card-dark .ant-form-item,
.ant-card-dark .ant-divider-inner-text,
.ant-card-dark .ant-modal-confirm-content,
.ant-card-dark .ant-modal-confirm-title,
.ant-card-dark .ant-progress-text,
.ant-card-dark .ant-modal-close,
.ant-card-dark i,
.ant-card-dark .ant-select-dropdown-menu-item,
.ant-card-dark .ant-calendar-month-select,
.ant-card-dark .ant-calendar-year-select,
.ant-card-dark .ant-calendar-date,
.ant-card-dark .ant-collapse>.ant-collapse-item>.ant-collapse-header,
.ant-card-dark .ant-empty-normal,
.ant-card-dark .ant-checkbox+span {
color: hsla(0,0%,100%,.65);
}
.ant-card-dark .ant-table-tbody>tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)>td,
.ant-card-dark .ant-select-dropdown-menu-item:hover:not(.ant-select-dropdown-menu-item-disabled),
.ant-card-dark .ant-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 .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-calendar-picker-clear {
color: hsla(0,0%,100%,.65);
background-color: #2e3b52;
}
.ant-card-dark .ant-select-disabled .ant-select-selection {
border: 1px solid rgba(255, 255, 255, 0.2);
background-color: #242c3a;
}
.ant-card-dark .ant-collapse-item {
color: hsla(0,0%,100%,.65);
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,
.ant-card-dark .ant-calendar-selected-day .ant-calendar-date {
color: hsla(0,0%,100%,.65);
background-color: #222a37;
}
.client-table-header {
background-color: #f0f2f5;
}
.client-table-odd-row {
background-color: #fafafa;
}
.ant-card-dark .client-table-header {
background-color: #1a212a;
color: hsla(0,0%,100%,.65);
}
.ant-card-dark .client-table-odd-row {
color: hsla(0,0%,100%,.65);
background-color: #242c3a;
}
.ant-card-dark .ant-calendar-last-month-cell .ant-calendar-date,
.ant-card-dark .ant-calendar-next-month-btn-day .ant-calendar-date {
color: hsla(0,0%,100%,.30);
}
.ant-drawer-dark {
color: hsla(0,0%,100%,.65);
}
.ant-drawer-dark .ant-drawer-wrapper-body,
.ant-drawer-dark .drawer-handle {
background-color: #1a212a;
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;
border-color: #15395b;
}
.ant-card-dark .ant-tag-green {
color: #6abe39;
background: #162312;
border-color: #274916;
}
.ant-card-dark .ant-tag-cyan {
color: #33bcb7;
background: #112123;
border-color: #144848;
}
.ant-card-dark .ant-tag-red {
color: #e84749;
background: #2a1215;
border-color: #58181c;
}
.ant-card-dark .ant-tag-orange {
color: #e89a3c;
background: #2b1d11;
border-color: #593815;
}
.ant-card-dark .ant-table-row-expand-icon,
.ant-card-dark .ant-checkbox-inner {
background: none;
}
.ant-card-dark .ant-switch-checked {
background-color: #0c61b0;
}
.ant-card-dark .ant-btn,
.ant-card-dark .ant-radio-button-wrapper {
color: hsla(0,0%,100%,.65);
background: none;
border: 1px solid hsla(0,0%,100%,.65);
}
.ant-card-dark .ant-radio-button-wrapper:hover {
color: #177ddc;
}
.ant-card-dark .ant-btn-primary {
color: hsla(0,0%,100%,.65);
background-color: #073763;
border-color: #1890ff;
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

@@ -155,6 +155,11 @@ class DBInbound {
const inbound = this.toInbound();
return inbound.genLink(this.address, this.remark, clientIndex);
}
get genInboundLinks() {
const inbound = this.toInbound();
return inbound.genInboundLinks(this.address, this.remark);
}
}
class AllSetting {
@@ -165,13 +170,18 @@ class AllSetting {
this.webCertFile = "";
this.webKeyFile = "";
this.webBasePath = "/";
this.sessionMaxAge = "";
this.expireDiff = "";
this.trafficDiff = "";
this.tgBotEnable = false;
this.tgBotToken = "";
this.tgBotChatId = 0;
this.tgRunTime = "";
this.tgBotChatId = "";
this.tgRunTime = "@daily";
this.tgBotBackup = false;
this.tgCpu = "";
this.xrayTemplateConfig = "";
this.timeLocation = "Asia/Shanghai";
this.timeLocation = "Asia/Tehran";
if (data == null) {
return

View File

@@ -24,6 +24,9 @@ const SSMethods = {
CHACHA20_POLY1305: 'chacha20-poly1305',
AES_256_GCM: 'aes-256-gcm',
AES_128_GCM: 'aes-128-gcm',
BLAKE3_AES_128_GCM: '2022-blake3-aes-128-gcm',
BLAKE3_AES_256_GCM: '2022-blake3-aes-256-gcm',
BLAKE3_CHACHA20_POLY1305: '2022-blake3-chacha20-poly1305',
};
const RULE_IP = {
@@ -40,9 +43,55 @@ const RULE_DOMAIN = {
SPEEDTEST: 'geosite:speedtest',
};
const FLOW_CONTROL = {
ORIGIN: "xtls-rprx-origin",
DIRECT: "xtls-rprx-direct",
const TLS_FLOW_CONTROL = {
VISION: "xtls-rprx-vision",
VISION_UDP443: "xtls-rprx-vision-udp443",
};
const TLS_VERSION_OPTION = {
TLS10: "1.0",
TLS11: "1.1",
TLS12: "1.2",
TLS13: "1.3",
}
const TLS_CIPHER_OPTION = {
RSA_AES_128_CBC: "TLS_RSA_WITH_AES_128_CBC_SHA",
RSA_AES_256_CBC: "TLS_RSA_WITH_AES_256_CBC_SHA",
RSA_AES_128_GCM: "TLS_RSA_WITH_AES_128_GCM_SHA256",
RSA_AES_256_GCM: "TLS_RSA_WITH_AES_256_GCM_SHA384",
AES_128_GCM: "TLS_AES_128_GCM_SHA256",
AES_256_GCM: "TLS_AES_256_GCM_SHA384",
CHACHA20_POLY1305: "TLS_CHACHA20_POLY1305_SHA256",
ECDHE_ECDSA_AES_128_CBC: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
ECDHE_ECDSA_AES_256_CBC: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
ECDHE_RSA_AES_128_CBC: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
ECDHE_RSA_AES_256_CBC: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
ECDHE_ECDSA_AES_128_GCM: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
ECDHE_ECDSA_AES_256_GCM: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
ECDHE_RSA_AES_128_GCM: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
ECDHE_RSA_AES_256_GCM: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
ECDHE_ECDSA_CHACHA20_POLY1305: "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
ECDHE_RSA_CHACHA20_POLY1305: "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
};
const UTLS_FINGERPRINT = {
UTLS_CHROME: "chrome",
UTLS_FIREFOX: "firefox",
UTLS_SAFARI: "safari",
UTLS_IOS: "ios",
UTLS_android: "android",
UTLS_EDGE: "edge",
UTLS_360: "360",
UTLS_QQ: "qq",
UTLS_RANDOM: "random",
UTLS_RANDOMIZED: "randomized",
};
const ALPN_OPTION = {
H3: "h3",
H2: "h2",
HTTP1: "http/1.1",
};
Object.freeze(Protocols);
@@ -50,7 +99,10 @@ Object.freeze(VmessMethods);
Object.freeze(SSMethods);
Object.freeze(RULE_IP);
Object.freeze(RULE_DOMAIN);
Object.freeze(FLOW_CONTROL);
Object.freeze(TLS_FLOW_CONTROL);
Object.freeze(TLS_VERSION_OPTION);
Object.freeze(TLS_CIPHER_OPTION);
Object.freeze(ALPN_OPTION);
class XrayCommonClass {
@@ -417,11 +469,20 @@ class GrpcStreamSettings extends XrayCommonClass {
class TlsStreamSettings extends XrayCommonClass {
constructor(serverName='',
certificates=[new TlsStreamSettings.Cert()], alpn=[]) {
minVersion = TLS_VERSION_OPTION.TLS10,
maxVersion = TLS_VERSION_OPTION.TLS12,
cipherSuites = '',
certificates=[new TlsStreamSettings.Cert()],
alpn=[],
settings=new TlsStreamSettings.Settings()) {
super();
this.server = serverName;
this.minVersion = minVersion;
this.maxVersion = maxVersion;
this.cipherSuites = cipherSuites;
this.certs = certificates;
this.alpn = alpn;
this.settings = settings;
}
addCert(cert) {
@@ -434,22 +495,34 @@ class TlsStreamSettings extends XrayCommonClass {
static fromJson(json={}) {
let certs;
let settings;
if (!ObjectUtil.isEmpty(json.certificates)) {
certs = json.certificates.map(cert => TlsStreamSettings.Cert.fromJson(cert));
}
if (!ObjectUtil.isEmpty(json.settings)) {
settings = new TlsStreamSettings.Settings(json.settings.allowInsecure , json.settings.fingerprint, json.settings.serverName);
}
return new TlsStreamSettings(
json.serverName,
json.minVersion,
json.maxVersion,
json.cipherSuites,
certs,
json.alpn
json.alpn,
settings,
);
}
toJson() {
return {
serverName: this.server,
minVersion: this.minVersion,
maxVersion: this.maxVersion,
cipherSuites: this.cipherSuites,
certificates: TlsStreamSettings.toJsonArray(this.certs),
alpn: this.alpn
alpn: this.alpn,
settings: this.settings,
};
}
}
@@ -495,10 +568,115 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
}
};
TlsStreamSettings.Settings = class extends XrayCommonClass {
constructor(allowInsecure = false, fingerprint = '', serverName = '') {
super();
this.allowInsecure = allowInsecure;
this.fingerprint = fingerprint;
this.serverName = serverName;
}
static fromJson(json = {}) {
return new TlsStreamSettings.Settings(
json.allowInsecure,
json.fingerprint,
json.servername,
);
}
toJson() {
return {
allowInsecure: this.allowInsecure,
fingerprint: this.fingerprint,
serverName: this.serverName,
};
}
};
class RealityStreamSettings extends XrayCommonClass {
constructor(show = false, xver = 0,
dest = 'microsoft.com:443',
serverNames = 'microsoft.com,www.microsoft.com',
privateKey = '', minClient = '', maxClient = '',
maxTimediff = 0, shortIds = [],
settings= new RealityStreamSettings.Settings()) {
super();
this.show = show;
this.xver = xver;
this.dest = dest;
this.serverNames = serverNames instanceof Array ? serverNames.join(",") : serverNames;
this.privateKey = privateKey;
this.minClient = minClient;
this.maxClient = maxClient;
this.maxTimediff = maxTimediff;
this.shortIds = shortIds instanceof Array ? shortIds.join(",") : shortIds;
this.settings = settings;
}
static fromJson(json = {}) {
let settings;
if (!ObjectUtil.isEmpty(json.settings)) {
settings = new RealityStreamSettings.Settings(json.settings.publicKey , json.settings.fingerprint, json.settings.serverName, json.settings.spiderX);
}
return new RealityStreamSettings(
json.show,
json.xver,
json.dest,
json.serverNames,
json.privateKey,
json.minClient,
json.maxClient,
json.maxTimediff,
json.shortIds,
json.settings,
);
}
toJson() {
return {
show: this.show,
xver: this.xver,
dest: this.dest,
serverNames: this.serverNames.split(","),
privateKey: this.privateKey,
minClient: this.minClient,
maxClient: this.maxClient,
maxTimediff: this.maxTimediff,
shortIds: this.shortIds.split(","),
settings: this.settings,
};
}
}
RealityStreamSettings.Settings = class extends XrayCommonClass {
constructor(publicKey = '', fingerprint = UTLS_FINGERPRINT.UTLS_FIREFOX, serverName = '', spiderX= '/') {
super();
this.publicKey = publicKey;
this.fingerprint = fingerprint;
this.serverName = serverName;
this.spiderX = spiderX;
}
static fromJson(json = {}) {
return new RealityStreamSettings.Settings(
json.publicKey,
json.fingerprint,
json.serverName,
json.spiderX,
);
}
toJson() {
return {
publicKey: this.publicKey,
fingerprint: this.fingerprint,
serverName: this.serverName,
spiderX: this.spiderX,
};
}
};
class StreamSettings extends XrayCommonClass {
constructor(network='tcp',
security='none',
tlsSettings=new TlsStreamSettings(),
realitySettings = new RealityStreamSettings(),
tcpSettings=new TcpStreamSettings(),
kcpSettings=new KcpStreamSettings(),
wsSettings=new WsStreamSettings(),
@@ -510,6 +688,7 @@ class StreamSettings extends XrayCommonClass {
this.network = network;
this.security = security;
this.tls = tlsSettings;
this.reality = realitySettings;
this.tcp = tcpSettings;
this.kcp = kcpSettings;
this.ws = wsSettings;
@@ -530,29 +709,25 @@ class StreamSettings extends XrayCommonClass {
}
}
get isXTls() {
return this.security === "xtls";
get isReality() {
return this.security === "reality";
}
set isXTls(isXTls) {
if (isXTls) {
this.security = 'xtls';
set isReality(isReality) {
if (isReality) {
this.security = 'reality';
} else {
this.security = 'none';
}
}
static fromJson(json={}) {
let tls;
if (json.security === "xtls") {
tls = TlsStreamSettings.fromJson(json.xtlsSettings);
} else {
tls = TlsStreamSettings.fromJson(json.tlsSettings);
}
return new StreamSettings(
json.network,
json.security,
tls,
TlsStreamSettings.fromJson(json.tlsSettings),
RealityStreamSettings.fromJson(json.realitySettings),
TcpStreamSettings.fromJson(json.tcpSettings),
KcpStreamSettings.fromJson(json.kcpSettings),
WsStreamSettings.fromJson(json.wsSettings),
@@ -568,7 +743,7 @@ class StreamSettings extends XrayCommonClass {
network: network,
security: this.security,
tlsSettings: this.isTls ? this.tls.toJson() : undefined,
xtlsSettings: this.isXTls ? this.tls.toJson() : undefined,
realitySettings: this.isReality ? this.reality.toJson() : undefined,
tcpSettings: network === 'tcp' ? this.tcp.toJson() : undefined,
kcpSettings: network === 'kcp' ? this.kcp.toJson() : undefined,
wsSettings: network === 'ws' ? this.ws.toJson() : undefined,
@@ -648,13 +823,13 @@ class Inbound extends XrayCommonClass {
}
}
get xtls() {
return this.stream.security === 'xtls';
get reality() {
return this.stream.security === 'reality';
}
set xtls(isXTls) {
if (isXTls) {
this.stream.security = 'xtls';
set reality(isReality) {
if (isReality) {
this.stream.security = 'reality';
} else {
this.stream.security = 'none';
}
@@ -710,7 +885,7 @@ class Inbound extends XrayCommonClass {
case Protocols.VLESS:
return this.settings.vlesses[0].flow;
case Protocols.TROJAN:
return this.settings.clients[0].flow;
return this.settings.trojans[0].flow;
default:
return "";
}
@@ -741,7 +916,7 @@ class Inbound extends XrayCommonClass {
get password() {
switch (this.protocol) {
case Protocols.TROJAN:
return this.settings.clients[0].password;
return this.settings.trojans[0].password;
case Protocols.SHADOWSOCKS:
return this.settings.password;
case Protocols.SOCKS:
@@ -763,7 +938,7 @@ class Inbound extends XrayCommonClass {
}
get serverName() {
if (this.stream.isTls || this.stream.isXTls) {
if (this.stream.isTls || this.stream.isReality) {
return this.stream.tls.server;
}
return "";
@@ -818,16 +993,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;
@@ -857,11 +1032,24 @@ class Inbound extends XrayCommonClass {
}
}
//this is used for xtls-rprx-vision
canEnableTlsFlow() {
if ((this.stream.security != 'none') && (this.network === "tcp")) {
switch (this.protocol) {
case Protocols.VLESS:
return true;
default:
return false;
}
}
return false;
}
canSetTls() {
return this.canEnableTls();
}
canEnableXTls() {
canEnableReality() {
switch (this.protocol) {
case Protocols.VLESS:
case Protocols.TROJAN:
@@ -869,13 +1057,22 @@ class Inbound extends XrayCommonClass {
default:
return false;
}
return this.network === "tcp";
switch (this.network) {
case "tcp":
case "http":
case "grpc":
return true;
default:
return false;
}
}
canEnableStream() {
switch (this.protocol) {
case Protocols.VMESS:
case Protocols.VLESS:
case Protocols.TROJAN:
case Protocols.SHADOWSOCKS:
return true;
default:
@@ -952,8 +1149,7 @@ class Inbound extends XrayCommonClass {
address = this.stream.tls.server;
}
}
remark = this.settings.vmesses[clientIndex].email ?? remark;
let obj = {
v: '2',
ps: remark,
@@ -966,6 +1162,10 @@ class Inbound extends XrayCommonClass {
host: host,
path: path,
tls: this.stream.security,
sni: this.stream.tls.settings.serverName,
fp: this.stream.tls.settings.fingerprint,
alpn: this.stream.tls.alpn.join(','),
allowInsecure: this.stream.tls.settings.allowInsecure,
};
return 'vmess://' + base64(JSON.stringify(obj, null, 2));
}
@@ -976,14 +1176,7 @@ class Inbound extends XrayCommonClass {
const port = this.port;
const type = this.stream.network;
const params = new Map();
remark = settings.vlesses[clientIndex].email ?? remark;
params.set("type", this.stream.network);
if (this.xtls) {
params.set("security", "xtls");
address = this.stream.tls.server;
} else {
params.set("security", this.stream.security);
}
switch (type) {
case "tcp":
const tcp = this.stream.tcp;
@@ -1029,15 +1222,43 @@ class Inbound extends XrayCommonClass {
break;
}
if (this.stream.security === 'tls') {
if (this.tls) {
params.set("security", "tls");
params.set("fp" , this.stream.tls.settings.fingerprint);
params.set("alpn", this.stream.tls.alpn);
if(this.stream.tls.settings.allowInsecure){
params.set("allowInsecure", "1");
}
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
address = this.stream.tls.server;
params.set("sni", address);
}
if (this.stream.tls.settings.serverName !== ''){
params.set("sni", this.stream.tls.settings.serverName);
}
if (type === "tcp" && this.settings.vlesses[clientIndex].flow.length > 0) {
params.set("flow", this.settings.vlesses[clientIndex].flow);
}
}
if (this.xtls) {
params.set("flow", this.settings.vlesses[clientIndex].flow);
if (this.reality) {
params.set("security", "reality");
params.set("pbk", this.stream.reality.settings.publicKey);
params.set("fp", this.stream.reality.settings.fingerprint);
if (!ObjectUtil.isArrEmpty(this.stream.reality.serverNames)) {
params.set("sni", this.stream.reality.serverNames.split(",")[0]);
}
if (this.stream.reality.shortIds.length > 0) {
params.set("sid", this.stream.reality.shortIds.split(",")[0]);
}
if (!ObjectUtil.isEmpty(this.stream.reality.settings.serverName)) {
address = this.stream.reality.settings.serverName;
}
if (!ObjectUtil.isEmpty(this.stream.reality.settings.spiderX)) {
params.set("spx", this.stream.reality.settings.spiderX);
}
if (this.stream.network === 'tcp' && !ObjectUtil.isEmpty(this.settings.vlesses[clientIndex].flow)) {
params.set("flow", this.settings.vlesses[clientIndex].flow);
}
}
const link = `vless://${uuid}@${address}:${port}`;
@@ -1055,22 +1276,144 @@ class Inbound extends XrayCommonClass {
if (!ObjectUtil.isEmpty(server)) {
address = server;
}
return 'ss://' + safeBase64(settings.method + ':' + settings.password + '@' + address + ':' + this.port)
+ '#' + encodeURIComponent(remark);
if (settings.method == SSMethods.BLAKE3_AES_128_GCM || settings.method == SSMethods.BLAKE3_AES_256_GCM || settings.method == SSMethods.BLAKE3_CHACHA20_POLY1305) {
return `ss://${settings.method}:${settings.password}@${address}:${this.port}#${encodeURIComponent(remark)}`;
} else {
return 'ss://' + safeBase64(settings.method + ':' + settings.password + '@' + address + ':' + this.port)
+ '#' + encodeURIComponent(remark);
}
}
genTrojanLink(address='', remark='', clientIndex=0) {
genTrojanLink(address = '', remark = '', clientIndex = 0) {
let settings = this.settings;
remark = settings.trojans[clientIndex].email ?? remark;
return `trojan://${settings.trojans[clientIndex].password}@${address}:${this.port}#${encodeURIComponent(remark)}`;
const port = this.port;
const type = this.stream.network;
const params = new Map();
params.set("type", this.stream.network);
switch (type) {
case "tcp":
const tcp = this.stream.tcp;
if (tcp.type === 'http') {
const request = tcp.request;
params.set("path", request.path.join(','));
const index = request.headers.findIndex(header => header.name.toLowerCase() === 'host');
if (index >= 0) {
const host = request.headers[index].value;
params.set("host", host);
}
params.set("headerType", 'http');
}
break;
case "kcp":
const kcp = this.stream.kcp;
params.set("headerType", kcp.type);
params.set("seed", kcp.seed);
break;
case "ws":
const ws = this.stream.ws;
params.set("path", ws.path);
const index = ws.headers.findIndex(header => header.name.toLowerCase() === 'host');
if (index >= 0) {
const host = ws.headers[index].value;
params.set("host", host);
}
break;
case "http":
const http = this.stream.http;
params.set("path", http.path);
params.set("host", http.host);
break;
case "quic":
const quic = this.stream.quic;
params.set("quicSecurity", quic.security);
params.set("key", quic.key);
params.set("headerType", quic.type);
break;
case "grpc":
const grpc = this.stream.grpc;
params.set("serviceName", grpc.serviceName);
break;
}
if (this.tls) {
params.set("security", "tls");
params.set("fp" , this.stream.tls.settings.fingerprint);
params.set("alpn", this.stream.tls.alpn);
if(this.stream.tls.settings.allowInsecure){
params.set("allowInsecure", "1");
}
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
address = this.stream.tls.server;
}
if (this.stream.tls.settings.serverName !== ''){
params.set("sni", this.stream.tls.settings.serverName);
}
}
if (this.reality) {
params.set("security", "reality");
params.set("pbk", this.stream.reality.settings.publicKey);
params.set("fp", this.stream.reality.settings.fingerprint);
if (!ObjectUtil.isArrEmpty(this.stream.reality.serverNames)) {
params.set("sni", this.stream.reality.serverNames.split(",")[0]);
}
if (this.stream.reality.shortIds.length > 0) {
params.set("sid", this.stream.reality.shortIds.split(",")[0]);
}
if (!ObjectUtil.isEmpty(this.stream.reality.settings.serverName)) {
address = this.stream.reality.settings.serverName;
}
if (!ObjectUtil.isEmpty(this.stream.reality.settings.spiderX)) {
params.set("spx", this.stream.reality.settings.spiderX);
}
if (this.stream.network === 'tcp' && !ObjectUtil.isEmpty(this.settings.trojans[clientIndex].flow)) {
params.set("flow", this.settings.trojans[clientIndex].flow);
}
}
const link = `trojan://${settings.trojans[clientIndex].password}@${address}:${this.port}#${encodeURIComponent(remark)}`;
const url = new URL(link);
for (const [key, value] of params) {
url.searchParams.set(key, value)
}
url.hash = encodeURIComponent(remark);
return url.toString();
}
genLink(address='', remark='', clientIndex=0) {
switch (this.protocol) {
case Protocols.VMESS: return this.genVmessLink(address, remark, clientIndex);
case Protocols.VLESS: return this.genVLESSLink(address, remark, clientIndex);
case Protocols.VMESS:
if (this.settings.vmesses[clientIndex].email != ""){
remark = this.settings.vmesses[clientIndex].email
}
return this.genVmessLink(address, remark, clientIndex);
case Protocols.VLESS:
if (this.settings.vlesses[clientIndex].email != ""){
remark = this.settings.vlesses[clientIndex].email
}
return this.genVLESSLink(address, remark, clientIndex);
case Protocols.SHADOWSOCKS: return this.genSSLink(address, remark);
case Protocols.TROJAN: return this.genTrojanLink(address, remark, clientIndex);
case Protocols.TROJAN:
if (this.settings.trojans[clientIndex].email != ""){
remark = this.settings.trojans[clientIndex].email
}
return this.genTrojanLink(address, remark, clientIndex);
default: return '';
}
}
genInboundLinks(address = '', remark = '') {
let link = '';
switch (this.protocol) {
case Protocols.VMESS:
case Protocols.VLESS:
case Protocols.TROJAN:
JSON.parse(this.settings).clients.forEach((_,index) => {
link += this.genLink(address, remark, index) + '\r\n';
});
return link;
case Protocols.SHADOWSOCKS:
return (this.genSSLink(address, remark) + '\r\n');
default: return '';
}
}
@@ -1188,13 +1531,16 @@ Inbound.VmessSettings = class extends Inbound.Settings {
}
};
Inbound.VmessSettings.Vmess = class extends XrayCommonClass {
constructor(id=RandomUtil.randomUUID(), alterId=0, email='', 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={}) {
@@ -1204,13 +1550,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);
}
@@ -1238,7 +1589,7 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
fallbacks=[],) {
super(protocol);
this.vlesses = vlesses;
this.decryption = decryption;
this.decryption = 'none'; // Using decryption is not implemented here
this.fallbacks = fallbacks;
}
@@ -1250,11 +1601,12 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
this.fallbacks.splice(index, 1);
}
// decryption should be set to static value
static fromJson(json={}) {
return new Inbound.VLESSSettings(
Protocols.VLESS,
json.clients.map(client => Inbound.VLESSSettings.VLESS.fromJson(client)),
json.decryption,
'none',
Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks),
);
}
@@ -1262,22 +1614,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=FLOW_CONTROL.DIRECT, email='', 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={}) {
@@ -1287,14 +1640,19 @@ Inbound.VLESSSettings.VLESS = 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);
}
@@ -1384,13 +1742,16 @@ Inbound.TrojanSettings = class extends Inbound.Settings {
}
};
Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
constructor(password=RandomUtil.randomSeq(10), flow=FLOW_CONTROL.DIRECT, email='', 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() {
@@ -1400,17 +1761,22 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
email: this.email,
totalGB: this.totalGB,
expiryTime: this.expiryTime,
enable: this.enable,
tgId: this.tgId,
subId: this.subId,
};
}
static fromJson(json={}) {
static fromJson(json = {}) {
return new Inbound.TrojanSettings.Trojan(
json.password,
json.flow,
json.email,
json.totalGB,
json.expiryTime,
json.enable,
json.tgId,
json.subId,
);
}
@@ -1418,6 +1784,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);
}
@@ -1479,8 +1848,8 @@ Inbound.TrojanSettings.Fallback = class extends XrayCommonClass {
Inbound.ShadowsocksSettings = class extends Inbound.Settings {
constructor(protocol,
method=SSMethods.AES_256_GCM,
password=RandomUtil.randomSeq(10),
method=SSMethods.BLAKE3_AES_256_GCM,
password=RandomUtil.randomSeq(44),
network='tcp,udp'
) {
super(protocol);
@@ -1508,11 +1877,12 @@ Inbound.ShadowsocksSettings = class extends Inbound.Settings {
};
Inbound.DokodemoSettings = class extends Inbound.Settings {
constructor(protocol, address, port, network='tcp,udp') {
constructor(protocol, address, port, network='tcp,udp', followRedirect=false) {
super(protocol);
this.address = address;
this.port = port;
this.network = network;
this.followRedirect = followRedirect;
}
static fromJson(json={}) {
@@ -1521,6 +1891,7 @@ Inbound.DokodemoSettings = class extends Inbound.Settings {
json.address,
json.port,
json.network,
json.followRedirect,
);
}
@@ -1529,6 +1900,7 @@ Inbound.DokodemoSettings = class extends Inbound.Settings {
address: this.address,
port: this.port,
network: this.network,
followRedirect: this.followRedirect,
};
}
};

View File

@@ -136,6 +136,16 @@ class RandomUtil {
return (c === 'x' ? r : (r & 0x7 | 0x8)).toString(16);
});
}
static randomText() {
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
var string = '';
var len = 6 + Math.floor(Math.random() * 5)
for(var ii=0; ii<len; ii++){
string += chars[Math.floor(Math.random() * chars.length)];
}
return string;
}
}
class ObjectUtil {
@@ -169,7 +179,7 @@ class ObjectUtil {
}
}
} else {
return obj.toString().indexOf(key) >= 0;
return obj.toString().toLowerCase().indexOf(key.toLowerCase()) >= 0;
}
return false;
}

View File

@@ -1,13 +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 {
@@ -22,21 +19,30 @@ func (a *APIController) initRouter(g *gin.RouterGroup) {
g.GET("/", a.inbounds)
g.GET("/get/:id", a.inbound)
g.GET("/getClientTraffics/:email", a.getClientTraffics)
g.POST("/add", a.addInbound)
g.POST("/del/:id", a.delInbound)
g.POST("/update/:id", a.updateInbound)
g.POST("/addClient", a.addInboundClient)
g.POST("/:id/delClient/:clientId", a.delInboundClient)
g.POST("/updateClient/:clientId", a.updateInboundClient)
g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
g.POST("/resetAllTraffics", a.resetAllTraffics)
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
g.POST("/delDepletedClients/:id", a.delDepletedClients)
a.inboundController = NewInboundController(g)
}
func (a *APIController) inbounds(c *gin.Context) {
a.inboundController.getInbounds(c)
}
func (a *APIController) inbound(c *gin.Context) {
a.inboundController.getInbound(c)
}
func (a *APIController) getClientTraffics(c *gin.Context) {
a.inboundController.getClientTraffics(c)
}
func (a *APIController) addInbound(c *gin.Context) {
a.inboundController.addInbound(c)
}
@@ -46,3 +52,24 @@ 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)
}
func (a *APIController) delDepletedClients(c *gin.Context) {
a.inboundController.delDepletedClients(c)
}

View File

@@ -12,7 +12,7 @@ type BaseController struct {
func (a *BaseController) checkLogin(c *gin.Context) {
if !session.IsLogin(c) {
if isAjax(c) {
pureJsonMsg(c, false, I18n(c , "pages.login.loginAgain"))
pureJsonMsg(c, false, I18n(c, "pages.login.loginAgain"))
} else {
c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
}
@@ -22,12 +22,11 @@ func (a *BaseController) checkLogin(c *gin.Context) {
}
}
func I18n(c *gin.Context, name string) string {
anyfunc, _ := c.Get("I18n")
i18n, _ := anyfunc.(func(key string, params ...string) (string, error))
func I18n(c *gin.Context , name string) string{
anyfunc, _ := c.Get("I18n")
i18n, _ := anyfunc.(func(key string, params ...string) (string, error))
message, _ := i18n(name)
message, _ := i18n(name)
return message;
return message
}

View File

@@ -31,7 +31,13 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
g.POST("/add", a.addInbound)
g.POST("/del/:id", a.delInbound)
g.POST("/update/:id", a.updateInbound)
g.POST("/resetClientTraffic/:email", a.resetClientTraffic)
g.POST("/addClient", a.addInboundClient)
g.POST("/:id/delClient/:clientId", a.delInboundClient)
g.POST("/updateClient/:clientId", a.updateInboundClient)
g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
g.POST("/resetAllTraffics", a.resetAllTraffics)
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
g.POST("/delDepletedClients/:id", a.delDepletedClients)
}
@@ -70,6 +76,15 @@ func (a *InboundController) getInbound(c *gin.Context) {
}
jsonObj(c, inbound, nil)
}
func (a *InboundController) getClientTraffics(c *gin.Context) {
email := c.Param("email")
clientTraffics, err := a.inboundService.GetClientTrafficByEmail(email)
if err != nil {
jsonMsg(c, "Error getting traffics", err)
return
}
jsonObj(c, clientTraffics, nil)
}
func (a *InboundController) addInbound(c *gin.Context) {
inbound := &model.Inbound{}
@@ -123,13 +138,118 @@ func (a *InboundController) updateInbound(c *gin.Context) {
}
}
func (a *InboundController) addInboundClient(c *gin.Context) {
data := &model.Inbound{}
err := c.ShouldBind(data)
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
return
}
err = a.inboundService.AddInboundClient(data)
if err != nil {
jsonMsg(c, "Something went wrong!", err)
return
}
jsonMsg(c, "Client(s) added", nil)
if err == nil {
a.xrayService.SetToNeedRestart()
}
}
func (a *InboundController) delInboundClient(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
return
}
clientId := c.Param("clientId")
err = a.inboundService.DelInboundClient(id, clientId)
if err != nil {
jsonMsg(c, "Something went wrong!", err)
return
}
jsonMsg(c, "Client deleted", nil)
if err == nil {
a.xrayService.SetToNeedRestart()
}
}
func (a *InboundController) updateInboundClient(c *gin.Context) {
clientId := c.Param("clientId")
inbound := &model.Inbound{}
err := c.ShouldBind(inbound)
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
return
}
err = a.inboundService.UpdateInboundClient(inbound, clientId)
if err != nil {
jsonMsg(c, "Something went wrong!", err)
return
}
jsonMsg(c, "Client updated", nil)
if err == nil {
a.xrayService.SetToNeedRestart()
}
}
func (a *InboundController) resetClientTraffic(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
return
}
email := c.Param("email")
err := a.inboundService.ResetClientTraffic(email)
err = a.inboundService.ResetClientTraffic(id, email)
if err != nil {
jsonMsg(c, "something worng!", err)
jsonMsg(c, "Something went wrong!", err)
return
}
jsonMsg(c, "traffic reseted", nil)
if err == nil {
a.xrayService.SetToNeedRestart()
}
}
func (a *InboundController) resetAllTraffics(c *gin.Context) {
err := a.inboundService.ResetAllTraffics()
if err != nil {
jsonMsg(c, "Something went wrong!", 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 went wrong!", err)
return
}
jsonMsg(c, "All traffics of client reseted", nil)
}
func (a *InboundController) delDepletedClients(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.DelDepletedClients(id)
if err != nil {
jsonMsg(c, "Something went wrong!", err)
return
}
jsonMsg(c, "All delpeted clients are deleted", nil)
}

View File

@@ -4,7 +4,6 @@ import (
"net/http"
"time"
"x-ui/logger"
"x-ui/web/job"
"x-ui/web/service"
"x-ui/web/session"
@@ -19,7 +18,9 @@ type LoginForm struct {
type IndexController struct {
BaseController
userService service.UserService
settingService service.SettingService
userService service.UserService
tgbot service.Tgbot
}
func NewIndexController(g *gin.RouterGroup) *IndexController {
@@ -46,7 +47,7 @@ func (a *IndexController) login(c *gin.Context) {
var form LoginForm
err := c.ShouldBind(&form)
if err != nil {
pureJsonMsg(c, false, I18n(c , "pages.login.toasts.invalidFormData"))
pureJsonMsg(c, false, I18n(c, "pages.login.toasts.invalidFormData"))
return
}
if form.Username == "" {
@@ -54,24 +55,34 @@ func (a *IndexController) login(c *gin.Context) {
return
}
if form.Password == "" {
pureJsonMsg(c, false, I18n(c , "pages.login.toasts.emptyPassword"))
pureJsonMsg(c, false, I18n(c, "pages.login.toasts.emptyPassword"))
return
}
user := a.userService.CheckUser(form.Username, form.Password)
timeStr := time.Now().Format("2006-01-02 15:04:05")
if user == nil {
job.NewStatsNotifyJob().UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0)
a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0)
logger.Infof("wrong username or password: \"%s\" \"%s\"", form.Username, form.Password)
pureJsonMsg(c, false, I18n(c , "pages.login.toasts.wrongUsernameOrPassword"))
pureJsonMsg(c, false, I18n(c, "pages.login.toasts.wrongUsernameOrPassword"))
return
} else {
logger.Infof("%s login success,Ip Address:%s\n", form.Username, getRemoteIp(c))
job.NewStatsNotifyJob().UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 1)
a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 1)
}
sessionMaxAge, err := a.settingService.GetSessionMaxAge()
if err != nil {
logger.Infof("Unable to get session's max age from DB")
}
err = session.SetMaxAge(c, sessionMaxAge*60)
if err != nil {
logger.Infof("Unable to set session's max age")
}
err = session.SetLoginUser(c, user)
logger.Info("user", user.Id, "login success")
jsonMsg(c, I18n(c , "pages.login.toasts.successLogin"), err)
jsonMsg(c, I18n(c, "pages.login.toasts.successLogin"), err)
}
func (a *IndexController) logout(c *gin.Context) {

View File

@@ -1,10 +1,11 @@
package controller
import (
"github.com/gin-gonic/gin"
"time"
"x-ui/web/global"
"x-ui/web/service"
"github.com/gin-gonic/gin"
)
type ServerController struct {
@@ -34,7 +35,13 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
g.Use(a.checkLogin)
g.POST("/status", a.status)
g.POST("/getXrayVersion", a.getXrayVersion)
g.POST("/stopXrayService", a.stopXrayService)
g.POST("/restartXrayService", a.restartXrayService)
g.POST("/installXray/:version", a.installXray)
g.POST("/logs/:count", a.getLogs)
g.POST("/getConfigJson", a.getConfigJson)
g.GET("/getDb", a.getDb)
g.POST("/getNewX25519Cert", a.getNewX25519Cert)
}
func (a *ServerController) refreshStatus() {
@@ -68,7 +75,7 @@ func (a *ServerController) getXrayVersion(c *gin.Context) {
versions, err := a.serverService.GetXrayVersions()
if err != nil {
jsonMsg(c, I18n(c , "getVersion"), err)
jsonMsg(c, I18n(c, "getVersion"), err)
return
}
@@ -81,5 +88,67 @@ func (a *ServerController) getXrayVersion(c *gin.Context) {
func (a *ServerController) installXray(c *gin.Context) {
version := c.Param("version")
err := a.serverService.UpdateXray(version)
jsonMsg(c, I18n(c , "install") + " xray", err)
jsonMsg(c, I18n(c, "install")+" xray", err)
}
func (a *ServerController) stopXrayService(c *gin.Context) {
a.lastGetStatusTime = time.Now()
err := a.serverService.StopXrayService()
if err != nil {
jsonMsg(c, "", err)
return
}
jsonMsg(c, "Xray stoped", err)
}
func (a *ServerController) restartXrayService(c *gin.Context) {
err := a.serverService.RestartXrayService()
if err != nil {
jsonMsg(c, "", err)
return
}
jsonMsg(c, "Xray restarted", err)
}
func (a *ServerController) getLogs(c *gin.Context) {
count := c.Param("count")
logs, err := a.serverService.GetLogs(count)
if err != nil {
jsonMsg(c, "getLogs", err)
return
}
jsonObj(c, logs, nil)
}
func (a *ServerController) getConfigJson(c *gin.Context) {
configJson, err := a.serverService.GetConfigJson()
if err != nil {
jsonMsg(c, "get config.json", err)
return
}
jsonObj(c, configJson, nil)
}
func (a *ServerController) getDb(c *gin.Context) {
db, err := a.serverService.GetDb()
if err != nil {
jsonMsg(c, "get Database", err)
return
}
// Set the headers for the response
c.Header("Content-Type", "application/octet-stream")
c.Header("Content-Disposition", "attachment; filename=x-ui.db")
// Write the file contents to the response
c.Writer.Write(db)
}
func (a *ServerController) getNewX25519Cert(c *gin.Context) {
cert, err := a.serverService.GetNewX25519Cert()
if err != nil {
jsonMsg(c, "get x25519 certificate", err)
return
}
jsonObj(c, cert, nil)
}

View File

@@ -2,11 +2,12 @@ package controller
import (
"errors"
"github.com/gin-gonic/gin"
"time"
"x-ui/web/entity"
"x-ui/web/service"
"x-ui/web/session"
"github.com/gin-gonic/gin"
)
type updateUserForm struct {
@@ -32,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)
@@ -40,37 +42,67 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) {
func (a *SettingController) getAllSetting(c *gin.Context) {
allSetting, err := a.settingService.GetAllSetting()
if err != nil {
jsonMsg(c, I18n(c , "pages.setting.toasts.getSetting"), err)
jsonMsg(c, I18n(c, "pages.setting.toasts.getSetting"), err)
return
}
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)
if err != nil {
jsonMsg(c, I18n(c , "pages.setting.toasts.modifySetting"), err)
jsonMsg(c, I18n(c, "pages.setting.toasts.modifySetting"), err)
return
}
err = a.settingService.UpdateAllSetting(allSetting)
jsonMsg(c, I18n(c , "pages.setting.toasts.modifySetting"), err)
jsonMsg(c, I18n(c, "pages.setting.toasts.modifySetting"), err)
}
func (a *SettingController) updateUser(c *gin.Context) {
form := &updateUserForm{}
err := c.ShouldBind(form)
if err != nil {
jsonMsg(c, I18n(c , "pages.setting.toasts.modifySetting"), err)
jsonMsg(c, I18n(c, "pages.setting.toasts.modifySetting"), err)
return
}
user := session.GetLoginUser(c)
if user.Username != form.OldUsername || user.Password != form.OldPassword {
jsonMsg(c, I18n(c , "pages.setting.toasts.modifyUser"), errors.New(I18n(c , "pages.setting.toasts.originalUserPassIncorrect")))
jsonMsg(c, I18n(c, "pages.setting.toasts.modifyUser"), errors.New(I18n(c, "pages.setting.toasts.originalUserPassIncorrect")))
return
}
if form.NewUsername == "" || form.NewPassword == "" {
jsonMsg(c,I18n(c , "pages.setting.toasts.modifyUser"), errors.New(I18n(c , "pages.setting.toasts.userPassMustBeNotEmpty")))
jsonMsg(c, I18n(c, "pages.setting.toasts.modifyUser"), errors.New(I18n(c, "pages.setting.toasts.userPassMustBeNotEmpty")))
return
}
err = a.userService.UpdateUser(user.Id, form.NewUsername, form.NewPassword)
@@ -79,10 +111,10 @@ func (a *SettingController) updateUser(c *gin.Context) {
user.Password = form.NewPassword
session.SetLoginUser(c, user)
}
jsonMsg(c, I18n(c , "pages.setting.toasts.modifyUser"), err)
jsonMsg(c, I18n(c, "pages.setting.toasts.modifyUser"), err)
}
func (a *SettingController) restartPanel(c *gin.Context) {
err := a.panelService.RestartPanel(time.Second * 3)
jsonMsg(c, I18n(c , "pages.setting.restartPanel"), err)
jsonMsg(c, I18n(c, "pages.setting.restartPanel"), err)
}

46
web/controller/sub.go Normal file
View File

@@ -0,0 +1,46 @@
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, header, err := a.subService.GetSubs(subId, host)
if err != nil || len(subs) == 0 {
c.String(400, "Error!")
} else {
result := ""
for _, sub := range subs {
result += sub + "\n"
}
// Add subscription-userinfo
c.Writer.Header().Set("Subscription-Userinfo", header)
c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
}
}

View File

@@ -46,12 +46,12 @@ func jsonMsgObj(c *gin.Context, msg string, obj interface{}, err error) {
if err == nil {
m.Success = true
if msg != "" {
m.Msg = msg + I18n(c , "success")
m.Msg = msg + I18n(c, "success")
}
} else {
m.Success = false
m.Msg = msg + I18n(c , "fail") + ": " + err.Error()
logger.Warning(msg + I18n(c , "fail") + ": ", err)
m.Msg = msg + I18n(c, "fail") + ": " + err.Error()
logger.Warning(msg+I18n(c, "fail")+": ", err)
}
c.JSON(http.StatusOK, m)
}
@@ -75,6 +75,7 @@ func html(c *gin.Context, name string, title string, data gin.H) {
data = gin.H{}
}
data["title"] = title
data["host"] = strings.Split(c.Request.Host, ":")[0]
data["request_uri"] = c.Request.RequestURI
data["base_path"] = c.GetString("base_path")
c.HTML(http.StatusOK, name, getContext(data))

View File

@@ -32,13 +32,17 @@ type AllSetting struct {
WebCertFile string `json:"webCertFile" form:"webCertFile"`
WebKeyFile string `json:"webKeyFile" form:"webKeyFile"`
WebBasePath string `json:"webBasePath" form:"webBasePath"`
SessionMaxAge int `json:"sessionMaxAge" form:"sessionMaxAge"`
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 int `json:"tgBotChatId" form:"tgBotChatId"`
TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"`
TgRunTime string `json:"tgRunTime" form:"tgRunTime"`
TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"`
TgCpu int `json:"tgCpu" form:"tgCpu"`
XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"`
TimeLocation string `json:"timeLocation" form:"timeLocation"`
TimeLocation string `json:"timeLocation" form:"timeLocation"`
}
func (s *AllSetting) CheckValid() error {

View File

@@ -7,11 +7,12 @@
<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;
}
</style>
<title>{{ i18n .title}}</title>
<title>{{ .host }}-{{ i18n .title}}</title>
</head>
{{end}}

View File

@@ -1,6 +1,7 @@
{{define "promptModal"}}
<a-modal id="prompt-modal" v-model="promptModal.visible" :title="promptModal.title"
:closable="true" @ok="promptModal.ok" :mask-closable="false"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:ok-text="promptModal.okText" cancel-text='{{ i18n "cancel" }}'>
<a-input id="prompt-modal-input" :type="promptModal.type"
v-model="promptModal.value"

View File

@@ -1,8 +1,11 @@
{{define "qrcodeModal"}}
<a-modal id="qrcode-modal" v-model="qrModal.visible" :title="qrModal.title"
:closable="true" width="300px" :ok-text="qrModal.okText"
cancel-text='{{ i18n "close" }}' :ok-button-props="{attrs:{id:'qr-modal-ok-btn'}}">
<canvas id="qrCode" style="width: 100%; height: 100%;"></canvas>
:closable="true"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:footer="null"
width="300px">
<a-tag color="green" style="margin-bottom: 10px;display: block;text-align: center;" >{{ i18n "pages.inbounds.clickOnQRcode" }}</a-tag>
<canvas @click="copyToClipboard()" id="qrCode" style="width: 100%; height: 100%;"></canvas>
</a-modal>
<script>
@@ -12,17 +15,15 @@
content: '',
inbound: new Inbound(),
dbInbound: new DBInbound(),
okText: '',
copyText: '',
qrcode: null,
clipboard: null,
visible: false,
show: function (title='', content='', dbInbound=new DBInbound(),okText='{{ i18n "copy" }}', copyText='') {
show: function (title='', content='', dbInbound=new DBInbound(), copyText='') {
this.title = title;
this.content = content;
this.dbInbound = dbInbound;
this.inbound = dbInbound.toInbound();
this.okText = okText;
if (ObjectUtil.isEmpty(copyText)) {
this.copyText = content;
} else {
@@ -30,12 +31,6 @@
}
this.visible = true;
qrModalApp.$nextTick(() => {
if (this.clipboard === null) {
this.clipboard = new ClipboardJS('#qr-modal-ok-btn', {
text: () => this.copyText,
});
this.clipboard.on('success', () => app.$message.success('{{ i18n "copied" }}'));
}
if (this.qrcode === null) {
this.qrcode = new QRious({
element: document.querySelector('#qrCode'),
@@ -57,6 +52,17 @@
data: {
qrModal: qrModal,
},
methods: {
copyToClipboard() {
this.qrModal.clipboard = new ClipboardJS('#qrCode', {
text: () => this.qrModal.copyText,
});
this.qrModal.clipboard.on('success', () => {
app.$message.success('{{ i18n "copied" }}')
this.qrModal.clipboard.destroy();
});
}
},
});
</script>

View File

@@ -1,9 +1,10 @@
{{define "textModal"}}
<a-modal id="text-modal" v-model="txtModal.visible" :title="txtModal.title"
:closable="true" ok-text='{{ i18n "copy" }}' cancel-text='{{ i18n "close" }}'
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:ok-button-props="{attrs:{id:'txt-modal-ok-btn'}}">
<a-button v-if="!ObjectUtil.isEmpty(txtModal.fileName)" type="primary" style="margin-bottom: 10px;"
@click="downloader.download(txtModal.fileName, txtModal.content)">
:href="'data:application/text;charset=utf-8,' + encodeURIComponent(txtModal.content)" :download="txtModal.fileName">
{{ i18n "download" }} [[ txtModal.fileName ]]
</a-button>
<a-input type="textarea" v-model="txtModal.content"
@@ -31,15 +32,6 @@
});
this.clipboard.on('success', () => app.$message.success('{{ i18n "copied" }}'));
}
if (this.qrcode === null) {
this.qrcode = new QRious({
element: document.querySelector('#qrCode'),
size: 260,
value: content,
});
} else {
this.qrcode.value = content;
}
});
},
close: function () {
@@ -48,6 +40,7 @@
};
const textModalApp = new Vue({
delimiters: ['[[', ']]'],
el: '#text-modal',
data: {
txtModal: txtModal,

View File

@@ -0,0 +1,255 @@
{{define "clientsBulkModal"}}
<a-modal id="client-bulk-modal" v-model="clientsBulkModal.visible" :title="clientsBulkModal.title" @ok="clientsBulkModal.ok"
:confirm-loading="clientsBulkModal.confirmLoading" :closable="true" :mask-closable="false"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:ok-text="clientsBulkModal.okText" cancel-text='{{ i18n "close" }}'>
<a-form layout="inline">
<table width="100%" class="ant-table-tbody">
<tr>
<td>{{ i18n "pages.client.method" }}</td>
<td>
<a-form-item>
<a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid" style="width: 250px"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option :value="0">Random</a-select-option>
<a-select-option :value="1">Random+Prefix</a-select-option>
<a-select-option :value="2">Random+Prefix+Num</a-select-option>
<a-select-option :value="3">Random+Prefix+Num+Postfix</a-select-option>
<a-select-option :value="4">Prefix+Num+Postfix</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr v-if="clientsBulkModal.emailMethod>1">
<td>{{ i18n "pages.client.first" }}</td>
<td>
<a-form-item>
<a-input-number v-model="clientsBulkModal.firstNum" :min="1"></a-input-number>
</a-form-item>
</td>
</tr>
<tr v-if="clientsBulkModal.emailMethod>1">
<td>{{ i18n "pages.client.last" }}</td>
<td>
<a-form-item>
<a-input-number v-model="clientsBulkModal.lastNum" :min="clientsBulkModal.firstNum"></a-input-number>
</a-form-item>
</td>
</tr>
<tr v-if="clientsBulkModal.emailMethod>0">
<td>{{ i18n "pages.client.prefix" }}</td>
<td>
<a-form-item>
<a-input v-model="clientsBulkModal.emailPrefix" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr v-if="clientsBulkModal.emailMethod>2">
<td>{{ i18n "pages.client.postfix" }}</td>
<td>
<a-form-item>
<a-input v-model="clientsBulkModal.emailPostfix" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr v-if="clientsBulkModal.emailMethod < 2">
<td>{{ i18n "pages.client.clientCount" }}</td>
<td>
<a-form-item>
<a-input-number v-model="clientsBulkModal.quantity" :min="1" :max="100"></a-input-number>
</a-form-item>
</td>
</tr>
<tr v-if="clientsBulkModal.inbound.canEnableTlsFlow()">
<td>Flow</td>
<td>
<a-form-item>
<a-select v-model="clientsBulkModal.flow" style="width: 250px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>Subscription</td>
<td>
<a-form-item>
<a-input v-model.trim="clientsBulkModal.subId" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>Telegram Username</td>
<td>
<a-form-item>
<a-input v-model.trim="clientsBulkModal.tgId" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-input-number v-model="clientsBulkModal.totalGB" :min="0"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.client.delayedStart" }}</td>
<td>
<a-form-item>
<a-switch v-model="clientsBulkModal.delayedStart" @click="clientsBulkModal.expiryTime=0"></a-switch>
</a-form-item>
</td>
</tr>
<tr v-if="clientsBulkModal.delayedStart">
<td>{{ i18n "pages.client.expireDays" }}</td>
<td>
<a-form-item>
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
</a-form-item>
</td>
</tr>
<tr v-else>
<td>
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
v-model="clientsBulkModal.expiryTime" style="width: 250px;"></a-date-picker>
</a-form-item>
</td>
</tr>
</table>
</a-form>
</a-modal>
<script>
const clientsBulkModal = {
visible: false,
confirmLoading: false,
title: '',
okText: '',
confirm: null,
dbInbound: new DBInbound(),
inbound: new Inbound(),
quantity: 1,
totalGB: 0,
expiryTime: '',
emailMethod: 0,
firstNum: 1,
lastNum: 1,
emailPrefix: "",
emailPostfix: "",
subId: "",
tgId: "",
flow: "",
delayedStart: false,
ok() {
clients = [];
method=clientsBulkModal.emailMethod;
if(method>1){
start=clientsBulkModal.firstNum;
end=clientsBulkModal.lastNum + 1;
} else {
start=0;
end=clientsBulkModal.quantity;
}
prefix = (method>0 && clientsBulkModal.emailPrefix.length>0) ? clientsBulkModal.emailPrefix : "";
useNum=(method>1);
postfix = (method>2 && clientsBulkModal.emailPostfix.length>0) ? clientsBulkModal.emailPostfix : "";
for (let i = start; i < end; i++) {
newClient = clientsBulkModal.newClient(clientsBulkModal.dbInbound.protocol);
if(method==4) newClient.email = "";
newClient.email += useNum ? prefix + i.toString() + postfix : prefix + postfix;
newClient.subId = clientsBulkModal.subId;
newClient.tgId = clientsBulkModal.tgId;
newClient._totalGB = clientsBulkModal.totalGB;
newClient._expiryTime = clientsBulkModal.expiryTime;
if(clientsBulkModal.inbound.canEnableTlsFlow()){
newClient.flow = clientsBulkModal.flow;
}
clients.push(newClient);
}
ObjectUtil.execute(clientsBulkModal.confirm, clients, clientsBulkModal.dbInbound.id);
},
show({ title='', okText='{{ i18n "sure" }}', dbInbound=null, confirm=(inbound, dbInbound)=>{} }) {
this.visible = true;
this.title = title;
this.okText = okText;
this.confirm = confirm;
this.quantity = 1;
this.totalGB = 0;
this.expiryTime = 0;
this.emailMethod= 0;
this.firstNum= 1;
this.lastNum= 1;
this.emailPrefix= "";
this.emailPostfix= "";
this.subId= "";
this.tgId= "";
this.flow= "";
this.dbInbound = new DBInbound(dbInbound);
this.inbound = dbInbound.toInbound();
this.delayedStart = false;
},
getClients(protocol, clientSettings) {
switch(protocol){
case Protocols.VMESS: return clientSettings.vmesses;
case Protocols.VLESS: return clientSettings.vlesses;
case Protocols.TROJAN: return clientSettings.trojans;
default: return null;
}
},
newClient(protocol) {
switch (protocol) {
case Protocols.VMESS: return new Inbound.VmessSettings.Vmess();
case Protocols.VLESS: return new Inbound.VLESSSettings.VLESS();
case Protocols.TROJAN: return new Inbound.TrojanSettings.Trojan();
default: return null;
}
},
close() {
clientsBulkModal.visible = false;
clientsBulkModal.loading(false);
},
loading(loading) {
clientsBulkModal.confirmLoading = loading;
},
};
const clientsBulkModalApp = new Vue({
delimiters: ['[[', ']]'],
el: '#client-bulk-modal',
data: {
clientsBulkModal,
get inbound() {
return this.clientsBulkModal.inbound;
},
get delayedExpireDays() {
return this.clientsBulkModal.expiryTime < 0 ? this.clientsBulkModal.expiryTime / -86400000 : 0;
},
set delayedExpireDays(days){
this.clientsBulkModal.expiryTime = -86400000 * days;
},
},
});
</script>
{{end}}

View File

@@ -0,0 +1,148 @@
{{define "clientsModal"}}
<a-modal id="client-modal" v-model="clientModal.visible" :title="clientModal.title" @ok="clientModal.ok"
:confirm-loading="clientModal.confirmLoading" :closable="true" :mask-closable="false"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:ok-text="clientModal.okText" cancel-text='{{ i18n "close" }}'>
{{template "form/client"}}
</a-modal>
<script>
const clientModal = {
visible: false,
confirmLoading: false,
title: '',
okText: '',
isEdit: false,
dbInbound: new DBInbound(),
inbound: new Inbound(),
clients: [],
clientStats: [],
oldClientId: "",
index: null,
isExpired: false,
delayedStart: false,
ok() {
if(clientModal.isEdit){
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id, clientModal.oldClientId);
} else {
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id);
}
},
show({ title='', okText='{{ i18n "sure" }}', index=null, dbInbound=null, confirm=()=>{}, isEdit=false }) {
this.visible = true;
this.title = title;
this.okText = okText;
this.isEdit = isEdit;
this.dbInbound = new DBInbound(dbInbound);
this.inbound = dbInbound.toInbound();
this.clients = this.getClients(this.inbound.protocol, this.inbound.settings);
this.index = index === null ? this.clients.length : index;
this.isExpired = isEdit ? this.inbound.isExpiry(this.index) : false;
this.delayedStart = false;
if (isEdit){
if (this.clients[index].expiryTime < 0){
this.delayedStart = true;
}
this.oldClientId = this.dbInbound.protocol == "trojan" ? this.clients[index].password : this.clients[index].id;
} else {
this.addClient(this.inbound.protocol, this.clients);
}
this.clientStats = this.dbInbound.clientStats.find(row => row.email === this.clients[this.index].email);
this.confirm = confirm;
},
getClients(protocol, clientSettings) {
switch(protocol){
case Protocols.VMESS: return clientSettings.vmesses;
case Protocols.VLESS: return clientSettings.vlesses;
case Protocols.TROJAN: return clientSettings.trojans;
default: return null;
}
},
addClient(protocol, clients) {
switch (protocol) {
case Protocols.VMESS: return clients.push(new Inbound.VmessSettings.Vmess());
case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS());
case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan());
default: return null;
}
},
close() {
clientModal.visible = false;
clientModal.loading(false);
},
loading(loading) {
clientModal.confirmLoading = loading;
},
};
const clientModalApp = new Vue({
delimiters: ['[[', ']]'],
el: '#client-modal',
data: {
clientModal,
get inbound() {
return this.clientModal.inbound;
},
get client() {
return this.clientModal.clients[this.clientModal.index];
},
get clientStats() {
return this.clientModal.clientStats;
},
get isEdit() {
return this.clientModal.isEdit;
},
get isTrafficExhausted() {
if(!clientStats) return false
if(clientStats.total <= 0) return false
if(clientStats.up + clientStats.down < clientStats.total) return false
return true
},
get isExpiry() {
return this.clientModal.isExpired
},
get statsColor() {
if(!clientStats) return 'blue'
if(clientStats.total <= 0) return 'blue'
else if(clientStats.total > 0 && (clientStats.down+clientStats.up) < clientStats.total) return 'cyan'
else return 'red'
},
get delayedExpireDays() {
return this.client && this.client.expiryTime < 0 ? this.client.expiryTime / -86400000 : 0;
},
set delayedExpireDays(days){
this.client.expiryTime = -86400000 * days;
},
},
methods: {
getNewEmail(client) {
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
var string = '';
var len = 6 + Math.floor(Math.random() * 5);
for(var ii=0; ii<len; ii++){
string += chars[Math.floor(Math.random() * chars.length)];
}
client.email = string;
},
resetClientTraffic(email,dbInboundId,iconElement) {
this.$confirm({
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '',
okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: async () => {
iconElement.disabled = true;
const msg = await HttpUtil.postWithModal('/xui/inbound/' + dbInboundId + '/resetClientTraffic/'+ email);
if (msg.success) {
this.clientModal.clientStats.up = 0;
this.clientModal.clientStats.down = 0;
}
iconElement.disabled = false;
},
})
},
},
});
</script>
{{end}}

View File

@@ -33,27 +33,50 @@
{{define "commonSider"}}
<a-layout-sider id="sider" collapsible breakpoint="md" collapsed-width="0">
<a-menu theme="dark" mode="inline" :selected-keys="['{{ .request_uri }}']"
<a-layout-sider :theme="siderDrawer.theme" id="sider" collapsible breakpoint="md" collapsed-width="0">
<a-menu :theme="siderDrawer.theme" mode="inline" selected-keys="">
<a-menu-item mode="inline">
<a-icon type="bg-colors"></a-icon>
<a-switch :default-checked="siderDrawer.isDarkTheme"
checked-children="☀"
un-checked-children="🌙"
@change="siderDrawer.changeTheme()"></a-switch>
</a-menu-item>
</a-menu>
<a-menu :theme="siderDrawer.theme" mode="inline" :selected-keys="['{{ .request_uri }}']"
@click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key">
{{template "menuItems" .}}
</a-menu>
</a-layout-sider>
<a-drawer id="sider-drawer" placement="left" :closable="false"
@close="siderDrawer.close()"
:visible="siderDrawer.visible" :wrap-style="{ padding: 0 }">
:visible="siderDrawer.visible"
:wrap-class-name="siderDrawer.isDarkTheme ? 'ant-drawer-dark' : ''"
:wrap-style="{ padding: 0 }">
<div class="drawer-handle" @click="siderDrawer.change()" slot="handle">
<a-icon :type="siderDrawer.visible ? 'close' : 'menu-fold'"></a-icon>
</div>
<a-menu theme="light" mode="inline" :selected-keys="['{{ .request_uri }}']"
<a-menu :theme="siderDrawer.theme" mode="inline" selected-keys="">
<a-menu-item mode="inline">
<a-icon type="bg-colors"></a-icon>
<a-switch :default-checked="siderDrawer.isDarkTheme"
checked-children="☀"
un-checked-children="🌙"
@change="siderDrawer.changeTheme()"></a-switch>
</a-menu-item>
</a-menu>
<a-menu :theme="siderDrawer.theme" mode="inline" :selected-keys="['{{ .request_uri }}']"
@click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key">
{{template "menuItems" .}}
</a-menu>
</a-drawer>
<script>
const darkClass = "ant-card-dark";
const bgDarkStyle = "background-color: #242c3a";
const siderDrawer = {
visible: false,
collapsed: false,
isDarkTheme: localStorage.getItem("dark-mode") === 'true' ? true : false,
show() {
this.visible = true;
},
@@ -62,6 +85,16 @@
},
change() {
this.visible = !this.visible;
},
toggleCollapsed() {
this.collapsed = !this.collapsed;
},
changeTheme() {
this.isDarkTheme = ! this.isDarkTheme;
localStorage.setItem("dark-mode", this.isDarkTheme);
},
get theme() {
return this.isDarkTheme ? 'dark' : 'light';
}
};

View File

@@ -9,7 +9,7 @@
<a-input :value="value" @input="$emit('input', $event.target.value)"></a-input>
</template>
<template v-else-if="type === 'number'">
<a-input type="number" :value="value" @input="$emit('input', $event.target.value)"></a-input>
<a-input type="number" :value="value" @input="$emit('input', $event.target.value)" :min="min"></a-input>
</template>
<template v-else-if="type === 'textarea'">
<a-textarea :value="value" @input="$emit('input', $event.target.value)" :auto-size="{ minRows: 10, maxRows: 10 }"></a-textarea>
@@ -25,7 +25,7 @@
{{define "component/setting"}}
<script>
Vue.component('setting-list-item', {
props: ["type", "title", "desc", "value"],
props: ["type", "title", "desc", "value", "min"],
template: `{{template "component/settingListItem"}}`,
});
</script>

View File

@@ -0,0 +1,149 @@
{{define "form/client"}}
<a-form layout="inline" v-if="client">
<template v-if="isEdit">
<a-tag v-if="isExpiry || isTrafficExhausted" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag>
</template>
<table width="100%" class="ant-table-tbody">
<tr>
<td>{{ i18n "pages.inbounds.enable" }}</td>
<td>
<a-form-item>
<a-switch v-model="client.enable"></a-switch>
</a-form-item>
</td>
</tr>
<tr>
<td>
<span>{{ i18n "pages.inbounds.Email" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.EmailDesc" }}</span>
</template>
<a-icon type="sync" @click="getNewEmail(client)"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-input v-model.trim="client.email" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr v-if="inbound.protocol === Protocols.TROJAN">
<td>password</td>
<td>
<a-form-item>
<a-input v-model.trim="client.password" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr v-if="inbound.protocol === Protocols.VMESS || inbound.protocol === Protocols.VLESS">
<td>ID</td>
<td>
<a-form-item>
<a-input v-model.trim="client.id" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr v-if="inbound.protocol === Protocols.VMESS">
<td>{{ i18n "additional" }}</td>
<td>
<a-form-item>
<a-input-number v-model.number="client.alterId"></a-input-number>
</a-form-item>
</td>
</tr>
<tr v-if="client.email">
<td>Subscription</td>
<td>
<a-form-item>
<a-input v-model.trim="client.subId" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr v-if="client.email">
<td>Telegram Username</td>
<td>
<a-form-item>
<a-input v-model.trim="client.tgId" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr v-if="inbound.canEnableTlsFlow()">
<td>Flow</td>
<td>
<a-form-item>
<a-select v-model="client.flow" style="width: 250px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
</a-form-item>
</td>
</tr>
<tr v-if="isEdit && clientStats">
<td>{{ i18n "usage" }}</td>
<td>
<a-tag :color="statsColor">
[[ sizeFormat(clientStats.up) ]] /
[[ sizeFormat(clientStats.down) ]]
([[ sizeFormat(clientStats.up + clientStats.down) ]])
</a-tag>
<a-tooltip>
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
<a-icon type="retweet" @click="resetClientTraffic(client.email,clientStats.inboundId,$event.target)" v-if="client.email.length > 0"></a-icon>
</a-tooltip>
</td>
</tr>
<tr>
<td>{{ i18n "pages.client.delayedStart" }}</td>
<td>
<a-form-item>
<a-switch v-model="clientModal.delayedStart" @click="client._expiryTime=0"></a-switch>
</a-form-item>
</td>
</tr>
<tr v-if="clientModal.delayedStart">
<td>{{ i18n "pages.client.expireDays" }}</td>
<td>
<a-form-item>
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
</a-form-item>
</td>
</tr>
<tr v-else>
<td>
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
v-model="client._expiryTime" style="width: 250px;"></a-date-picker>
<a-tag color="red" v-if="isExpiry">Expired</a-tag>
</a-form-item>
</td>
</tr>
</table>
</a-form>
{{end}}

View File

@@ -1,57 +1,91 @@
{{define "form/inbound"}}
<!-- base -->
<a-form layout="inline">
<a-form-item label='{{ i18n "remark" }}'>
<a-input v-model.trim="dbInbound.remark"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "enable" }}'>
<a-switch v-model="dbInbound.enable"></a-switch>
</a-form-item>
<a-form-item label='{{ i18n "protocol" }}'>
<a-select v-model="inbound.protocol" style="width: 160px;">
<a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<span slot="label">
{{ i18n "monitor" }}
<a-tooltip>
<table width="100%" class="ant-table-tbody">
<tr>
<td>{{ i18n "enable" }}</td>
<td>
<a-form-item>
<a-switch v-model="dbInbound.enable"></a-switch>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "remark" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="dbInbound.remark" style="width: 250px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "protocol" }}</td>
<td>
<a-form-item>
<a-select v-model="inbound.protocol" style="width: 250px;" :disabled="isEdit" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "monitor" }}
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.monitorDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="inbound.listen"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.port" }}'>
<a-input type="number" v-model.number="inbound.port"></a-input>
</a-form-item>
<a-form-item>
<span slot="label">
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="dbInbound.totalGB" :min="0"></a-input-number>
</a-form-item>
<a-form-item>
<span slot="label">
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
v-model="dbInbound._expiryTime" style="width: 300px;"></a-date-picker>
</a-form-item>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.listen" style="width: 250px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.port" }}</td>
<td>
<a-form-item>
<a-input-number v-model.number="inbound.port"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td>
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-input-number v-model="dbInbound.totalGB" :min="0"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td>
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
v-model="dbInbound._expiryTime" style="width: 250px;"></a-date-picker>
</a-form-item>
</td>
</tr>
</table>
</a-form>
<!-- vmess settings -->

View File

@@ -1,17 +1,42 @@
{{define "form/dokodemo"}}
<a-form layout="inline">
<a-form-item label='{{ i18n "pages.inbounds.targetAddress"}}'>
<a-input v-model.trim="inbound.settings.address"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.destinationPort"}}'>
<a-input type="number" v-model.number="inbound.settings.port"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.network"}}'>
<a-select v-model="inbound.settings.network" style="width: 100px;">
<a-select-option value="tcp,udp">tcp+udp</a-select-option>
<a-select-option value="tcp">tcp</a-select-option>
<a-select-option value="udp">udp</a-select-option>
</a-select>
</a-form-item>
<table width="100%" class="ant-table-tbody">
<tr>
<td>{{ i18n "pages.inbounds.targetAddress"}}</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.settings.address"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.destinationPort"}}</td>
<td>
<a-form-item>
<a-input-number v-model.number="inbound.settings.port"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.network"}}</td>
<td>
<a-form-item>
<a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value="tcp,udp">tcp+udp</a-select-option>
<a-select-option value="tcp">tcp</a-select-option>
<a-select-option value="udp">udp</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>FollowRedirect</td>
<td>
<a-form-item>
<a-switch v-model="inbound.settings.followRedirect"></a-switch>
</a-form-item>
</td>
</tr>
</table>
</a-form>
{{end}}

View File

@@ -1,10 +1,22 @@
{{define "form/http"}}
<a-form layout="inline">
<a-form-item label='{{ i18n "username"}}'>
<a-input v-model.trim="inbound.settings.accounts[0].user"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "password" }}'>
<a-input v-model.trim="inbound.settings.accounts[0].pass"></a-input>
</a-form-item>
<table width="100%" class="ant-table-tbody">
<tr>
<td>{{ i18n "username"}}</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.settings.accounts[0].user"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "password" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.settings.accounts[0].pass"></a-input>
</a-form-item>
</td>
</tr>
</table>
</a-form>
{{end}}

View File

@@ -1,19 +1,36 @@
{{define "form/shadowsocks"}}
<a-form layout="inline">
<a-form-item label='{{ i18n "encryption" }}'>
<a-select v-model="inbound.settings.method" style="width: 165px;">
<a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='{{ i18n "password" }}'>
<a-input v-model.trim="inbound.settings.password"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.network" }}'>
<a-select v-model="inbound.settings.network" style="width: 100px;">
<a-select-option value="tcp,udp">tcp+udp</a-select-option>
<a-select-option value="tcp">tcp</a-select-option>
<a-select-option value="udp">udp</a-select-option>
</a-select>
</a-form-item>
<table width="100%" class="ant-table-tbody">
<tr>
<td>{{ i18n "encryption" }}</td>
<td>
<a-form-item>
<a-select v-model="inbound.settings.method" style="width: 165px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "password" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.settings.password"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.network" }}</td>
<td>
<a-form-item>
<a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value="tcp,udp">tcp+udp</a-select-option>
<a-select-option value="tcp">tcp</a-select-option>
<a-select-option value="udp">udp</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
</table>
</a-form>
{{end}}

View File

@@ -1,24 +1,50 @@
{{define "form/socks"}}
<a-form layout="inline">
<!-- <a-form-item label="Password authentication">-->
<a-form-item label='{{ i18n "password" }}'>
<a-switch :checked="inbound.settings.auth === 'password'"
@change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"></a-switch>
</a-form-item>
<table width="100%" class="ant-table-tbody">
<tr>
<td>{{ i18n "password" }}</td>
<td>
<a-form-item>
<a-switch :checked="inbound.settings.auth === 'password'"
@change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"></a-switch>
</a-form-item>
</td>
</tr>
<template v-if="inbound.settings.auth === 'password'">
<a-form-item label='{{ i18n "username" }}'>
<a-input v-model.trim="inbound.settings.accounts[0].user"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "password" }}'>
<a-input v-model.trim="inbound.settings.accounts[0].pass"></a-input>
</a-form-item>
<tr>
<td>{{ i18n "username" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.settings.accounts[0].user"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "password" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.settings.accounts[0].pass"></a-input>
</a-form-item>
</td>
</tr>
</template>
<a-form-item label='{{ i18n "pages.inbounds.enable" }} udp'>
<a-switch v-model="inbound.settings.udp"></a-switch>
</a-form-item>
<a-form-item v-if="inbound.settings.udp"
label="IP">
<a-input v-model.trim="inbound.settings.ip"></a-input>
</a-form-item>
<tr>
<td>{{ i18n "pages.inbounds.enable" }} udp</td>
<td>
<a-form-item>
<a-switch v-model="inbound.settings.udp"></a-switch>
</a-form-item>
</td>
</tr>
<tr>
<td>IP</td>
<td>
<a-form-item v-if="inbound.settings.udp">
<a-input v-model.trim="inbound.settings.ip"></a-input>
</a-form-item>
</td>
</tr>
</table>
</a-form>
{{end}}

View File

@@ -1,143 +1,148 @@
{{define "form/trojan"}}
<a-form layout="inline">
<label>{{ i18n "clients"}} </label>
<a-collapse activeKey="0" v-for="(trojan, index) in inbound.settings.trojans"
:key="`trojan-${index}`">
<a-collapse-panel :class="getHeaderStyle(trojan.email)" :header="getHeaderText(trojan.email)">
<a-tag v-if="isExpiry(index) || ((getUpStats(trojan.email) + getDownStats(trojan.email)) > trojan.totalGB && trojan.totalGB != 0)" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag>
<a-form layout="inline">
<a-form-item>
<span slot="label">
Email
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.trojans.slice(0,1)" v-if="!isEdit">
<a-collapse-panel header="{{ i18n "pages.inbounds.client" }}">
<table width="100%" class="ant-table-tbody">
<tr>
<td>
<span>{{ i18n "pages.inbounds.Email" }}</span>
<a-tooltip>
<template slot="title">
The email must be completely unique
<span>{{ i18n "pages.inbounds.EmailDesc" }}</span>
</template>
<!--Renew Svg Icon-->
<svg
@click="getNewEmail(trojan)"
xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="anticon anticon-question-circle" viewBox="0 0 16 16"> <path d="M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41zm-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9z"/> <path fill-rule="evenodd" d="M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z"/> </svg>
<a-icon @click="getNewEmail(client)" type="sync"> </a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="trojan.email"></a-input>
</a-form-item>
</a-form>
<a-form-item label="password">
<a-input v-model.trim="trojan.password"></a-input>
</a-form-item>
<a-form-item v-if="inbound.xtls" label="flow">
<a-select v-model="trojan.flow" style="width: 150px">
<a-select-option value="">{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<span slot="label">
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="trojan._totalGB" :min="0"></a-input-number>
</a-form-item>
<a-form-item>
<span slot="label">
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
v-model="trojan._expiryTime" style="width: 300px;"></a-date-picker>
</a-form-item>
<a-form layout="inline">
<a-tooltip v-if="trojan._totalGB > 0">
<template slot="title">
{{ i18n "pages.inbounds.resetTraffic" }}
</template>
<span style="color: #FF4D4F">
<a-icon type="delete" @click="resetClientTraffic(trojan,$event)"></a-icon>
</span>
</a-tooltip>
<a-tag color="blue">[[ sizeFormat(getUpStats(trojan.email)) ]] / [[ sizeFormat(getDownStats(trojan.email)) ]]</a-tag>
<a-tag v-if="trojan._totalGB > 0" color="red">used : [[ sizeFormat(getUpStats(trojan.email) + getDownStats(trojan.email)) ]]</a-tag>
<a-tag v-show="inbound.settings.trojans.length > 1">
<svg
v-show="inbound.settings.trojans.length > 1"
@click="removeClient(index, inbound.settings.trojans)"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
class="ml-2 cursor-pointer"
>
<path fill="none" d="M0 0h24v24H0z" />
<path
fill="#EC4899"
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm0-9.414l2.828-2.829 1.415 1.415L13.414 12l2.829 2.828-1.415 1.415L12 13.414l-2.828 2.829-1.415-1.415L10.586 12 7.757 9.172l1.415-1.415L12 10.586z"
/>
</svg>
</a-tag>
</a-form>
</td>
<td>
<a-form-item>
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>password</td>
<td>
<a-form-item>
<a-input v-model.trim="client.password" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>Subscription</td>
<td>
<a-form-item v-if="client.email">
<a-input v-model.trim="client.subId" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>Telegram Username</td>
<td>
<a-form-item v-if="client.email">
<a-input v-model.trim="client.tgId" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.client.delayedStart" }}</td>
<td>
<a-form-item>
<a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
</a-form-item>
</td>
</tr>
<tr v-if="delayedStart">
<td>{{ i18n "pages.client.expireDays" }}</td>
<td>
<a-form-item>
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
</a-form-item>
</td>
</tr>
<tr v-else>
<td>
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
v-model="client._expiryTime" style="width: 200px;"></a-date-picker>
</a-form-item>
</td>
</tr>
</table>
</a-collapse-panel>
</a-collapse>
<a-tag>
<svg
@click="addClient(inbound.protocol, inbound.settings.trojans)"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
class="ml-2 cursor-pointer"
>
<path fill="none" d="M0 0h24v24H0z" />
<path
fill="green"
d="M11 11V7h2v4h4v2h-4v4h-2v-4H7v-2h4zm1 11C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16z"
/>
</svg>
</a-tag>
<a-collapse v-else>
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.trojans.length">
<table width="100%">
<tr class="client-table-header">
<th v-for="col in Object.keys(inbound.settings.trojans[0]).slice(0, 3)">[[ col ]]</th>
</tr>
<tr v-for="(client, index) in inbound.settings.trojans" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
</tr>
</table>
</a-collapse-panel>
</a-collapse>
<template v-if="inbound.isTcp && inbound.tls">
<a-form layout="inline">
<a-form-item label="Fallbacks">
<a-row>
<a-button type="primary" size="small"
@click="inbound.settings.addTrojanFallback()">
+
</a-button>
</a-row>
</a-form-item>
</a-form>
<a-form layout="inline">
<a-form-item label="fallbacks">
<a-row>
<a-button type="primary" size="small"
@click="inbound.settings.addTrojanFallback()">
+
</a-button>
</a-row>
</a-form-item>
</a-form>
<!-- trojan fallbacks -->
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline">
<a-divider>
fallback[[ index + 1 ]]
<a-icon type="delete" @click="() => inbound.settings.delTrojanFallback(index)"
style="color: rgb(255, 77, 79);cursor: pointer;"/>
</a-divider>
<a-form-item label="name">
<a-input v-model="fallback.name"></a-input>
</a-form-item>
<a-form-item label="alpn">
<a-input v-model="fallback.alpn"></a-input>
</a-form-item>
<a-form-item label="path">
<a-input v-model="fallback.path"></a-input>
</a-form-item>
<a-form-item label="dest">
<a-input v-model="fallback.dest"></a-input>
</a-form-item>
<a-form-item label="xver">
<a-input type="number" v-model.number="fallback.xver"></a-input>
</a-form-item>
<a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
</a-form>
<!-- trojan fallbacks -->
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline">
<a-divider>
fallback[[ index + 1 ]]
<a-icon type="delete" @click="() => inbound.settings.delTrojanFallback(index)"
style="color: rgb(255, 77, 79);cursor: pointer;"/>
</a-divider>
<a-form-item label="name">
<a-input v-model="fallback.name"></a-input>
</a-form-item>
<a-form-item label="alpn">
<a-input v-model="fallback.alpn"></a-input>
</a-form-item>
<a-form-item label="path">
<a-input v-model="fallback.path"></a-input>
</a-form-item>
<a-form-item label="dest">
<a-input v-model="fallback.dest"></a-input>
</a-form-item>
<a-form-item label="xver">
<a-input-number v-model.number="fallback.xver"></a-input-number>
</a-form-item>
<a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
</a-form>
</template>
{{end}}

View File

@@ -1,144 +1,159 @@
{{define "form/vless"}}
<a-form layout="inline">
<label>{{ i18n "clients"}}</label>
<a-collapse activeKey="0" v-for="(vless, index) in inbound.settings.vlesses"
:key="`vless-${index}`">
<a-collapse-panel :class="getHeaderStyle(vless.email)" :header="getHeaderText(vless.email)">
<a-tag v-if="isExpiry(index) || ((getUpStats(vless.email) + getDownStats(vless.email)) > vless.totalGB && vless.totalGB != 0)" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag>
<a-form layout="inline">
<a-form-item>
<span slot="label">
Email
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit">
<a-collapse-panel header="{{ i18n "pages.inbounds.client" }}">
<table width="100%" class="ant-table-tbody">
<tr>
<td>
<span>{{ i18n "pages.inbounds.Email" }}</span>
<a-tooltip>
<template slot="title">
The email must be completely unique
<span>{{ i18n "pages.inbounds.EmailDesc" }}</span>
</template>
<!--Renew Svg Icon-->
<svg
@click="getNewEmail(vless)"
xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="anticon anticon-question-circle" viewBox="0 0 16 16"> <path d="M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41zm-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9z"/> <path fill-rule="evenodd" d="M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z"/> </svg>
<a-icon @click="getNewEmail(client)" type="sync"> </a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="vless.email"></a-input>
</a-form-item>
</a-form>
<a-form-item label="id">
<a-input v-model.trim="vless.id"></a-input>
</a-form-item>
<a-form-item v-if="inbound.xtls" label="flow">
<a-select v-model="vless.flow" style="width: 150px">
<a-select-option value="">{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<span slot="label">
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="vless._totalGB" :min="0"></a-input-number>
</a-form-item>
<a-form-item>
<span slot="label">
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
v-model="vless._expiryTime" style="width: 300px;"></a-date-picker>
</a-form-item>
<a-form layout="inline">
<a-tooltip v-if="vless._totalGB > 0">
<template slot="title">
{{ i18n "pages.inbounds.resetTraffic" }}
</template>
<span style="color: #FF4D4F">
<a-icon type="delete" @click="resetClientTraffic(vless,$event)"></a-icon>
</span>
</a-tooltip>
<a-tag color="blue">[[ sizeFormat(getUpStats(vless.email)) ]] / [[ sizeFormat(getDownStats(vless.email)) ]]</a-tag>
<a-tag v-if="vless._totalGB > 0" color="red">used : [[ sizeFormat(getUpStats(vless.email) + getDownStats(vless.email)) ]]</a-tag>
<a-tag v-show="inbound.settings.vlesses.length > 1">
<svg
@click="removeClient(index, inbound.settings.vlesses)"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 22 22"
width="22"
height="22"
class="mt-2 cursor-pointer"
>
<path fill="none" d="M0 0h24v24H0z" />
<path
fill="#EC4899"
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm0-9.414l2.828-2.829 1.415 1.415L13.414 12l2.829 2.828-1.415 1.415L12 13.414l-2.828 2.829-1.415-1.415L10.586 12 7.757 9.172l1.415-1.415L12 10.586z"
/>
</svg>
</a-tag>
</a-form>
</td>
<td>
<a-form-item>
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>id</td>
<td>
<a-form-item>
<a-input v-model.trim="client.id" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr v-if="inbound.canEnableTlsFlow()">
<td>flow</td>
<td>
<a-form-item>
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 200px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>Subscription</td>
<td>
<a-form-item v-if="client.email">
<a-input v-model.trim="client.subId" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>Telegram Username</td>
<td>
<a-form-item v-if="client.email">
<a-input v-model.trim="client.tgId" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.client.delayedStart" }}</td>
<td>
<a-form-item>
<a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
</a-form-item>
</td>
</tr>
<tr v-if="delayedStart">
<td>{{ i18n "pages.client.expireDays" }}</td>
<td>
<a-form-item>
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
</a-form-item>
</td>
</tr>
<tr v-else>
<td>
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
v-model="client._expiryTime" style="width: 200px;"></a-date-picker>
</a-form-item>
</td>
</tr>
</table>
</a-collapse-panel>
</a-collapse>
<a-tag>
<svg
@click="addClient(inbound.protocol, inbound.settings.vlesses)"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
class="ml-2 cursor-pointer"
>
<path fill="none" d="M0 0h24v24H0z" />
<path
fill="green"
d="M11 11V7h2v4h4v2h-4v4h-2v-4H7v-2h4zm1 11C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16z"
/>
</svg>
</a-tag>
<a-collapse v-else>
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.vlesses.length">
<table width="100%">
<tr class="client-table-header">
<th v-for="col in Object.keys(inbound.settings.vlesses[0]).slice(0, 3)">[[ col ]]</th>
</tr>
<tr v-for="(client, index) in inbound.settings.vlesses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
</tr>
</table>
</a-collapse-panel>
</a-collapse>
<template v-if="inbound.isTcp && inbound.tls">
<a-form layout="inline">
<a-form-item label="Fallbacks">
<a-row>
<a-button type="primary" size="small"
@click="inbound.settings.addFallback()">
+
</a-button>
</a-row>
</a-form-item>
</a-form>
<a-form layout="inline">
<a-form-item label="fallbacks">
<a-row>
<a-button type="primary" size="small"
@click="inbound.settings.addFallback()">
+
</a-button>
</a-row>
</a-form-item>
</a-form>
<!-- vless fallbacks -->
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline">
<a-divider>
fallback[[ index + 1 ]]
<a-icon type="delete" @click="() => inbound.settings.delFallback(index)"
style="color: rgb(255, 77, 79);cursor: pointer;"/>
</a-divider>
<a-form-item label="name">
<a-input v-model="fallback.name"></a-input>
</a-form-item>
<a-form-item label="alpn">
<a-input v-model="fallback.alpn"></a-input>
</a-form-item>
<a-form-item label="path">
<a-input v-model="fallback.path"></a-input>
</a-form-item>
<a-form-item label="dest">
<a-input v-model="fallback.dest"></a-input>
</a-form-item>
<a-form-item label="xver">
<a-input type="number" v-model.number="fallback.xver"></a-input>
</a-form-item>
<a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
</a-form>
<!-- vless fallbacks -->
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline">
<a-divider>
fallback[[ index + 1 ]]
<a-icon type="delete" @click="() => inbound.settings.delFallback(index)"
style="color: rgb(255, 77, 79);cursor: pointer;"/>
</a-divider>
<a-form-item label="name">
<a-input v-model="fallback.name"></a-input>
</a-form-item>
<a-form-item label="alpn">
<a-input v-model="fallback.alpn"></a-input>
</a-form-item>
<a-form-item label="path">
<a-input v-model="fallback.path"></a-input>
</a-form-item>
<a-form-item label="dest">
<a-input v-model="fallback.dest"></a-input>
</a-form-item>
<a-form-item label="xver">
<a-input-number v-model.number="fallback.xver"></a-input-number>
</a-form-item>
<a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
</a-form>
</template>
{{end}}

View File

@@ -1,115 +1,124 @@
{{define "form/vmess"}}
<a-form layout="inline">
<label>{{ i18n "clients"}}</label>
<a-collapse activeKey="0" v-for="(vmess, index) in inbound.settings.vmesses"
:key="`vmess-${index}`">
<a-collapse-panel :class="getHeaderStyle(vmess.email)" :header="getHeaderText(vmess.email)">
<a-tag v-if="isExpiry(index) || ((getUpStats(vmess.email) + getDownStats(vmess.email)) > vmess.totalGB && vmess.totalGB != 0)" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag>
<a-form layout="inline">
<a-form-item>
<span slot="label">
Email
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vmesses.slice(0,1)" v-if="!isEdit">
<a-collapse-panel header="{{ i18n "pages.inbounds.client" }}">
<table width="100%" class="ant-table-tbody">
<tr>
<td>
<span>{{ i18n "pages.inbounds.Email" }}</span>
<a-tooltip>
<template slot="title">
The email must be completely unique
<span>{{ i18n "pages.inbounds.EmailDesc" }}</span>
</template>
<!--Renew Svg Icon-->
<svg
@click="getNewEmail(vmess)"
xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="anticon anticon-question-circle" viewBox="0 0 16 16"> <path d="M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41zm-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9z"/> <path fill-rule="evenodd" d="M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z"/> </svg>
<a-icon type="sync" @click="getNewEmail(client)"></a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="vmess.email"></a-input>
</a-form-item>
</a-form>
<a-form-item label="id">
<a-input v-model.trim="vmess.id"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "additional" }} ID'>
<a-input type="number" v-model.number="vmess.alterId"></a-input>
</a-form-item>
<a-form-item>
<span slot="label">
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="vmess._totalGB" :min="0"></a-input-number>
</a-form-item>
<a-form-item>
<span slot="label">
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
v-model="vmess._expiryTime" style="width: 300px;"></a-date-picker>
</a-form-item>
<a-form layout="inline">
<a-tooltip v-if="vmess._totalGB > 0">
<template slot="title">
{{ i18n "pages.inbounds.resetTraffic" }}
</template>
<span style="color: #FF4D4F">
<a-icon type="delete" @click="resetClientTraffic(vmess,$event)"></a-icon>
</span>
</a-tooltip>
<a-tag color="blue">[[ sizeFormat(getUpStats(vmess.email)) ]] / [[ sizeFormat(getDownStats(vmess.email)) ]]</a-tag>
<a-tag v-if="vmess._totalGB > 0" color="red">used : [[ sizeFormat(getUpStats(vmess.email) + getDownStats(vmess.email)) ]]</a-tag>
<a-tag v-show="inbound.settings.vmesses.length > 1">
<!--Remove Svg Icon-->
<svg
@click="removeClient(index, inbound.settings.vmesses)"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 22 22"
width="22"
height="22"
class="mt-2 cursor-pointer"
>
<path fill="none" d="M0 0h24v24H0z" />
<path
fill="#EC4899"
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm0-9.414l2.828-2.829 1.415 1.415L13.414 12l2.829 2.828-1.415 1.415L12 13.414l-2.828 2.829-1.415-1.415L10.586 12 7.757 9.172l1.415-1.415L12 10.586z"
/>
</svg>
</a-tag>
</a-form>
</td>
<td>
<a-form-item>
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>id</td>
<td>
<a-form-item>
<a-input v-model.trim="client.id" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "additional" }}</td>
<td>
<a-form-item>
<a-input-number v-model.number="client.alterId"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td>Subscription</td>
<td>
<a-form-item v-if="client.email">
<a-input v-model.trim="client.subId" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>Telegram Username</td>
<td>
<a-form-item v-if="client.email">
<a-input v-model.trim="client.tgId" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.client.delayedStart" }}</td>
<td>
<a-form-item>
<a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
</a-form-item>
</td>
</tr>
<tr v-if="delayedStart">
<td>{{ i18n "pages.client.expireDays" }}</td>
<td>
<a-form-item>
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
</a-form-item>
</td>
</tr>
<tr v-else>
<td>
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
v-model="client._expiryTime" style="width: 200px;"></a-date-picker>
</a-form-item>
</td>
</tr>
</table>
</a-collapse-panel>
</a-collapse>
<a-collapse v-else>
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.vmesses.length">
<table width="100%">
<tr class="client-table-header">
<th v-for="col in Object.keys(inbound.settings.vmesses[0]).slice(0, 3)">[[ col ]]</th>
</tr>
<tr v-for="(client, index) in inbound.settings.vmesses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
</tr>
</table>
</a-collapse-panel>
</a-collapse>
<a-tag>
<svg
@click="addClient(inbound.protocol, inbound.settings.vmesses)"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
class="ml-2 cursor-pointer"
>
<path fill="none" d="M0 0h24v24H0z" />
<path
fill="green"
d="M11 11V7h2v4h4v2h-4v4h-2v-4H7v-2h4zm1 11C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16z"
/>
</svg>
</a-tag>
<a-form layout="inline">
<a-form-item label='{{ i18n "pages.inbounds.disableInsecureEncryption" }}'>
<a-switch v-model.number="inbound.settings.disableInsecure"></a-switch>
<a-switch v-model="inbound.settings.disableInsecure"></a-switch>
</a-form-item>
</a-form>
{{end}}

View File

@@ -1,7 +1,14 @@
{{define "form/streamGRPC"}}
<a-form layout="inline">
<a-form-item label="serviceName">
<a-input v-model.trim="inbound.stream.grpc.serviceName"></a-input>
</a-form-item>
<table width="100%" class="ant-table-tbody">
<tr>
<td>serviceName</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.stream.grpc.serviceName" style="width: 250px;"></a-input>
</a-form-item>
</td>
</tr>
</table>
</a-form>
{{end}}

View File

@@ -1,12 +1,24 @@
{{define "form/streamHTTP"}}
<a-form layout="inline">
<a-form-item label='{{ i18n "path" }}'>
<a-input v-model.trim="inbound.stream.http.path"></a-input>
</a-form-item>
<a-form-item label="host">
<a-row v-for="(host, index) in inbound.stream.http.host">
<a-input v-model.trim="inbound.stream.http.host[index]"></a-input>
</a-row>
</a-form-item>
<table width="100%" class="ant-table-tbody">
<tr>
<td>{{ i18n "path" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.stream.http.path" style="width: 250px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>host</td>
<td>
<a-form-item>
<a-row v-for="(host, index) in inbound.stream.http.host">
<a-input v-model.trim="inbound.stream.http.host[index]" style="width: 250px;"></a-input>
</a-row>
</a-form-item>
</td>
</tr>
</table>
</a-form>
{{end}}

View File

@@ -1,38 +1,85 @@
{{define "form/streamKCP"}}
<a-form layout="inline">
<a-form-item label='{{ i18n "camouflage" }}'>
<a-select v-model="inbound.stream.kcp.type" style="width: 280px;">
<a-select-option value="none">nonenot camouflage</a-select-option>
<a-select-option value="srtp">srtpcamouflage video call</a-select-option>
<a-select-option value="utp">utpcamouflage BT download</a-select-option>
<a-select-option value="wechat-video">wechat-videocamouflage WeChat video</a-select-option>
<a-select-option value="dtls">dtlscamouflage DTLS 1.2 packages</a-select-option>
<a-select-option value="wireguard">wireguardcamouflage wireguard packages</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='{{ i18n "password" }}'>
<a-input v-model.number="inbound.stream.kcp.seed"></a-input>
</a-form-item>
<a-form-item label="mtu">
<a-input type="number" v-model.number="inbound.stream.kcp.mtu"></a-input>
</a-form-item>
<a-form-item label="tti (ms)">
<a-input type="number" v-model.number="inbound.stream.kcp.tti"></a-input>
</a-form-item>
<a-form-item label="uplink capacity (MB/S)">
<a-input type="number" v-model.number="inbound.stream.kcp.upCap"></a-input>
</a-form-item>
<a-form-item label="downlink capacity (MB/S)">
<a-input type="number" v-model.number="inbound.stream.kcp.downCap"></a-input>
</a-form-item>
<a-form-item label="congestion">
<a-switch v-model="inbound.stream.kcp.congestion"></a-switch>
</a-form-item>
<a-form-item label="read buffer size (MB)">
<a-input type="number" v-model.number="inbound.stream.kcp.readBuffer"></a-input>
</a-form-item>
<a-form-item label="write buffer size (MB)">
<a-input type="number" v-model.number="inbound.stream.kcp.writeBuffer"></a-input>
</a-form-item>
<table width="100%" class="ant-table-tbody">
<tr>
<td>{{ i18n "camouflage" }}</td>
<td>
<a-form-item>
<a-select v-model="inbound.stream.kcp.type" style="width: 250px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value="none">none (not camouflage)</a-select-option>
<a-select-option value="srtp">srtp (video call)</a-select-option>
<a-select-option value="utp">utp (BT download)</a-select-option>
<a-select-option value="wechat-video">wechat-video (WeChat video)</a-select-option>
<a-select-option value="dtls">dtls (DTLS 1.2 packages)</a-select-option>
<a-select-option value="wireguard">wireguard (wireguard packages)</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "password" }}</td>
<td>
<a-form-item>
<a-input v-model="inbound.stream.kcp.seed" style="width: 250px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>mtu</td>
<td>
<a-form-item>
<a-input-number v-model.number="inbound.stream.kcp.mtu"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td>tti (ms)</td>
<td>
<a-form-item>
<a-input-number v-model.number="inbound.stream.kcp.tti"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td>uplink capacity (MB/S)</td>
<td>
<a-form-item>
<a-input-number v-model.number="inbound.stream.kcp.upCap"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td>downlink capacity (MB/S)</td>
<td>
<a-form-item>
<a-input-number v-model.number="inbound.stream.kcp.downCap"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td>congestion</td>
<td>
<a-form-item>
<a-switch v-model="inbound.stream.kcp.congestion"></a-switch>
</a-form-item>
</td>
</tr>
<tr>
<td>read buffer size (MB)</td>
<td>
<a-form-item>
<a-input-number v-model.number="inbound.stream.kcp.readBuffer"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td>write buffer size (MB)</td>
<td>
<a-form-item>
<a-input-number v-model.number="inbound.stream.kcp.writeBuffer"></a-input-number>
</a-form-item>
</td>
</tr>
</table>
</a-form>
{{end}}

View File

@@ -1,24 +1,41 @@
{{define "form/streamQUIC"}}
<a-form layout="inline">
<a-form-item label='{{ i18n "pages.inbounds.stream.quic.encryption" }}'>
<a-select v-model="inbound.stream.quic.security" style="width: 165px;">
<a-select-option value="none">none</a-select-option>
<a-select-option value="aes-128-gcm">aes-128-gcm</a-select-option>
<a-select-option value="chacha20-poly1305">chacha20-poly1305</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='{{ i18n "password" }}'>
<a-input v-model.trim="inbound.stream.quic.key"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "camouflage" }}'>
<a-select v-model="inbound.stream.quic.type" style="width: 280px;">
<a-select-option value="none">nonenot camouflage</a-select-option>
<a-select-option value="srtp">srtpcamouflage video call</a-select-option>
<a-select-option value="utp">utpcamouflage BT download</a-select-option>
<a-select-option value="wechat-video">wechat-videocamouflage WeChat video</a-select-option>
<a-select-option value="dtls">dtlscamouflage DTLS 1.2 packages</a-select-option>
<a-select-option value="wireguard">wireguardcamouflage wireguard packages</a-select-option>
</a-select>
</a-form-item>
<table width="100%" class="ant-table-tbody">
<tr>
<td>{{ i18n "pages.inbounds.stream.quic.encryption" }}</td>
<td>
<a-form-item>
<a-select v-model="inbound.stream.quic.security" style="width: 200px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value="none">none</a-select-option>
<a-select-option value="aes-128-gcm">aes-128-gcm</a-select-option>
<a-select-option value="chacha20-poly1305">chacha20-poly1305</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "password" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.stream.quic.key" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "camouflage" }}</td>
<td>
<a-form-item>
<a-select v-model="inbound.stream.quic.type" style="width: 200px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value="none">none (not camouflage)</a-select-option>
<a-select-option value="srtp">srtp (video call)</a-select-option>
<a-select-option value="utp">utp (BT download)</a-select-option>
<a-select-option value="wechat-video">wechat-video (WeChat video)</a-select-option>
<a-select-option value="dtls">dtls (DTLS 1.2 packages)</a-select-option>
<a-select-option value="wireguard">wireguard (wireguard packages)</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
</table>
</a-form>
{{end}}

View File

@@ -1,8 +1,9 @@
{{define "form/streamSettings"}}
<!-- select stream network -->
<a-form layout="inline">
<a-form-item label='{{ i18n "transmission" }}'>
<a-select v-model="inbound.stream.network" @change="streamNetworkChange">
<a-form-item label="{{ i18n "transmission" }}">
<a-select v-model="inbound.stream.network" @change="streamNetworkChange"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value="tcp">tcp</a-select-option>
<a-select-option value="kcp">kcp</a-select-option>
<a-select-option value="ws">ws</a-select-option>

View File

@@ -13,74 +13,109 @@
</a-form>
<!-- tcp request -->
<a-form v-if="inbound.stream.tcp.type === 'http'"
layout="inline">
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestVersion" }}'>
<a-input v-model.trim="inbound.stream.tcp.request.version"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestMethod" }}'>
<a-input v-model.trim="inbound.stream.tcp.request.method"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestPath" }}'>
<a-row v-for="(path, index) in inbound.stream.tcp.request.path">
<a-input v-model.trim="inbound.stream.tcp.request.path[index]"></a-input>
</a-row>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.general.requestHeader" }}'>
<a-row>
<a-button size="small"
@click="inbound.stream.tcp.request.addHeader('Host', 'xxx.com')">
+
</a-button>
</a-row>
<a-input-group v-for="(header, index) in inbound.stream.tcp.request.headers">
<a-input style="width: 50%" v-model.trim="header.name"
addon-before='{{ i18n "pages.inbounds.stream.general.name" }}'></a-input>
<a-input style="width: 50%" v-model.trim="header.value"
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
<template slot="addonAfter">
<a-button size="small"
@click="inbound.stream.tcp.request.removeHeader(index)">
-
</a-button>
</template>
</a-input>
</a-input-group>
</a-form-item>
</a-form>
<!-- tcp response -->
<a-form v-if="inbound.stream.tcp.type === 'http'"
layout="inline">
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseVersion" }}'>
<a-input v-model.trim="inbound.stream.tcp.response.version"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseStatus" }}'>
<a-input v-model.trim="inbound.stream.tcp.response.status"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseStatusDescription" }}'>
<a-input v-model.trim="inbound.stream.tcp.response.reason"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseHeader" }}'>
<a-row>
<a-button size="small"
@click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')">
+
</a-button>
</a-row>
<a-input-group v-for="(header, index) in inbound.stream.tcp.response.headers">
<a-input style="width: 50%" v-model.trim="header.name"
addon-before='{{ i18n "pages.inbounds.stream.general.name" }}'></a-input>
<a-input style="width: 50%" v-model.trim="header.value"
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
<template slot="addonAfter">
<a-button size="small"
@click="inbound.stream.tcp.response.removeHeader(index)">
-
</a-button>
</template>
</a-input>
</a-input-group>
</a-form-item>
<a-form v-if="inbound.stream.tcp.type === 'http'" layout="inline">
<table width="100%" class="ant-table-tbody">
<tr>
<td>{{ i18n "pages.inbounds.stream.tcp.requestVersion" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.stream.tcp.request.version" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.stream.tcp.requestMethod" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.stream.tcp.request.method" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.stream.tcp.requestPath" }}</td>
<td>
<a-form-item>
<a-row v-for="(path, index) in inbound.stream.tcp.request.path">
<a-input v-model.trim="inbound.stream.tcp.request.path[index]" style="width: 200px;"></a-input>
</a-row>
</a-form-item>
</td>
</tr>
<tr>
<td colspan="2">
<a-form-item label='{{ i18n "pages.inbounds.stream.general.requestHeader" }}'>
<a-row>
<a-button size="small"
@click="inbound.stream.tcp.request.addHeader('Host', 'xxx.com')">
+
</a-button>
</a-row>
<a-input-group v-for="(header, index) in inbound.stream.tcp.request.headers">
<a-input style="width: 50%" v-model.trim="header.name"
addon-before='{{ i18n "pages.inbounds.stream.general.name" }}'></a-input>
<a-input style="width: 50%" v-model.trim="header.value"
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
<template slot="addonAfter">
<a-button size="small"
@click="inbound.stream.tcp.request.removeHeader(index)">
-
</a-button>
</template>
</a-input>
</a-input-group>
</a-form-item>
</td>
</tr>
<!-- tcp response -->
<tr>
<td>{{ i18n "pages.inbounds.stream.tcp.responseVersion" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.stream.tcp.response.version" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.stream.tcp.responseStatus" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.stream.tcp.response.status" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.stream.tcp.responseStatusDescription" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.stream.tcp.response.reason" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td colspan="2">
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseHeader" }}'>
<a-row>
<a-button size="small"
@click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')">
+
</a-button>
</a-row>
<a-input-group v-for="(header, index) in inbound.stream.tcp.response.headers">
<a-input style="width: 50%" v-model.trim="header.name"
addon-before='{{ i18n "pages.inbounds.stream.general.name" }}'></a-input>
<a-input style="width: 50%" v-model.trim="header.value"
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
<template slot="addonAfter">
<a-button size="small"
@click="inbound.stream.tcp.response.removeHeader(index)">
-
</a-button>
</template>
</a-input>
</a-input-group>
</a-form-item>
</td>
</tr>
</table>
</a-form>
{{end}}

View File

@@ -1,33 +1,47 @@
{{define "form/streamWS"}}
<a-form layout="inline">
<a-form-item label="acceptProxyProtocol">
<a-switch v-model="inbound.stream.ws.acceptProxyProtocol"></a-switch>
</a-form-item>
</a-form>
<a-form layout="inline">
<a-form-item label='{{ i18n "path" }}'>
<a-input v-model.trim="inbound.stream.ws.path"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.general.requestHeader" }}'>
<a-row>
<a-button size="small"
@click="inbound.stream.ws.addHeader('Host', '')">
+
</a-button>
</a-row>
<a-input-group v-for="(header, index) in inbound.stream.ws.headers">
<a-input style="width: 50%" v-model.trim="header.name"
addon-before='{{ i18n "pages.inbounds.stream.general.name"}}'></a-input>
<a-input style="width: 50%" v-model.trim="header.value"
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
<template slot="addonAfter">
<a-button size="small"
@click="inbound.stream.ws.removeHeader(index)">
-
</a-button>
</template>
</a-input>
</a-input-group>
</a-form-item>
<table width="100%" class="ant-table-tbody">
<tr>
<td>acceptProxyProtocol</td>
<td>
<a-form-item>
<a-switch v-model="inbound.stream.ws.acceptProxyProtocol"></a-switch>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "path" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.stream.ws.path" style="width: 250px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td colspan="2">
<a-form-item label='{{ i18n "pages.inbounds.stream.general.requestHeader" }}'>
<a-row>
<a-button size="small"
@click="inbound.stream.ws.addHeader('Host', '')">
+
</a-button>
</a-row>
<a-input-group v-for="(header, index) in inbound.stream.ws.headers">
<a-input style="width: 50%" v-model.trim="header.name"
addon-before='{{ i18n "pages.inbounds.stream.general.name"}}'></a-input>
<a-input style="width: 50%" v-model.trim="header.value"
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
<template slot="addonAfter">
<a-button size="small"
@click="inbound.stream.ws.removeHeader(index)">
-
</a-button>
</template>
</a-input>
</a-input-group>
</a-form-item>
</td>
</tr>
</table>
</a-form>
{{end}}

View File

@@ -1,53 +1,240 @@
{{define "form/tlsSettings"}}
<!-- tls enable -->
<a-form layout="inline" v-if="inbound.canSetTls()">
<a-form-item label="tls">
<a-form v-if="inbound.canSetTls()" layout="inline">
<a-form-item label="TLS">
<a-switch v-model="inbound.tls">
</a-switch>
</a-form-item>
<a-form-item v-if="inbound.canEnableXTls()" label="xtls">
<a-switch v-model="inbound.xtls"></a-switch>
<a-form-item v-if="inbound.canEnableReality()" label="Reality">
<a-switch v-model="inbound.reality"></a-switch>
</a-form-item>
</a-form>
<!-- tls settings -->
<a-form v-if="inbound.tls || inbound.xtls"
layout="inline">
<a-form-item label='{{ i18n "domainName" }}'>
<a-input v-model.trim="inbound.stream.tls.server"></a-input>
</a-form-item>
<a-form-item label="alpn" placeholder="http/1.1,h2">
<a-input v-model.trim="inbound.stream.tls.alpn"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "certificate" }}'>
<a-radio-group v-model="inbound.stream.tls.certs[0].useFile"
button-style="solid">
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
</a-radio-group>
</a-form-item>
<template v-if="inbound.stream.tls.certs[0].useFile">
<a-form-item label='{{ i18n "pages.inbounds.publicKeyPath" }}'>
<a-input v-model.trim="inbound.stream.tls.certs[0].certFile"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
<a-input v-model.trim="inbound.stream.tls.certs[0].keyFile"></a-input>
</a-form-item>
<a-form v-if="inbound.tls" layout="inline">
<table width="100%" class="ant-table-tbody">
<tr>
<td>SNI</td>
<td>
<a-form-item placeholder="Server Name Indication" v-if="inbound.tls">
<a-input v-model.trim="inbound.stream.tls.settings.serverName" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>CipherSuites</td>
<td>
<a-form-item>
<a-select v-model="inbound.stream.tls.cipherSuites" style="width: 250px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value="">auto</a-select-option>
<a-select-option v-for="key in TLS_CIPHER_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>MinVersion</td>
<td>
<a-form-item>
<a-select v-model="inbound.stream.tls.minVersion" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>MaxVersion</td>
<td>
<a-form-item>
<a-select v-model="inbound.stream.tls.maxVersion" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>uTLS</td>
<td>
<a-form-item>
<a-select v-model="inbound.stream.tls.settings.fingerprint" style="width: 250px">
<a-select-option value=''>None</a-select-option>
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "domainName" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.stream.tls.server" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>Alpn</td>
<td>
<a-form-item>
<a-checkbox-group v-model="inbound.stream.tls.alpn">
<a-checkbox v-for="key in ALPN_OPTION" :value="key">[[ key ]]</a-checkbox>
</a-checkbox-group>
</a-form-item>
</td>
</tr>
<tr>
<td>Allow insecure</td>
<td>
<a-form-item>
<a-switch v-model="inbound.stream.tls.settings.allowInsecure"></a-switch>
</a-form-item>
</td>
</tr>
<tr>
<td colspan="2">
<a-form-item label="{{ i18n "certificate" }}">
<a-radio-group v-model="inbound.stream.tls.certs[0].useFile" button-style="solid">
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
</a-radio-group>
</a-form-item>
</td>
</tr>
<template v-if="inbound.stream.tls.certs[0].useFile">
<tr>
<td>{{ i18n "pages.inbounds.publicKeyPath" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.stream.tls.certs[0].certFile" style="width:250px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.keyPath" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.stream.tls.certs[0].keyFile" style="width:250px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td></td>
<td>
<a-button type="primary" icon="import" @click="setDefaultCertData">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
</td>
</tr>
</template>
<template v-else>
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
<a-input type="textarea" :rows="2"
v-model="inbound.stream.tls.certs[0].cert"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.keyContent" }}'>
<a-input type="textarea" :rows="2"
v-model="inbound.stream.tls.certs[0].key"></a-input>
</a-form-item>
<tr>
<td>{{ i18n "pages.inbounds.publicKeyContent" }}</td>
<td>
<a-form-item>
<a-input type="textarea" :rows="3" style="width:250px;" v-model="inbound.stream.tls.certs[0].cert"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.keyContent" }}</td>
<td>
<a-form-item>
<a-input type="textarea" :rows="3" style="width:250px;" v-model="inbound.stream.tls.certs[0].key"></a-input>
</a-form-item>
</td>
</tr>
</template>
</table>
</a-form>
<a-form layout="inline" v-else-if = "inbound.stream.network === 'tcp' ">
<a-form-item label="tcp-acceptProxyProtocol">
<a-switch v-model="inbound.stream.tcp.acceptProxyProtocol"></a-switch>
</a-form-item>
<!-- reality settings -->
<a-form v-if="inbound.reality" layout="inline">
<table width="100%" class="ant-table-tbody">
<tr>
<td>{{ i18n "domainName" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.stream.reality.settings.serverName" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>Show</td>
<td>
<a-form-item>
<a-switch v-model="inbound.stream.reality.show"></a-switch>
</a-form-item>
</td>
</tr>
<tr>
<td>Xver</td>
<td>
<a-form-item>
<a-input-number v-model.number="inbound.stream.reality.xver" :min="0"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td>uTLS</td>
<td>
<a-form-item >
<a-select v-model="inbound.stream.reality.settings.fingerprint" style="width: 250px">
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>Dest</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.stream.reality.dest" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>Server Names</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.stream.reality.serverNames" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>Short Ids</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.stream.reality.shortIds" style="width:250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>SpiderX</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.stream.reality.settings.spiderX" style="width:250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>Private Key</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.stream.reality.privateKey" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>Public Key</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.stream.reality.settings.publicKey" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td></td>
<td>
<a-button type="primary" icon="import" @click="getNewX25519Cert">Get new cert</a-button>
</td>
</tr>
</table>
</a-form>
{{end}}

View File

@@ -1,34 +1,48 @@
{{define "client_table"}}
<template slot="actions" slot-scope="text, client, index">
<a-dropdown>
<a-icon @click="e => e.preventDefault()" type="menu"></a-icon>
<template #overlay>
<a-menu>
<a-menu-item v-if="record.hasLink()" @click="showQrcode(record,index);"><a-icon type="qrcode"></a-icon>{{ i18n "qrCode" }}</a-menu-item>
<a-menu-item @click="showInfo(record,index);"><a-icon type="info-circle"></a-icon>{{ i18n "info" }}</a-menu-item>
<a-menu-item @click="resetClientTraffic(client,record,$event)"><a-icon type="retweet"></a-icon>{{ i18n "pages.inbounds.resetTraffic" }}</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<a-tooltip>
<template slot="title">{{ i18n "qrCode" }}</template>
<a-icon style="font-size: 24px;" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record,index);"></a-icon>
</a-tooltip>
<a-tooltip>
<template slot="title">{{ i18n "pages.client.edit" }}</template>
<a-icon style="font-size: 24px;" type="edit" @click="openEditClient(record.id,client);"></a-icon>
</a-tooltip>
<a-tooltip>
<template slot="title">{{ i18n "info" }}</template>
<a-icon style="font-size: 24px;" type="info-circle" @click="showInfo(record,index);"></a-icon>
</a-tooltip>
<a-tooltip>
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
<a-icon style="font-size: 24px;" type="retweet" @click="resetClientTraffic(client,record.id)" v-if="client.email.length > 0"></a-icon>
</a-tooltip>
<a-tooltip>
<template slot="title"><span style="color: #FF4D4F"> {{ i18n "delete"}}</span></template>
<a-icon style="font-size: 24px;" type="delete" v-if="isRemovable(record.id)" @click="delClient(record.id,client)"></a-icon>
</a-tooltip>
</template>
<template slot="enable" slot-scope="text, client, index">
<a-switch v-model="client.enable" @change="switchEnableClient(record.id,client)"></a-switch>
</template>
<template slot="client" slot-scope="text, client">
[[ client.email ]]
<a-tag v-if="!isClientEnabled(record, client.email)" color="red"> expired</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 v-if="client._totalGB === 0" color="blue">{{ i18n "usage" }}: [[ sizeFormat(getUpStats(record, client.email) + getDownStats(record, client.email)) ]]</a-tag>
<a-tag v-if="client._totalGB > 0 && !isTrafficExhausted(record, client.email)" color="green">{{ i18n "usage" }}: [[ sizeFormat(getUpStats(record, client.email) + getDownStats(record, client.email)) ]] / [[client._totalGB]]GB</a-tag>
<a-tag v-if="client._totalGB > 0 && isTrafficExhausted(record, client.email)" color="red">{{ i18n "usage" }}: [[ sizeFormat(getUpStats(record, client.email) + getDownStats(record, client.email)) ]] / [[client._totalGB]]GB</a-tag>
<a-tag color="blue">[[ sizeFormat(getUpStats(record, client.email)) ]] / [[ sizeFormat(getDownStats(record, client.email)) ]]</a-tag>
<template v-if="client._totalGB > 0">
<a-tag v-if="isTrafficExhausted(record, client.email)" color="red">[[client._totalGB]]GB</a-tag>
<a-tag v-else color="cyan">[[client._totalGB]]GB</a-tag>
</template>
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
</template>
<template slot="expiryTime" slot-scope="text, client, index">
<template v-if="client._expiryTime > 0">
<a-tag v-if="isExpiry(record, index)" color="red">
[[ DateUtil.formatMillis(client._expiryTime) ]]
</a-tag>
<a-tag v-else color="blue">
<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

@@ -1,9 +1,12 @@
{{define "inboundInfoModal"}}
<a-modal id="inbound-info-modal" v-model="infoModal.visible" title='{{ i18n "pages.inbounds.details"}}'
:closable="true"
:mask-closable="true"
:footer="null"
>
<a-modal id="inbound-info-modal"
v-model="infoModal.visible" title='{{ i18n "pages.inbounds.details"}}'
:closable="true"
:mask-closable="true"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:footer="null"
width="600px"
>
<table style="margin-bottom: 10px; width: 100%;">
<tr><td>
<table>
@@ -41,40 +44,163 @@
</template>
</table>
</td></tr>
<tr colspan="2">
<tr colspan="2" v-if="dbInbound.hasLink()">
<td v-if="inbound.tls">
tls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
tls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
</td>
<td v-else-if="inbound.xtls">
xtls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
xtls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
<td v-else-if="inbound.reality">
reality: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
reality Destination: <a-tag :color="inbound.stream.reality.dest ? 'green' : 'orange'">[[ inbound.stream.reality.dest ]]</a-tag>
</td>
<td v-else>tls: <a-tag color="red">{{ i18n "disabled" }}</a-tag>
</td>
</tr>
</table>
<template v-if="infoModal.clientSettings">
<a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
<template v-if="dbInbound.hasLink()">
<p>Client URL:</p>
<p>[[ infoModal.link ]]</p>
<button class="btn" id="copy-url-link"><a-icon type="snippets"></a-icon>{{ i18n "copy" }}</button>
<table style="margin-bottom: 10px;">
<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>
<tr>
<td>
<a-tag v-if="infoModal.clientStats" :color="statsColor(infoModal.clientStats)">
[[ sizeFormat(infoModal.clientStats['up']) ]] /
[[ sizeFormat(infoModal.clientStats['down']) ]]
([[ sizeFormat(infoModal.clientStats['up'] + infoModal.clientStats['down']) ]])
</a-tag>
</td>
<td>
<a-tag v-if="infoModal.clientSettings.totalGB > 0" :color="statsColor(infoModal.clientStats)">[[ sizeFormat(infoModal.clientSettings.totalGB) ]]</a-tag>
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
</td>
<td>
<template v-if="infoModal.clientSettings.expiryTime > 0">
<a-tag :color="infoModal.isExpired ? 'red' : 'blue'">
[[ 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>
</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>
<template v-else>
<a-divider></a-divider>
<table v-if="inbound.protocol == Protocols.SHADOWSOCKS" style="margin-bottom: 10px; width: 100%;">
<tr>
<th>{{ i18n "encryption" }}</th>
<th>{{ i18n "password" }}</th>
<th>{{ i18n "pages.inbounds.network" }}</th>
</tr><tr>
<td><a-tag color="green">[[ inbound.settings.method ]]</a-tag></td>
<td><a-tag color="blue">[[ inbound.settings.password ]]</a-tag></td>
<td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td>
</tr>
</table>
<table v-if="inbound.protocol == Protocols.DOKODEMO" style="margin-bottom: 10px; width: 100%;">
<tr>
<th>{{ i18n "pages.inbounds.targetAddress" }}</th>
<th>{{ i18n "pages.inbounds.destinationPort" }}</th>
<th>{{ i18n "pages.inbounds.network" }}</th>
<th>FollowRedirect</th>
</tr><tr>
<td><a-tag color="green">[[ inbound.settings.address ]]</a-tag></td>
<td><a-tag color="blue">[[ inbound.settings.port ]]</a-tag></td>
<td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td>
<td><a-tag color="blue">[[ inbound.settings.followRedirect ]]</a-tag></td>
</tr>
</table>
</table>
<table v-if="inbound.protocol == Protocols.SOCKS" style="margin-bottom: 10px; width: 100%;">
<tr>
<th>{{ i18n "password" }} Auth</th>
<th>{{ i18n "pages.inbounds.enable" }} udp</th>
<th>IP</th>
</tr><tr>
<td><a-tag color="green">[[ inbound.settings.auth ]]</a-tag></td>
<td><a-tag color="blue">[[ inbound.settings.udp]]</a-tag></td>
<td><a-tag color="green">[[ inbound.settings.ip ]]</a-tag></td>
</tr><tr v-if="inbound.settings.auth == 'password'">
<td> </td>
<td>{{ i18n "username" }}</td>
<td>{{ i18n "password" }}</td>
</tr><tr v-for="account,index in inbound.settings.accounts">
<td><a-tag color="green">[[ index ]]</a-tag></td>
<td><a-tag color="blue">[[ account.user ]]</a-tag></td>
<td><a-tag color="green">[[ account.pass ]]</a-tag></td>
</tr>
</table>
</table>
<table v-if="inbound.protocol == Protocols.HTTP" style="margin-bottom: 10px; width: 100%;">
<tr>
<th> </th>
<th>{{ i18n "username" }}</th>
<th>{{ i18n "password" }}</th>
</tr><tr v-for="account,index in inbound.settings.accounts">
<td><a-tag color="green">[[ index ]]</a-tag></td>
<td><a-tag color="blue">[[ account.user ]]</a-tag></td>
<td><a-tag color="green">[[ account.pass ]]</a-tag></td>
</tr>
</table>
</table>
</template>
<div v-if="dbInbound.hasLink()">
<a-divider>URL</a-divider>
<p>[[ infoModal.link ]]</p>
<button class="ant-btn ant-btn-primary" id="copy-url-link"><a-icon type="snippets"></a-icon>{{ i18n "copy" }}</button>
</div>
</a-modal>
<script>
const infoModal = {
visible: false,
inbound: new Inbound(),
dbInbound: new DBInbound(),
settings: null,
clientSettings: null,
clientStats: [],
upStats: 0,
downStats: 0,
clipboard: null,
link: null,
index: 0,
show(dbInbound, index=0) {
index: null,
isExpired: false,
show(dbInbound, index) {
this.index = index;
this.inbound = dbInbound.toInbound();
this.dbInbound = new DBInbound(dbInbound);
this.link = dbInbound.genLink(index);
this.settings = JSON.parse(this.inbound.settings);
this.clientSettings = this.settings.clients ? Object.values(this.settings.clients)[index] : null;
this.isExpired = this.inbound.isExpiry(index);
this.clientStats = this.settings.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : [];
this.visible = true;
infoModalApp.$nextTick(() => {
if (this.clipboard === null) {
@@ -100,18 +226,27 @@
},
get inbound() {
return this.infoModal.inbound;
}
},
get isActive() {
if(infoModal.clientStats){
return infoModal.clientStats.enable;
}
return infoModal.dbInbound.isEnable;
},
get isEnable() {
if(infoModal.clientSettings){
return infoModal.clientSettings.enable;
}
return infoModal.dbInbound.isEnable;
},
get subBase() {
return window.location.protocol + "//" + window.location.hostname + (window.location.port ? ":" + window.location.port:"") + basePath + "sub/";
},
get tgBase() {
return "https://t.me/"
},
},
methods: {
setQrCode(elmentId,index) {
content = infoModal.inbound.genLink(infoModal.dbInbound.address,infoModal.dbInbound.remark,index)
new QRious({
element: document.querySelector('#'+elmentId),
size: 260,
value: content,
});
},
copyTextToClipboard(elmentId,content) {
this.infoModal.clipboard = new ClipboardJS('#' + elmentId, {
text: () => content,
@@ -120,6 +255,12 @@
app.$message.success('{{ i18n "copied" }}')
this.infoModal.clipboard.destroy();
});
},
statsColor(stats) {
if(!stats) return 'blue'
if(stats['total'] === 0) return 'blue'
else if(stats['total'] > 0 && (stats['down']+stats['up']) < stats['total']) return 'cyan'
else return 'red'
}
},

View File

@@ -1,6 +1,7 @@
{{define "inboundModal"}}
<a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title" @ok="inModal.ok"
:confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
{{template "form/inbound"}}
</a-modal>
@@ -42,6 +43,14 @@
loading(loading) {
inModal.confirmLoading = loading;
},
getClients(protocol, clientSettings) {
switch(protocol){
case Protocols.VMESS: return clientSettings.vmesses;
case Protocols.VLESS: return clientSettings.vlesses;
case Protocols.TROJAN: return clientSettings.trojans;
default: return null;
}
},
};
const protocols = {
@@ -61,6 +70,7 @@
inModal: inModal,
Protocols: protocols,
SSMethods: SSMethods,
delayedStart: false,
get inbound() {
return inModal.inbound;
},
@@ -69,106 +79,48 @@
},
get isEdit() {
return inModal.isEdit;
}
},
get client() {
return inModal.getClients(this.inbound.protocol, this.inbound.settings)[0];
},
get delayedExpireDays() {
return this.client && this.client.expiryTime < 0 ? this.client.expiryTime / -86400000 : 0;
},
set delayedExpireDays(days){
this.client.expiryTime = -86400000 * days;
},
},
methods: {
streamNetworkChange(oldValue) {
if (oldValue === 'kcp') {
this.inModal.inbound.tls = false;
streamNetworkChange() {
if (!inModal.inbound.canSetTls()) {
this.inModal.inbound.stream.security = 'none';
}
if (!inModal.inbound.canEnableReality()) {
this.inModal.inbound.reality = false;
}
},
addClient(protocol, clients) {
switch (protocol) {
case Protocols.VMESS: return clients.push(new Inbound.VmessSettings.Vmess());
case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS());
case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan());
default: return null;
}
setDefaultCertData(){
inModal.inbound.stream.tls.certs[0].certFile = app.defaultCert;
inModal.inbound.stream.tls.certs[0].keyFile = app.defaultKey;
},
removeClient(index, clients) {
clients.splice(index, 1);
},
async resetClientTraffic(client,event) {
const msg = await HttpUtil.post('/xui/inbound/resetClientTraffic/'+ client.email);
async getNewX25519Cert(){
inModal.loading(true);
const msg = await HttpUtil.post('/server/getNewX25519Cert');
inModal.loading(false);
if (!msg.success) {
return;
}
clientStats = this.inbound.clientStats
if(clientStats.length > 0)
{
for (const key in clientStats) {
if (Object.hasOwnProperty.call(clientStats, key)) {
if(clientStats[key]['email'] == client.email){
clientStats[key]['up'] = 0
clientStats[key]['down'] = 0
}
}
}
}
inModal.inbound.stream.reality.privateKey = msg.obj.privateKey;
inModal.inbound.stream.reality.settings.publicKey = msg.obj.publicKey;
},
isExpiry(index) {
return this.inbound.isExpiry(index)
},
getUpStats(email) {
clientStats = this.inbound.clientStats
if(clientStats.length > 0)
{
for (const key in clientStats) {
if (Object.hasOwnProperty.call(clientStats, key)) {
if(clientStats[key]['email'] == email)
return clientStats[key]['up']
}
}
}
},
getDownStats(email) {
clientStats = this.inbound.clientStats
if(clientStats.length > 0)
{
for (const key in clientStats) {
if (Object.hasOwnProperty.call(clientStats, key)) {
if(clientStats[key]['email'] == email)
return clientStats[key]['down']
}
}
}
},
isClientEnable(email) {
clientStats = this.inbound.clientStats
if(clientStats.length > 0)
{
for (const key in clientStats) {
if (Object.hasOwnProperty.call(clientStats, key)) {
if(clientStats[key]['email'] == email)
return clientStats[key]['enable']
}
}
}
},
getHeaderText(email) {
if(email == "")
return "Add Client"
return email + (this.isClientEnable(email) == true ? ' Active' : ' Deactive')
},
getHeaderStyle(email) {
return (this.isClientEnable(email) == true ? '' : 'deactive-client')
},
getNewEmail(client) {
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
var string = '';
var len = 6 + Math.floor(Math.random() * 5)
var len = 6 + Math.floor(Math.random() * 5);
for(var ii=0; ii<len; ii++){
string += chars[Math.floor(Math.random() * chars.length)];
}
client.email = string + "@gmail.com"
client.email = string;
}
},
});

View File

@@ -15,7 +15,7 @@
<body>
<a-layout id="app" v-cloak>
{{ template "commonSider" . }}
<a-layout id="content-layout">
<a-layout id="content-layout" :style="siderDrawer.isDarkTheme ? bgDarkStyle : ''">
<a-layout-content>
<a-spin :spinning="spinning" :delay="500" tip="loading">
<transition name="list" appear>
@@ -24,7 +24,7 @@
</a-tag>
</transition>
<transition name="list" appear>
<a-card hoverable style="margin-bottom: 20px;">
<a-card hoverable style="margin-bottom: 20px;" :class="siderDrawer.isDarkTheme ? darkClass : ''">
<a-row>
<a-col :xs="24" :sm="24" :lg="12">
{{ i18n "pages.inbounds.totalDownUp" }}:
@@ -38,13 +38,56 @@
{{ i18n "pages.inbounds.inboundCount" }}:
<a-tag color="green">[[ dbInbounds.length ]]</a-tag>
</a-col>
<a-col :xs="24" :sm="24" :lg="12">
{{ i18n "clients" }}:
<a-tag color="green">[[ total.clients ]]</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>
</transition>
<transition name="list" appear>
<a-card hoverable>
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
<div slot="title">
<a-button type="primary" icon="plus" @click="openAddInbound"></a-button>
<a-button type="primary" icon="plus" @click="openAddInbound">{{ i18n "pages.inbounds.addInbound" }}</a-button>
<a-dropdown :trigger="['click']">
<a-button type="primary" icon="menu">{{ i18n "pages.inbounds.generalActions" }}</a-button>
<a-menu slot="overlay" @click="a => generalActions(a)" :theme="siderDrawer.theme">
<a-menu-item key="export">
<a-icon type="export"></a-icon>
{{ i18n "pages.inbounds.export" }}
</a-menu-item>
<a-menu-item key="resetInbounds">
<a-icon type="reload"></a-icon>
{{ i18n "pages.inbounds.resetAllTraffic" }}
</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="delDepletedClients">
<a-icon type="rest"></a-icon>
{{ i18n "pages.inbounds.delDepletedClients" }}
</a-menu-item>
</a-menu>
</a-dropdown>
</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"
@@ -56,7 +99,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)">
<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" }}
@@ -65,9 +108,40 @@
<a-icon type="edit"></a-icon>
{{ i18n "edit" }}
</a-menu-item>
<template v-if="dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess">
<a-menu-item key="addClient">
<a-icon type="user-add"></a-icon>
{{ i18n "pages.client.add"}}
</a-menu-item>
<a-menu-item key="addBulkClient">
<a-icon type="usergroup-add"></a-icon>
{{ i18n "pages.client.bulk"}}
</a-menu-item>
<a-menu-item key="resetClients">
<a-icon type="file-done"></a-icon>
{{ i18n "pages.inbounds.resetInboundClientTraffics"}}
</a-menu-item>
<a-menu-item key="export">
<a-icon type="export"></a-icon>
{{ i18n "pages.inbounds.export"}}
</a-menu-item>
<a-menu-item key="delDepletedClients">
<a-icon type="rest"></a-icon>
{{ i18n "pages.inbounds.delDepletedClients" }}
</a-menu-item>
</template>
<template v-else>
<a-menu-item key="showInfo">
<a-icon type="info-circle"></a-icon>
{{ i18n "info"}}
</a-menu-item>
</template>
<a-menu-item key="resetTraffic">
<a-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"}}
@@ -77,7 +151,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.isReality" color="cyan">reality</a-tag>
</template>
</template>
<template slot="clients" slot-scope="text, dbInbound">
<template v-if="clientCount[dbInbound.id]">
<a-tag style="margin:0;" color="green">[[ clientCount[dbInbound.id].clients ]]</a-tag>
<a-popover title="{{ i18n "disabled" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
<template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].deactive">[[ clientEmail ]]</p>
</template>
<a-tag style="margin:0; padding: 0 2px;" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag>
</a-popover>
<a-popover title="{{ i18n "depleted" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
<template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].depleted">[[ clientEmail ]]</p>
</template>
<a-tag style="margin:0; padding: 0 2px;" color="red" v-if="clientCount[dbInbound.id].depleted.length">[[ clientCount[dbInbound.id].depleted.length ]]</a-tag>
</a-popover>
<a-popover title="{{ i18n "depletingSoon" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
<template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].expiring">[[ clientEmail ]]</p>
</template>
<a-tag style="margin:0; padding: 0 2px;" color="orange" v-if="clientCount[dbInbound.id].expiring.length">[[ clientCount[dbInbound.id].expiring.length ]]</a-tag>
</a-popover>
</template>
</template>
<template slot="traffic" slot-scope="text, dbInbound">
<a-tag color="blue">[[ sizeFormat(dbInbound.up) ]] / [[ sizeFormat(dbInbound.down) ]]</a-tag>
@@ -87,16 +189,8 @@
</template>
<a-tag v-else color="green">{{ i18n "unlimited" }}</a-tag>
</template>
<template slot="stream" slot-scope="text, dbInbound, index">
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
<a-tag color="green">[[ inbounds[index].stream.network ]]</a-tag>
<a-tag v-if="inbounds[index].stream.isTls" color="blue">tls</a-tag>
<a-tag v-if="inbounds[index].stream.isXTls" color="blue">xtls</a-tag>
</template>
<template v-else>{{ i18n "none" }}</template>
</template>
<template slot="enable" slot-scope="text, dbInbound">
<a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound)"></a-switch>
<a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound.id)"></a-switch>
</template>
<template slot="expiryTime" slot-scope="text, dbInbound">
<template v-if="dbInbound.expiryTime > 0">
@@ -128,15 +222,6 @@
>
{{template "client_table"}}
</a-table>
<a-table
v-else
:row-key="client => client.id"
:columns="innerOneColumns"
:data-source="record"
:pagination="false"
>
{{template "client_table"}}
</a-table>
</template>
</a-table>
</a-card>
@@ -147,11 +232,10 @@
</a-layout>
{{template "js" .}}
<script>
const columns = [{
title: '{{ i18n "pages.inbounds.operate" }}',
align: 'center',
width: 30,
width: 40,
scopedSlots: { customRender: 'action' },
}, {
title: '{{ i18n "pages.inbounds.enable" }}',
@@ -159,35 +243,35 @@
width: 40,
scopedSlots: { customRender: 'enable' },
}, {
title: "Id",
title: "ID",
align: 'center',
dataIndex: "id",
width: 30,
}, {
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: 30,
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: 150,
width: 120,
scopedSlots: { customRender: 'traffic' },
}, {
title: '{{ i18n "pages.inbounds.transportConfig" }}',
align: 'center',
width: 60,
scopedSlots: { customRender: 'stream' },
}, {
title: '{{ i18n "pages.inbounds.expireDate" }}',
align: 'center',
@@ -196,23 +280,21 @@
}];
const innerColumns = [
{ title: '', width: 50, 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: 100, scopedSlots: { customRender: 'traffic' } },
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 80, scopedSlots: { customRender: 'expiryTime' } },
{ title: 'UID', width: 150, dataIndex: "id" },
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 70, scopedSlots: { customRender: 'traffic' } },
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
{ title: 'UID', width: 120, dataIndex: "id" },
];
const innerTrojanColumns = [
{ title: '', width: 50, 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: 100, scopedSlots: { customRender: 'traffic' } },
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 80, scopedSlots: { customRender: 'expiryTime' } },
{ title: 'Password', width: 150, dataIndex: "password" },
];
const innerOneColumns = [
{ title: '', width: 50, scopedSlots: { customRender: 'actions' } },
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 70, scopedSlots: { customRender: 'traffic' } },
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
{ title: 'Password', width: 120, dataIndex: "password" },
];
const app = new Vue({
@@ -225,6 +307,11 @@
dbInbounds: [],
searchKey: '',
searchedInbounds: [],
expireDiff: 0,
trafficDiff: 0,
defaultCert: '',
defaultKey: '',
clientCount: {},
},
methods: {
loading(spinning=true) {
@@ -238,6 +325,19 @@
return;
}
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);
@@ -245,11 +345,48 @@
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();
@@ -257,24 +394,72 @@
this.searchedInbounds.splice(0, this.searchedInbounds.length);
this.dbInbounds.forEach(inbound => {
if (ObjectUtil.deepSearch(inbound, key)) {
this.searchedInbounds.push(inbound);
const newInbound = new DBInbound(inbound);
const inboundSettings = JSON.parse(inbound.settings);
if (inboundSettings.hasOwnProperty('clients')){
const searchedSettings = { "clients": [] };
inboundSettings.clients.forEach(client => {
if (ObjectUtil.deepSearch(client, key)){
searchedSettings.clients.push(client);
}
});
newInbound.settings = Inbound.Settings.fromJson(inbound.protocol, searchedSettings);
}
this.searchedInbounds.push(newInbound);
}
});
}
},
generalActions(action){
switch (action.key) {
case "export":
this.exportAllLinks();
break;
case "resetInbounds":
this.resetAllTraffic();
break;
case "resetClients":
this.resetAllClientTraffics(-1);
break;
case "delDepletedClients":
this.delDepletedClients(-1)
break;
}
},
clickAction(action, dbInbound) {
switch (action.key) {
case "qrcode":
this.showQrcode(dbInbound);
break;
case "showInfo":
this.showInfo(dbInbound);
break;
case "edit":
this.openEditInbound(dbInbound);
this.openEditInbound(dbInbound.id);
break;
case "addClient":
this.openAddClient(dbInbound.id)
break;
case "addBulkClient":
this.openAddBulkClient(dbInbound.id)
break;
case "export":
this.inboundLinks(dbInbound.id);
break;
case "resetTraffic":
this.resetTraffic(dbInbound);
this.resetTraffic(dbInbound.id);
break;
case "resetClients":
this.resetAllClientTraffics(dbInbound.id);
break;
case "clone":
this.openCloneInbound(dbInbound);
break;
case "delete":
this.delInbound(dbInbound);
this.delInbound(dbInbound.id);
break;
case "delDepletedClients":
this.delDepletedClients(dbInbound.id)
break;
}
},
@@ -291,7 +476,8 @@
isEdit: false
});
},
openEditInbound(dbInbound) {
openEditInbound(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
const inbound = dbInbound.toInbound();
inModal.show({
title: '{{ i18n "pages.inbounds.modifyInbound"}}',
@@ -306,6 +492,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 = {
@@ -320,9 +539,10 @@
port: inbound.port,
protocol: inbound.protocol,
settings: inbound.settings.toString(),
streamSettings: inbound.stream.toString(),
sniffing: inbound.canSniffing() ? inbound.sniffing.toString() : '{}',
};
if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString();
if (inbound.canSniffing()) data.sniffing = inbound.sniffing.toString();
await this.submit('/xui/inbound/add', data, inModal);
},
async updateInbound(inbound, dbInbound) {
@@ -338,15 +558,80 @@
port: inbound.port,
protocol: inbound.protocol,
settings: inbound.settings.toString(),
streamSettings: inbound.stream.toString(),
sniffing: inbound.canSniffing() ? inbound.sniffing.toString() : '{}',
};
if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString();
if (inbound.canSniffing()) data.sniffing = inbound.sniffing.toString();
await this.submit(`/xui/inbound/update/${dbInbound.id}`, data, inModal);
},
resetTraffic(dbInbound) {
openAddClient(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
clientModal.show({
title: '{{ i18n "pages.client.add"}}',
okText: '{{ i18n "pages.client.submitAdd"}}',
dbInbound: dbInbound,
confirm: async (clients, dbInboundId) => {
clientModal.loading();
await this.addClient(clients, dbInboundId);
clientModal.close();
},
isEdit: false
});
},
openAddBulkClient(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
clientsBulkModal.show({
title: '{{ i18n "pages.client.bulk"}} ' + dbInbound.remark,
okText: '{{ i18n "pages.client.bulk"}}',
dbInbound: dbInbound,
confirm: async (clients, dbInboundId) => {
clientsBulkModal.loading();
await this.addClient(clients, dbInboundId);
clientsBulkModal.close();
},
});
},
openEditClient(dbInboundId, client) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
clients = this.getInboundClients(dbInbound);
index = this.findIndexOfClient(clients, client);
clientModal.show({
title: '{{ i18n "pages.client.edit"}}',
okText: '{{ i18n "pages.client.submitEdit"}}',
dbInbound: dbInbound,
index: index,
confirm: async (client, dbInboundId, clientId) => {
clientModal.loading();
await this.updateClient(client, dbInboundId, clientId);
clientModal.close();
},
isEdit: true
});
},
findIndexOfClient(clients,client) {
firstKey = Object.keys(client)[0];
return clients.findIndex(c => c[firstKey] === client[firstKey]);
},
async addClient(clients, dbInboundId) {
const data = {
id: dbInboundId,
settings: '{"clients": [' + clients.toString() +']}',
};
await this.submit(`/xui/inbound/addClient`, data);
},
async updateClient(client, dbInboundId, clientId) {
const data = {
id: dbInboundId,
settings: '{"clients": [' + client.toString() +']}',
};
await this.submit(`/xui/inbound/updateClient/${clientId}`, data);
},
resetTraffic(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
this.$confirm({
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '',
okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => {
@@ -357,15 +642,36 @@
},
});
},
delInbound(dbInbound) {
delInbound(dbInboundId) {
this.$confirm({
title: '{{ i18n "pages.inbounds.deleteInbound"}}',
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '',
okText: '{{ i18n "delete"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/del/' + dbInbound.id),
onOk: () => this.submit('/xui/inbound/del/' + dbInboundId),
});
},
delClient(dbInboundId,client) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
clientId = dbInbound.protocol == "trojan" ? client.password : client.id;
this.$confirm({
title: '{{ i18n "pages.inbounds.deleteInbound"}}',
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '',
okText: '{{ i18n "delete"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit(`/xui/inbound/${dbInboundId}/delClient/${clientId}`),
});
},
getClients(protocol, clientSettings) {
switch(protocol){
case Protocols.VMESS: return clientSettings.vmesses;
case Protocols.VLESS: return clientSettings.vlesses;
case Protocols.TROJAN: return clientSettings.trojans;
default: return null;
}
},
showQrcode(dbInbound, clientIndex) {
const link = dbInbound.genLink(clientIndex);
qrModal.show('{{ i18n "qrCode"}}', link, dbInbound);
@@ -373,11 +679,23 @@
showInfo(dbInbound, index) {
infoModal.show(dbInbound, index);
},
switchEnable(dbInbound) {
this.submit(`/xui/inbound/update/${dbInbound.id}`, dbInbound);
switchEnable(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
this.submit(`/xui/inbound/update/${dbInboundId}`, dbInbound);
},
async submit(url, data, modal) {
const msg = await HttpUtil.postWithModal(url, data, modal);
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;
clientId = dbInbound.protocol == "trojan" ? clients[index].password : clients[index].id;
await this.updateClient(clients[index],dbInboundId, clientId);
this.loading(false);
},
async submit(url, data) {
const msg = await HttpUtil.postWithModal(url, data);
if (msg.success) {
await this.getDBInbounds();
}
@@ -391,90 +709,81 @@
return dbInbound.toInbound().settings.trojans
}
},
resetClientTraffic(client,inbound,event) {
resetClientTraffic(client,dbInboundId) {
this.$confirm({
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '',
okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => {
this.resetClTraffic(client,inbound,event);
},
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'),
});
},
async resetClTraffic(client,inbound,event) {
const msg = await HttpUtil.post('/xui/inbound/resetClientTraffic/'+ client.email);
if (!msg.success) {
return;
}
clientStats = inbound.clientStats
if(clientStats.length > 0)
{
for (const key in clientStats) {
if (Object.hasOwnProperty.call(clientStats, key)) {
if(clientStats[key]['email'] == client.email){
clientStats[key]['up'] = 0
clientStats[key]['down'] = 0
}
}
}
}
resetAllClientTraffics(dbInboundId) {
this.$confirm({
title: dbInboundId>0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficTitle"}}' : '{{ i18n "pages.inbounds.resetAllClientTrafficTitle"}}',
content: dbInboundId>0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficContent"}}' : '{{ i18n "pages.inbounds.resetAllClientTrafficContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '',
okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/resetAllClientTraffics/' + dbInboundId),
})
},
delDepletedClients(dbInboundId) {
this.$confirm({
title: '{{ i18n "pages.inbounds.delDepletedClientsTitle"}}',
content: '{{ i18n "pages.inbounds.delDepletedClientsContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '',
okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/delDepletedClients/' + dbInboundId),
})
},
isExpiry(dbInbound, index) {
return dbInbound.toInbound().isExpiry(index)
},
getUpStats(dbInbound, email) {
clientStats = dbInbound.clientStats
if(clientStats.length > 0)
{
for (const key in clientStats) {
if (Object.hasOwnProperty.call(clientStats, key)) {
if(clientStats[key]['email'] == email)
return clientStats[key]['up']
}
}
}
if(email.length == 0) return 0
clientStats = dbInbound.clientStats.find(stats => stats.email === email)
return clientStats ? clientStats.up : 0
},
getDownStats(dbInbound, email) {
clientStats = dbInbound.clientStats
if(clientStats.length > 0)
{
for (const key in clientStats) {
if (Object.hasOwnProperty.call(clientStats, key)) {
if(clientStats[key]['email'] == email)
return clientStats[key]['down']
}
}
}
if(email.length == 0) return 0
clientStats = dbInbound.clientStats.find(stats => stats.email === email)
return clientStats ? clientStats.down : 0
},
isTrafficExhausted(dbInbound, email) {
clientStats = dbInbound.clientStats
if(clientStats.length > 0)
{
for (const key in clientStats) {
if (Object.hasOwnProperty.call(clientStats, key)) {
if(clientStats[key]['email'] == email)
return clientStats[key]['down']+clientStats[key]['up'] > clientStats[key]['total']
}
}
}
if(email.length == 0) return false
clientStats = dbInbound.clientStats.find(stats => stats.email === email)
return clientStats ? clientStats.down + clientStats.up > clientStats.total : false
},
isClientEnabled(dbInbound, email) {
clientStats = dbInbound.clientStats
if(clientStats.length > 0)
{
for (const key in clientStats) {
if (Object.hasOwnProperty.call(clientStats, key)) {
if(clientStats[key]['email'] == email)
return clientStats[key]['enable']
}
}
clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null
return clientStats ? clientStats['enable'] : true
},
isRemovable(dbInbound_id){
return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInbound_id)).length > 1
},
inboundLinks(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
txtModal.show('{{ i18n "pages.inbounds.export"}}',dbInbound.genInboundLinks,dbInbound.remark);
},
exportAllLinks() {
let copyText = '';
for (const dbInbound of this.dbInbounds) {
copyText += dbInbound.genInboundLinks
}
txtModal.show('{{ i18n "pages.inbounds.export"}}',copyText,'All-Inbounds');
},
},
watch: {
@@ -483,18 +792,30 @@
}, 500)
},
mounted() {
this.getDefaultSettings();
this.getDBInbounds();
},
computed: {
total() {
let down = 0, up = 0;
for (let i = 0; i < this.dbInbounds.length; ++i) {
down += this.dbInbounds[i].down;
up += this.dbInbounds[i].up;
}
let clients = 0, deactive = [], depleted = [], expiring = [];
this.dbInbounds.forEach(dbInbound => {
down += dbInbound.down;
up += dbInbound.up;
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: clients,
deactive: deactive,
depleted: depleted,
expiring: expiring,
};
}
},
@@ -507,5 +828,7 @@
{{template "qrcodeModal"}}
{{template "textModal"}}
{{template "inboundInfoModal"}}
{{template "clientsModal"}}
{{template "clientsBulkModal"}}
</body>
</html>

View File

@@ -11,28 +11,34 @@
.ant-col-sm-24 {
margin-top: 10px;
}
.ant-card-dark h2 {
color: hsla(0,0%,100%,.65);
}
</style>
<body>
<a-layout id="app" v-cloak>
{{ template "commonSider" . }}
<a-layout id="content-layout">
<a-layout id="content-layout" :style="siderDrawer.isDarkTheme ? bgDarkStyle : ''">
<a-layout-content>
<a-spin :spinning="spinning" :delay="200" :tip="loadingTip"/>
<transition name="list" appear>
<a-row>
<a-card hoverable>
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
<a-row>
<a-col :sm="24" :md="12">
<a-row>
<a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal"
:stroke-color="status.cpu.color"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:percent="status.cpu.percent"></a-progress>
<div>CPU</div>
</a-col>
<a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal"
:stroke-color="status.mem.color"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:percent="status.mem.percent"></a-progress>
<div>
{{ i18n "pages.index.memory"}}: [[ sizeFormat(status.mem.current) ]] / [[ sizeFormat(status.mem.total) ]]
@@ -45,6 +51,7 @@
<a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal"
:stroke-color="status.swap.color"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:percent="status.swap.percent"></a-progress>
<div>
swap: [[ sizeFormat(status.swap.current) ]] / [[ sizeFormat(status.swap.total) ]]
@@ -53,6 +60,7 @@
<a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal"
:stroke-color="status.disk.color"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:percent="status.disk.percent"></a-progress>
<div>
{{ i18n "pages.index.hard"}}: [[ sizeFormat(status.disk.current) ]] / [[ sizeFormat(status.disk.total) ]]
@@ -67,23 +75,15 @@
<transition name="list" appear>
<a-row>
<a-col :sm="24" :md="12">
<a-card hoverable>
{{ 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" @click="openSelectV2rayVersion">[[ status.xray.version ]]</a-tag>
<a-tag color="blue" @click="openSelectV2rayVersion">{{ i18n "pages.index.xraySwitch"}}</a-tag>
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
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>
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
{{ i18n "pages.index.operationHours" }}:
<a-tag color="#87d068">[[ formatSecond(status.uptime) ]]</a-tag>
<a-tag color="green">[[ formatSecond(status.uptime) ]]</a-tag>
<a-tooltip>
<template slot="title">
{{ i18n "pages.index.operationHoursDesc" }}
@@ -93,12 +93,35 @@
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable>
<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] ]]
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable>
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
tcp / udp {{ i18n "pages.index.connectionCount" }}: [[ status.tcpCount ]] / [[ status.udpCount ]]
<a-tooltip>
<template slot="title">
@@ -109,7 +132,7 @@
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable>
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
<a-row>
<a-col :span="12">
<a-icon type="arrow-up"></a-icon>
@@ -135,7 +158,7 @@
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable>
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
<a-row>
<a-col :span="12">
<a-icon type="cloud-upload"></a-icon>
@@ -166,7 +189,8 @@
</a-layout>
<a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}'
:closable="true" @ok="() => versionModal.visible = false"
ok-text='{{ i18n "confirm" }}' cancel-text='{{ i18n "cancel"}}'>
:class="siderDrawer.isDarkTheme ? darkClass : ''"
footer="">
<h2>{{ i18n "pages.index.xraySwitchClick"}}</h2>
<h2>{{ i18n "pages.index.xraySwitchClickDesk"}}</h2>
<template v-for="version, index in versionModal.versions">
@@ -176,8 +200,39 @@
</a-tag>
</template>
</a-modal>
<a-modal id="log-modal" v-model="logModal.visible" title="X-UI logs"
:closable="true" @ok="() => logModal.visible = false" @cancel="() => logModal.visible = false"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
width="800px"
footer="">
<a-form layout="inline">
<a-form-item label="Count">
<a-select v-model="logModal.rows"
style="width: 80px"
@change="openLogs(logModal.rows)"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value="10">10</a-select-option>
<a-select-option value="20">20</a-select-option>
<a-select-option value="50">50</a-select-option>
<a-select-option value="100">100</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<button class="ant-btn ant-btn-primary" @click="openLogs(logModal.rows)"><a-icon type="sync"></a-icon> Reload</button>
</a-form-item>
<a-form-item>
<a-button type="primary" style="margin-bottom: 10px;"
:href="'data:application/text;charset=utf-8,' + encodeURIComponent(logModal.logs)" download="x-ui.log">
{{ i18n "download" }} x-ui.log
</a-button>
</a-form-item>
</a-form>
<a-input type="textarea" v-model="logModal.logs" disabled="true"
:autosize="{ minRows: 10, maxRows: 22}"></a-input>
</a-modal>
</a-layout>
{{template "js" .}}
{{template "textModal"}}
<script>
const State = {
@@ -269,6 +324,20 @@
},
};
const logModal = {
visible: false,
logs: '',
rows: 20,
show(logs, rows) {
this.visible = true;
this.rows = rows;
this.logs = logs.join("\n");
},
hide() {
this.visible = false;
},
};
const app = new Vue({
delimiters: ['[[', ']]'],
el: '#app',
@@ -276,6 +345,7 @@
siderDrawer,
status: new Status(),
versionModal,
logModal,
spinning: false,
loadingTip: '{{ i18n "loading"}}',
},
@@ -307,6 +377,7 @@
title: '{{ i18n "pages.index.xraySwitchVersionDialog"}}',
content: '{{ i18n "pages.index.xraySwitchVersionDialogDesc"}}' + ` ${version}?`,
okText: '{{ i18n "confirm"}}',
class: siderDrawer.isDarkTheme ? darkClass : '',
cancelText: '{{ i18n "cancel"}}',
onOk: async () => {
versionModal.hide();
@@ -316,6 +387,45 @@
},
});
},
//here add stop xray function
async stopXrayService() {
this.loading(true);
const msg = await HttpUtil.post('server/stopXrayService');
this.loading(false);
if (!msg.success) {
return;
}
},
//here add restart xray function
async restartXrayService() {
this.loading(true);
const msg = await HttpUtil.post('server/restartXrayService');
this.loading(false);
if (!msg.success) {
return;
}
},
async openLogs(rows){
this.loading(true);
const msg = await HttpUtil.post('server/logs/'+rows);
this.loading(false);
if (!msg.success) {
return;
}
logModal.show(msg.obj,rows);
},
async 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() {
while (true) {

View File

@@ -20,14 +20,14 @@
display: block;
}
.ant-tabs-top-bar {
:not(.ant-card-dark)>.ant-tabs-top-bar {
background: white;
}
</style>
<body>
<a-layout id="app" v-cloak>
{{ template "commonSider" . }}
<a-layout id="content-layout">
<a-layout id="content-layout" :style="siderDrawer.isDarkTheme ? bgDarkStyle : ''">
<a-layout-content>
<a-spin :spinning="spinning" :delay="500" tip="loading">
<a-space direction="vertical">
@@ -35,35 +35,39 @@
<a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n "pages.setting.save" }}</a-button>
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.setting.restartPanel" }}</a-button>
</a-space>
<a-tabs default-active-key="1">
<a-tabs default-active-key="1" :class="siderDrawer.isDarkTheme ? darkClass : ''">
<a-tab-pane key="1" tab='{{ i18n "pages.setting.panelConfig"}}'>
<a-list item-layout="horizontal" style="background: white">
<a-list item-layout="horizontal" :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65);': 'background: white;'">
<setting-list-item type="text" title='{{ i18n "pages.setting.panelListeningIP"}}' desc='{{ i18n "pages.setting.panelListeningIPDesc"}}' v-model="allSetting.webListen"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.setting.panelPort"}}' desc='{{ i18n "pages.setting.panelPortDesc"}}' v-model.number="allSetting.webPort"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.setting.publicKeyPath"}}' desc='{{ i18n "pages.setting.publicKeyPathDesc"}}' v-model="allSetting.webCertFile"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.setting.privateKeyPath"}}' desc='{{ i18n "pages.setting.privateKeyPathDesc"}}' v-model="allSetting.webKeyFile"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.setting.panelUrlPath"}}' desc='{{ i18n "pages.setting.panelUrlPathDesc"}}' v-model="allSetting.webBasePath"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.setting.sessionMaxAge" }}' desc='{{ i18n "pages.setting.sessionMaxAgeDesc" }}' v-model="allSetting.sessionMaxAge" :min="0"></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-row style="padding: 20px">
<a-col :lg="24" :xl="12">
<a-list-item-meta title="Language"/>
</a-col>
<a-col :lg="24" :xl="12">
<temlate>
<template>
<a-select
ref="selectLang"
v-model="lang"
@change="setLang(lang)"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
style="width: 100%"
>
<a-select-option :value="l.value" label="China" v-for="l in supportLangs" >
<a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs">
<span role="img" aria-label="l.name" v-text="l.icon"></span>
&nbsp;&nbsp;<span v-text="l.name"></span>
</a-select-option>
</a-select>
</temlate>
</template>
</a-col>
</a-row>
@@ -71,7 +75,7 @@
</a-list>
</a-tab-pane>
<a-tab-pane key="2" tab='{{ i18n "pages.setting.userSetting"}}'>
<a-form style="background: white; padding: 20px">
<a-form :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65); padding: 20px;': 'background: white; padding: 20px;'">
<a-form-item label='{{ i18n "pages.setting.oldUsername"}}'>
<a-input v-model="user.oldUsername" style="max-width: 300px"></a-input>
</a-form-item>
@@ -87,26 +91,43 @@
style="max-width: 300px"></a-input>
</a-form-item>
<a-form-item>
<!-- <a-button type="primary" @click="updateUser">修改</a-button>-->
<!-- <a-button type="primary" @click="updateUser">Revise</a-button>-->
<a-button type="primary" @click="updateUser">{{ i18n "confirm" }}</a-button>
</a-form-item>
</a-form>
</a-tab-pane>
<a-tab-pane key="3" tab='{{ i18n "pages.setting.xrayConfiguration"}}'>
<a-list item-layout="horizontal" style="background: white">
<a-list item-layout="horizontal" :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65);': 'background: white;'">
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigTorrent"}}' desc='{{ i18n "pages.setting.xrayConfigTorrentDesc"}}' v-model="torrentSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigPrivateIp"}}' desc='{{ i18n "pages.setting.xrayConfigPrivateIpDesc"}}' v-model="privateIpSettings"></setting-list-item>
<a-divider>{{ i18n "pages.setting.advancedTemplate"}}</a-divider>
<a-collapse>
<a-collapse-panel header="{{ i18n "pages.setting.xrayConfigInbounds"}}">
<setting-list-item type="textarea" title='{{ i18n "pages.setting.xrayConfigInbounds"}}' desc='{{ i18n "pages.setting.xrayConfigInboundsDesc"}}' v-model ="inboundSettings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header="{{ i18n "pages.setting.xrayConfigOutbounds"}}">
<setting-list-item type="textarea" title='{{ i18n "pages.setting.xrayConfigOutbounds"}}' desc='{{ i18n "pages.setting.xrayConfigOutboundsDesc"}}' v-model ="outboundSettings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header="{{ i18n "pages.setting.xrayConfigRoutings"}}">
<setting-list-item type="textarea" title='{{ i18n "pages.setting.xrayConfigRoutings"}}' desc='{{ i18n "pages.setting.xrayConfigRoutingsDesc"}}' v-model ="routingRuleSettings"></setting-list-item>
</a-collapse-panel>
</a-collapse>
<a-divider>{{ i18n "pages.setting.completeTemplate"}}</a-divider>
<setting-list-item type="textarea" title='{{ i18n "pages.setting.xrayConfigTemplate"}}' desc='{{ i18n "pages.setting.xrayConfigTemplateDesc"}}' v-model="allSetting.xrayTemplateConfig"></setting-list-item>
</a-list>
</a-tab-pane>
<a-tab-pane key="4" tab='{{ i18n "pages.setting.TGReminder"}}'>
<a-list item-layout="horizontal" style="background: white">
<a-list item-layout="horizontal" :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65);': 'background: white;'">
<setting-list-item type="switch" title='{{ i18n "pages.setting.telegramBotEnable" }}' desc='{{ i18n "pages.setting.telegramBotEnableDesc" }}' v-model="allSetting.tgBotEnable"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.setting.telegramToken"}}' desc='{{ i18n "pages.setting.telegramTokenDesc"}}' v-model="allSetting.tgBotToken"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.setting.telegramChatId"}}' desc='{{ i18n "pages.setting.telegramChatIdDesc"}}' v-model.number="allSetting.tgBotChatId"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.setting.telegramChatId"}}' desc='{{ i18n "pages.setting.telegramChatIdDesc"}}' v-model="allSetting.tgBotChatId"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.setting.telegramNotifyTime"}}' desc='{{ i18n "pages.setting.telegramNotifyTimeDesc"}}' v-model="allSetting.tgRunTime"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.setting.tgNotifyBackup" }}' desc='{{ i18n "pages.setting.tgNotifyBackupDesc" }}' v-model="allSetting.tgBotBackup"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.setting.tgNotifyCpu" }}' desc='{{ i18n "pages.setting.tgNotifyCpuDesc" }}' v-model="allSetting.tgCpu" :min="0" :max="100"></setting-list-item>
</a-list>
</a-tab-pane>
<a-tab-pane key="5" tab='{{ i18n "pages.setting.otherSetting"}}'>
<a-list item-layout="horizontal" style="background: white">
<a-list item-layout="horizontal" :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65);': 'background: white;'">
<setting-list-item type="text" title='{{ i18n "pages.setting.timeZonee"}}' desc='{{ i18n "pages.setting.timeZoneDesc"}}' v-model="allSetting.timeLocation"></setting-list-item>
</a-list>
</a-tab-pane>
@@ -189,6 +210,102 @@
this.saveBtnDisable = this.oldAllSetting.equals(this.allSetting);
}
},
computed: {
templateSettings: {
get: function () { return this.allSetting.xrayTemplateConfig ? JSON.parse(this.allSetting.xrayTemplateConfig) : null ; },
set: function (newValue) { this.allSetting.xrayTemplateConfig = JSON.stringify(newValue, null, 2) },
},
inboundSettings: {
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.inbounds, null, 2) : null; },
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.inbounds = JSON.parse(newValue)
this.templateSettings = newTemplateSettings
},
},
outboundSettings: {
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.outbounds, null, 2) : null; },
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.outbounds = JSON.parse(newValue)
this.templateSettings = newTemplateSettings
},
},
routingRuleSettings: {
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.routing.rules, null, 2) : null; },
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.routing.rules = JSON.parse(newValue)
this.templateSettings = newTemplateSettings
},
},
torrentSettings: {
get: function () {
torrentFilter = false
if(this.templateSettings != null){
this.templateSettings.routing.rules.forEach(routingRule => {
if(routingRule.hasOwnProperty("protocol")){
if (routingRule.protocol[0] === "bittorrent" && routingRule.outboundTag == "blocked"){
torrentFilter = true
}
}
});
}
return torrentFilter
},
set: function (newValue) {
newTemplateSettings = JSON.parse(this.allSetting.xrayTemplateConfig);
if (newValue){
newTemplateSettings.routing.rules.push(JSON.parse("{\"outboundTag\": \"blocked\",\"protocol\": [\"bittorrent\"],\"type\": \"field\"}"))
}
else {
newTemplateSettings.routing.rules = [];
this.templateSettings.routing.rules.forEach(routingRule => {
if (routingRule.hasOwnProperty('protocol')){
if (routingRule.protocol[0] === "bittorrent" && routingRule.outboundTag == "blocked"){
return;
}
}
newTemplateSettings.routing.rules.push(routingRule);
});
}
this.templateSettings = newTemplateSettings
},
},
privateIpSettings: {
get: function () {
localIpFilter = false
if(this.templateSettings != null){
this.templateSettings.routing.rules.forEach(routingRule => {
if(routingRule.hasOwnProperty("ip")){
if (routingRule.ip[0] === "geoip:private" && routingRule.outboundTag == "blocked"){
localIpFilter = true
}
}
});
}
return localIpFilter
},
set: function (newValue) {
newTemplateSettings = JSON.parse(this.allSetting.xrayTemplateConfig);
if (newValue){
newTemplateSettings.routing.rules.push(JSON.parse("{\"outboundTag\": \"blocked\",\"ip\": [\"geoip:private\"],\"type\": \"field\"}"))
}
else {
newTemplateSettings.routing.rules = [];
this.templateSettings.routing.rules.forEach(routingRule => {
if (routingRule.hasOwnProperty('ip')){
if (routingRule.ip[0] === "geoip:private" && routingRule.outboundTag == "blocked"){
return;
}
}
newTemplateSettings.routing.rules.push(routingRule);
});
}
this.templateSettings = newTemplateSettings
},
},
}
});
</script>

View File

@@ -0,0 +1,30 @@
package job
import (
"fmt"
"time"
"x-ui/web/service"
"github.com/shirou/gopsutil/v3/cpu"
)
type CheckCpuJob struct {
tgbotService service.Tgbot
settingService service.SettingService
}
func NewCheckCpuJob() *CheckCpuJob {
return new(CheckCpuJob)
}
// Here run is a interface method of Job interface
func (j *CheckCpuJob) Run() {
threshold, _ := j.settingService.GetTgCpu()
// get latest status of server
percent, err := cpu.Percent(1*time.Second, false)
if err == nil && percent[0] > float64(threshold) {
msg := fmt.Sprintf("🔴 CPU usage %.2f%% is more than threshold %d%%", percent[0], threshold)
j.tgbotService.SendMsgToTgbotAdmins(msg)
}
}

View File

@@ -1,14 +1,7 @@
package job
import (
"fmt"
"net"
"os"
"time"
"x-ui/logger"
"x-ui/util/common"
"x-ui/web/service"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
type LoginStatus byte
@@ -19,230 +12,18 @@ const (
)
type StatsNotifyJob struct {
enable bool
xrayService service.XrayService
inboundService service.InboundService
settingService service.SettingService
xrayService service.XrayService
tgbotService service.Tgbot
}
func NewStatsNotifyJob() *StatsNotifyJob {
return new(StatsNotifyJob)
}
func (j *StatsNotifyJob) SendMsgToTgbot(msg string) {
//Telegram bot basic info
tgBottoken, err := j.settingService.GetTgBotToken()
if err != nil || tgBottoken == "" {
logger.Warning("sendMsgToTgbot failed,GetTgBotToken fail:", err)
return
}
tgBotid, err := j.settingService.GetTgBotChatId()
if err != nil {
logger.Warning("sendMsgToTgbot failed,GetTgBotChatId fail:", err)
return
}
bot, err := tgbotapi.NewBotAPI(tgBottoken)
if err != nil {
fmt.Println("get tgbot error:", err)
return
}
bot.Debug = true
fmt.Printf("Authorized on account %s", bot.Self.UserName)
info := tgbotapi.NewMessage(int64(tgBotid), msg)
//msg.ReplyToMessageID = int(tgBotid)
bot.Send(info)
}
//Here run is a interface method of Job interface
// Here run is a interface method of Job interface
func (j *StatsNotifyJob) Run() {
if !j.xrayService.IsXrayRunning() {
return
}
var info string
//get hostname
name, err := os.Hostname()
if err != nil {
fmt.Println("get hostname error:", err)
return
}
info = fmt.Sprintf("Hostname:%s\r\n", name)
//get ip address
var ip string
netInterfaces, err := net.Interfaces()
if err != nil {
fmt.Println("net.Interfaces failed, err:", err.Error())
return
}
for i := 0; i < len(netInterfaces); i++ {
if (netInterfaces[i].Flags & net.FlagUp) != 0 {
addrs, _ := netInterfaces[i].Addrs()
for _, address := range addrs {
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
ip = ipnet.IP.String()
break
} else {
ip = ipnet.IP.String()
break
}
}
}
}
}
info += fmt.Sprintf("IP:%s\r\n \r\n", ip)
//get traffic
inbouds, err := j.inboundService.GetAllInbounds()
if err != nil {
logger.Warning("StatsNotifyJob run failed:", err)
return
}
//NOTE:If there no any sessions here,need to notify here
//TODO:分节点推送,自动转化格式
for _, inbound := range inbouds {
info += fmt.Sprintf("Node name:%s\r\nPort:%d\r\nUpload↑:%s\r\nDownload↓:%s\r\nTotal:%s\r\n", inbound.Remark, inbound.Port, common.FormatTraffic(inbound.Up), common.FormatTraffic(inbound.Down), common.FormatTraffic((inbound.Up + inbound.Down)))
if inbound.ExpiryTime == 0 {
info += fmt.Sprintf("Expire date:unlimited\r\n \r\n")
} else {
info += fmt.Sprintf("Expire date:%s\r\n \r\n", time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
}
}
j.SendMsgToTgbot(info)
}
func (j *StatsNotifyJob) UserLoginNotify(username string, ip string, time string, status LoginStatus) {
if username == "" || ip == "" || time == "" {
logger.Warning("UserLoginNotify failed,invalid info")
return
}
var msg string
//get hostname
name, err := os.Hostname()
if err != nil {
fmt.Println("get hostname error:", err)
return
}
if status == LoginSuccess {
msg = fmt.Sprintf("Successfully logged-in to the panel\r\nHostname:%s\r\n", name)
} else if status == LoginFail {
msg = fmt.Sprintf("Login to the panel was unsuccessful\r\nHostname:%s\r\n", name)
}
msg += fmt.Sprintf("Time:%s\r\n", time)
msg += fmt.Sprintf("Username:%s\r\n", username)
msg += fmt.Sprintf("IP:%s\r\n", ip)
j.SendMsgToTgbot(msg)
}
var numericKeyboard = tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("Get Usage", "get_usage"),
),
)
func (j *StatsNotifyJob) OnReceive() *StatsNotifyJob {
tgBottoken, err := j.settingService.GetTgBotToken()
if err != nil || tgBottoken == "" {
logger.Warning("sendMsgToTgbot failed,GetTgBotToken fail:", err)
return j
}
bot, err := tgbotapi.NewBotAPI(tgBottoken)
if err != nil {
fmt.Println("get tgbot error:", err)
return j
}
bot.Debug = false
u := tgbotapi.NewUpdate(0)
u.Timeout = 10
updates := bot.GetUpdatesChan(u)
for update := range updates {
if update.Message == nil {
if update.CallbackQuery != nil {
// Respond to the callback query, telling Telegram to show the user
// a message with the data received.
callback := tgbotapi.NewCallback(update.CallbackQuery.ID, update.CallbackQuery.Data)
if _, err := bot.Request(callback); err != nil {
logger.Warning(err)
}
// And finally, send a message containing the data received.
msg := tgbotapi.NewMessage(update.CallbackQuery.Message.Chat.ID, "")
switch update.CallbackQuery.Data {
case "get_usage":
msg.Text = "for get your usage send command like this : \n <code>/usage uuid | id</code> \n example : <code>/usage fc3239ed-8f3b-4151-ff51-b183d5182142</code>"
msg.ParseMode = "HTML"
}
if _, err := bot.Send(msg); err != nil {
logger.Warning(err)
}
}
continue
}
if !update.Message.IsCommand() { // ignore any non-command Messages
continue
}
// Create a new MessageConfig. We don't have text yet,
// so we leave it empty.
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "")
// Extract the command from the Message.
switch update.Message.Command() {
case "help":
msg.Text = "What you need?"
msg.ReplyMarkup = numericKeyboard
case "start":
msg.Text = "Hi :) \n What you need?"
msg.ReplyMarkup = numericKeyboard
case "status":
msg.Text = "bot is ok."
case "usage":
msg.Text = j.getClientUsage(update.Message.CommandArguments())
default:
msg.Text = "I don't know that command, /help"
msg.ReplyMarkup = numericKeyboard
}
if _, err := bot.Send(msg); err != nil {
logger.Warning(err)
}
}
return j
}
func (j *StatsNotifyJob) getClientUsage(id string) string {
traffic , err := j.inboundService.GetClientTrafficById(id)
if err != nil {
logger.Warning(err)
return "something wrong!"
}
expiryTime := ""
if traffic.ExpiryTime == 0 {
expiryTime = fmt.Sprintf("unlimited")
} else {
expiryTime = fmt.Sprintf("%s", time.Unix((traffic.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
}
total := ""
if traffic.Total == 0 {
total = fmt.Sprintf("unlimited")
} else {
total = fmt.Sprintf("%s", common.FormatTraffic((traffic.Total)))
}
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
total, expiryTime)
return output
j.tgbotService.SendReport()
}

View File

@@ -28,11 +28,10 @@ func (j *XrayTrafficJob) Run() {
if err != nil {
logger.Warning("add traffic failed:", err)
}
err = j.inboundService.AddClientTraffic(clientTraffics)
if err != nil {
logger.Warning("add client traffic failed:", err)
}
}

View File

@@ -1,7 +1,6 @@
{
"log": {
"loglevel": "warning",
"access": "./access.log"
"loglevel": "warning"
},
"api": {

View File

@@ -3,6 +3,7 @@ package service
import (
"encoding/json"
"fmt"
"strings"
"time"
"x-ui/database"
"x-ui/database/model"
@@ -54,7 +55,7 @@ func (s *InboundService) getClients(inbound *model.Inbound) ([]model.Client, err
settings := map[string][]model.Client{}
json.Unmarshal([]byte(inbound.Settings), &settings)
if settings == nil {
return nil, fmt.Errorf("Setting is null")
return nil, fmt.Errorf("setting is null")
}
clients := settings["clients"]
@@ -64,28 +65,45 @@ func (s *InboundService) getClients(inbound *model.Inbound) ([]model.Client, err
return clients, nil
}
func (s *InboundService) checkEmailsExist(emails map[string]bool, ignoreId int) (string, error) {
func (s *InboundService) getAllEmails() ([]string, error) {
db := database.GetDB()
var inbounds []*model.Inbound
db = db.Model(model.Inbound{}).Where("Protocol in ?", []model.Protocol{model.VMess, model.VLESS, model.Trojan})
if ignoreId > 0 {
db = db.Where("id != ?", ignoreId)
}
db = db.Find(&inbounds)
if db.Error != nil {
return "", db.Error
}
var emails []string
err := db.Raw(`
SELECT JSON_EXTRACT(client.value, '$.email')
FROM inbounds,
JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client
`).Scan(&emails).Error
for _, inbound := range inbounds {
clients, err := s.getClients(inbound)
if err != nil {
return "", err
if err != nil {
return nil, err
}
return emails, nil
}
func (s *InboundService) contains(slice []string, str string) bool {
for _, s := range slice {
if s == str {
return true
}
}
return false
}
for _, client := range clients {
if emails[client.Email] {
func (s *InboundService) checkEmailsExistForClients(clients []model.Client) (string, error) {
allEmails, err := s.getAllEmails()
if err != nil {
return "", err
}
var emails []string
for _, client := range clients {
if client.Email != "" {
if s.contains(emails, client.Email) {
return client.Email, nil
}
if s.contains(allEmails, client.Email) {
return client.Email, nil
}
emails = append(emails, client.Email)
}
}
return "", nil
@@ -96,16 +114,23 @@ func (s *InboundService) checkEmailExistForInbound(inbound *model.Inbound) (stri
if err != nil {
return "", err
}
emails := make(map[string]bool)
allEmails, err := s.getAllEmails()
if err != nil {
return "", err
}
var emails []string
for _, client := range clients {
if client.Email != "" {
if emails[client.Email] {
if s.contains(emails, client.Email) {
return client.Email, nil
}
emails[client.Email] = true
if s.contains(allEmails, client.Email) {
return client.Email, nil
}
emails = append(emails, client.Email)
}
}
return s.checkEmailsExist(emails, inbound.Id)
return "", nil
}
func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, error) {
@@ -125,11 +150,18 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, err
return inbound, common.NewError("Duplicate email:", existEmail)
}
clients, err := s.getClients(inbound)
if err != nil {
return inbound, err
}
db := database.GetDB()
err = db.Save(inbound).Error
if err == nil {
s.UpdateClientStat(inbound.Id, inbound.Settings)
for _, client := range clients {
s.AddClientStat(inbound.Id, &client)
}
}
return inbound, err
}
@@ -168,6 +200,10 @@ func (s *InboundService) AddInbounds(inbounds []*model.Inbound) error {
func (s *InboundService) DelInbound(id int) error {
db := database.GetDB()
err := db.Where("inbound_id = ?", id).Delete(xray.ClientTraffic{}).Error
if err != nil {
return err
}
return db.Delete(model.Inbound{}, id).Error
}
@@ -190,14 +226,6 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
return inbound, common.NewError("Port already exists:", inbound.Port)
}
existEmail, err := s.checkEmailExistForInbound(inbound)
if err != nil {
return inbound, err
}
if existEmail != "" {
return inbound, common.NewError("Duplicate email:", existEmail)
}
oldInbound, err := s.GetInbound(inbound.Id)
if err != nil {
return inbound, err
@@ -216,47 +244,226 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
oldInbound.Sniffing = inbound.Sniffing
oldInbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
s.UpdateClientStat(inbound.Id, inbound.Settings)
db := database.GetDB()
return inbound, db.Save(oldInbound).Error
}
func (s *InboundService) AddTraffic(traffics []*xray.Traffic) (err error) {
func (s *InboundService) AddInboundClient(data *model.Inbound) error {
clients, err := s.getClients(data)
if err != nil {
return err
}
var settings map[string]interface{}
err = json.Unmarshal([]byte(data.Settings), &settings)
if err != nil {
return err
}
interfaceClients := settings["clients"].([]interface{})
existEmail, err := s.checkEmailsExistForClients(clients)
if err != nil {
return err
}
if existEmail != "" {
return common.NewError("Duplicate email:", existEmail)
}
oldInbound, err := s.GetInbound(data.Id)
if err != nil {
return err
}
var oldSettings map[string]interface{}
err = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings)
if err != nil {
return err
}
oldClients := oldSettings["clients"].([]interface{})
oldClients = append(oldClients, interfaceClients...)
oldSettings["clients"] = oldClients
newSettings, err := json.MarshalIndent(oldSettings, "", " ")
if err != nil {
return err
}
oldInbound.Settings = string(newSettings)
for _, client := range clients {
if len(client.Email) > 0 {
s.AddClientStat(data.Id, &client)
}
}
db := database.GetDB()
return db.Save(oldInbound).Error
}
func (s *InboundService) DelInboundClient(inboundId int, clientId string) error {
oldInbound, err := s.GetInbound(inboundId)
if err != nil {
logger.Error("Load Old Data Error")
return err
}
var settings map[string]interface{}
err = json.Unmarshal([]byte(oldInbound.Settings), &settings)
if err != nil {
return err
}
email := ""
client_key := "id"
if oldInbound.Protocol == "trojan" {
client_key = "password"
}
inerfaceClients := settings["clients"].([]interface{})
var newClients []interface{}
for _, client := range inerfaceClients {
c := client.(map[string]interface{})
c_id := c[client_key].(string)
if c_id == clientId {
email = c["email"].(string)
} else {
newClients = append(newClients, client)
}
}
settings["clients"] = newClients
newSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return err
}
oldInbound.Settings = string(newSettings)
db := database.GetDB()
err = s.DelClientStat(db, email)
if err != nil {
logger.Error("Delete stats Data Error")
return err
}
return db.Save(oldInbound).Error
}
func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId string) error {
clients, err := s.getClients(data)
if err != nil {
return err
}
var settings map[string]interface{}
err = json.Unmarshal([]byte(data.Settings), &settings)
if err != nil {
return err
}
inerfaceClients := settings["clients"].([]interface{})
oldInbound, err := s.GetInbound(data.Id)
if err != nil {
return err
}
oldClients, err := s.getClients(oldInbound)
if err != nil {
return err
}
oldEmail := ""
clientIndex := 0
for index, oldClient := range oldClients {
oldClientId := ""
if oldInbound.Protocol == "trojan" {
oldClientId = oldClient.Password
} else {
oldClientId = oldClient.ID
}
if clientId == oldClientId {
oldEmail = oldClient.Email
clientIndex = index
break
}
}
if len(clients[0].Email) > 0 && clients[0].Email != oldEmail {
existEmail, err := s.checkEmailsExistForClients(clients)
if err != nil {
return err
}
if existEmail != "" {
return common.NewError("Duplicate email:", existEmail)
}
}
var oldSettings map[string]interface{}
err = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings)
if err != nil {
return err
}
settingsClients := oldSettings["clients"].([]interface{})
settingsClients[clientIndex] = inerfaceClients[0]
oldSettings["clients"] = settingsClients
newSettings, err := json.MarshalIndent(oldSettings, "", " ")
if err != nil {
return err
}
oldInbound.Settings = string(newSettings)
db := database.GetDB()
if len(clients[0].Email) > 0 {
if len(oldEmail) > 0 {
err = s.UpdateClientStat(oldEmail, &clients[0])
if err != nil {
return err
}
} else {
s.AddClientStat(data.Id, &clients[0])
}
} else {
err = s.DelClientStat(db, oldEmail)
if err != nil {
return err
}
}
return db.Save(oldInbound).Error
}
func (s *InboundService) AddTraffic(traffics []*xray.Traffic) error {
if len(traffics) == 0 {
return nil
}
db := database.GetDB()
db = db.Model(model.Inbound{})
tx := db.Begin()
defer func() {
if err != nil {
tx.Rollback()
} else {
tx.Commit()
}
}()
for _, traffic := range traffics {
if traffic.IsInbound {
err = tx.Where("tag = ?", traffic.Tag).
UpdateColumn("up", gorm.Expr("up + ?", traffic.Up)).
UpdateColumn("down", gorm.Expr("down + ?", traffic.Down)).
Error
if err != nil {
return
// Update traffics in a single transaction
err := database.GetDB().Transaction(func(tx *gorm.DB) error {
for _, traffic := range traffics {
if traffic.IsInbound {
update := tx.Model(&model.Inbound{}).Where("tag = ?", traffic.Tag).
Updates(map[string]interface{}{
"up": gorm.Expr("up + ?", traffic.Up),
"down": gorm.Expr("down + ?", traffic.Down),
})
if update.Error != nil {
return update.Error
}
}
}
}
return
return nil
})
return err
}
func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err error) {
if len(traffics) == 0 {
return nil
}
db := database.GetDB()
dbInbound := db.Model(model.Inbound{})
db = db.Model(xray.ClientTraffic{})
db := database.GetDB()
tx := db.Begin()
defer func() {
if err != nil {
tx.Rollback()
@@ -264,55 +471,90 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
tx.Commit()
}
}()
txInbound := dbInbound.Begin()
defer func() {
if err != nil {
txInbound.Rollback()
} else {
txInbound.Commit()
}
}()
emails := make([]string, 0, len(traffics))
for _, traffic := range traffics {
inbound := &model.Inbound{}
err := txInbound.Where("settings like ?", "%"+traffic.Email+"%").First(inbound).Error
traffic.InboundId = inbound.Id
if err != nil {
if err == gorm.ErrRecordNotFound {
// delete removed client record
clientErr := s.DelClientStat(tx, traffic.Email)
logger.Warning(err, traffic.Email, clientErr)
}
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
}
}
if tx.Where("inbound_id = ?", inbound.Id).Where("email = ?", traffic.Email).
UpdateColumn("enable", true).
UpdateColumn("expiry_time", traffic.ExpiryTime).
UpdateColumn("total", traffic.Total).
UpdateColumn("up", gorm.Expr("up + ?", traffic.Up)).
UpdateColumn("down", gorm.Expr("down + ?", traffic.Down)).RowsAffected == 0 {
err = tx.Create(traffic).Error
}
if err != nil {
logger.Warning("AddClientTraffic update data ", err)
continue
}
emails = append(emails, traffic.Email)
}
return
dbClientTraffics := make([]*xray.ClientTraffic, 0, len(traffics))
err = db.Model(xray.ClientTraffic{}).Where("email IN (?)", emails).Find(&dbClientTraffics).Error
if err != nil {
return err
}
dbClientTraffics, err = s.adjustTraffics(tx, dbClientTraffics)
if err != nil {
return err
}
for dbTraffic_index := range dbClientTraffics {
for traffic_index := range traffics {
if dbClientTraffics[dbTraffic_index].Email == traffics[traffic_index].Email {
dbClientTraffics[dbTraffic_index].Up += traffics[traffic_index].Up
dbClientTraffics[dbTraffic_index].Down += traffics[traffic_index].Down
break
}
}
}
err = tx.Save(dbClientTraffics).Error
if err != nil {
logger.Warning("AddClientTraffic update data ", err)
}
return nil
}
func (s *InboundService) adjustTraffics(tx *gorm.DB, dbClientTraffics []*xray.ClientTraffic) ([]*xray.ClientTraffic, error) {
inboundIds := make([]int, 0, len(dbClientTraffics))
for _, dbClientTraffic := range dbClientTraffics {
if dbClientTraffic.ExpiryTime < 0 {
inboundIds = append(inboundIds, dbClientTraffic.InboundId)
}
}
if len(inboundIds) > 0 {
var inbounds []*model.Inbound
err := tx.Model(model.Inbound{}).Where("id IN (?)", inboundIds).Find(&inbounds).Error
if err != nil {
return nil, err
}
for inbound_index := range inbounds {
settings := map[string]interface{}{}
json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings)
clients, ok := settings["clients"].([]interface{})
if ok {
var newClients []interface{}
for client_index := range clients {
c := clients[client_index].(map[string]interface{})
for traffic_index := range dbClientTraffics {
if c["email"] == dbClientTraffics[traffic_index].Email {
oldExpiryTime := c["expiryTime"].(float64)
newExpiryTime := (time.Now().Unix() * 1000) - int64(oldExpiryTime)
c["expiryTime"] = newExpiryTime
dbClientTraffics[traffic_index].ExpiryTime = newExpiryTime
break
}
}
newClients = append(newClients, interface{}(c))
}
settings["clients"] = newClients
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return nil, err
}
inbounds[inbound_index].Settings = string(modifiedSettings)
}
}
err = tx.Save(inbounds).Error
if err != nil {
logger.Warning("AddClientTraffic update inbounds ", err)
logger.Error(inbounds)
}
}
return dbClientTraffics, nil
}
func (s *InboundService) DisableInvalidInbounds() (int64, error) {
@@ -335,33 +577,48 @@ func (s *InboundService) DisableInvalidClients() (int64, error) {
count := result.RowsAffected
return count, err
}
func (s *InboundService) UpdateClientStat(inboundId int, inboundSettings string) error {
func (s *InboundService) RemoveOrphanedTraffics() {
db := database.GetDB()
db.Exec(`
DELETE FROM client_traffics
WHERE email NOT IN (
SELECT JSON_EXTRACT(client.value, '$.email')
FROM inbounds,
JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client
)
`)
}
func (s *InboundService) AddClientStat(inboundId int, client *model.Client) error {
db := database.GetDB()
// get settings clients
settings := map[string][]model.Client{}
json.Unmarshal([]byte(inboundSettings), &settings)
clients := settings["clients"]
for _, client := range clients {
result := db.Model(xray.ClientTraffic{}).
Where("inbound_id = ? and email = ?", inboundId, client.Email).
Updates(map[string]interface{}{"enable": true, "total": client.TotalGB, "expiry_time": client.ExpiryTime})
if result.RowsAffected == 0 {
clientTraffic := xray.ClientTraffic{}
clientTraffic.InboundId = inboundId
clientTraffic.Email = client.Email
clientTraffic.Total = client.TotalGB
clientTraffic.ExpiryTime = client.ExpiryTime
clientTraffic.Enable = true
clientTraffic.Up = 0
clientTraffic.Down = 0
db.Create(&clientTraffic)
}
err := result.Error
if err != nil {
return err
}
clientTraffic := xray.ClientTraffic{}
clientTraffic.InboundId = inboundId
clientTraffic.Email = client.Email
clientTraffic.Total = client.TotalGB
clientTraffic.ExpiryTime = client.ExpiryTime
clientTraffic.Enable = true
clientTraffic.Up = 0
clientTraffic.Down = 0
result := db.Create(&clientTraffic)
err := result.Error
if err != nil {
return err
}
return nil
}
func (s *InboundService) UpdateClientStat(email string, client *model.Client) error {
db := database.GetDB()
result := db.Model(xray.ClientTraffic{}).
Where("email = ?", email).
Updates(map[string]interface{}{
"enable": true,
"email": client.Email,
"total": client.TotalGB,
"expiry_time": client.ExpiryTime})
err := result.Error
if err != nil {
return err
}
return nil
}
@@ -369,13 +626,12 @@ func (s *InboundService) DelClientStat(tx *gorm.DB, email string) error {
return tx.Where("email = ?", email).Delete(xray.ClientTraffic{}).Error
}
func (s *InboundService) ResetClientTraffic(clientEmail string) error {
func (s *InboundService) ResetClientTraffic(id int, clientEmail string) error {
db := database.GetDB()
result := db.Model(xray.ClientTraffic{}).
Where("email = ?", clientEmail).
Update("up", 0).
Update("down", 0)
Where("inbound_id = ? and email = ?", id, clientEmail).
Updates(map[string]interface{}{"enable": true, "up": 0, "down": 0})
err := result.Error
@@ -384,12 +640,172 @@ func (s *InboundService) ResetClientTraffic(clientEmail string) error {
}
return nil
}
func (s *InboundService) GetClientTrafficById(uuid string) (traffic *xray.ClientTraffic, err error) {
func (s *InboundService) ResetAllClientTraffics(id int) error {
db := database.GetDB()
whereText := "inbound_id "
if id == -1 {
whereText += " > ?"
} else {
whereText += " = ?"
}
result := db.Model(xray.ClientTraffic{}).
Where(whereText, 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) DelDepletedClients(id int) (err error) {
db := database.GetDB()
tx := db.Begin()
defer func() {
if err == nil {
tx.Commit()
} else {
tx.Rollback()
}
}()
whereText := "inbound_id "
if id < 0 {
whereText += "> ?"
} else {
whereText += "= ?"
}
depletedClients := []xray.ClientTraffic{}
err = db.Model(xray.ClientTraffic{}).Where(whereText+" and enable = ?", id, false).Select("inbound_id, GROUP_CONCAT(email) as email").Group("inbound_id").Find(&depletedClients).Error
if err != nil {
return err
}
for _, depletedClient := range depletedClients {
emails := strings.Split(depletedClient.Email, ",")
oldInbound, err := s.GetInbound(depletedClient.InboundId)
if err != nil {
return err
}
var oldSettings map[string]interface{}
err = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings)
if err != nil {
return err
}
oldClients := oldSettings["clients"].([]interface{})
var newClients []interface{}
for _, client := range oldClients {
deplete := false
c := client.(map[string]interface{})
for _, email := range emails {
if email == c["email"].(string) {
deplete = true
break
}
}
if !deplete {
newClients = append(newClients, client)
}
}
if len(newClients) > 0 {
oldSettings["clients"] = newClients
newSettings, err := json.MarshalIndent(oldSettings, "", " ")
if err != nil {
return err
}
oldInbound.Settings = string(newSettings)
err = tx.Save(oldInbound).Error
if err != nil {
return err
}
} else {
// Delete inbound if no client remains
s.DelInbound(depletedClient.InboundId)
}
}
err = tx.Where(whereText+" and enable = ?", id, false).Delete(xray.ClientTraffic{}).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)
return nil, err
}
}
return traffics, err
}
func (s *InboundService) GetClientTrafficByEmail(email string) (traffic *xray.ClientTraffic, err error) {
db := database.GetDB()
var traffics []*xray.ClientTraffic
err = db.Model(xray.ClientTraffic{}).Where("email = ?", email).Find(&traffics).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
logger.Warning(err)
return nil, err
}
}
return traffics[0], err
}
func (s *InboundService) SearchClientTraffic(query string) (traffic *xray.ClientTraffic, err error) {
db := database.GetDB()
inbound := &model.Inbound{}
traffic = &xray.ClientTraffic{}
err = db.Model(model.Inbound{}).Where("settings like ?", "%"+uuid+"%").First(inbound).Error
err = db.Model(model.Inbound{}).Where("settings like ?", "%\""+query+"\"%").First(inbound).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
logger.Warning(err)
@@ -403,9 +819,17 @@ func (s *InboundService) GetClientTrafficById(uuid string) (traffic *xray.Client
json.Unmarshal([]byte(inbound.Settings), &settings)
clients := settings["clients"]
for _, client := range clients {
if uuid == client.ID {
if client.ID == query && client.Email != "" {
traffic.Email = client.Email
break
}
if client.Password == query && client.Email != "" {
traffic.Email = client.Email
break
}
}
if traffic.Email == "" {
return nil, err
}
err = db.Model(xray.ClientTraffic{}).Where("email = ?", traffic.Email).First(traffic).Error
if err != nil {
@@ -414,3 +838,67 @@ func (s *InboundService) GetClientTrafficById(uuid string) (traffic *xray.Client
}
return traffic, err
}
func (s *InboundService) SearchInbounds(query string) ([]*model.Inbound, error) {
db := database.GetDB()
var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Preload("ClientStats").Where("remark like ?", "%"+query+"%").Find(&inbounds).Error
if err != nil && err != gorm.ErrRecordNotFound {
return nil, err
}
return inbounds, nil
}
func (s *InboundService) MigrationRequirements() {
db := database.GetDB()
var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Where("protocol IN (?)", []string{"vmess", "vless", "trojan"}).Find(&inbounds).Error
if err != nil && err != gorm.ErrRecordNotFound {
return
}
for inbound_index := range inbounds {
settings := map[string]interface{}{}
json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings)
clients, ok := settings["clients"].([]interface{})
if ok {
var newClients []interface{}
for client_index := range clients {
c := clients[client_index].(map[string]interface{})
// Add email='' if it is not exists
if _, ok := c["email"]; !ok {
c["email"] = ""
}
// Remove "flow": "xtls-rprx-direct"
if _, ok := c["flow"]; ok {
if c["flow"] == "xtls-rprx-direct" {
c["flow"] = ""
}
}
newClients = append(newClients, interface{}(c))
}
settings["clients"] = newClients
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return
}
inbounds[inbound_index].Settings = string(modifiedSettings)
}
modelClients, err := s.getClients(inbounds[inbound_index])
if err != nil {
return
}
for _, modelClient := range modelClients {
if len(modelClient.Email) > 0 {
var count int64
db.Model(xray.ClientTraffic{}).Where("email = ?", modelClient.Email).Count(&count)
if count == 0 {
s.AddClientStat(inbounds[inbound_index].Id, &modelClient)
}
}
}
}
db.Save(inbounds)
}

View File

@@ -9,18 +9,21 @@ import (
"io/fs"
"net/http"
"os"
"os/exec"
"runtime"
"strings"
"time"
"x-ui/config"
"x-ui/logger"
"x-ui/util/sys"
"x-ui/xray"
"github.com/shirou/gopsutil/cpu"
"github.com/shirou/gopsutil/disk"
"github.com/shirou/gopsutil/host"
"github.com/shirou/gopsutil/load"
"github.com/shirou/gopsutil/mem"
"github.com/shirou/gopsutil/net"
"github.com/shirou/gopsutil/v3/cpu"
"github.com/shirou/gopsutil/v3/disk"
"github.com/shirou/gopsutil/v3/host"
"github.com/shirou/gopsutil/v3/load"
"github.com/shirou/gopsutil/v3/mem"
"github.com/shirou/gopsutil/v3/net"
)
type ProcessState string
@@ -191,13 +194,39 @@ func (s *ServerService) GetXrayVersions() ([]string, error) {
if err != nil {
return nil, err
}
versions := make([]string, 0, len(releases))
var versions []string
for _, release := range releases {
versions = append(versions, release.TagName)
if release.TagName >= "v1.8.0" {
versions = append(versions, release.TagName)
}
}
return versions, nil
}
func (s *ServerService) StopXrayService() (string error) {
err := s.xrayService.StopXray()
if err != nil {
logger.Error("stop xray failed:", err)
return err
}
return nil
}
func (s *ServerService) RestartXrayService() (string error) {
s.xrayService.StopXray()
defer func() {
err := s.xrayService.RestartXray(true)
if err != nil {
logger.Error("start xray failed:", err)
}
}()
return nil
}
func (s *ServerService) downloadXRay(version string) (string, error) {
osName := runtime.GOOS
arch := runtime.GOARCH
@@ -300,3 +329,92 @@ func (s *ServerService) UpdateXray(version string) error {
return nil
}
func (s *ServerService) GetLogs(count string) ([]string, error) {
// Define the journalctl command and its arguments
var cmdArgs []string
if runtime.GOOS == "linux" {
cmdArgs = []string{"journalctl", "-u", "x-ui", "--no-pager", "-n", count}
} else {
return []string{"Unsupported operating system"}, nil
}
// Run the command
cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return nil, err
}
lines := strings.Split(out.String(), "\n")
return lines, nil
}
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
}
func (s *ServerService) GetNewX25519Cert() (interface{}, error) {
// Run the command
cmd := exec.Command(xray.GetBinaryPath(), "x25519")
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return nil, err
}
lines := strings.Split(out.String(), "\n")
privateKeyLine := strings.Split(lines[0], ":")
publicKeyLine := strings.Split(lines[1], ":")
privateKey := strings.TrimSpace(privateKeyLine[1])
publicKey := strings.TrimSpace(publicKeyLine[1])
keyPair := map[string]interface{}{
"privateKey": privateKey,
"publicKey": publicKey,
}
return keyPair, nil
}

View File

@@ -28,11 +28,16 @@ var defaultValueMap = map[string]string{
"webKeyFile": "",
"secret": random.Seq(32),
"webBasePath": "/",
"sessionMaxAge": "0",
"expireDiff": "0",
"trafficDiff": "0",
"timeLocation": "Asia/Tehran",
"tgBotEnable": "false",
"tgBotToken": "",
"tgBotChatId": "0",
"tgRunTime": "",
"tgBotChatId": "",
"tgRunTime": "@daily",
"tgBotBackup": "false",
"tgCpu": "0",
}
type SettingService struct {
@@ -202,30 +207,38 @@ func (s *SettingService) SetTgBotToken(token string) error {
return s.setString("tgBotToken", token)
}
func (s *SettingService) GetTgBotChatId() (int, error) {
return s.getInt("tgBotChatId")
func (s *SettingService) GetTgBotChatId() (string, error) {
return s.getString("tgBotChatId")
}
func (s *SettingService) SetTgBotChatId(chatId int) error {
return s.setInt("tgBotChatId", chatId)
}
func (s *SettingService) SetTgbotenabled(value bool) error {
return s.setBool("tgBotEnable", value)
func (s *SettingService) SetTgBotChatId(chatIds string) error {
return s.setString("tgBotChatId", chatIds)
}
func (s *SettingService) GetTgbotenabled() (bool, error) {
return s.getBool("tgBotEnable")
}
func (s *SettingService) SetTgbotRuntime(time string) error {
return s.setString("tgRunTime", time)
func (s *SettingService) SetTgbotenabled(value bool) error {
return s.setBool("tgBotEnable", value)
}
func (s *SettingService) GetTgbotRuntime() (string, error) {
return s.getString("tgRunTime")
}
func (s *SettingService) SetTgbotRuntime(time string) error {
return s.setString("tgRunTime", time)
}
func (s *SettingService) GetTgBotBackup() (bool, error) {
return s.getBool("tgBotBackup")
}
func (s *SettingService) GetTgCpu() (int, error) {
return s.getInt("tgCpu")
}
func (s *SettingService) GetPort() (int, error) {
return s.getInt("webPort")
}
@@ -242,6 +255,18 @@ func (s *SettingService) GetKeyFile() (string, error) {
return s.getString("webKeyFile")
}
func (s *SettingService) GetExpireDiff() (int, error) {
return s.getInt("expireDiff")
}
func (s *SettingService) GetTrafficDiff() (int, error) {
return s.getInt("trafficDiff")
}
func (s *SettingService) GetSessionMaxAge() (int, error) {
return s.getInt("sessionMaxAge")
}
func (s *SettingService) GetSecret() ([]byte, error) {
secret, err := s.getString("secret")
if secret == defaultValueMap["secret"] {

547
web/service/sub.go Normal file
View File

@@ -0,0 +1,547 @@
package service
import (
"encoding/base64"
"fmt"
"net/url"
"strings"
"x-ui/database"
"x-ui/database/model"
"x-ui/logger"
"x-ui/xray"
"github.com/goccy/go-json"
"gorm.io/gorm"
)
type SubService struct {
address string
inboundService InboundService
}
func (s *SubService) GetSubs(subId string, host string) ([]string, string, error) {
s.address = host
var result []string
var header string
var traffic xray.ClientTraffic
var clientTraffics []xray.ClientTraffic
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)
clientTraffics = append(clientTraffics, s.getClientTraffics(inbound.ClientStats, client.Email))
}
}
}
for index, clientTraffic := range clientTraffics {
if index == 0 {
traffic.Up = clientTraffic.Up
traffic.Down = clientTraffic.Down
traffic.Total = clientTraffic.Total
if clientTraffic.ExpiryTime > 0 {
traffic.ExpiryTime = clientTraffic.ExpiryTime
}
} else {
traffic.Up += clientTraffic.Up
traffic.Down += clientTraffic.Down
if traffic.Total == 0 || clientTraffic.Total == 0 {
traffic.Total = 0
} else {
traffic.Total += clientTraffic.Total
}
if clientTraffic.ExpiryTime != traffic.ExpiryTime {
traffic.ExpiryTime = 0
}
}
}
header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
return result, header, nil
}
func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
db := database.GetDB()
var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Preload("ClientStats").Where("settings like ?", fmt.Sprintf(`%%"subId": "%s"%%`, subId)).Find(&inbounds).Error
if err != nil && err != gorm.ErrRecordNotFound {
return nil, err
}
return inbounds, nil
}
func (s *SubService) getClientTraffics(traffics []xray.ClientTraffic, email string) xray.ClientTraffic {
for _, traffic := range traffics {
if traffic.Email == email {
return traffic
}
}
return xray.ClientTraffic{}
}
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 == "reality" {
params["security"] = "reality"
realitySetting, _ := stream["realitySettings"].(map[string]interface{})
realitySettings, _ := searchKey(realitySetting, "settings")
if realitySetting != nil {
if sniValue, ok := searchKey(realitySetting, "serverNames"); ok {
sNames, _ := sniValue.([]interface{})
params["sni"], _ = sNames[0].(string)
}
if pbkValue, ok := searchKey(realitySettings, "publicKey"); ok {
params["pbk"], _ = pbkValue.(string)
}
if sidValue, ok := searchKey(realitySetting, "shortIds"); ok {
shortIds, _ := sidValue.([]interface{})
params["sid"], _ = shortIds[0].(string)
}
if fpValue, ok := searchKey(realitySettings, "fingerprint"); ok {
if fp, ok := fpValue.(string); ok && len(fp) > 0 {
params["fp"] = fp
}
}
if spxValue, ok := searchKey(realitySettings, "spiderX"); ok {
if spx, ok := spxValue.(string); ok && len(spx) > 0 {
params["spx"] = spx
}
}
if serverName, ok := searchKey(realitySettings, "serverName"); ok {
if sname, ok := serverName.(string); ok && len(sname) > 0 {
address = sname
}
}
}
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
params["flow"] = clients[clientIndex].Flow
}
}
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 == "reality" {
params["security"] = "reality"
realitySetting, _ := stream["realitySettings"].(map[string]interface{})
realitySettings, _ := searchKey(realitySetting, "settings")
if realitySetting != nil {
if sniValue, ok := searchKey(realitySetting, "serverNames"); ok {
sNames, _ := sniValue.([]interface{})
params["sni"], _ = sNames[0].(string)
}
if pbkValue, ok := searchKey(realitySettings, "publicKey"); ok {
params["pbk"], _ = pbkValue.(string)
}
if sidValue, ok := searchKey(realitySettings, "shortIds"); ok {
shortIds, _ := sidValue.([]interface{})
params["sid"], _ = shortIds[0].(string)
}
if fpValue, ok := searchKey(realitySettings, "fingerprint"); ok {
if fp, ok := fpValue.(string); ok && len(fp) > 0 {
params["fp"] = fp
}
}
if spxValue, ok := searchKey(realitySettings, "spiderX"); ok {
if spx, ok := spxValue.(string); ok && len(spx) > 0 {
params["spx"] = spx
}
}
if serverName, ok := searchKey(realitySettings, "serverName"); ok {
if sname, ok := serverName.(string); ok && len(sname) > 0 {
address = sname
}
}
}
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
params["flow"] = clients[clientIndex].Flow
}
}
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{})
if len(hosts) > 0 {
return hosts[0].(string)
} else {
return ""
}
case interface{}:
return v.(string)
}
}
}
return ""
}

610
web/service/tgbot.go Normal file
View File

@@ -0,0 +1,610 @@
package service
import (
"fmt"
"net"
"os"
"strconv"
"strings"
"time"
"x-ui/config"
"x-ui/database/model"
"x-ui/logger"
"x-ui/util/common"
"x-ui/xray"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
var bot *tgbotapi.BotAPI
var adminIds []int64
var isRunning bool
type LoginStatus byte
const (
LoginSuccess LoginStatus = 1
LoginFail LoginStatus = 0
)
type Tgbot struct {
inboundService InboundService
settingService SettingService
serverService ServerService
lastStatus *Status
}
func (t *Tgbot) NewTgbot() *Tgbot {
return new(Tgbot)
}
func (t *Tgbot) Start() error {
tgBottoken, err := t.settingService.GetTgBotToken()
if err != nil || tgBottoken == "" {
logger.Warning("Get TgBotToken failed:", err)
return err
}
tgBotid, err := t.settingService.GetTgBotChatId()
if err != nil {
logger.Warning("Get GetTgBotChatId failed:", err)
return err
}
for _, adminId := range strings.Split(tgBotid, ",") {
id, err := strconv.Atoi(adminId)
if err != nil {
logger.Warning("Failed to get IDs from GetTgBotChatId:", err)
return err
}
adminIds = append(adminIds, int64(id))
}
bot, err = tgbotapi.NewBotAPI(tgBottoken)
if err != nil {
fmt.Println("Get tgbot's api error:", err)
return err
}
bot.Debug = false
// listen for TG bot income messages
if !isRunning {
logger.Info("Starting Telegram receiver ...")
go t.OnReceive()
isRunning = true
}
return nil
}
func (t *Tgbot) IsRunnging() bool {
return isRunning
}
func (t *Tgbot) Stop() {
bot.StopReceivingUpdates()
logger.Info("Stop Telegram receiver ...")
isRunning = false
adminIds = nil
}
func (t *Tgbot) OnReceive() {
u := tgbotapi.NewUpdate(0)
u.Timeout = 10
updates := bot.GetUpdatesChan(u)
for update := range updates {
tgId := update.FromChat().ID
chatId := update.FromChat().ChatConfig().ChatID
isAdmin := checkAdmin(tgId)
if update.Message == nil {
if update.CallbackQuery != nil {
t.asnwerCallback(update.CallbackQuery, isAdmin)
}
} else {
if update.Message.IsCommand() {
t.answerCommand(update.Message, chatId, isAdmin)
}
}
}
}
func (t *Tgbot) answerCommand(message *tgbotapi.Message, chatId int64, isAdmin bool) {
msg := ""
// Extract the command from the Message.
switch message.Command() {
case "help":
msg = "This bot is providing you some specefic data from the server.\n\n Please choose:"
case "start":
msg = "Hello <i>" + message.From.FirstName + "</i> 👋"
if isAdmin {
hostname, _ := os.Hostname()
msg += "\nWelcome to <b>" + hostname + "</b> management bot"
}
msg += "\n\nI can do some magics for you, please choose:"
case "status":
msg = "bot is ok ✅"
case "usage":
if len(message.CommandArguments()) > 1 {
if isAdmin {
t.searchClient(chatId, message.CommandArguments())
} else {
t.searchForClient(chatId, message.CommandArguments())
}
} else {
msg = "❗Please provide a text for search!"
}
case "inbound":
if isAdmin {
t.searchInbound(chatId, message.CommandArguments())
} else {
msg = "❗ Unknown command"
}
default:
msg = "❗ Unknown command"
}
t.SendAnswer(chatId, msg, isAdmin)
}
func (t *Tgbot) asnwerCallback(callbackQuery *tgbotapi.CallbackQuery, isAdmin bool) {
// Respond to the callback query, telling Telegram to show the user
// a message with the data received.
callback := tgbotapi.NewCallback(callbackQuery.ID, callbackQuery.Data)
if _, err := bot.Request(callback); err != nil {
logger.Warning(err)
}
switch callbackQuery.Data {
case "get_usage":
t.SendMsgToTgbot(callbackQuery.From.ID, t.getServerUsage())
case "inbounds":
t.SendMsgToTgbot(callbackQuery.From.ID, t.getInboundUsages())
case "deplete_soon":
t.SendMsgToTgbot(callbackQuery.From.ID, t.getExhausted())
case "get_backup":
t.sendBackup(callbackQuery.From.ID)
case "client_traffic":
t.getClientUsage(callbackQuery.From.ID, callbackQuery.From.UserName)
case "client_commands":
t.SendMsgToTgbot(callbackQuery.From.ID, "To search for statistics, just use folowing command:\r\n \r\n<code>/usage [UID|Password]</code>\r\n \r\nUse UID for vmess/vless and Password for Trojan.")
case "commands":
t.SendMsgToTgbot(callbackQuery.From.ID, "Search for a client email:\r\n<code>/usage email</code>\r\n \r\nSearch for inbounds (with client stats):\r\n<code>/inbound [remark]</code>")
}
}
func checkAdmin(tgId int64) bool {
for _, adminId := range adminIds {
if adminId == tgId {
return true
}
}
return false
}
func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) {
var numericKeyboard = tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("Server Usage", "get_usage"),
tgbotapi.NewInlineKeyboardButtonData("Get DB Backup", "get_backup"),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("Get Inbounds", "inbounds"),
tgbotapi.NewInlineKeyboardButtonData("Deplete soon", "deplete_soon"),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("Commands", "commands"),
),
)
var numericKeyboardClient = tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("Get Usage", "client_traffic"),
tgbotapi.NewInlineKeyboardButtonData("Commands", "client_commands"),
),
)
msgConfig := tgbotapi.NewMessage(chatId, msg)
msgConfig.ParseMode = "HTML"
if isAdmin {
msgConfig.ReplyMarkup = numericKeyboard
} else {
msgConfig.ReplyMarkup = numericKeyboardClient
}
_, err := bot.Send(msgConfig)
if err != nil {
logger.Warning("Error sending telegram message :", err)
}
}
func (t *Tgbot) SendMsgToTgbot(tgid int64, msg string) {
var allMessages []string
limit := 2000
// paging message if it is big
if len(msg) > limit {
messages := strings.Split(msg, "\r\n \r\n")
lastIndex := -1
for _, message := range messages {
if (len(allMessages) == 0) || (len(allMessages[lastIndex])+len(message) > limit) {
allMessages = append(allMessages, message)
lastIndex++
} else {
allMessages[lastIndex] += "\r\n \r\n" + message
}
}
} else {
allMessages = append(allMessages, msg)
}
for _, message := range allMessages {
info := tgbotapi.NewMessage(tgid, message)
info.ParseMode = "HTML"
_, err := bot.Send(info)
if err != nil {
logger.Warning("Error sending telegram message :", err)
}
time.Sleep(500 * time.Millisecond)
}
}
func (t *Tgbot) SendMsgToTgbotAdmins(msg string) {
for _, adminId := range adminIds {
t.SendMsgToTgbot(adminId, msg)
}
}
func (t *Tgbot) SendReport() {
runTime, err := t.settingService.GetTgbotRuntime()
if err == nil && len(runTime) > 0 {
t.SendMsgToTgbotAdmins("🕰 Scheduled reports: " + runTime + "\r\nDate-Time: " + time.Now().Format("2006-01-02 15:04:05"))
}
info := t.getServerUsage()
t.SendMsgToTgbotAdmins(info)
exhausted := t.getExhausted()
t.SendMsgToTgbotAdmins(exhausted)
backupEnable, err := t.settingService.GetTgBotBackup()
if err == nil && backupEnable {
for _, adminId := range adminIds {
t.sendBackup(int64(adminId))
}
}
}
func (t *Tgbot) getServerUsage() string {
var info string
//get hostname
name, err := os.Hostname()
if err != nil {
logger.Error("get hostname error:", err)
name = ""
}
info = fmt.Sprintf("💻 Hostname: %s\r\n", name)
info += fmt.Sprintf("🚀X-UI Version: %s\r\n", config.GetVersion())
//get ip address
var ip string
var ipv6 string
netInterfaces, err := net.Interfaces()
if err != nil {
logger.Error("net.Interfaces failed, err:", err.Error())
info += "🌐 IP: Unknown\r\n \r\n"
} else {
for i := 0; i < len(netInterfaces); i++ {
if (netInterfaces[i].Flags & net.FlagUp) != 0 {
addrs, _ := netInterfaces[i].Addrs()
for _, address := range addrs {
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
ip += ipnet.IP.String() + " "
} else if ipnet.IP.To16() != nil && !ipnet.IP.IsLinkLocalUnicast() {
ipv6 += ipnet.IP.String() + " "
}
}
}
}
}
info += fmt.Sprintf("🌐IP: %s\r\n🌐IPv6: %s\r\n", ip, ipv6)
}
// get latest status of server
t.lastStatus = t.serverService.GetStatus(t.lastStatus)
info += fmt.Sprintf("🔌Server Uptime: %d days\r\n", int(t.lastStatus.Uptime/86400))
info += fmt.Sprintf("📈Server Load: %.1f, %.1f, %.1f\r\n", t.lastStatus.Loads[0], t.lastStatus.Loads[1], t.lastStatus.Loads[2])
info += fmt.Sprintf("📋Server Memory: %s/%s\r\n", common.FormatTraffic(int64(t.lastStatus.Mem.Current)), common.FormatTraffic(int64(t.lastStatus.Mem.Total)))
info += fmt.Sprintf("🔹TcpCount: %d\r\n", t.lastStatus.TcpCount)
info += fmt.Sprintf("🔸UdpCount: %d\r\n", t.lastStatus.UdpCount)
info += fmt.Sprintf("🚦Traffic: %s (↑%s,↓%s)\r\n", common.FormatTraffic(int64(t.lastStatus.NetTraffic.Sent+t.lastStatus.NetTraffic.Recv)), common.FormatTraffic(int64(t.lastStatus.NetTraffic.Sent)), common.FormatTraffic(int64(t.lastStatus.NetTraffic.Recv)))
info += fmt.Sprintf("Xray status: %s", t.lastStatus.Xray.State)
return info
}
func (t *Tgbot) UserLoginNotify(username string, ip string, time string, status LoginStatus) {
if username == "" || ip == "" || time == "" {
logger.Warning("UserLoginNotify failed,invalid info")
return
}
var msg string
// Get hostname
name, err := os.Hostname()
if err != nil {
logger.Warning("get hostname error:", err)
return
}
if status == LoginSuccess {
msg = fmt.Sprintf("✅ Successfully logged-in to the panel\r\nHostname:%s\r\n", name)
} else if status == LoginFail {
msg = fmt.Sprintf("❗ Login to the panel was unsuccessful\r\nHostname:%s\r\n", name)
}
msg += fmt.Sprintf("⏰ Time:%s\r\n", time)
msg += fmt.Sprintf("🆔 Username:%s\r\n", username)
msg += fmt.Sprintf("🌐 IP:%s\r\n", ip)
t.SendMsgToTgbotAdmins(msg)
}
func (t *Tgbot) getInboundUsages() string {
info := ""
// get traffic
inbouds, err := t.inboundService.GetAllInbounds()
if err != nil {
logger.Warning("GetAllInbounds run failed:", err)
info += "❌ Failed to get inbounds"
} else {
// NOTE:If there no any sessions here,need to notify here
// TODO:Sub-node push, automatic conversion format
for _, inbound := range inbouds {
info += fmt.Sprintf("📍Inbound:%s\r\nPort:%d\r\n", inbound.Remark, inbound.Port)
info += fmt.Sprintf("Traffic: %s (↑%s,↓%s)\r\n", common.FormatTraffic((inbound.Up + inbound.Down)), common.FormatTraffic(inbound.Up), common.FormatTraffic(inbound.Down))
if inbound.ExpiryTime == 0 {
info += "Expire date: ♾ Unlimited\r\n \r\n"
} else {
info += fmt.Sprintf("Expire date:%s\r\n \r\n", time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
}
}
}
return info
}
func (t *Tgbot) getClientUsage(chatId int64, tgUserName string) {
if len(tgUserName) == 0 {
msg := "Your configuration is not found!\nYou should configure your telegram username and ask Admin to add it to your configuration."
t.SendMsgToTgbot(chatId, msg)
return
}
traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserName)
if err != nil {
logger.Warning(err)
msg := "❌ Something went wrong!"
t.SendMsgToTgbot(chatId, msg)
return
}
if len(traffics) == 0 {
msg := "Your configuration is not found!\nPlease ask your Admin to use your telegram username in your configuration(s).\n\nYour username: <b>@" + tgUserName + "</b>"
t.SendMsgToTgbot(chatId, msg)
return
}
for _, traffic := range traffics {
expiryTime := ""
if traffic.ExpiryTime == 0 {
expiryTime = "♾Unlimited"
} else if traffic.ExpiryTime < 0 {
expiryTime = fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000)
} else {
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
}
total := ""
if traffic.Total == 0 {
total = "♾Unlimited"
} else {
total = common.FormatTraffic((traffic.Total))
}
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
total, expiryTime)
t.SendMsgToTgbot(chatId, output)
}
t.SendAnswer(chatId, "Please choose:", false)
}
func (t *Tgbot) searchClient(chatId int64, email string) {
traffic, err := t.inboundService.GetClientTrafficByEmail(email)
if err != nil {
logger.Warning(err)
msg := "❌ Something went wrong!"
t.SendMsgToTgbot(chatId, msg)
return
}
if traffic == nil {
msg := "No result!"
t.SendMsgToTgbot(chatId, msg)
return
}
expiryTime := ""
if traffic.ExpiryTime == 0 {
expiryTime = "♾Unlimited"
} else if traffic.ExpiryTime < 0 {
expiryTime = fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000)
} else {
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
}
total := ""
if traffic.Total == 0 {
total = "♾Unlimited"
} else {
total = common.FormatTraffic((traffic.Total))
}
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
total, expiryTime)
t.SendMsgToTgbot(chatId, output)
}
func (t *Tgbot) searchInbound(chatId int64, remark string) {
inbouds, err := t.inboundService.SearchInbounds(remark)
if err != nil {
logger.Warning(err)
msg := "❌ Something went wrong!"
t.SendMsgToTgbot(chatId, msg)
return
}
for _, inbound := range inbouds {
info := ""
info += fmt.Sprintf("📍Inbound:%s\r\nPort:%d\r\n", inbound.Remark, inbound.Port)
info += fmt.Sprintf("Traffic: %s (↑%s,↓%s)\r\n", common.FormatTraffic((inbound.Up + inbound.Down)), common.FormatTraffic(inbound.Up), common.FormatTraffic(inbound.Down))
if inbound.ExpiryTime == 0 {
info += "Expire date: ♾ Unlimited\r\n \r\n"
} else {
info += fmt.Sprintf("Expire date:%s\r\n \r\n", time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
}
t.SendMsgToTgbot(chatId, info)
for _, traffic := range inbound.ClientStats {
expiryTime := ""
if traffic.ExpiryTime == 0 {
expiryTime = "♾Unlimited"
} else if traffic.ExpiryTime < 0 {
expiryTime = fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000)
} else {
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
}
total := ""
if traffic.Total == 0 {
total = "♾Unlimited"
} else {
total = common.FormatTraffic((traffic.Total))
}
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
total, expiryTime)
t.SendMsgToTgbot(chatId, output)
}
}
}
func (t *Tgbot) searchForClient(chatId int64, query string) {
traffic, err := t.inboundService.SearchClientTraffic(query)
if err != nil {
logger.Warning(err)
msg := "❌ Something went wrong!"
t.SendMsgToTgbot(chatId, msg)
return
}
if traffic == nil {
msg := "No result!"
t.SendMsgToTgbot(chatId, msg)
return
}
expiryTime := ""
if traffic.ExpiryTime == 0 {
expiryTime = "♾Unlimited"
} else if traffic.ExpiryTime < 0 {
expiryTime = fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000)
} else {
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
}
total := ""
if traffic.Total == 0 {
total = "♾Unlimited"
} else {
total = common.FormatTraffic((traffic.Total))
}
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
total, expiryTime)
t.SendMsgToTgbot(chatId, output)
}
func (t *Tgbot) getExhausted() string {
trDiff := int64(0)
exDiff := int64(0)
now := time.Now().Unix() * 1000
var exhaustedInbounds []model.Inbound
var exhaustedClients []xray.ClientTraffic
var disabledInbounds []model.Inbound
var disabledClients []xray.ClientTraffic
output := ""
TrafficThreshold, err := t.settingService.GetTrafficDiff()
if err == nil && TrafficThreshold > 0 {
trDiff = int64(TrafficThreshold) * 1073741824
}
ExpireThreshold, err := t.settingService.GetExpireDiff()
if err == nil && ExpireThreshold > 0 {
exDiff = int64(ExpireThreshold) * 86400000
}
inbounds, err := t.inboundService.GetAllInbounds()
if err != nil {
logger.Warning("Unable to load Inbounds", err)
}
for _, inbound := range inbounds {
if inbound.Enable {
if (inbound.ExpiryTime > 0 && (inbound.ExpiryTime-now < exDiff)) ||
(inbound.Total > 0 && (inbound.Total-(inbound.Up+inbound.Down) < trDiff)) {
exhaustedInbounds = append(exhaustedInbounds, *inbound)
}
if len(inbound.ClientStats) > 0 {
for _, client := range inbound.ClientStats {
if client.Enable {
if (client.ExpiryTime > 0 && (client.ExpiryTime-now < exDiff)) ||
(client.Total > 0 && (client.Total-(client.Up+client.Down) < trDiff)) {
exhaustedClients = append(exhaustedClients, client)
}
} else {
disabledClients = append(disabledClients, client)
}
}
}
} else {
disabledInbounds = append(disabledInbounds, *inbound)
}
}
output += fmt.Sprintf("Exhausted Inbounds count:\r\n🛑 Disabled: %d\r\n🔜 Deplete soon: %d\r\n \r\n", len(disabledInbounds), len(exhaustedInbounds))
if len(exhaustedInbounds) > 0 {
output += "Exhausted Inbounds:\r\n"
for _, inbound := range exhaustedInbounds {
output += fmt.Sprintf("📍Inbound:%s\r\nPort:%d\r\nTraffic: %s (↑%s,↓%s)\r\n", inbound.Remark, inbound.Port, common.FormatTraffic((inbound.Up + inbound.Down)), common.FormatTraffic(inbound.Up), common.FormatTraffic(inbound.Down))
if inbound.ExpiryTime == 0 {
output += "Expire date: ♾Unlimited\r\n \r\n"
} else {
output += fmt.Sprintf("Expire date:%s\r\n \r\n", time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
}
}
}
output += fmt.Sprintf("Exhausted Clients count:\r\n🛑 Exhausted: %d\r\n🔜 Deplete soon: %d\r\n \r\n", len(disabledClients), len(exhaustedClients))
if len(exhaustedClients) > 0 {
output += "Exhausted Clients:\r\n"
for _, traffic := range exhaustedClients {
expiryTime := ""
if traffic.ExpiryTime == 0 {
expiryTime = "♾Unlimited"
} else if traffic.ExpiryTime < 0 {
expiryTime += fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000)
} else {
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
}
total := ""
if traffic.Total == 0 {
total = "♾Unlimited"
} else {
total = common.FormatTraffic((traffic.Total))
}
output += fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire date: %s\r\n \r\n",
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
total, expiryTime)
}
}
return output
}
func (t *Tgbot) sendBackup(chatId int64) {
sendingTime := time.Now().Format("2006-01-02 15:04:05")
t.SendMsgToTgbot(chatId, "Backup time: "+sendingTime)
file := tgbotapi.FilePath(config.GetDBPath())
msg := tgbotapi.NewDocument(chatId, file)
_, err := bot.Send(msg)
if err != nil {
logger.Warning("Error in uploading backup: ", err)
}
file = tgbotapi.FilePath(xray.GetConfigPath())
msg = tgbotapi.NewDocument(chatId, file)
_, err = bot.Send(msg)
if err != nil {
logger.Warning("Error in uploading config.json: ", err)
}
}

View File

@@ -46,8 +46,7 @@ func (s *UserService) UpdateUser(id int, username string, password string) error
db := database.GetDB()
return db.Model(model.User{}).
Where("id = ?", id).
Update("username", username).
Update("password", password).
Updates(map[string]interface{}{"username": username, "password": password}).
Error
}

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,8 +102,31 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
}
}
settings["clients"] = clients
modifiedSettings, err := json.Marshal(settings)
// 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)
}
if c["flow"] == "xtls-rprx-vision-udp443" {
c["flow"] = "xtls-rprx-vision"
}
}
final_clients = append(final_clients, interface{}(c))
}
settings["clients"] = final_clients
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return nil, err
}
@@ -160,5 +184,5 @@ func (s *XrayService) SetToNeedRestart() {
}
func (s *XrayService) IsNeedRestartAndSetFalse() bool {
return isNeedXrayRestart.CAS(true, false)
return isNeedXrayRestart.CompareAndSwap(true, false)
}

View File

@@ -2,9 +2,10 @@ package session
import (
"encoding/gob"
"x-ui/database/model"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"x-ui/database/model"
)
const (
@@ -21,6 +22,15 @@ func SetLoginUser(c *gin.Context, user *model.User) error {
return s.Save()
}
func SetMaxAge(c *gin.Context, maxAge int) error {
s := sessions.Default(c)
s.Options(sessions.Options{
Path: "/",
MaxAge: maxAge,
})
return s.Save()
}
func GetLoginUser(c *gin.Context) *model.User {
s := sessions.Default(c)
obj := s.Get(loginUser)

View File

@@ -1,48 +1,51 @@
"username" = "username"
"password" = "password"
"login" = "login"
"confirm" = "confirm"
"cancel" = "cancel"
"close" = "close"
"copy" = "copy"
"copied" = "copied"
"download" = "download"
"remark" = "remark"
"enable" = "enable"
"protocol" = "protocol"
"search" = "search"
"username" = "Username"
"password" = "Password"
"login" = "Login"
"confirm" = "Confirm"
"cancel" = "Cancel"
"close" = "Close"
"copy" = "Copy"
"copied" = "Copied"
"download" = "Download"
"remark" = "Remark"
"enable" = "Enable"
"protocol" = "Protocol"
"search" = "Search"
"loading" = "Loading"
"second" = "second"
"minute" = "minute"
"hour" = "hour"
"day" = "day"
"check" = "check"
"indefinite" = "indefinite"
"unlimited" = "unlimited"
"none" = "none"
"second" = "Second"
"minute" = "Minute"
"hour" = "Hour"
"day" = "Day"
"check" = "Check"
"indefinite" = "Indefinite"
"unlimited" = "Unlimited"
"none" = "None"
"qrCode" = "QR Code"
"info" = "More information"
"edit" = "edit"
"delete" = "delete"
"reset" = "reset"
"edit" = "Edit"
"delete" = "Delete"
"reset" = "Reset"
"copySuccess" = "Copy successfully"
"sure" = "Sure"
"encryption" = "encryption"
"transmission" = "transmission"
"host" = "host"
"path" = "path"
"camouflage" = "camouflage"
"enabled" = "enabled"
"disabled" = "disabled"
"domainName" = "domain name"
"additional" = "alter"
"encryption" = "Encryption"
"transmission" = "Transmission"
"host" = "Host"
"path" = "Path"
"camouflage" = "Camouflage"
"status" = "Status"
"enabled" = "Enabled"
"disabled" = "Disabled"
"depleted" = "Depleted"
"depletingSoon" = "Depleting soon"
"domainName" = "Domain name"
"additional" = "Alter ID"
"monitor" = "Listen IP"
"certificate" = "certificat"
"fail" = "fail"
"success" = " success"
"getVersion" = "get version"
"install" = "install"
"certificate" = "Certificate"
"fail" = "Fail"
"success" = " Success"
"getVersion" = "Get version"
"install" = "Install"
"clients" = "Clients"
"usage" = "Usage"
@@ -64,12 +67,13 @@
"wrongUsernameOrPassword" = "Invalid username or password"
"successLogin" = "Login"
[pages.index]
"title" = "system status"
"memory" = "memory"
"hard" = "hard disk"
"title" = "System status"
"memory" = "Memory"
"hard" = "Hard disk"
"xrayStatus" = "xray Status"
"stopXray" = "Stop"
"restartXray" = "Restart"
"xraySwitch" = "Switch Version"
"xraySwitchClick" = "Click on the version you want to switch"
"xraySwitchClickDesk" = "Please choose carefully, older versions may have incompatible configurations"
@@ -82,73 +86,107 @@
"downSpeed" = "Total download speed for all network cards"
"totalSent" = "Total upload traffic of all network cards since system startup"
"totalReceive" = "Total download traffic of all network cards since system startup"
"xraySwitchVersionDialog" = "switch xray version"
"xraySwitchVersionDialogDesc" = "whether to switch the xray version to"
"xraySwitchVersionDialog" = "Switch xray version"
"xraySwitchVersionDialogDesc" = "Whether to switch the xray version to"
"dontRefreshh" = "Installation is in progress, please do not refresh this page"
[pages.inbounds]
"title" = "Inbounds"
"totalDownUp" = "Total uploads/downloads"
"totalUsage" = "Total usage"
"inboundCount" = "Number of inbound"
"operate" = "operate"
"enable" = "enable"
"remark" = "remark"
"protocol" = "protocol"
"port" = "port"
"traffic" = "traffic"
"details" = "details"
"transportConfig" = "transport config"
"expireDate" = "expire date"
"resetTraffic" = "reset traffic"
"addInbound" = "addInbound"
"addTo" = "Add To"
"revise" = "Revise"
"operate" = "Operate"
"enable" = "Enable"
"remark" = "Remark"
"protocol" = "Protocol"
"port" = "Port"
"traffic" = "Traffic"
"details" = "Details"
"transportConfig" = "Transport config"
"expireDate" = "Expire date"
"resetTraffic" = "Reset traffic"
"addInbound" = "Add Inbound"
"generalActions" = "General Actions"
"addTo" = "Create"
"revise" = "Update"
"modifyInbound" = "Modify InBound"
"deleteInbound" = "Delete Inbound"
"deleteInboundContent" = "Are you sure you want to delete inbound?"
"resetTrafficContent" = "Are you sure you want to reset traffic?"
"copyLink" = "Copy Link"
"address" = "address"
"network" = "network"
"destinationPort" = "destination port"
"targetAddress" = "target address"
"address" = "Address"
"network" = "Network"
"destinationPort" = "Destination port"
"targetAddress" = "Target address"
"disableInsecureEncryption" = "Disable insecure encryption"
"monitorDesc" = "Leave blank by default"
"meansNoLimit" = "means no limit"
"totalFlow" = "total flow"
"meansNoLimit" = "Means no limit"
"totalFlow" = "Total flow"
"leaveBlankToNeverExpire" = "Leave blank to never expire"
"noRecommendKeepDefault" = "There are no special requirements to keep the default"
"certificatePath" = "certificate file path"
"certificateContent" = "certificate file content"
"publicKeyPath" = "public key file path"
"publicKeyContent" = "public key content"
"keyPath" = "key file path"
"keyContent" = "key content"
"clickOnQRcode" = "click on QR Code to Copy"
"certificatePath" = "Certificate file path"
"certificateContent" = "Certificate file content"
"publicKeyPath" = "Public key file path"
"publicKeyContent" = "Public key content"
"keyPath" = "Key file path"
"keyContent" = "Key content"
"clickOnQRcode" = "Click on QR Code to Copy"
"client" = "Client"
"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 ?"
"resetInboundClientTraffics" = "Reset Clients Traffic"
"resetInboundClientTrafficTitle" = "Reset all clients traffic"
"resetInboundClientTrafficContent" = "Are you sure to reset all traffics of this inbound's clients ?"
"resetAllClientTraffics" = "Reset All Clients Traffic"
"resetAllClientTrafficTitle" = "Reset all clients traffic"
"resetAllClientTrafficContent" = "Are you sure to reset all traffics of all clients ?"
"delDepletedClients" = "Delete depleted clients"
"delDepletedClientsTitle" = "Delete depleted clients"
"delDepletedClientsContent" = "Are you sure to delete all depleted clients ?"
"Email" = "Email"
"EmailDesc" = "The Email Must Be Completely Unique"
"setDefaultCert" = "Set cert from panel"
[pages.client]
"add" = "Add client"
"edit" = "Edit client"
"submitAdd" = "Add client"
"submitEdit" = "Save changes"
"clientCount" = "Number of clients"
"bulk" = "Add bulk"
"method" = "Method"
"first" = "First"
"last" = "Last"
"prefix" = "Prefix"
"postfix" = "Postfix"
"delayedStart" = "Start after first use"
"expireDays" = "Expire days"
"days" = "day(s)"
[pages.inbounds.toasts]
"obtain" = "Obtain"
[pages.inbounds.stream.general]
"requestHeader" = "request header"
"name" = "name"
"value" = "value"
"requestHeader" = "Request header"
"name" = "Name"
"value" = "Value"
[pages.inbounds.stream.tcp]
"requestVersion" = "request version"
"requestMethod" = "request method"
"requestPath" = "request path"
"responseVersion" = "response version"
"responseStatus" = "response status"
"responseStatusDescription" = "response status description"
"responseHeader" = "response header"
"requestVersion" = "Request version"
"requestMethod" = "Request method"
"requestPath" = "Request path"
"responseVersion" = "Response version"
"responseStatus" = "Response status"
"responseStatusDescription" = "Response status description"
"responseHeader" = "Response header"
[pages.inbounds.stream.quic]
"encryption" = "encryption"
"encryption" = "Encryption"
[pages.setting]
"title" = "Setting"
@@ -166,30 +204,52 @@
"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"
"panelUrlPath" = "Panel url root path"
"panelUrlPathDesc" = "Must start with '/' and end with '/', restart the panel to take effect"
"oldUsername" = "Current Username"
"currentPassword" = "Current Password"
"newUsername" = "New Username"
"newPassword" = "New Password"
"xrayConfigTemplate" = "xray Configuration Template"
"xrayConfigTemplateDesc" = "Generate the final xray configuration file based on this template, restart the panel to take effect"
"advancedTemplate" = "Advanced template parts"
"completeTemplate" = "Complete template of Xray configuration"
"xrayConfigTemplate" = "Xray Configuration Template"
"xrayConfigTemplateDesc" = "Generate the final xray configuration file based on this template, restart the panel to take effect."
"xrayConfigTorrent" = "Ban bittorrent usage"
"xrayConfigTorrentDesc" = "Change the configuration template to avoid using bittorrent by users, restart the panel to take effect"
"xrayConfigPrivateIp" = "Ban private ip range to connect"
"xrayConfigPrivateIpDesc" = "Change the configuration template to avoid connecting with private IP ranges, restart the panel to take effect"
"xrayConfigInbounds" = "Configuration of Inbounds"
"xrayConfigInboundsDesc" = "Change the configuration template to accept special clients, restart the panel to take effect"
"xrayConfigOutbounds" = "Configuration of Outbounds"
"xrayConfigOutboundsDesc" = "Change the configuration template to define outgoing ways for this server, restart the panel to take effect"
"xrayConfigRoutings" = "Configuration of Routing rules"
"xrayConfigRoutingsDesc" = "Change the configuration template to define Routing rules for this server, restart the panel to take effect"
"telegramBotEnable" = "Enable telegram bot"
"telegramBotEnableDesc" = "Restart the panel to take effect"
"telegramToken" = "Telegram Token"
"telegramTokenDesc" = "Restart the panel to take effect"
"telegramChatId" = "Telegram ChatId"
"telegramChatIdDesc" = "Restart the panel to take effect"
"telegramChatId" = "Telegram Admin ChatIds"
"telegramChatIdDesc" = "Multi chatIDs separated by comma. Restart the panel to take effect"
"telegramNotifyTime" = "Telegram bot notification time"
"telegramNotifyTimeDesc" = "Using Crontab timing format, restart the panel to take effect"
"telegramNotifyTimeDesc" = "Using Crontab timing format. Restart the panel to take effect"
"tgNotifyBackup" = "Database backup"
"tgNotifyBackupDesc" = "Sending database backup file with report notification. Restart the panel to take effect"
"sessionMaxAge" = "Session maximum age"
"sessionMaxAgeDesc" = "The time that you can stay login (unit: minute)"
"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"
"timeZoneDesc" = "The scheduled task runs according to the time in the time zone, and restarts the panel to take effect"
[pages.setting.toasts]
"modifySetting" = "modify setting"
"getSetting" = "get setting"
"modifyUser" = "modify user"
"modifySetting" = "Modify setting"
"getSetting" = "Get setting"
"modifyUser" = "Modify user"
"originalUserPassIncorrect" = "The original user name or original password is incorrect"
"userPassMustBeNotEmpty" = "New username and new password cannot be empty"

View File

@@ -33,12 +33,15 @@
"host" = "آدرس"
"path" = "مسیر"
"camouflage" = "استتار"
"status" = "وضعیت"
"enabled" = "فعال"
"disabled" = "disabled"
"disabled" = "غیرفعال"
"depleted" = "منقضی"
"depletingSoon" = "در حال انقضا"
"domainName" = "آدرس دامنه"
"additional" = "آی دی جایگزین"
"monitor" = "آی پی اتصال"
"certificate" = "سرتیفیکیت"
"certificate" = "گواهی دیجیتال"
"fail" = "خطا"
"success" = " موفق"
"getVersion" = "دریافت ورژن"
@@ -48,7 +51,7 @@
[menu]
"dashboard" = "وضعیت سیستم"
"inbounds" = "سروریس ها"
"inbounds" = "سرویس ها"
"setting" = "تنظیمات پنل"
"logout" = "خروج"
"link" = "دیگر"
@@ -64,18 +67,19 @@
"wrongUsernameOrPassword" = "نام کاربری و رمز عبور اشتباه میباشد"
"successLogin" = "خوش آمدید"
[pages.index]
"title" = "وضعیت سیستم"
"memory" = "حافظه رم"
"hard" = "حافظه دیسک"
"xrayStatus" = "وضعیت Xray"
"stopXray" = "توقف"
"restartXray" = "شروع مجدد"
"xraySwitch" = "تغییر ورژن"
"xraySwitchClick" = "ورژن مورد نظر را انتخاب کنید"
"xraySwitchClickDesk" = "لطفا با دقت انتخاب کنید ، در صورت انتخاب اشتباه امکان قطعی سیستم وجود دارد ."
"operationHours" = "ساعت فعال"
"operationHoursDesc" = "ساعت فعال بعد از شروع سیستم"
"systemLoad" = "سرعت لود سیستم"
"operationHours" = "مدت فعالیت"
"operationHoursDesc" = "مدت فعالیت سیستم بعد از روشن شدن"
"systemLoad" = "بار روی سیستم"
"connectionCount" = "تعداد کانکشن ها"
"connectionCountDesc" = "تعداد کانکشن ها برای کل شبکه"
"upSpeed" = "سرعت آپلود در حال حاضر سیستم"
@@ -86,7 +90,6 @@
"xraySwitchVersionDialogDesc" = "آیا از تغییر ورژن مطمئن هستین"
"dontRefreshh" = "در حال نصب ، لطفا رفرش نکنید "
[pages.inbounds]
"title" = "کاربران"
"totalDownUp" = "جمع آپلود/دانلود"
@@ -103,6 +106,7 @@
"expireDate" = "تاریخ انقضا"
"resetTraffic" = "ریست ترافیک"
"addInbound" = "اضافه کردن سرویس"
"generalActions" = "عملیات کلی"
"addTo" = "اضافه کردن"
"revise" = "ویرایش"
"modifyInbound" = "ویرایش سرویس"
@@ -114,7 +118,7 @@
"network" = "شبکه"
"destinationPort" = "پورت مقصد"
"targetAddress" = "آدرس مقصد"
"disableInsecureEncryption" = "رمزگذاری ناامن را غیرفعال کنید"
"disableInsecureEncryption" = "غیرفعال سازی رمزگذاری ناامن"
"monitorDesc" = "به طور پیش فرض خالی بگذارید"
"meansNoLimit" = "یعنی بدون محدودیت"
"totalFlow" = "کل ترافیک"
@@ -122,12 +126,47 @@
"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" = "آیا مطمئن هستید که میخواهید تمام ترافیک سرویس ها را ریست کنید؟"
"resetInboundClientTraffics" = "ریست ترافیک کاربران"
"resetInboundClientTrafficTitle" = "ریست ترافیک کل کاربران"
"resetInboundClientTrafficContent" = "آیا مطمئن هستید که میخواهید تمام ترافیک کاربران این سرویس را ریست کنید؟"
"resetAllClientTraffics" = "ریست ترافیک کاربران"
"resetAllClientTrafficTitle" = "ریست ترافیک کل کاربران"
"resetAllClientTrafficContent" = "آیا مطمئن هستید که میخواهید تمام ترافیک کاربران را ریست کنید؟"
"delDepletedClients" = "حذف کاربران منقضی"
"delDepletedClientsTitle" = "حذف کاربران منقضی"
"delDepletedClientsContent" = "آیا مطمئن هستید مه میخواهید تمامی کاربران منقضی شده را حذف کنید؟"
"Email" = "ایمیل"
"EmailDesc" = "ایمیل باید کاملا منحصر به فرد باشد"
"setDefaultCert" = "استفاده از گواهی پنل"
[pages.client]
"add" = "کاربر جدید"
"edit" = "ویرایش کاربر"
"submitAdd" = "اضافه کردن"
"submitEdit" = "ذخیره تغییرات"
"clientCount" = "تعداد کاربران"
"bulk" = "انبوه سازی"
"method" = "روش"
"first" = "از"
"last" = "تا"
"prefix" = "پیشوند"
"postfix" = "پسوند"
"delayedStart" = "شروع بعد از اولین استفاده"
"expireDays" = "روزهای اعتبار"
"days" = "(روز)"
[pages.inbounds.toasts]
"obtain" = "Obtain"
@@ -149,7 +188,6 @@
[pages.inbounds.stream.quic]
"encryption" = "رمزنگاری"
[pages.setting]
"title" = "تنظیمات"
"save" = "ذخیره"
@@ -164,9 +202,9 @@
"panelListeningIPDesc" = "برای استفاده از تمام IP ها به طور پیش فرض خالی بگذارید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"panelPort" = "پورت پنل"
"panelPortDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
"publicKeyPath" = "مسیر فایل پنل Certificate.crt"
"publicKeyPath" = "مسیر فایل گواهی کلید عمومی پنل"
"publicKeyPathDesc" = "باید یک مسیر مطلق باشد که با / شروع می شود . پنل را مجدداً راه اندازی کنید تا اعمال شود"
"privateKeyPath" = "مسیر فایل پنل private.key"
"privateKeyPath" = "مسیر فایل گواهی کلید خصوصی پنل"
"privateKeyPathDesc" = "باید یک مسیر مطلق باشد که با / شروع می شود . پنل را مجدداً راه اندازی کنید تا اعمال شود"
"panelUrlPath" = "آدرس روت پنل"
"panelUrlPathDesc" = "باید با '/' شروع شود و با '/' تمام شود. پنل را مجدداً راه اندازی کنید تا اعمال شود"
@@ -174,16 +212,38 @@
"currentPassword" = "رمز عبور فعلی"
"newUsername" = "نام کاربری جدید"
"newPassword" = "رمز عبور جدید"
"xrayConfigTemplate" = "تنظیمات قالب Xray"
"xrayConfigTemplateDesc" = "فایل پیکربندی xray نهایی را بر اساس این الگو ایجاد کنید. لطفاً این را تغییر ندهید مگر اینکه دقیقاً بدانید که چه کاری انجام می دهید! پنل را مجدداً راه اندازی کنید تا اعمال شود"
"advancedTemplate" = "بخش های پیشرفته الگو"
"completeTemplate" = "الگوی کامل تنظیمات ایکس ری"
"xrayConfigTemplate" = "تنظیمات الگو ایکس ری"
"xrayConfigTemplateDesc" = "فایل پیکربندی ایکس ری نهایی بر اساس این الگو ایجاد میشود. لطفاً این را تغییر ندهید مگر اینکه دقیقاً بدانید که چه کاری انجام می دهید! پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigTorrent" = "فیلتر کردن بیت تورنت"
"xrayConfigTorrentDesc" = "الگوی تنظیمات را برای فیلتر کردن پروتکل بیت تورنت برای کاربران تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigPrivateIp" = "جلوگیری از اتصال آی پی های نامعتبر"
"xrayConfigPrivateIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آی پی های نامعتبر و بسته های سرگردان تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigInbounds" = "تنظیمات ورودی"
"xrayConfigInboundsDesc" = "میتوانید الگوی تنظیمات را برای ورودی های خاص تنظیم نمایید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigOutbounds" = "تنظیمات خروجی"
"xrayConfigOutboundsDesc" = "میتوانید الگوی تنظیمات را برای خروجی اینترنت تنظیم نمایید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigRoutings" = "تنظیمات قوانین مسیریابی"
"xrayConfigRoutingsDesc" = "میتوانید الگوی تنظیمات را برای مسیریابی تنظیم نمایید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"telegramBotEnable" = "فعالسازی ربات تلگرام"
"telegramBotEnableDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
"telegramToken" = "توکن تلگرام"
"telegramTokenDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
"telegramChatId" = "آی دی تلگرام مدیریت . از ربات @getidsbot آی دی خود را دریافت کنید"
"telegramChatIdDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
"telegramChatId" = "آی دی تلگرام مدیریت"
"telegramChatIdDesc" = "با استفاده از کاما میتونید چند آی دی را از هم جدا کنید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"telegramNotifyTime" = "مدت زمان نوتیفیکیشن ربات تلگرام"
"telegramNotifyTimeDesc" = "از فرمت زمان بندی Crontab استفاده کنید . پنل را مجدداً راه اندازی کنید تا اعمال شود"
"telegramNotifyTimeDesc" = "از فرمت زمان بندی لینوکس استفاده کنید . پنل را مجدداً راه اندازی کنید تا اعمال شود"
"tgNotifyBackup" = "پشتیبان گیری از پایگاه داده"
"tgNotifyBackupDesc" = "ارسال کپی فایل پایگاه داده به همراه گزارش دوره ای"
"sessionMaxAge" = "بیشینه زمان جلسه وب"
"sessionMaxAgeDesc" = "بیشینه زمانی که میتوانید لاگین بمانید (واحد: دقیقه)"
"expireTimeDiff" = "آستانه زمان باقی مانده"
"expireTimeDiffDesc" = "فاصله زمانی هشدار تا رسیدن به زمان انقضا (واحد: روز)"
"trafficDiff" = "آستانه ترافیک باقی مانده"
"trafficDiffDesc" = "فاصله زمانی هشدار تا رسیدن به اتمام ترافیک (واحد: گیگابایت)"
"tgNotifyCpu" = "آستانه هشدار درصد پردازنده"
"tgNotifyCpuDesc" = "این ربات تلگرام در صورت استفاده پردازنده بیشتر از این درصد برای شما پیام ارسال می کند.(واحد: درصد)"
"timeZonee" = "منظقه زمانی"
"timeZoneDesc" = "وظایف برنامه ریزی شده بر اساس این منطقه زمانی اجرا می شوند. پنل را مجدداً راه اندازی می کند تا اعمال شود"

View File

@@ -33,10 +33,13 @@
"host" = "主持人"
"path" = "小路"
"camouflage" = "伪装"
"status" = "状态"
"enabled" = "开启"
"disabled" = "关闭"
"depleted" = "耗尽"
"depletingSoon" = "即将耗尽"
"domainName" = "域名"
"additional" = "额外"
"additional" = "额外 ID"
"monitor" = "监听"
"certificate" = "证书"
"fail" = "失败"
@@ -44,6 +47,7 @@
"getVersion" = "获取版本"
"install" = "安装"
"clients" = "客户端"
"usage" = "用法"
[menu]
"dashboard" = "系统状态"
@@ -68,6 +72,8 @@
"memory" = "内存"
"hard" = "硬盘"
"xrayStatus" = "xray 状态"
"stopXray" = "停止"
"restartXray" = "重启"
"xraySwitch" = "切换版本"
"xraySwitchClick" = "点击你想切换的版本"
"xraySwitchClickDesk" = "请谨慎选择,旧版本可能配置不兼容"
@@ -84,7 +90,6 @@
"xraySwitchVersionDialogDesc" = "是否切换 xray 版本至"
"dontRefreshh" = "安装中,请不要刷新此页面"
[pages.inbounds]
"title" = "入站列表"
"totalDownUp" = "总上传 / 下载"
@@ -101,6 +106,7 @@
"expireDate" = "到期时间"
"resetTraffic" = "重置流量"
"addInbound" = "添加入"
"generalActions" = "通用操作"
"addTo" = "添加"
"revise" = "修改"
"modifyInbound" = "修改入站"
@@ -124,7 +130,43 @@
"publicKeyContent" = "公钥内容"
"keyPath" = "密钥文件路径"
"keyContent" = "密钥内容"
"clickOnQRcode" = "click on QR Code to Copy"
"clickOnQRcode" = "点击二维码复制"
"client" = "客户"
"export" = "导出链接"
"Clone" = "克隆"
"cloneInbound" = "创造"
"cloneInboundContent" = "此入站的所有项目除 Port、Listening IP、Clients 将应用于克隆"
"resetAllTraffic" = "重置所有入站流量"
"resetAllTrafficTitle" = "重置所有入站流量"
"resetAllTrafficContent" = "您确定要重置所有入站流量吗?"
"resetInboundClientTraffics" = "重置客户端流量"
"resetInboundClientTrafficTitle" = "重置所有客户端流量"
"resetInboundClientTrafficContent" = "您确定要重置此入站客户端的所有流量吗?"
"resetAllClientTraffics" = "重置所有客户端流量"
"resetAllClientTrafficTitle" = "重置所有客户端流量"
"resetAllClientTrafficContent" = "你确定要重置所有客户端的所有流量吗?"
"delDepletedClients" = "删除耗尽的客户端"
"delDepletedClientsTitle" = "删除耗尽的客户"
"delDepletedClientsContent" = "你确定要删除所有耗尽的客户端吗?"
"Email" = "电子邮件"
"EmailDesc" = "电子邮件必须完全唯"
"setDefaultCert" = "从面板设置证书"
[pages.client]
"add" = "添加客户端"
"edit" = "编辑客户"
"submitAdd" = "添加客户端"
"submitEdit" = "保存修改"
"clientCount" = "客户数量"
"bulk" = "批量创建"
"method" = "方法"
"first" = "第一"
"last" = "最后"
"prefix" = "前缀"
"postfix" = "后缀"
"delayedStart" = "首次使用后开始"
"expireDays" = "过期天数"
"days" = "天"
[pages.inbounds.toasts]
"obtain" = "获取"
@@ -146,7 +188,6 @@
[pages.inbounds.stream.quic]
"encryption" = "加密"
[pages.setting]
"title" = "设置"
"save" = "保存配置"
@@ -171,16 +212,38 @@
"currentPassword" = "原密码"
"newUsername" = "新用户名"
"newPassword" = "新密码"
"xrayConfigTemplate" = "xray 配置模版"
"xrayConfigTemplateDesc" = "以该模版为基础生成最终的 xray 配置文件,重启面板生效"
"advancedTemplate" = "高级模板部件"
"completeTemplate" = "Xray 配置的完整模板"
"xrayConfigTemplate" = "xray 配置模板"
"xrayConfigTemplateDesc" = "以该模型为基础生成最终的xray配置文件重新启动面板生成效率"
"xrayConfigTorrent" = "禁止使用 bittorrent"
"xrayConfigTorrentDesc" = "更改配置模板避免用户使用bittorrent重启面板生效"
"xrayConfigPrivateIp" = "禁止私人 ip 范围连接"
"xrayConfigPrivateIpDesc" = "更改配置模板以避免连接私有 IP 范围,重启面板生效"
"xrayConfigInbounds" = "入站配置"
"xrayConfigInboundsDesc" = "更改配置模板接受特殊客户端,重启面板生效"
"xrayConfigOutbounds" = "出站配置"
"xrayConfigOutboundsDesc" = "更改配置模板定义此服务器的传出方式,重启面板生效"
"xrayConfigRoutings" = "路由规则配置"
"xrayConfigRoutingsDesc" = "更改配置模板为该服务器定义路由规则,重启面板生效"
"telegramBotEnable" = "启用电报机器人"
"telegramBotEnableDesc" = "重启面板生效"
"telegramToken" = "电报机器人TOKEN"
"telegramTokenDesc" = "重启面板生效"
"telegramChatId" = "电报机器人ChatId"
"telegramChatId" = "以逗号分隔的多个 chatID 重启面板生效"
"telegramChatIdDesc" = "重启面板生效"
"telegramNotifyTime" = "电报机器人通知时间"
"telegramNotifyTimeDesc" = "采用Crontab定时格式,重启面板生效"
"tgNotifyBackup" = "数据库备份"
"tgNotifyBackupDesc" = "正在发送数据库备份文件和报告通知。重启面板生效"
"sessionMaxAge" = "会话最大年龄"
"sessionMaxAgeDesc" = "您可以保持登录状态的时间(单位:分钟)"
"expireTimeDiff" = "耗尽时间阈值"
"expireTimeDiffDesc" = "到期前检测耗尽(单位:天)"
"trafficDiff" = "耗尽流量阈值"
"trafficDiffDesc" = "完成流量前检测耗尽单位GB"
"tgNotifyCpu" = "CPU 百分比警报阈值"
"tgNotifyCpuDesc" = "如果 CPU 使用率超过此百分比(单位:%),此 talegram bot 将向您发送通知"
"timeZonee" = "时区"
"timeZoneDesc" = "定时任务按照该时区的时间运行,重启面板生效"
@@ -189,5 +252,4 @@
"getSetting" = "获取设置"
"modifyUser" = "修改用户"
"originalUserPassIncorrect" = "原用户名或原密码错误"
"userPassMustBeNotEmpty" = "新用户名和新密码不能为空"
"userPassMustBeNotEmpty" = "新用户名和新密码不能为空"

View File

@@ -21,11 +21,11 @@ import (
"x-ui/web/network"
"x-ui/web/service"
"github.com/BurntSushi/toml"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
"github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/pelletier/go-toml/v2"
"github.com/robfig/cron/v3"
"golang.org/x/text/language"
)
@@ -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,10 +88,11 @@ type Server struct {
server *controller.ServerController
xui *controller.XUIController
api *controller.APIController
sub *controller.SUBController
xrayService service.XrayService
settingService service.SettingService
inboundService service.InboundService
tgbotService service.Tgbot
cron *cron.Cron
@@ -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
}
@@ -298,19 +308,19 @@ func (s *Server) startTask() {
if err != nil {
logger.Warning("start xray failed:", err)
}
// 每 30 秒检查一次 xray 是否在运行
// Check whether xray is running every 30 seconds
s.cron.AddJob("@every 30s", job.NewCheckXrayRunningJob())
go func() {
time.Sleep(time.Second * 5)
// 每 10 秒统计一次流量,首次启动延迟 5 秒,与重启 xray 的时间错开
// Statistics every 10 seconds, start the delay for 5 seconds for the first time, and staggered with the time to restart xray
s.cron.AddJob("@every 10s", job.NewXrayTrafficJob())
}()
// 每 30 秒检查一次 inbound 流量超出和到期的情况
// Check the inbound traffic every 30 seconds that the traffic exceeds and expires
s.cron.AddJob("@every 30s", job.NewCheckInboundJob())
// 每一天提示一次流量情况,上海时间8点30
// Make a traffic condition every day, 8:30
var entry cron.EntryID
isTgbotenabled, err := s.settingService.GetTgbotenabled()
if (err == nil) && (isTgbotenabled) {
@@ -320,20 +330,25 @@ func (s *Server) startTask() {
runtime = "@daily"
}
logger.Infof("Tg notify enabled,run at %s", runtime)
entry, err = s.cron.AddJob(runtime, job.NewStatsNotifyJob())
_, err = s.cron.AddJob(runtime, job.NewStatsNotifyJob())
if err != nil {
logger.Warning("Add NewStatsNotifyJob error", err)
return
}
// listen for TG bot income messages
go job.NewStatsNotifyJob().OnReceive()
// Check CPU load and alarm to TgBot if threshold passes
cpuThreshold, err := s.settingService.GetTgCpu()
if (err == nil) && (cpuThreshold > 0) {
s.cron.AddJob("@every 10s", job.NewCheckCpuJob())
}
} else {
s.cron.Remove(entry)
}
}
func (s *Server) Start() (err error) {
//这是一个匿名函数,没没有函数名
//This is an anonymous function, no function name
defer func() {
if err != nil {
s.Stop()
@@ -403,6 +418,12 @@ func (s *Server) Start() (err error) {
s.httpServer.Serve(listener)
}()
isTgbotenabled, err := s.settingService.GetTgbotenabled()
if (err == nil) && (isTgbotenabled) {
tgBot := s.tgbotService.NewTgbot()
tgBot.Start()
}
return nil
}
@@ -412,6 +433,9 @@ func (s *Server) Stop() error {
if s.cron != nil {
s.cron.Stop()
}
if s.tgbotService.IsRunnging() {
s.tgbotService.Stop()
}
var err1 error
var err2 error
if s.httpServer != nil {

63
x-ui.sh
View File

@@ -20,46 +20,41 @@ function LOGI() {
# check root
[[ $EUID -ne 0 ]] && LOGE "ERROR: You must be root to run this script! \n" && exit 1
# check os
if [[ -f /etc/redhat-release ]]; then
release="centos"
elif cat /etc/issue | grep -Eqi "debian"; then
release="debian"
elif cat /etc/issue | grep -Eqi "ubuntu"; then
release="ubuntu"
elif cat /etc/issue | grep -Eqi "centos|red hat|redhat"; then
release="centos"
elif cat /proc/version | grep -Eqi "debian"; then
release="debian"
elif cat /proc/version | grep -Eqi "ubuntu"; then
release="ubuntu"
elif cat /proc/version | grep -Eqi "centos|red hat|redhat"; then
release="centos"
# Check OS and set release variable
if [[ -f /etc/os-release ]]; then
source /etc/os-release
release=$ID
elif [[ -f /usr/lib/os-release ]]; then
source /usr/lib/os-release
release=$ID
else
LOGE "check system OS failed,please contact with author! \n" && exit 1
echo "Failed to check the system OS, please contact the author!" >&2
exit 1
fi
echo "The OS release is: $release"
os_version=""
os_version=$(grep -i version_id /etc/os-release | cut -d \" -f2 | cut -d . -f1)
# os version
if [[ -f /etc/os-release ]]; then
os_version=$(awk -F'[= ."]' '/VERSION_ID/{print $3}' /etc/os-release)
fi
if [[ -z "$os_version" && -f /etc/lsb-release ]]; then
os_version=$(awk -F'[= ."]+' '/DISTRIB_RELEASE/{print $2}' /etc/lsb-release)
fi
if [[ x"${release}" == x"centos" ]]; then
if [[ ${os_version} -le 6 ]]; then
LOGE "please use CentOS 7 or higher version! \n" && exit 1
fi
elif [[ x"${release}" == x"ubuntu" ]]; then
if [[ ${os_version} -lt 16 ]]; then
LOGE "please use Ubuntu 16 or higher version\n" && exit 1
fi
elif [[ x"${release}" == x"debian" ]]; then
if [[ "${release}" == "centos" ]]; then
if [[ ${os_version} -lt 8 ]]; then
LOGE "please use Debian 8 or higher version\n" && exit 1
echo -e "${red} Please use CentOS 8 or higher ${plain}\n" && exit 1
fi
elif [[ "${release}" == "ubuntu" ]]; then
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
fi
elif [[ "${release}" == "debian" ]]; then
if [[ ${os_version} -lt 10 ]]; then
echo -e "${red} Please use Debian 10 or higher ${plain}\n" && exit 1
fi
fi

View File

@@ -1,12 +1,12 @@
package xray
type ClientTraffic struct {
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
InboundId int `json:"inboundId" form:"inboundId"`
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
InboundId int `json:"inboundId" form:"inboundId"`
Enable bool `json:"enable" form:"enable"`
Email string `json:"email" form:"email" gorm:"unique"`
Up int64 `json:"up" form:"up"`
Down int64 `json:"down" form:"down"`
Email string `json:"email" form:"email" gorm:"unique"`
Up int64 `json:"up" form:"up"`
Down int64 `json:"down" form:"down"`
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
Total int64 `json:"total" form:"total"`
}

View File

@@ -14,6 +14,7 @@ import (
"runtime"
"strings"
"time"
"x-ui/config"
"x-ui/util/common"
"github.com/Workiva/go-datastructures/queue"
@@ -29,19 +30,19 @@ func GetBinaryName() string {
}
func GetBinaryPath() string {
return "bin/" + GetBinaryName()
return config.GetBinFolderPath() + "/" + GetBinaryName()
}
func GetConfigPath() string {
return "bin/config.json"
return config.GetBinFolderPath() + "/config.json"
}
func GetGeositePath() string {
return "bin/geosite.dat"
return config.GetBinFolderPath() + "/geosite.dat"
}
func GetGeoipPath() string {
return "bin/geoip.dat"
return config.GetBinFolderPath() + "/geoip.dat"
}
func stopProcess(p *Process) {