Compare commits

...

38 Commits
1.5.2 ... 1.5.4

Author SHA1 Message Date
Alireza Ahmadi
a6199526da v1.5.4
Plus some fixes and decoration
2023-08-26 12:05:21 +02:00
Alireza Ahmadi
da5253d98c [sub] support client-side group name 2023-08-25 23:42:26 +02:00
Alireza Ahmadi
7adc8755f8 [sub] support optional usage info in Remark #453 2023-08-25 23:32:10 +02:00
Alireza Ahmadi
af5d681c22 Transparent Proxy with sockopt Stream Setting 2023-08-25 20:26:59 +02:00
Alireza Ahmadi
28a3fc813c [db] Enbancement add traffic fully transactional
- Remove expiration process of client/inbound with separate cron
- Combine expiration process to add traffic
- Combine calculation of all the traffics to one database transaction
2023-08-25 18:24:40 +02:00
Alireza Ahmadi
55ae60594f [tls] change default tls verion to 1.2-1.3 2023-08-25 16:44:36 +02:00
Alireza Ahmadi
7cb99d47e2 [ss] add ietf methods #507 2023-08-25 16:41:56 +02:00
dependabot[bot]
c3970a4978 Bump gorm.io/gorm from 1.25.3 to 1.25.4 (#502)
Bumps [gorm.io/gorm](https://github.com/go-gorm/gorm) from 1.25.3 to 1.25.4.
- [Release notes](https://github.com/go-gorm/gorm/releases)
- [Commits](https://github.com/go-gorm/gorm/compare/v1.25.3...v1.25.4)

---
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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-22 21:48:24 +02:00
MasterKia
5daec0cf9e Attribution (#497) 2023-08-22 21:48:12 +02:00
Alireza Ahmadi
1b0de200c0 Show ALPN order #483 2023-08-22 21:11:29 +02:00
Alireza Ahmadi
2c53d987eb Merge branch 'main' of https://github.com/alireza0/x-ui 2023-08-17 13:33:09 +02:00
Alireza Ahmadi
fc725a56c3 fix switch enable function for clients 2023-08-17 13:32:59 +02:00
dependabot[bot]
01028530c2 Bump gorm.io/driver/sqlite from 1.5.2 to 1.5.3 (#492)
Bumps [gorm.io/driver/sqlite](https://github.com/go-gorm/sqlite) from 1.5.2 to 1.5.3.
- [Commits](https://github.com/go-gorm/sqlite/compare/v1.5.2...v1.5.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-17 13:29:47 +02:00
shahin-io
d986ec5c3c Update x-ui.sh (#487) 2023-08-14 02:19:38 +02:00
Ho3ein
812e145f97 Remove duplicate code to make random text (#484) 2023-08-12 14:58:33 +02:00
Ho3ein
5261a884bf remove v2-ui (#485) 2023-08-12 14:55:06 +02:00
Alireza Ahmadi
c6816d2531 fix finding client issue
Reference:
https://github.com/MHSanaei/3x-ui/issues/884
2023-08-12 14:53:03 +02:00
dependabot[bot]
19073469c5 Bump gorm.io/gorm from 1.25.2 to 1.25.3 (#477)
Bumps [gorm.io/gorm](https://github.com/go-gorm/gorm) from 1.25.2 to 1.25.3.
- [Release notes](https://github.com/go-gorm/gorm/releases)
- [Commits](https://github.com/go-gorm/gorm/compare/v1.25.2...v1.25.3)

---
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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-11 20:06:44 +02:00
MMR
c3b1f6a13d change bootmortis project to MasterKia fork (#478)
* change bootmortis project to MasterKia fork

کامل ترین لیست سایت های تبلیغات ایرانی، پروژه https://github.com/MasterKia/PersianBlocker است که به صورت مستمر نیز آپدیت می‌شود. پروژه https://github.com/bootmortis/iran-hosted-domains هم از همین لیست استفاده می‌کرد. مدتی پیش bootmortis تصمیم گرفت منبع سایت تبلیغات خود را عوض کند، نه به این دلیل که لیست کامل‌تری وجود دارد، بلکه به دلیل اینکه پروژه PersianBlocker از لایسنس GPL استفاده میکرد و پروژه bootmortis/iran-hosted-domains از لایسنس MIT استفاده میکرد و نمی‌توانست بدون تغییر لایسنس از آن منبع استفاده کند. شرح کامل ماجرا:
bootmortis/iran-hosted-domains#27
بعد از آن MasterKia پروژه iran-hosted-domains را با لایسنس GPL فورک کرد و لیست خود را که کامل تر بود را دوباره برگرداند.
از این جهت که x-ui شما هم لایسنس GPL دارد و محدودیت استفاده از لیست کامل‌تر را ندارد، پیشنهاد می‌کنم پروژه https://github.com/MasterKia/iran-hosted-domains جایگزین https://github.com/bootmortis/iran-hosted-domains/ شود

* Update DockerInitFiles.sh
2023-08-11 20:05:52 +02:00
Alireza Ahmadi
11b758743a Clean legacy vmess
https://github.com/XTLS/Xray-core/pull/2199
2023-08-11 10:24:04 +02:00
Alireza Ahmadi
a92e3b598f Merge branch 'main' of https://github.com/alireza0/x-ui 2023-08-10 21:07:22 +02:00
Alireza Ahmadi
413fa468d1 add tls ocspStapling #475 2023-08-10 21:07:13 +02:00
Alireza Ahmadi
db32b581db Merge pull request #473 from Saph1s/main
Translation - RU
2023-08-09 21:04:51 +02:00
Saph1s
078b408e4f Translation update 2023-08-09 01:42:21 +03:00
Saph1s
b04a892596 Translation Update 2023-08-09 01:31:10 +03:00
Saph1s
3304daff7e Translation - RU 2023-08-09 00:30:13 +03:00
Alireza Ahmadi
9e3c7b1db6 v1.5.3 2023-08-08 19:58:30 +02:00
Alireza Ahmadi
ab6c6c0ca6 [bash] separate cloudflare/local acme routines 2023-08-08 19:31:25 +02:00
Alireza Ahmadi
9c0890dd9b [ss] fix 2022 links 2023-08-08 19:14:19 +02:00
Alireza Ahmadi
eee0503200 add system info to main page 2023-08-06 19:10:39 +02:00
Alireza Ahmadi
f3c539dd73 Merge pull request #465 from alireza0/dependabot/go_modules/golang.org/x/text-0.12.0
Bump golang.org/x/text from 0.11.0 to 0.12.0
2023-08-05 19:04:57 +02:00
Alireza Ahmadi
c0464f1d97 multi user HTTP & Socks inbounds 2023-08-05 17:43:28 +02:00
Alireza Ahmadi
b87474d70c [front] better info modal 2023-08-05 17:42:38 +02:00
Alireza Ahmadi
063309dbb7 [front] better table design 2023-08-05 17:41:20 +02:00
dependabot[bot]
294b680972 Bump golang.org/x/text from 0.11.0 to 0.12.0
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.11.0 to 0.12.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.11.0...v0.12.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-08-04 21:12:42 +00:00
Alireza Ahmadi
f55478422b [install] stop service after download #464 2023-08-03 21:11:24 +02:00
Alireza Ahmadi
619e0c69cd Merge pull request #463 from alireza0/dependabot/go_modules/github.com/shirou/gopsutil/v3-3.23.7
Bump github.com/shirou/gopsutil/v3 from 3.23.6 to 3.23.7
2023-08-02 15:19:05 +02:00
dependabot[bot]
da5dc3a04f Bump github.com/shirou/gopsutil/v3 from 3.23.6 to 3.23.7
Bumps [github.com/shirou/gopsutil/v3](https://github.com/shirou/gopsutil) from 3.23.6 to 3.23.7.
- [Release notes](https://github.com/shirou/gopsutil/releases)
- [Commits](https://github.com/shirou/gopsutil/compare/v3.23.6...v3.23.7)

---
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-08-01 21:24:53 +00:00
50 changed files with 839 additions and 676 deletions

View File

@@ -14,7 +14,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: 'stable'
go-version: '1.20'
- name: build linux amd64 version
run: |
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o xui-release -v main.go
@@ -31,7 +31,7 @@ jobs:
rm -f Xray-linux-64.zip geoip.dat geosite.dat iran.dat
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
wget https://github.com/bootmortis/iran-hosted-domains/releases/latest/download/iran.dat
wget https://github.com/MasterKia/iran-hosted-domains/releases/latest/download/iran.dat
mv xray xray-linux-amd64
cd ..
cd ..
@@ -72,7 +72,7 @@ jobs:
rm -f Xray-linux-64.zip geoip.dat geosite.dat iran.dat
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
wget https://github.com/bootmortis/iran-hosted-domains/releases/latest/download/iran.dat
wget https://github.com/Masterkia/iran-hosted-domains/releases/latest/download/iran.dat
mv xray xray-linux-arm64
cd ..
cd ..
@@ -113,7 +113,7 @@ jobs:
rm -f Xray-linux-64.zip geoip.dat geosite.dat iran.dat
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
wget https://github.com/bootmortis/iran-hosted-domains/releases/latest/download/iran.dat
wget https://github.com/Masterkia/iran-hosted-domains/releases/latest/download/iran.dat
mv xray xray-linux-s390x
cd ..
cd ..

View File

@@ -17,5 +17,5 @@ rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat iran.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"
wget "https://github.com/bootmortis/iran-hosted-domains/releases/latest/download/iran.dat"
cd ../../
wget "https://github.com/Masterkia/iran-hosted-domains/releases/latest/download/iran.dat"
cd ../../

View File

@@ -217,20 +217,6 @@ Reference syntax:
- Multi language bot
</details>
# 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`
> Please `Close v2-ui` and `restart x-ui`, otherwise the inbound of v2-ui will cause a `port conflict with the inbound of x-ui`
```sh
x-ui v2-ui
```
# T-Shoots:
**If you upgrade from an old version or other forks, for enable traffic for users you should do :**
@@ -281,6 +267,11 @@ restart panel
- [HexaSoftwareTech](https://github.com/HexaSoftwareTech/)
- [MHSanaei](https://github.com/MHSanaei)
# Acknowledgment
- [Iran Hosted Domains](https://github.com/bootmortis/iran-hosted-domains) (License: **MIT**): _A comprehensive list of Iranian domains and services that are hosted within the country._
- [PersianBlocker](https://github.com/MasterKia/PersianBlocker) (License: **AGPLv3**): _An optimal and extensive list to block ads and trackers on Persian websites._
## Stargazers over time
[![Stargazers over time](https://starchart.cc/alireza0/x-ui.svg)](https://starchart.cc/alireza0/x-ui)

View File

@@ -1 +1 @@
1.5.2
1.5.4

10
go.mod
View File

@@ -12,13 +12,13 @@ require (
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pelletier/go-toml/v2 v2.0.9
github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v3 v3.23.6
github.com/shirou/gopsutil/v3 v3.23.7
github.com/xtls/xray-core v1.8.3
go.uber.org/atomic v1.11.0
golang.org/x/text v0.11.0
golang.org/x/text v0.12.0
google.golang.org/grpc v1.57.0
gorm.io/driver/sqlite v1.5.2
gorm.io/gorm v1.25.2
gorm.io/driver/sqlite v1.5.3
gorm.io/gorm v1.25.4
)
require (
@@ -80,7 +80,7 @@ require (
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
golang.org/x/mod v0.11.0 // indirect
golang.org/x/net v0.11.0 // indirect
golang.org/x/sys v0.9.0 // indirect
golang.org/x/sys v0.10.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.10.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect

20
go.sum
View File

@@ -220,8 +220,8 @@ github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c/go.mod h1:eu
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U=
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shirou/gopsutil/v3 v3.23.6 h1:5y46WPI9QBKBbK7EEccUPNXpJpNrvPuTD0O2zHEHT08=
github.com/shirou/gopsutil/v3 v3.23.6/go.mod h1:j7QX50DrXYggrpN30W0Mo+I4/8U2UUIQrnrhqUeWrAU=
github.com/shirou/gopsutil/v3 v3.23.7 h1:C+fHO8hfIppoJ1WdsVm1RoI0RwXoNdfTK7yWXV0wVj4=
github.com/shirou/gopsutil/v3 v3.23.7/go.mod h1:c4gnmoRC0hQuaLqvxnx1//VXQ0Ms/X9UnJF8pddY5z4=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
@@ -366,8 +366,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.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=
@@ -376,8 +376,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
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/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
@@ -433,10 +433,10 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.5.2 h1:TpQ+/dqCY4uCigCFyrfnrJnrW9zjpelWVoEVNy5qJkc=
gorm.io/driver/sqlite v1.5.2/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
gorm.io/gorm v1.25.2 h1:gs1o6Vsa+oVKG/a9ElL3XgyGfghFfkKA2SInQaCyMho=
gorm.io/gorm v1.25.2/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/driver/sqlite v1.5.3 h1:7/0dUgX28KAcopdfbRWWl68Rflh6osa4rDh+m51KL2g=
gorm.io/driver/sqlite v1.5.3/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
gorm.io/gorm v1.25.4 h1:iyNd8fNAe8W9dvtlgeRI5zSVZPsq3OpcTu37cYcpCmw=
gorm.io/gorm v1.25.4/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
gvisor.dev/gvisor v0.0.0-20220901235040-6ca97ef2ce1c h1:m5lcgWnL3OElQNVyp3qcncItJ2c0sQlSGjYK2+nJTA4=
gvisor.dev/gvisor v0.0.0-20220901235040-6ca97ef2ce1c/go.mod h1:TIvkJD0sxe8pIob3p6T8IzxXunlp6yfgktvTNp+DGNM=

View File

@@ -113,7 +113,6 @@ config_after_install() {
}
install_x-ui() {
systemctl stop x-ui
cd /usr/local/
if [ $# == 0 ]; then
@@ -140,6 +139,7 @@ install_x-ui() {
fi
if [[ -e /usr/local/x-ui/ ]]; then
systemctl stop x-ui
rm /usr/local/x-ui/ -rf
fi
@@ -173,7 +173,6 @@ install_x-ui() {
echo -e "x-ui enable - Enable x-ui on system startup"
echo -e "x-ui disable - Disable x-ui on system startup"
echo -e "x-ui log - Check x-ui logs"
echo -e "x-ui v2-ui - Migrate v2-ui Account data to x-ui"
echo -e "x-ui update - Update x-ui"
echo -e "x-ui install - Install x-ui"
echo -e "x-ui uninstall - Uninstall x-ui"

20
main.go
View File

@@ -12,7 +12,6 @@ import (
"x-ui/database"
"x-ui/logger"
"x-ui/sub"
"x-ui/v2ui"
"x-ui/web"
"x-ui/web/global"
"x-ui/web/service"
@@ -252,10 +251,6 @@ func main() {
runCmd := flag.NewFlagSet("run", flag.ExitOnError)
v2uiCmd := flag.NewFlagSet("v2-ui", flag.ExitOnError)
var dbPath string
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
@@ -282,7 +277,6 @@ func main() {
fmt.Println()
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")
}
@@ -303,16 +297,6 @@ func main() {
runWebServer()
case "migrate":
migrateDb()
case "v2-ui":
err := v2uiCmd.Parse(os.Args[2:])
if err != nil {
fmt.Println(err)
return
}
err = v2ui.MigrateFromV2UI(dbPath)
if err != nil {
fmt.Println("migrate from v2-ui failed:", err)
}
case "setting":
err := settingCmd.Parse(os.Args[2:])
if err != nil {
@@ -334,12 +318,10 @@ func main() {
updateTgbotEnableSts(enabletgbot)
}
default:
fmt.Println("except 'run' or 'v2-ui' or 'setting' subcommands")
fmt.Println("except 'run' or 'setting' subcommands")
fmt.Println()
runCmd.Usage()
fmt.Println()
v2uiCmd.Usage()
fmt.Println()
settingCmd.Usage()
}
}

View File

@@ -27,9 +27,10 @@ func (a *SUBController) initRouter(g *gin.RouterGroup) {
func (a *SUBController) subs(c *gin.Context) {
subEncrypt, _ := a.settingService.GetSubEncrypt()
subShowInfo, _ := a.settingService.GetSubShowInfo()
subId := c.Param("subid")
host := strings.Split(c.Request.Host, ":")[0]
subs, headers, err := a.subService.GetSubs(subId, host)
subs, headers, err := a.subService.GetSubs(subId, host, subShowInfo)
if err != nil || len(subs) == 0 {
c.String(400, "Error!")
} else {

View File

@@ -5,9 +5,11 @@ import (
"fmt"
"net/url"
"strings"
"time"
"x-ui/database"
"x-ui/database/model"
"x-ui/logger"
"x-ui/util/common"
"x-ui/web/service"
"x-ui/xray"
@@ -16,12 +18,14 @@ import (
type SubService struct {
address string
showInfo bool
inboundService service.InboundService
settingServics service.SettingService
}
func (s *SubService) GetSubs(subId string, host string) ([]string, []string, error) {
func (s *SubService) GetSubs(subId string, host string, showInfo bool) ([]string, []string, error) {
s.address = host
s.showInfo = showInfo
var result []string
var headers []string
var traffic xray.ClientTraffic
@@ -139,10 +143,8 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
if inbound.Protocol != model.VMess {
return ""
}
remark := fmt.Sprintf("%s-%s", inbound.Remark, email)
obj := map[string]interface{}{
"v": "2",
"ps": remark,
"add": s.address,
"port": inbound.Port,
"type": "none",
@@ -241,7 +243,7 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
links := ""
for index, d := range domains {
domain := d.(map[string]interface{})
obj["ps"] = remark + "-" + domain["remark"].(string)
obj["ps"] = s.genRemark(inbound, email, domain["remark"].(string))
obj["add"] = domain["domain"].(string)
if index > 0 {
links += "\n"
@@ -252,6 +254,8 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
return links
}
obj["ps"] = s.genRemark(inbound, email, "")
jsonStr, _ := json.MarshalIndent(obj, "", " ")
return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
}
@@ -407,13 +411,12 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
// Set the new query values on the URL
url.RawQuery = q.Encode()
remark := fmt.Sprintf("%s-%s", inbound.Remark, email)
if len(domains) > 0 {
links := ""
for index, d := range domains {
domain := d.(map[string]interface{})
url.Fragment = remark + "-" + domain["remark"].(string)
url.Fragment = s.genRemark(inbound, email, domain["remark"].(string))
url.Host = fmt.Sprintf("%s:%d", domain["domain"].(string), port)
if index > 0 {
links += "\n"
@@ -423,7 +426,7 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
return links
}
url.Fragment = remark
url.Fragment = s.genRemark(inbound, email, "")
return url.String()
}
@@ -572,13 +575,11 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
// Set the new query values on the URL
url.RawQuery = q.Encode()
remark := fmt.Sprintf("%s-%s", inbound.Remark, email)
if len(domains) > 0 {
links := ""
for index, d := range domains {
domain := d.(map[string]interface{})
url.Fragment = remark + "-" + domain["remark"].(string)
url.Fragment = s.genRemark(inbound, email, domain["remark"].(string))
url.Host = fmt.Sprintf("%s:%d", domain["domain"].(string), port)
if index > 0 {
links += "\n"
@@ -588,7 +589,7 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
return links
}
url.Fragment = remark
url.Fragment = s.genRemark(inbound, email, "")
return url.String()
}
@@ -671,12 +672,55 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
// Set the new query values on the URL
url.RawQuery = q.Encode()
remark := fmt.Sprintf("%s-%s", inbound.Remark, clients[clientIndex].Email)
url.Fragment = remark
url.Fragment = s.genRemark(inbound, email, "")
return url.String()
}
func (s *SubService) genRemark(inbound *model.Inbound, email string, extra string) string {
var remark []string
if len(email) > 0 {
if len(inbound.Remark) > 0 {
remark = append(remark, inbound.Remark)
}
remark = append(remark, email)
if len(extra) > 0 {
remark = append(remark, extra)
}
} else {
return inbound.Remark
}
if s.showInfo {
statsExist := false
var stats xray.ClientTraffic
for _, clientStat := range inbound.ClientStats {
if clientStat.Email == email {
stats = clientStat
statsExist = true
break
}
}
// Get remained days
if statsExist {
if !stats.Enable {
return fmt.Sprintf("⛔N/A-%s", strings.Join(remark, "-"))
}
if vol := stats.Total - (stats.Up + stats.Down); vol > 0 {
remark = append(remark, fmt.Sprintf("%s%s", common.FormatTraffic(vol), "📊"))
}
now := time.Now().Unix()
switch exp := stats.ExpiryTime / 1000; {
case exp > 0:
remark = append(remark, fmt.Sprintf("%d%s⏳", (exp-now)/86400, "Days"))
case exp < 0:
remark = append(remark, fmt.Sprintf("%d%s⏳", exp/-86400, "Days"))
}
}
}
return strings.Join(remark, "-")
}
func searchKey(data interface{}, key string) (interface{}, bool) {
switch val := data.(type) {
case map[string]interface{}:

View File

@@ -1,28 +0,0 @@
package v2ui
import (
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var v2db *gorm.DB
func initDB(dbPath string) error {
c := &gorm.Config{
Logger: logger.Discard,
}
var err error
v2db, err = gorm.Open(sqlite.Open(dbPath), c)
if err != nil {
return err
}
return nil
}
func getV2Inbounds() ([]*V2Inbound, error) {
inbounds := make([]*V2Inbound, 0)
err := v2db.Model(V2Inbound{}).Find(&inbounds).Error
return inbounds, err
}

View File

@@ -1,41 +0,0 @@
package v2ui
import "x-ui/database/model"
type V2Inbound struct {
Id int `gorm:"primaryKey;autoIncrement"`
Port int `gorm:"unique"`
Listen string
Protocol string
Settings string
StreamSettings string
Tag string `gorm:"unique"`
Sniffing string
Remark string
Up int64
Down int64
Enable bool
}
func (i *V2Inbound) TableName() string {
return "inbound"
}
func (i *V2Inbound) ToInbound(userId int) *model.Inbound {
return &model.Inbound{
UserId: userId,
Up: i.Up,
Down: i.Down,
Total: 0,
Remark: i.Remark,
Enable: i.Enable,
ExpiryTime: 0,
Listen: i.Listen,
Port: i.Port,
Protocol: model.Protocol(i.Protocol),
Settings: i.Settings,
StreamSettings: i.StreamSettings,
Tag: i.Tag,
Sniffing: i.Sniffing,
}
}

View File

@@ -1,51 +0,0 @@
package v2ui
import (
"fmt"
"x-ui/config"
"x-ui/database"
"x-ui/database/model"
"x-ui/util/common"
"x-ui/web/service"
)
func MigrateFromV2UI(dbPath string) error {
err := initDB(dbPath)
if err != nil {
return common.NewError("init v2-ui database failed:", err)
}
err = database.InitDB(config.GetDBPath())
if err != nil {
return common.NewError("init x-ui database failed:", err)
}
v2Inbounds, err := getV2Inbounds()
if err != nil {
return common.NewError("get v2-ui inbounds failed:", err)
}
if len(v2Inbounds) == 0 {
fmt.Println("migrate v2-ui inbounds success: 0")
return nil
}
userService := service.UserService{}
user, err := userService.GetFirstUser()
if err != nil {
return common.NewError("get x-ui user failed:", err)
}
inbounds := make([]*model.Inbound, 0)
for _, v2inbound := range v2Inbounds {
inbounds = append(inbounds, v2inbound.ToInbound(user.Id))
}
inboundService := service.InboundService{}
err = inboundService.AddInbounds(inbounds)
if err != nil {
return common.NewError("add x-ui inbounds failed:", err)
}
fmt.Println("migrate v2-ui inbounds success:", len(inbounds))
return nil
}

View File

@@ -240,6 +240,7 @@ body {
.ant-card-dark .ant-collapse-content,
.ant-card-dark .ant-calendar,
.ant-card-dark .ant-table-placeholder,
.ant-card-dark .ant-select-selection__choice,
.ant-card-dark .ant-input-group-addon {
color: hsla(0,0%,100%,.65);
background-color: #262f3d;

View File

@@ -192,6 +192,7 @@ class AllSetting {
this.subKeyFile = "";
this.subUpdates = 0;
this.subEncrypt = true;
this.subShowInfo = false;
this.timeLocation = "Asia/Tehran";

View File

@@ -19,7 +19,9 @@ const SSMethods = {
AES_256_GCM: 'aes-256-gcm',
AES_128_GCM: 'aes-128-gcm',
CHACHA20_POLY1305: 'chacha20-poly1305',
CHACHA20_IETF_POLY1305: 'chacha20-ietf-poly1305',
XCHACHA20_POLY1305: 'xchacha20-poly1305',
XCHACHA20_IETF_POLY1305: 'xchacha20-ietf-poly1305',
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',
@@ -35,7 +37,7 @@ const TLS_VERSION_OPTION = {
TLS11: "1.1",
TLS12: "1.2",
TLS13: "1.3",
}
};
const TLS_CIPHER_OPTION = {
RSA_AES_128_CBC: "TLS_RSA_WITH_AES_128_CBC_SHA",
@@ -71,9 +73,9 @@ const UTLS_FINGERPRINT = {
};
const ALPN_OPTION = {
HTTP1: "http/1.1",
H2: "h2",
H3: "h3",
H2: "h2",
HTTP1: "http/1.1",
};
const SNIFFING_OPTION = {
@@ -459,8 +461,8 @@ class GrpcStreamSettings extends XrayCommonClass {
class TlsStreamSettings extends XrayCommonClass {
constructor(serverName='',
minVersion = TLS_VERSION_OPTION.TLS10,
maxVersion = TLS_VERSION_OPTION.TLS12,
minVersion = TLS_VERSION_OPTION.TLS12,
maxVersion = TLS_VERSION_OPTION.TLS13,
cipherSuites = '',
rejectUnknownSni = false,
certificates=[new TlsStreamSettings.Cert()],
@@ -522,13 +524,14 @@ class TlsStreamSettings extends XrayCommonClass {
}
TlsStreamSettings.Cert = class extends XrayCommonClass {
constructor(useFile=true, certificateFile='', keyFile='', certificate='', key='') {
constructor(useFile=true, certificateFile='', keyFile='', certificate='', key='', ocspStapling=3600) {
super();
this.useFile = useFile;
this.certFile = certificateFile;
this.keyFile = keyFile;
this.cert = certificate instanceof Array ? certificate.join('\n') : certificate;
this.key = key instanceof Array ? key.join('\n') : key;
this.ocspStapling = ocspStapling;
}
static fromJson(json={}) {
@@ -536,13 +539,15 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
return new TlsStreamSettings.Cert(
true,
json.certificateFile,
json.keyFile,
json.keyFile, '', '',
json.ocspStapling,
);
} else {
return new TlsStreamSettings.Cert(
false, '', '',
json.certificate.join('\n'),
json.key.join('\n'),
json.ocspStapling,
);
}
}
@@ -552,11 +557,13 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
return {
certificateFile: this.certFile,
keyFile: this.keyFile,
ocspStapling: this.ocspStapling,
};
} else {
return {
certificate: this.cert.split('\n'),
key: this.key.split('\n'),
ocspStapling: this.ocspStapling,
};
}
}
@@ -669,6 +676,35 @@ RealityStreamSettings.Settings = class extends XrayCommonClass {
}
};
class SockoptStreamSettings extends XrayCommonClass {
constructor(acceptProxyProtocol = false, tcpFastOpen = false, mark = 0, tproxy="off") {
super();
this.acceptProxyProtocol = acceptProxyProtocol;
this.tcpFastOpen = tcpFastOpen;
this.mark = mark;
this.tproxy = tproxy;
}
static fromJson(json = {}) {
if (Object.keys(json).length === 0) return undefined;
return new SockoptStreamSettings(
json.acceptProxyProtocol,
json.tcpFastOpen,
json.mark,
json.tproxy,
);
}
toJson() {
return {
acceptProxyProtocol: this.acceptProxyProtocol,
tcpFastOpen: this.tcpFastOpen,
mark: this.mark,
tproxy: this.tproxy,
};
}
}
class StreamSettings extends XrayCommonClass {
constructor(network='tcp',
security='none',
@@ -680,6 +716,7 @@ class StreamSettings extends XrayCommonClass {
httpSettings=new HttpStreamSettings(),
quicSettings=new QuicStreamSettings(),
grpcSettings=new GrpcStreamSettings(),
sockopt = undefined,
) {
super();
this.network = network;
@@ -692,6 +729,7 @@ class StreamSettings extends XrayCommonClass {
this.http = httpSettings;
this.quic = quicSettings;
this.grpc = grpcSettings;
this.sockopt = sockopt;
}
get isTls() {
@@ -718,6 +756,14 @@ class StreamSettings extends XrayCommonClass {
}
}
get sockoptSwitch() {
return this.sockopt != undefined;
}
set sockoptSwitch(value) {
this.sockopt = value ? new SockoptStreamSettings() : undefined;
}
static fromJson(json={}) {
return new StreamSettings(
@@ -731,6 +777,7 @@ class StreamSettings extends XrayCommonClass {
HttpStreamSettings.fromJson(json.httpSettings),
QuicStreamSettings.fromJson(json.quicSettings),
GrpcStreamSettings.fromJson(json.grpcSettings),
SockoptStreamSettings.fromJson(json.sockopt),
);
}
@@ -747,6 +794,7 @@ class StreamSettings extends XrayCommonClass {
httpSettings: network === 'http' ? this.http.toJson() : undefined,
quicSettings: network === 'quic' ? this.quic.toJson() : undefined,
grpcSettings: network === 'grpc' ? this.grpc.toJson() : undefined,
sockopt: this.sockopt != undefined ? this.sockopt.toJson() : undefined,
};
}
}
@@ -904,7 +952,7 @@ class Inbound extends XrayCommonClass {
} else if (this.isWs) {
return this.stream.ws.path;
} else if (this.isH2) {
return this.stream.http.path[0];
return this.stream.http.path;
}
return null;
}
@@ -1279,8 +1327,8 @@ class Inbound extends XrayCommonClass {
}
let password = new Array();
if (this.isSSMultiUser) password.push(settings.shadowsockses[clientIndex].password);
if (this.isSS2022) password.push(settings.password);
if (this.isSSMultiUser) password.push(settings.shadowsockses[clientIndex].password);
let link = `ss://${safeBase64(settings.method + ':' + password.join(':'))}@${address}:${this.port}`;
const url = new URL(link);
@@ -1411,10 +1459,10 @@ class Inbound extends XrayCommonClass {
JSON.parse(this.settings).clients.forEach((client,index) => {
if(this.tls && !ObjectUtil.isArrEmpty(this.stream.tls.settings.domains)){
this.stream.tls.settings.domains.forEach((domain) => {
link += this.genLink(domain.domain, remark + '-' + client.email + '-' + domain.remark, index) + '\r\n';
link += this.genLink(domain.domain, [remark, client.email, domain.remark].filter(x => x.length > 0).join('-'), index) + '\r\n';
});
} else {
link += this.genLink(address, remark + '-' + client.email, index) + '\r\n';
link += this.genLink(address, [remark, client.email].filter(x => x.length > 0).join('-'), index) + '\r\n';
}
});
return link;
@@ -1492,11 +1540,9 @@ Inbound.Settings = class extends XrayCommonClass {
Inbound.VmessSettings = class extends Inbound.Settings {
constructor(protocol,
vmesses=[new Inbound.VmessSettings.Vmess()],
disableInsecureEncryption=false) {
vmesses=[new Inbound.VmessSettings.Vmess()]) {
super(protocol);
this.vmesses = vmesses;
this.disableInsecure = disableInsecureEncryption;
}
indexOfVmessById(id) {
@@ -1521,19 +1567,17 @@ Inbound.VmessSettings = class extends Inbound.Settings {
return new Inbound.VmessSettings(
Protocols.VMESS,
json.clients.map(client => Inbound.VmessSettings.Vmess.fromJson(client)),
ObjectUtil.isEmpty(json.disableInsecureEncryption) ? false : json.disableInsecureEncryption,
);
}
toJson() {
return {
clients: Inbound.VmessSettings.toJsonArray(this.vmesses),
disableInsecureEncryption: this.disableInsecure,
};
}
};
Inbound.VmessSettings.Vmess = class extends XrayCommonClass {
constructor(id=RandomUtil.randomUUID(), email=RandomUtil.randomText(), totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomText(16,16)) {
constructor(id=RandomUtil.randomUUID(), email=RandomUtil.randomLowerAndNum(9), totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16)) {
super();
this.id = id;
this.email = email;
@@ -1621,7 +1665,7 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
};
Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
constructor(id=RandomUtil.randomUUID(), flow='', email=RandomUtil.randomText(), totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomText(16,16)) {
constructor(id=RandomUtil.randomUUID(), flow='', email=RandomUtil.randomLowerAndNum(9), totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16)) {
super();
this.id = id;
this.flow = flow;
@@ -1742,7 +1786,7 @@ Inbound.TrojanSettings = class extends Inbound.Settings {
}
};
Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
constructor(password=RandomUtil.randomSeq(10), email=RandomUtil.randomText(), totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomText(16,16)) {
constructor(password=RandomUtil.randomSeq(10), email=RandomUtil.randomLowerAndNum(9), totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16)) {
super();
this.password = password;
this.email = email;
@@ -1878,7 +1922,7 @@ Inbound.ShadowsocksSettings = class extends Inbound.Settings {
};
Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
constructor(method='', password=RandomUtil.randomShadowsocksPassword(), email=RandomUtil.randomText(), totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomText(16,16)) {
constructor(method='', password=RandomUtil.randomShadowsocksPassword(), email=RandomUtil.randomLowerAndNum(9), totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16)) {
super();
this.method = method;
this.password = password;

View File

@@ -75,17 +75,7 @@ class PromiseUtil {
}
}
const seq = [
'a', 'b', 'c', 'd', 'e', 'f', 'g',
'h', 'i', 'j', 'k', 'l', 'm', 'n',
'o', 'p', 'q', 'r', 's', 't',
'u', 'v', 'w', 'x', 'y', 'z',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'A', 'B', 'C', 'D', 'E', 'F', 'G',
'H', 'I', 'J', 'K', 'L', 'M', 'N',
'O', 'P', 'Q', 'R', 'S', 'T',
'U', 'V', 'W', 'X', 'Y', 'Z'
];
const seq = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
class RandomUtil {
static randomIntRange(min, max) {
@@ -121,16 +111,6 @@ class RandomUtil {
});
}
static randomText(minLen = 6, varLen = 5) {
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
var string = '';
var len = minLen + Math.floor(Math.random() * varLen);
for (var ii = 0; ii < len; ii++) {
string += chars[Math.floor(Math.random() * chars.length)];
}
return string;
}
static randomShadowsocksPassword() {
let array = new Uint8Array(32);
window.crypto.getRandomValues(array);

View File

@@ -65,6 +65,7 @@ func (a *SettingController) getDefaultSettings(c *gin.Context) {
"subKeyFile": func() (interface{}, error) { return a.settingService.GetSubKeyFile() },
"subCertFile": func() (interface{}, error) { return a.settingService.GetSubCertFile() },
"subEncrypt": func() (interface{}, error) { return a.settingService.GetSubEncrypt() },
"subShowInfo": func() (interface{}, error) { return a.settingService.GetSubShowInfo() },
}
result := make(map[string]interface{})

View File

@@ -55,6 +55,7 @@ type AllSetting struct {
SubKeyFile string `json:"subKeyFile" form:"subKeyFile"`
SubUpdates int `json:"subUpdates" form:"subUpdates"`
SubEncrypt bool `json:"subEncrypt" form:"subEncrypt"`
SubShowInfo bool `json:"subShowInfo" form:"subShowInfo"`
}
func (s *AllSetting) CheckValid() error {

View File

@@ -35,15 +35,16 @@
this.inbound = dbInbound.toInbound();
settings = JSON.parse(this.inbound.settings);
this.client = settings.clients[clientIndex];
remark = this.dbInbound.remark + ( this.client ? "-" + this.client.email : '');
remark = [this.dbInbound.remark, ( this.client ? this.client.email : '')].filter(Boolean).join('-');
address = this.dbInbound.address;
this.subId = '';
this.qrcodes = [];
if (this.inbound.tls && !ObjectUtil.isArrEmpty(this.inbound.stream.tls.settings.domains)) {
this.inbound.stream.tls.settings.domains.forEach((domain) => {
remarkText = [remark, domain.remark].filter(Boolean).join('-');
this.qrcodes.push({
remark: remark + "-" + domain.remark,
link: this.inbound.genLink(domain.domain, remark + "-" + domain.remark, clientIndex)
remark: remarkText,
link: this.inbound.genLink(domain.domain, remarkText, clientIndex)
});
});
} else {
@@ -84,7 +85,7 @@
},
genSubLink(subID) {
const { domain: host, port, tls: isTLS, path: base } = app.subSettings;
return buildURL({ host, port, isTLS, base, path: subID });
return buildURL({ host, port, isTLS, base, path: subID+'?name='+subID });
}
},
updated() {

View File

@@ -119,15 +119,6 @@
},
},
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"}}',

View File

@@ -19,7 +19,7 @@
<template slot="title">
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template>
<a-icon type="sync" @click="getNewEmail(client)"></a-icon>
<a-icon type="sync" @click="client.email = RandomUtil.randomLowerAndNum(9)"></a-icon>
</a-tooltip>
</td>
<td>
@@ -47,7 +47,7 @@
</td>
</tr>
<tr v-if="client.email && app.subSettings.enable">
<td>Subscription <a-icon @click="client.subId = RandomUtil.randomText(16,16)" type="sync"></a-icon></td>
<td>Subscription <a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"></a-icon></td>
<td>
<a-form-item>
<a-input v-model.trim="client.subId" style="width: 250px"></a-input>

View File

@@ -1,22 +1,19 @@
{{define "form/http"}}
<a-form layout="inline">
<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-item>
<a-row>
<a-button size="small" @click="inbound.settings.addAccount(new Inbound.SocksSettings.SocksAccount())">+</a-button>
</a-row>
<a-input-group v-for="(account, index) in inbound.settings.accounts">
<a-input style="width: 45%" v-model.trim="account.user"
addon-before='{{ i18n "username" }}'></a-input>
<a-input style="width: 55%" v-model.trim="account.pass"
addon-before='{{ i18n "password" }}'>
<template slot="addonAfter">
<a-button size="small" @click="inbound.settings.delAccount(index)">-</a-button>
</template>
</a-input>
</a-input-group>
</a-form-item>
</a-form>
{{end}}

View File

@@ -11,7 +11,7 @@
<template slot="title">
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template>
<a-icon @click="getNewEmail(client)" type="sync"> </a-icon>
<a-icon @click="client.email = RandomUtil.randomLowerAndNum(9)" type="sync"> </a-icon>
</a-tooltip>
</td>
<td>
@@ -31,7 +31,7 @@
</td>
</tr>
<tr v-if="client.email && app.subSettings.enable">
<td>Subscription <a-icon @click="client.subId = RandomUtil.randomText(16,16)" type="sync"></a-icon></td>
<td>Subscription <a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"></a-icon></td>
<td>
<a-form-item>
<a-input v-model.trim="client.subId" style="width: 200px;"></a-input>

View File

@@ -10,24 +10,25 @@
</a-form-item>
</td>
</tr>
<template v-if="inbound.settings.auth === 'password'">
<tr>
<td>{{ i18n "username" }}</td>
<td>
<tr v-if="inbound.settings.auth === 'password'">
<td colspan="2">
<a-form-item>
<a-input v-model.trim="inbound.settings.accounts[0].user"></a-input>
<a-row>
<a-button size="small" @click="inbound.settings.addAccount(new Inbound.SocksSettings.SocksAccount())">+</a-button>
</a-row>
<a-input-group v-for="(account, index) in inbound.settings.accounts">
<a-input style="width: 45%" v-model.trim="account.user"
addon-before='{{ i18n "username" }}'></a-input>
<a-input style="width: 55%" v-model.trim="account.pass"
addon-before='{{ i18n "password" }}'>
<template slot="addonAfter">
<a-button size="small" @click="inbound.settings.delAccount(index)">-</a-button>
</template>
</a-input>
</a-input-group>
</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>
<tr>
<td>{{ i18n "pages.inbounds.enable" }} udp</td>
<td>
@@ -36,10 +37,10 @@
</a-form-item>
</td>
</tr>
<tr>
<tr v-if="inbound.settings.udp">
<td>IP</td>
<td>
<a-form-item v-if="inbound.settings.udp">
<a-form-item>
<a-input v-model.trim="inbound.settings.ip"></a-input>
</a-form-item>
</td>

View File

@@ -10,7 +10,7 @@
<template slot="title">
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template>
<a-icon @click="getNewEmail(client)" type="sync"> </a-icon>
<a-icon @click="client.email = RandomUtil.randomLowerAndNum(9)" type="sync"> </a-icon>
</a-tooltip>
</td>
<td>
@@ -28,7 +28,7 @@
</td>
</tr>
<tr v-if="client.email && app.subSettings.enable">
<td>Subscription <a-icon @click="client.subId = RandomUtil.randomText(16,16)" type="sync"></a-icon></td>
<td>Subscription <a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"></a-icon></td>
<td>
<a-form-item>
<a-input v-model.trim="client.subId" style="width: 200px;"></a-input>

View File

@@ -1,7 +1,7 @@
{{define "form/vless"}}
<a-form layout="inline">
<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" }}">
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
<table width="100%" class="ant-table-tbody">
<tr>
<td>
@@ -10,7 +10,7 @@
<template slot="title">
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template>
<a-icon @click="getNewEmail(client)" type="sync"> </a-icon>
<a-icon @click="client.email = RandomUtil.randomLowerAndNum(9)" type="sync"> </a-icon>
</a-tooltip>
</td>
<td>
@@ -39,7 +39,7 @@
</td>
</tr>
<tr v-if="client.email && app.subSettings.enable">
<td>Subscription <a-icon @click="client.subId = RandomUtil.randomText(16,16)" type="sync"></a-icon></td>
<td>Subscription <a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"></a-icon></td>
<td>
<a-form-item>
<a-input v-model.trim="client.subId" style="width: 200px;"></a-input>

View File

@@ -1,7 +1,7 @@
{{define "form/vmess"}}
<a-form layout="inline">
<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" }}">
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
<table width="100%" class="ant-table-tbody">
<tr>
<td>
@@ -10,7 +10,7 @@
<template slot="title">
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template>
<a-icon type="sync" @click="getNewEmail(client)"></a-icon>
<a-icon type="sync" @click="client.email = RandomUtil.randomLowerAndNum(9)"></a-icon>
</a-tooltip>
</td>
<td>
@@ -28,7 +28,7 @@
</td>
</tr>
<tr v-if="client.email && app.subSettings.enable">
<td>Subscription <a-icon @click="client.subId = RandomUtil.randomText(16,16)" type="sync"></a-icon></td>
<td>Subscription <a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"></a-icon></td>
<td>
<a-form-item>
<a-input v-model.trim="client.subId" style="width: 200px;"></a-input>
@@ -110,9 +110,4 @@
</table>
</a-collapse-panel>
</a-collapse>
<a-form layout="inline">
<a-form-item label='{{ i18n "pages.inbounds.disableInsecureEncryption" }}'>
<a-switch v-model="inbound.settings.disableInsecure"></a-switch>
</a-form-item>
</a-form>
{{end}}

View File

@@ -1,5 +1,6 @@
{{define "form/sniffing"}}
<a-form layout="inline">
<a-divider dashed style="margin:0;">
<a-form-item>
<span slot="label">
sniffing
@@ -12,6 +13,7 @@
</span>
<a-switch v-model="inbound.sniffing.enabled"></a-switch>
</a-form-item>
</a-divider>
<a-form-item>
<a-checkbox-group v-model="inbound.sniffing.destOverride" v-if="inbound.sniffing.enabled">
<a-checkbox v-for="key,value in SNIFFING_OPTION" :value="key">[[ value ]]</a-checkbox>

View File

@@ -1,6 +1,7 @@
{{define "form/streamSettings"}}
<!-- select stream network -->
<a-form layout="inline">
<a-divider dashed style="margin:0;">
<a-form-item label="{{ i18n "transmission" }}">
<a-select v-model="inbound.stream.network" @change="streamNetworkChange"
:dropdown-class-name="themeSwitcher.darkCardClass">
@@ -12,6 +13,7 @@
<a-select-option value="grpc">grpc</a-select-option>
</a-select>
</a-form-item>
</a-divider>
</a-form>
<!-- tcp -->
@@ -43,4 +45,8 @@
<template v-if="inbound.stream.network === 'grpc'">
{{template "form/streamGRPC"}}
</template>
<!-- sockopt -->
<template>
{{template "form/streamSockopt"}}
</template>
{{end}}

View File

@@ -0,0 +1,48 @@
{{define "form/streamSockopt"}}
<a-form layout="inline">
<a-divider dashed style="margin:0;">
<a-form-item label="Transparent Proxy">
<a-switch v-model="inbound.stream.sockoptSwitch"></a-switch>
</a-form-item>
</a-divider>
<table width="100%" class="ant-table-tbody" v-if="inbound.stream.sockoptSwitch">
<tr>
<td>Accept Proxy Protocol</td>
<td>
<a-form-item>
<a-switch v-model="inbound.stream.sockopt.acceptProxyProtocol"></a-switch>
</a-form-item>
</td>
</tr>
<tr>
<td>TCP FastOpen</td>
<td>
<a-form-item>
<a-switch v-model.trim="inbound.stream.sockopt.tcpFastOpen"></a-switch>
</a-form-item>
</td>
</tr>
<tr>
<td>Route Mark</td>
<td>
<a-form-item>
<a-input-number v-model="inbound.stream.sockopt.mark" :min="0"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td>T-Proxy</td>
<td>
<a-form-item>
<a-select v-model="inbound.stream.sockopt.tproxy" style="width: 250px;"
:dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="off">OFF</a-select-option>
<a-select-option value="redirect">Redirect</a-select-option>
<a-select-option value="tproxy">T-Proxy</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
</table>
</a-form>
{{end}}

View File

@@ -1,6 +1,7 @@
{{define "form/tlsSettings"}}
<!-- tls enable -->
<a-form v-if="inbound.canSetTls()" layout="inline">
<a-divider dashed style="margin:0;">
<a-form-item label="TLS">
<a-switch v-model="inbound.tls">
</a-switch>
@@ -8,6 +9,7 @@
<a-form-item v-if="inbound.canEnableReality()" label="Reality">
<a-switch v-model="inbound.reality"></a-switch>
</a-form-item>
</a-divider>
</a-form>
<!-- tls settings -->
@@ -94,12 +96,16 @@
</td>
</tr>
<tr>
<td>Alpn</td>
<td>ALPN</td>
<td>
<a-form-item>
<a-checkbox-group v-model="inbound.stream.tls.alpn">
<a-checkbox v-for="key,value in ALPN_OPTION" :value="key">[[ value ]]</a-checkbox>
</a-checkbox-group>
<a-select
mode="multiple"
style="width: 250px"
:dropdown-class-name="themeSwitcher.darkCardClass"
v-model="inbound.stream.tls.alpn">
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
@@ -174,6 +180,14 @@
</td>
</tr>
</template>
<tr>
<td>ocspStapling</td>
<td>
<a-form-item>
<a-input-number v-model.number="cert.ocspStapling" :min="0"></a-input-number>
</a-form-item>
</td>
</tr>
</template>
</table>
</a-form>

View File

@@ -45,17 +45,28 @@
</template>
</table>
</td></tr>
<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.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>
<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.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>
<table v-if="dbInbound.isSS" style="margin-bottom: 10px; width: 100%;">
<tr>
<td>{{ i18n "encryption" }}</td>
<td><a-tag color="green">[[ inbound.settings.method ]]</a-tag></td>
</tr><tr v-if="inbound.isSS2022">
<td>{{ i18n "password" }}</td>
<td><a-tag color="blue">[[ inbound.settings.password ]]</a-tag></td>
</tr><tr>
<td>{{ i18n "pages.inbounds.network" }}</td>
<td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td>
</tr>
</table>
<template v-if="infoModal.clientSettings">
@@ -163,19 +174,7 @@
</template>
</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>
<template v-if="inbound.protocol == Protocols.SHADOWSOCKS && !inbound.isSSMultiUser">
<template v-if="dbInbound.isSS && !inbound.isSSMultiUser">
<a-divider>URL</a-divider>
<a-row v-for="(link,index) in infoModal.links">
<a-col :span="22"><a-tag color="cyan">[[ link.remark ]]</a-tag><br />[[ link.link ]]</a-col>
@@ -201,17 +200,19 @@
<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%;">
<table v-if="dbInbound.isSocks" style="margin-bottom: 10px; width: 100%;">
<tr>
<th>{{ i18n "password" }} Auth</th>
<th>{{ i18n "pages.inbounds.enable" }} udp</th>
<th>IP</th>
</tr><tr>
</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'">
</tr>
<template v-if="inbound.settings.auth == 'password'">
<tr>
<td> </td>
<td>{{ i18n "username" }}</td>
<td>{{ i18n "password" }}</td>
@@ -220,9 +221,9 @@
<td><a-tag color="blue">[[ account.user ]]</a-tag></td>
<td><a-tag color="green">[[ account.pass ]]</a-tag></td>
</tr>
</template>
</table>
</table>
<table v-if="inbound.protocol == Protocols.HTTP" style="margin-bottom: 10px; width: 100%;">
<table v-if="dbInbound.isHTTP" style="margin-bottom: 10px; width: 100%;">
<tr>
<th> </th>
<th>{{ i18n "username" }}</th>
@@ -233,7 +234,6 @@
<td><a-tag color="green">[[ account.pass ]]</a-tag></td>
</tr>
</table>
</table>
</template>
</a-modal>
<script>
@@ -260,14 +260,15 @@
this.clientSettings = this.settings.clients ? Object.values(this.settings.clients)[index] : null;
this.isExpired = this.inbound.isExpiry(index);
this.clientStats = this.settings.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : [];
remark = this.dbInbound.remark + ( this.clientSettings ? "-" + this.clientSettings.email : '');
remark = [this.dbInbound.remark, ( this.clientSettings ? this.clientSettings.email : '')].filter(Boolean).join('-');
address = this.dbInbound.address;
this.links = [];
if (this.inbound.tls && !ObjectUtil.isArrEmpty(this.inbound.stream.tls.settings.domains)) {
this.inbound.stream.tls.settings.domains.forEach((domain) => {
remarkText = [remark, domain.remark].filter(Boolean).join('-');
this.links.push({
remark: remark + "-" + domain.remark,
link: this.inbound.genLink(domain.domain, remark + "-" + domain.remark, index)
remark: remarkText,
link: this.inbound.genLink(domain.domain, remarkText, index)
});
});
} else {
@@ -291,7 +292,7 @@
},
genSubLink(subID) {
const { domain: host, port, tls: isTLS, path: base } = app.subSettings;
return buildURL({ host, port, isTLS, base, path: subID });
return buildURL({ host, port, isTLS, base, path: subID+'?name='+subID });
}
};

View File

@@ -110,7 +110,7 @@
if (this.inModal.inbound.settings.shadowsockses.length ==0){
this.inModal.inbound.settings.shadowsockses = [new Inbound.ShadowsocksSettings.Shadowsocks()];
}
if (["aes-128-gcm", "aes-256-gcm", "chacha20-poly1305", "xchacha20-poly1305"].includes(this.inModal.inbound.settings.method)) {
if (!this.inModal.inbound.isSS2022) {
this.inModal.inbound.settings.shadowsockses.forEach(client => {
client.method = this.inModal.inbound.settings.method;
})
@@ -139,15 +139,6 @@
inModal.inbound.stream.reality.privateKey = msg.obj.privateKey;
inModal.inbound.stream.reality.settings.publicKey = msg.obj.publicKey;
},
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;
}
},
});

View File

@@ -11,6 +11,9 @@
.ant-col-sm-24 {
margin-top: 10px;
}
tr.hideExpandIcon .ant-table-row-expand-icon {
display: none;
}
</style>
<body>
@@ -121,8 +124,12 @@
</a-radio-group>
<a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
:data-source="searchedInbounds"
:loading="spinning" :scroll="{ x: 1300 }"
:loading="spinning" :scroll="{ x: 1200 }"
:pagination="false"
:expand-icon-as-cell="false"
:expand-row-by-click="false"
:expand-icon-column-index="0"
:row-class-name="dbInbound => (dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess || (dbInbound.isSS && dbInbound.toInbound().isSSMultiUser) ? '' : 'hideExpandIcon')"
style="margin-top: 20px"
@change="() => getDBInbounds()">
<template slot="action" slot-scope="text, dbInbound">
@@ -137,7 +144,7 @@
<a-icon type="qrcode"></a-icon>
{{ i18n "qrCode" }}
</a-menu-item>
<template v-if="dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess || dbInbound.toInbound().isSSMultiUser">
<template v-if="dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess || (dbInbound.isSS && dbInbound.toInbound().isSSMultiUser)">
<a-menu-item key="addClient">
<a-icon type="user-add"></a-icon>
{{ i18n "pages.client.add"}}
@@ -254,6 +261,7 @@
:columns="innerColumns"
:data-source="getInboundClients(record)"
:pagination="false"
style="margin-left: 20px;"
>
{{template "client_table"}}
</a-table>
@@ -263,6 +271,7 @@
:columns="innerTrojanColumns"
:data-source="getInboundClients(record)"
:pagination="false"
style="margin-left: 20px;"
>
{{template "client_table"}}
</a-table>
@@ -278,6 +287,11 @@
{{template "component/themeSwitcher" .}}
<script>
const columns = [{
title: "ID",
align: 'right',
dataIndex: "id",
width: 30,
}, {
title: '{{ i18n "pages.inbounds.operate" }}',
align: 'center',
width: 30,
@@ -287,11 +301,6 @@
align: 'center',
width: 30,
scopedSlots: { customRender: 'enable' },
}, {
title: "ID",
align: 'center',
dataIndex: "id",
width: 20,
}, {
title: '{{ i18n "pages.inbounds.remark" }}',
align: 'center',
@@ -328,8 +337,8 @@
{ 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: 50, scopedSlots: { customRender: 'traffic' } },
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 50, scopedSlots: { customRender: 'expiryTime' } },
{ title: '{{ i18n "pages.inbounds.traffic" }}', width: 60, scopedSlots: { customRender: 'traffic' } },
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 55, scopedSlots: { customRender: 'expiryTime' } },
{ title: 'UUID', width: 120, dataIndex: "id" },
];
@@ -338,8 +347,8 @@
{ 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: 50, scopedSlots: { customRender: 'traffic' } },
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 50, scopedSlots: { customRender: 'expiryTime' } },
{ title: 'Password', width: 120, dataIndex: "password" },
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 55, scopedSlots: { customRender: 'expiryTime' } },
{ title: '{{ i18n "password" }}', width: 165, dataIndex: "password" },
];
const app = new Vue({
@@ -717,8 +726,7 @@
});
},
findIndexOfClient(clients, client) {
firstKey = Object.keys(client)[0];
return clients.findIndex(c => c[firstKey] === client[firstKey]);
return clients.findIndex(item => JSON.stringify(item) === JSON.stringify(client));
},
async addClient(clients, dbInboundId) {
const data = {
@@ -824,6 +832,7 @@
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
inbound = dbInbound.toInbound();
clients = this.getClients(dbInbound.protocol, inbound.settings);
client.enable = !client.enable; // For finding correct index in findIndexOfClient() function
index = this.findIndexOfClient(clients, client);
clients[index].enable = !clients[index].enable;
clientId = this.getClientId(dbInbound.protocol, clients[index]);

View File

@@ -42,7 +42,7 @@
:stroke-color="status.cpu.color"
:class="themeSwitcher.darkCardClass"
:percent="status.cpu.percent"></a-progress>
<div>CPU</div>
<div>CPU ([[ status.cpuCount ]]core)</div>
</a-col>
<a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal"
@@ -92,13 +92,16 @@
<a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass">
{{ i18n "pages.index.operationHours" }}:
<a-tag color="green">[[ formatSecond(status.uptime) ]]</a-tag>
xray
<a-tag color="green">[[ formatSecond(status.appStats.uptime) ]]</a-tag>
os
<a-tooltip>
<template slot="title">
{{ i18n "pages.index.operationHoursDesc" }}
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
<a-tag color="green">[[ formatSecond(status.uptime) ]]</a-tag>
</a-card>
</a-col>
<a-col :sm="24" :md="12">
@@ -131,7 +134,36 @@
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass">
tcp / udp {{ i18n "pages.index.connectionCount" }}: [[ status.tcpCount ]] / [[ status.udpCount ]]
{{ i18n "usage"}}:
Memory [[ sizeFormat(status.appStats.mem) ]] -
Threads [[ status.appStats.threads ]]
</a-tooltip>
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass">
Host: [[ status.hostInfo.hostname ]] -
<template v-if="status.hostInfo.ipv4">IPv4:
<a-tooltip>
<template slot="title">
[[ status.hostInfo.ipv4 ]]
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</template>
<template v-if="status.hostInfo.ipv6">IPv6:
<a-tooltip>
<template slot="title">
[[ status.hostInfo.ipv6 ]]
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</template>
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass">
{{ i18n "pages.index.connectionCount" }}: tcp: [[ status.tcpCount ]] udp: [[ status.udpCount ]]
<a-tooltip>
<template slot="title">
{{ i18n "pages.index.connectionCountDesc" }}
@@ -263,7 +295,7 @@
<a-icon type="warning" style="color: inherit; font-size: 20px;"></a-icon>
[[ backupModal.description ]]
</p>
<a-space direction="horizontal" align="center" style="margin-bottom: 10px;">
<a-space direction="horizontal" style="text-align: center" style="margin-bottom: 10px;">
<a-button type="primary" @click="exportDatabase()">
[[ backupModal.exportText ]]
</a-button>
@@ -315,6 +347,7 @@
class Status {
constructor(data) {
this.cpu = new CurTotal(0, 0);
this.cpuCount = 0;
this.disk = new CurTotal(0, 0);
this.loads = [0, 0, 0];
this.mem = new CurTotal(0, 0);
@@ -324,12 +357,16 @@
this.tcpCount = 0;
this.udpCount = 0;
this.uptime = 0;
this.appUptime = 0;
this.appStats = {threads: 0, mem: 0, uptime: 0};
this.hostInfo = {hostname:"", ipv4: "", ipv6: ""};
this.xray = {state: State.Stop, errorMsg: "", version: "", color: ""};
if (data == null) {
return;
}
this.cpu = new CurTotal(data.cpu, 100);
this.cpuCount = data.cpuCount;
this.disk = new CurTotal(data.disk.current, data.disk.total);
this.loads = data.loads.map(load => toFixed(load, 2));
this.mem = new CurTotal(data.mem.current, data.mem.total);
@@ -339,6 +376,9 @@
this.tcpCount = data.tcpCount;
this.udpCount = data.udpCount;
this.uptime = data.uptime;
this.appUptime = data.appUptime;
this.appStats = data.appStats;
this.hostInfo = data.hostInfo;
this.xray = data.xray;
switch (this.xray.state) {
case State.Running:

View File

@@ -338,6 +338,7 @@
<a-list item-layout="horizontal" :style="themeSwitcher.textStyle">
<setting-list-item type="switch" title='{{ i18n "pages.settings.subEnable"}}' desc='{{ i18n "pages.settings.subEnableDesc"}}' v-model="allSetting.subEnable"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.subEncrypt"}}' desc='{{ i18n "pages.settings.subEncryptDesc"}}' v-model="allSetting.subEncrypt"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.subShowInfo"}}' desc='{{ i18n "pages.settings.subShowInfoDesc"}}' v-model="allSetting.subShowInfo"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.subListen"}}' desc='{{ i18n "pages.settings.subListenDesc"}}' v-model="allSetting.subListen"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.subDomain"}}' desc='{{ i18n "pages.settings.subDomainDesc"}}' v-model="allSetting.subDomain"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.settings.subPort"}}' desc='{{ i18n "pages.settings.subPortDesc"}}' v-model.number="allSetting.subPort"></setting-list-item>

View File

@@ -1,37 +0,0 @@
package job
import (
"x-ui/logger"
"x-ui/web/service"
)
type CheckInboundJob struct {
xrayService service.XrayService
inboundService service.InboundService
}
func NewCheckInboundJob() *CheckInboundJob {
return new(CheckInboundJob)
}
func (j *CheckInboundJob) Run() {
needRestart, count, err := j.inboundService.DisableInvalidClients()
if err != nil {
logger.Warning("Error in disabling invalid clients:", err)
} else if count > 0 {
logger.Debugf("%v clients disabled", count)
if needRestart {
j.xrayService.SetToNeedRestart()
}
}
needRestart, count, err = j.inboundService.DisableInvalidInbounds()
if err != nil {
logger.Warning("Error in disabling invalid inbounds:", err)
} else if count > 0 {
logger.Debugf("%v inbounds disabled", count)
if needRestart {
j.xrayService.SetToNeedRestart()
}
}
}

View File

@@ -24,14 +24,12 @@ func (j *XrayTrafficJob) Run() {
logger.Warning("get xray traffic failed:", err)
return
}
err = j.inboundService.AddTraffic(traffics)
err, needRestart := j.inboundService.AddTraffic(traffics, clientTraffics)
if err != nil {
logger.Warning("add traffic failed:", err)
}
err = j.inboundService.AddClientTraffic(clientTraffics)
if err != nil {
logger.Warning("add client traffic failed:", err)
if needRestart {
j.xrayService.SetToNeedRestart()
}
}

View File

@@ -194,38 +194,6 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
return inbound, needRestart, err
}
func (s *InboundService) AddInbounds(inbounds []*model.Inbound) error {
for _, inbound := range inbounds {
exist, err := s.checkPortExist(inbound.Port, 0)
if err != nil {
return err
}
if exist {
return common.NewError("Port already exists:", inbound.Port)
}
}
db := database.GetDB()
tx := db.Begin()
var err error
defer func() {
if err == nil {
tx.Commit()
} else {
tx.Rollback()
}
}()
for _, inbound := range inbounds {
err = tx.Save(inbound).Error
if err != nil {
return err
}
}
return nil
}
func (s *InboundService) DelInbound(id int) (bool, error) {
db := database.GetDB()
@@ -659,35 +627,8 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
return needRestart, tx.Save(oldInbound).Error
}
func (s *InboundService) AddTraffic(traffics []*xray.Traffic) error {
if len(traffics) == 0 {
return nil
}
// 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 nil
})
return err
}
func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err error) {
if len(traffics) == 0 {
return nil
}
func (s *InboundService) AddTraffic(inboundTraffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) {
var err error
db := database.GetDB()
tx := db.Begin()
@@ -698,13 +639,64 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
tx.Commit()
}
}()
err = s.addInboundTraffic(tx, inboundTraffics)
if err != nil {
return err, false
}
err = s.addClientTraffic(tx, clientTraffics)
if err != nil {
return err, false
}
needRestart1, count, err := s.disableInvalidClients(tx)
if err != nil {
logger.Warning("Error in disabling invalid clients:", err)
} else if count > 0 {
logger.Debugf("%v clients disabled", count)
}
needRestart2, count, err := s.disableInvalidInbounds(tx)
if err != nil {
logger.Warning("Error in disabling invalid inbounds:", err)
} else if count > 0 {
logger.Debugf("%v inbounds disabled", count)
}
return nil, (needRestart1 || needRestart2)
}
func (s *InboundService) addInboundTraffic(tx *gorm.DB, traffics []*xray.Traffic) error {
if len(traffics) == 0 {
return nil
}
var err error
for _, traffic := range traffics {
if traffic.IsInbound {
err = tx.Model(&model.Inbound{}).Where("tag = ?", traffic.Tag).
Updates(map[string]interface{}{
"up": gorm.Expr("up + ?", traffic.Up),
"down": gorm.Expr("down + ?", traffic.Down),
}).Error
if err != nil {
return err
}
}
}
return nil
}
func (s *InboundService) addClientTraffic(tx *gorm.DB, traffics []*xray.ClientTraffic) (err error) {
if len(traffics) == 0 {
return nil
}
emails := make([]string, 0, len(traffics))
for _, traffic := range traffics {
emails = append(emails, traffic.Email)
}
dbClientTraffics := make([]*xray.ClientTraffic, 0, len(traffics))
err = db.Model(xray.ClientTraffic{}).Where("email IN (?)", emails).Find(&dbClientTraffics).Error
err = tx.Model(xray.ClientTraffic{}).Where("email IN (?)", emails).Find(&dbClientTraffics).Error
if err != nil {
return err
}
@@ -789,14 +781,13 @@ func (s *InboundService) adjustTraffics(tx *gorm.DB, dbClientTraffics []*xray.Cl
return dbClientTraffics, nil
}
func (s *InboundService) DisableInvalidInbounds() (bool, int64, error) {
db := database.GetDB()
func (s *InboundService) disableInvalidInbounds(tx *gorm.DB) (bool, int64, error) {
now := time.Now().Unix() * 1000
needRestart := false
if p != nil {
var tags []string
err := db.Table("inbounds").
err := tx.Table("inbounds").
Select("inbounds.tag").
Where("((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ?", now, true).
Scan(&tags).Error
@@ -816,7 +807,7 @@ func (s *InboundService) DisableInvalidInbounds() (bool, int64, error) {
s.xrayApi.Close()
}
result := db.Model(model.Inbound{}).
result := tx.Model(model.Inbound{}).
Where("((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ?", now, true).
Update("enable", false)
err := result.Error
@@ -824,8 +815,7 @@ func (s *InboundService) DisableInvalidInbounds() (bool, int64, error) {
return needRestart, count, err
}
func (s *InboundService) DisableInvalidClients() (bool, int64, error) {
db := database.GetDB()
func (s *InboundService) disableInvalidClients(tx *gorm.DB) (bool, int64, error) {
now := time.Now().Unix() * 1000
needRestart := false
@@ -835,7 +825,7 @@ func (s *InboundService) DisableInvalidClients() (bool, int64, error) {
Email string
}
err := db.Table("inbounds").
err := tx.Table("inbounds").
Select("inbounds.tag, client_traffics.email").
Joins("JOIN client_traffics ON inbounds.id = client_traffics.inbound_id").
Where("((client_traffics.total > 0 AND client_traffics.up + client_traffics.down >= client_traffics.total) OR (client_traffics.expiry_time > 0 AND client_traffics.expiry_time <= ?)) AND client_traffics.enable = ?", now, true).
@@ -855,7 +845,7 @@ func (s *InboundService) DisableInvalidClients() (bool, int64, error) {
}
s.xrayApi.Close()
}
result := db.Model(xray.ClientTraffic{}).
result := tx.Model(xray.ClientTraffic{}).
Where("((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ?", now, true).
Update("enable", false)
err := result.Error

View File

@@ -39,9 +39,10 @@ const (
)
type Status struct {
T time.Time `json:"-"`
Cpu float64 `json:"cpu"`
Mem struct {
T time.Time `json:"-"`
Cpu float64 `json:"cpu"`
CpuCount int `json:"cpuCount"`
Mem struct {
Current uint64 `json:"current"`
Total uint64 `json:"total"`
} `json:"mem"`
@@ -70,6 +71,16 @@ type Status struct {
Sent uint64 `json:"sent"`
Recv uint64 `json:"recv"`
} `json:"netTraffic"`
AppStats struct {
Threads uint32 `json:"threads"`
Mem uint64 `json:"mem"`
Uptime uint64 `json:"uptime"`
} `json:"appStats"`
HostInfo struct {
HostName string `json:"hostname"`
Ipv4 string `json:"ipv4"`
Ipv6 string `json:"ipv6"`
} `json:"hostInfo"`
}
type Release struct {
@@ -176,6 +187,36 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
}
status.Xray.Version = s.xrayService.GetXrayVersion()
var rtm runtime.MemStats
runtime.ReadMemStats(&rtm)
status.AppStats.Mem = rtm.Sys
status.AppStats.Threads = uint32(runtime.NumGoroutine())
status.CpuCount = runtime.NumCPU()
if p.IsRunning() {
status.AppStats.Uptime = p.GetUptime()
} else {
status.AppStats.Uptime = 0
}
status.HostInfo.HostName, _ = os.Hostname()
// get ip address
netInterfaces, _ := net.Interfaces()
for i := 0; i < len(netInterfaces); i++ {
if len(netInterfaces[i].Flags) > 2 && netInterfaces[i].Flags[0] == "up" && netInterfaces[i].Flags[1] != "loopback" {
addrs := netInterfaces[i].Addrs
for _, address := range addrs {
if strings.Contains(address.Addr, ".") {
status.HostInfo.Ipv4 += address.Addr + " "
} else if address.Addr[0:6] != "fe80::" {
status.HostInfo.Ipv6 += address.Addr + " "
}
}
}
}
return status
}

View File

@@ -51,6 +51,7 @@ var defaultValueMap = map[string]string{
"subKeyFile": "",
"subUpdates": "12",
"subEncrypt": "true",
"subShowInfo": "false",
}
type SettingService struct {
@@ -377,6 +378,10 @@ func (s *SettingService) GetSubEncrypt() (bool, error) {
return s.getBool("subEncrypt")
}
func (s *SettingService) GetSubShowInfo() (bool, error) {
return s.getBool("subShowInfo")
}
func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error {
if err := allSetting.CheckValid(); err != nil {
return err

View File

@@ -69,7 +69,7 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
return nil, err
}
s.inboundService.DisableInvalidClients()
s.inboundService.AddTraffic(nil, nil)
inbounds, err := s.inboundService.GetAllInbounds()
if err != nil {

View File

@@ -127,7 +127,6 @@
"network" = "Network"
"destinationPort" = "Destination Port"
"targetAddress" = "Target Address"
"disableInsecureEncryption" = "Disable Insecure Encryption"
"monitorDesc" = "Leave blank by default"
"meansNoLimit" = "Means No Limit"
"totalFlow" = "Total Flow"
@@ -268,7 +267,8 @@
"subUpdatesDesc" = "Interval hours between updates in client application"
"subEncrypt" = "Encrypt configs"
"subEncryptDesc" = "Encrypt the returned configs in subscription"
"subShowInfo" = "Show usage info"
"subShowInfoDesc" = "Show remianed traffic and date after config name"
[pages.settings.templates]
"title" = "Templates"

View File

@@ -127,7 +127,6 @@
"network" = "شبکه"
"destinationPort" = "پورت مقصد"
"targetAddress" = "آدرس مقصد"
"disableInsecureEncryption" = "غیرفعال سازی رمزگذاری ناامن"
"monitorDesc" = "به طور پیش فرض خالی بگذارید"
"meansNoLimit" = "یعنی بدون محدودیت"
"totalFlow" = "کل ترافیک"
@@ -267,6 +266,8 @@
"subUpdatesDesc" = "ساعت های فاصله بین به روز رسانی در برنامه کاربر"
"subEncrypt" = "رمزگذاری کانفیگ ها"
"subEncryptDesc" = "رمزگذاری کانفیگ های بازگشتی سابسکریپشن"
"subShowInfo" = "نمایش اطلاعات مصرف"
"subShowInfoDesc" = "ترافیک و زمان باقیمانده را در هر کانفیگ نمایش میدهد"
[pages.settings.templates]
"title" = "الگوها"

View File

@@ -1,94 +1,94 @@
"username" = "имя пользователя"
"password" = "пароль"
"login" = "логин"
"confirm" = "подтвердить"
"cancel" = "отмена"
"close" = "закрыть"
"copy" = "копировать"
"copied" = "скопировано"
"download" = "скачать"
"remark" = "примечание"
"enable" = "включить"
"protocol" = "протокол"
"search" = "поиск"
"username" = "Имя пользователя"
"password" = "Пароль"
"login" = "Войти"
"confirm" = "Подтвердить"
"cancel" = "Отмена"
"close" = "Закрыть"
"copy" = "Копировать"
"copied" = "Скопировано"
"download" = "Скачать"
"remark" = "Примечание"
"enable" = "Включить"
"protocol" = "Протокол"
"search" = "Поиск"
"filter" = "Фильтр"
"loading" = "загрузка"
"second" = "секунда"
"minute" = "минута"
"hour" = "час"
"day" = "день"
"loading" = "Загрузка"
"second" = "Секунда"
"minute" = "Минута"
"hour" = "Час"
"day" = "День"
"check" = "просмотр"
"indefinite" = "бессрочно"
"unlimited" = "безлимитно"
"none" = "пусто"
"indefinite" = "Бессрочно"
"unlimited" = "Безлимитно"
"none" = "Пусто"
"qrCode" = "QR-код"
"info" = "больше информации"
"edit" = "изменить"
"delete" = "удалить"
"reset" = "обнулить"
"copySuccess" = "скопировано"
"sure" = "да"
"info" = "Больше информации"
"edit" = "Изменить"
"delete" = "Удалить"
"reset" = "Обнулить"
"copySuccess" = "Успешно скопировано"
"sure" = "Да"
"encryption" = "Шифрование"
"transmission" = "протокол передачи"
"host" = "хост"
"path" = "путь"
"camouflage" = "маскировка"
"status" = "статус"
"enabled" = "включено"
"disabled" = "отключено"
"depleted" = "исчерпано"
"depletingSoon" = "почти исчерпано"
"domainName" = "домен"
"monitor" = "порт IP"
"certificate" = "сертификат"
"fail" = "неудача"
"success" = "успешно"
"getVersion" = "узнать версию"
"transmission" = "Протокол передачи"
"host" = "Хост"
"path" = "Путь"
"camouflage" = "Маскировка"
"status" = "Статус"
"enabled" = "Включено"
"disabled" = "Отключено"
"depleted" = "Отключены"
"depletingSoon" = "Почти отключены"
"domainName" = "Домен"
"monitor" = "Прослушиваемый IP"
"certificate" = "Сертификат"
"fail" = "Неудачно"
"success" = "Успешно"
"getVersion" = "Узнать версию"
"install" = "установка"
"clients" = "клиенты"
"usage" = "использование"
"remained" = "остались"
"clients" = "Клиенты"
"usage" = "Использовано"
"remained" = "Осталось"
"secAlertTitle" = "Предупреждение системы безопасности"
"secAlertSsl" = "Это соединение не защищено. Пожалуйста, воздержитесь от ввода конфиденциальной информации, пока TLS не будет активирован для защиты данных"
"secAlertSsl" = "Это соединение не защищено. Пожалуйста, воздержитесь от ввода конфиденциальной информации до тех пор, пока не будет активирован TLS для защиты данных"
[menu]
"dashboard" = "статус системы"
"inbounds" = "пользователи"
"settings" = "настройки"
"logout" = "выход"
"link" = "другое"
"dashboard" = "Статус системы"
"inbounds" = "Подключения"
"settings" = "Настройки"
"logout" = "Выйти"
"link" = "Другое"
[pages.login]
"title" = "логин"
"loginAgain" = "Время пребывания в сети вышло. Пожалуйста, войдите в систему снова"
"title" = "Войти"
"loginAgain" = "Время сессии истекло. Пожалуйста, войдите в систему снова"
[pages.login.toasts]
"invalidFormData" = "Недопустимый формат данных"
"emptyUsername" = "Введите имя пользователя"
"emptyPassword" = "Введите пароль"
"wrongUsernameOrPassword" = "Неверное имя пользователя или пароль"
"successLogin" = "успешный вход"
"successLogin" = "Успешный вход"
[pages.index]
"title" = "статус системы"
"memory" = "память"
"hard" = "жесткий диск"
"xrayStatus" = "статус Xray"
"stopXray" = "стоп"
"restartXray" = "рестарт Xray"
"xraySwitch" = "переключить версию"
"title" = "Статус системы"
"memory" = "ОЗУ"
"hard" = "Место на диске"
"xrayStatus" = "Статус Xray"
"stopXray" = "Остановка"
"restartXray" = "Перезапуск Xray"
"xraySwitch" = "Сменить версию"
"xraySwitchClick" = "Выберите желаемую версию"
"xraySwitchClickDesk" = "Выбирайте внимательно, так как старые версии могут быть несовместимы с текущими конфигурациями"
"operationHours" = "Часы работы"
"operationHoursDesc" = "Аптайм системы: время системы в сети"
"operationHours" = "Время работы"
"operationHoursDesc" = "Время работы системы: время с момента запуска."
"systemLoad" = "Системная нагрузка"
"connectionCount" = "количество соединений"
"connectionCount" = "Количество соединений"
"connectionCountDesc" = "Всего подключений по всем сетям»"
"upSpeed" = "Общая скорость upload"
"downSpeed" = "Общая скорость download"
"upSpeed" = "Общая скорость отдачи"
"downSpeed" = "Общая скорость получения"
"totalSent" = "Общий объем загруженных данных с момента запуска системы"
"totalReceive" = "Общий объем полученных данных с момента запуска системы"
"xraySwitchVersionDialog" = "переключить версию Xray"
"xraySwitchVersionDialog" = "Переключить версию Xray"
"xraySwitchVersionDialogDesc" = "Вы точно хотите сменить версию Xray?"
"dontRefresh" = "Установка. Не обновляйте эту страницу"
"logs" = "Логи"
@@ -100,39 +100,38 @@
"importDatabase" = "Импорт базы данных"
[pages.inbounds]
"title" = "пользователи"
"totalDownUp" = "Всего входящих/исходящих"
"title" = "Подключения"
"totalDownUp" = "Всего получено/отправлено"
"totalUsage" = "Всего использовано"
"inboundCount" = "Количество пользователей"
"inboundCount" = "Количество подключений"
"operate" = "Меню"
"enable" = "Включить"
"remark" = "Примечание"
"protocol" = "Протокол"
"port" = "Порт"
"traffic" = "Траффик"
"traffic" = "Трафик"
"details" = "Подробнее"
"transportConfig" = "Перенести"
"expireDate" = "Дата окончания"
"resetTraffic" = "Обнулить траффик"
"addInbound" = "Добавить пользователя"
"resetTraffic" = "Обнулить трафик"
"addInbound" = "Добавить подключение"
"generalActions" = "Общие действия"
"create" = "Создать"
"update" = "Обновить"
"modifyInbound" = "Изменить данные"
"deleteInbound" = "Удалить пользователя"
"deleteInboundContent" = "Подтвердите удаление пользователя?"
"resetTrafficContent" = "Подтвердите обнуление траффика?"
"deleteInbound" = "Удалить подключение"
"deleteInboundContent" = "Вы уверены, что хотите удалить подключение?"
"resetTrafficContent" = "Подтвердите обнуление трафика?"
"copyLink" = "Копировать ключ"
"address" = "Адрес"
"network" = "Сеть"
"destinationPort" = "Порт назначения"
"targetAddress" = "Целевой адрес"
"disableInsecureEncryption" = "Отключить небезопасное шифрование"
"monitorDesc" = "Оставьте пустым по умолчанию"
"meansNoLimit" = "Значит без ограничений"
"totalFlow" = "Общий расход"
"leaveBlankToNeverExpire" = "Оставьте пустым, чтобы никогда не истекать"
"noRecommendKeepDefault" = "Нет требований для сохранения настроек по умолчанию"
"leaveBlankToNeverExpire" = "Оставьте пустым, чтобы сделать бессрочно"
"noRecommendKeepDefault" = "Нет особых требований для сохранения настроек по умолчанию"
"certificatePath" = "Путь файла сертификата"
"certificateContent" = "Содержимое файла сертификата"
"publicKeyPath" = "Путь к публичному ключу"
@@ -143,21 +142,21 @@
"client" = "Клиент"
"export" = "Поделиться ключом"
"clone" = "Клонировать"
"cloneInbound" = "Клонировать пользователя"
"cloneInboundContent" = "Все настройки этого пользователя, кроме порта, порт прослушки и клиентов, будут клонированы"
"cloneInbound" = "Клонировать подключение"
"cloneInboundContent" = "Все настройки этого подключения, кроме порта, порт прослушки и клиентов, будут клонированы"
"cloneInboundOk" = "Клонировать"
"resetAllTraffic" = "Обнулить весь траффик"
"resetAllTrafficTitle" = "Обнуление всего траффика"
"resetAllTrafficContent" = "Подтверждаете обнуление всего траффика пользователей?"
"resetInboundClientTraffics" = "Обнулить траффик пользователей"
"resetInboundClientTrafficTitle" = "Обнуление траффика пользователей"
"resetInboundClientTrafficContent" = "Вы уверены, что хотите обнулить весь трафик для этих пользователей?"
"resetAllClientTraffics" = "Обнулить весь траффик пользователей"
"resetAllClientTrafficTitle" = "Обнуление всего траффика пользователей"
"resetAllClientTrafficContent" = "Подтверждаете обнуление всего траффика пользователей?"
"delDepletedClients" = "Удалить отключенных пользователей"
"delDepletedClientsTitle" = "Удаление отключенных пользователей"
"delDepletedClientsContent" = "Подтверждаете удаление отключенных пользователей?"
"resetAllTraffic" = "Обнулить весь трафик"
"resetAllTrafficTitle" = "Обнуление всего трафика"
"resetAllTrafficContent" = "Вы уверены, что хотите сбросить трафик всех подключений?"
"resetInboundClientTraffics" = "Обнулить трафик клиентов"
"resetInboundClientTrafficTitle" = "Обнуление трафика клиентов"
"resetInboundClientTrafficContent" = "Вы уверены, что хотите сбросить весь трафик для клиентов этого подключения?"
"resetAllClientTraffics" = "Обнулить весь трафик клиентов"
"resetAllClientTrafficTitle" = "Обнуление всего трафика клиентов"
"resetAllClientTrafficContent" = "Вы уверены, что хотите сбросить трафик для всех клиентов?"
"delDepletedClients" = "Удалить отключенных клиентов"
"delDepletedClientsTitle" = "Удаление отключенных клиентов"
"delDepletedClientsContent" = "Вы уверены, что хотите удалить всех отключенных клиентов?"
"email" = "Email"
"emailDesc" = "Пожалуйста, укажите уникальный Email"
"setDefaultCert" = "Установить сертификат с панели"
@@ -165,12 +164,12 @@
"subscriptionDesc" = "вы можете найти свою ссылку подписки в разделе «Подробнее», также вы можете использовать одно и то же имя для нескольких конфигов"
[pages.client]
"add" = "Добавить пользователя"
"edit" = "Редактировать пользователя"
"submitAdd" = "Добавить пользователя"
"add" = "Добавить клиента"
"edit" = "Редактировать клиента"
"submitAdd" = "Добавить клиента"
"submitEdit" = "Сохранить изменения"
"clientCount" = "Количество пользователей"
"bulk" = "Добавить несколько"
"clientCount" = "Количество клиентов"
"bulk" = "Добавить несколько клиентов"
"method" = "Метод"
"first" = "Первый"
"last" = "Последний"
@@ -184,18 +183,18 @@
"obtain" = "Получить"
[pages.inbounds.stream.general]
"requestHeader" = "Требуется заголовок"
"requestHeader" = "Заголовок запроса"
"name" = "Имя"
"value" = "Значение"
[pages.inbounds.stream.tcp]
"requestVersion" = "Требуется версия"
"requestMethod" = "Требуется метод"
"requestPath" = "Требуется путь"
"responseVersion" = "Указать версию"
"responseStatus" = "Указать статус"
"responseStatusDescription" = "Указать примечание статуса"
"responseHeader" = "Указать заголовок"
"requestVersion" = "Версия запроса"
"requestMethod" = "Метод запроса"
"requestPath" = "Петь запроса"
"responseVersion" = "Версия ответа"
"responseStatus" = "Статус ответа"
"responseStatusDescription" = "Описание статуса ответа"
"responseHeader" = "Заголовок ответа"
[pages.inbounds.stream.quic]
"encryption" = "Шифрование"
@@ -203,113 +202,115 @@
[pages.settings]
"title" = "Настройки"
"save" = "Сохранить"
"infoDesc" = "Каждое изменение здесь необходимо сохранить и перезапустить панель, чтобы оно вступило в силу"
"restartPanel" = "Рестарт панели"
"restartPanelDesc" = "Подтвердите рестарт панели? ОК для рестарта панели через 3 сек. Если вы не можете пользоваться панелью после рестарта, пожалуйста, посмотрите лог панели на сервере"
"infoDesc" = "Все внесенные здесь изменения должны быть сохранены. Чтобы изменения вступили в силу, перезапустите панель."
"restartPanel" = "Перезапуск панели"
"restartPanelDesc" = "Вы уверены, что хотите перезапустить панель? Нажмите OK для перезапуска через 3 секунды. Если после перезапуска не удается получить доступ к панели, просмотрите информацию журнала панели на сервере."
"resetDefaultConfig" = "Сбросить всё по-умолчанию"
"panelConfig" = "Настройки панели"
"userSettings" = "Настройки безопасности"
"xrayConfiguration" = "Конфигурация Xray"
"TGBotSettings" = "Настройки Телеграм-бота"
"panelListeningIP" = "IP-порт панели"
"panelListeningIPDesc" = "Оставьте пустым для работы с любого IP. Перезагрузите панель для применения настроек"
"panelListeningIP" = "IP-адрес прослушивания панели"
"panelListeningIPDesc" = "Оставьте пустым, чтобы прослушивать все IP-адреса."
"panelListeningDomain" = "Домен прослушивания панели"
"panelListeningDomainDesc" = "По умолчанию оставьте пустым, чтобы отслеживать все домены и IP-адреса"
"panelListeningDomainDesc" = "Оставьте пустым, чтобы прослушивать все домены и IP-адреса"
"panelPort" = "Порт панели"
"panelPortDesc" = "Перезагрузите панель для применения настроек"
"panelPortDesc" = "Номер порта для доступа к панели"
"publicKeyPath" = "Путь к файлу публичного ключа сертификата панели"
"publicKeyPathDesc" = "Введите полный путь, начинающийся с «/». Перезагрузите панель для применения настроек"
"publicKeyPathDesc" = "Введите полный путь, начинающийся с «/»."
"privateKeyPath" = "Путь к файлу приватного ключа сертификата панели"
"privateKeyPathDesc" = "Введите полный путь, начинающийся с «/». Перезагрузите панель для применения настроек"
"privateKeyPathDesc" = "Введите полный путь, начинающийся с «/»."
"panelUrlPath" = "Корневой путь URL-адреса панели"
"panelUrlPathDesc" = "Должен начинаться с «/» и заканчиваться на «/». Перезагрузите панель для применения настроек"
"oldUsername" = "Имя пользователя сейчас"
"currentPassword" = "Пароль сейчас"
"panelUrlPathDesc" = "Должен начинаться с «/» и заканчиваться на «/»."
"oldUsername" = "Текущее имя пользователя"
"currentPassword" = "Текущий пароль"
"newUsername" = "Новое имя пользователя"
"newPassword" = "Новый пароль"
"telegramBotEnable" = "Включить Телеграм-бота"
"telegramBotEnableDesc" = "Перезагрузите панель для применения настроек"
"telegramBotEnableDesc" = "Ваш telegram-бот будет взаимодействовать с панелью"
"telegramToken" = "Токен Телеграм-бота"
"telegramTokenDesc" = "Перезагрузите панель для применения настроек"
"telegramTokenDesc" = "Токен, который вы получили от @BotFather"
"telegramChatId" = "Телеграм-ID админа бота"
"telegramChatIdDesc" = "Множественные идентификаторы чата, разделенные запятыми. Чтобы получить свои идентификаторы чатов, используйте @userinfobot или команду '/id' в боте."
"telegramChatIdDesc" = "Несколько идентификаторов чата, разделенных запятой. Используйте @userinfobot или команду '/id' в боте для получения идентификаторов чата."
"telegramNotifyTime" = "Частота уведомлений телеграм-бота"
"telegramNotifyTimeDesc" = "Используйте формат Crontab. Перезагрузите панель для применения настроек"
"telegramNotifyTimeDesc" = "Используйте временной формат Crontab."
"tgNotifyBackup" = "Резервное копирование базы данных"
"tgNotifyBackupDesc" = "Включать файл резервной копии базы данных с уведомлением об отчете. Перезагрузите панель для применения настроек"
"tgNotifyBackupDesc" = "Включение отправки файла резервной копии базы данных с уведомлением об отчете"
"tgNotifyLogin" = "Уведомление о входе"
"tgNotifyLoginDesc" = "Отображает имя пользователя, IP-адрес и время, когда кто-то пытается войти в вашу панель."
"sessionMaxAge" = "Продолжительность сессии"
"sessionMaxAgeDesc" = "Продолжительность сессии в системе (значение: минута)"
"sessionMaxAgeDesc" = "Продолжительность сессии в системе (единица измерения: минута)"
"expireTimeDiff" = "Порог истечения срока сессии для уведомления"
"expireTimeDiffDesc" = "Получение уведомления об истечении срока действия сессии до достижения порогового значения (значение: день)"
"trafficDiff" = "Порог траффика для уведомления"
"trafficDiffDesc" = "Получение уведомления об исчерпании трафика до достижения порога (значение: ГБ)"
"expireTimeDiffDesc" = "Получение уведомления об истечении срока действия сессии до достижения порогового значения (единица измерения: день)"
"trafficDiff" = "Порог трафика для уведомления"
"trafficDiffDesc" = "Получение уведомления об исчерпании трафика до достижения порога (единица измерения: ГБ)"
"tgNotifyCpu" = "Порог нагрузки на ЦП для уведомления"
"tgNotifyCpuDesc" = "Получение уведомления, если нагрузка на ЦП превышает этот порог (значение:%)"
"timeZone" = "Временная зона"
"timeZoneDesc" = "Запланированные задачи выполняются в соответствии со временем в этом часовом поясе. Перезагрузите панель для применения настроек"
"tgNotifyCpuDesc" = "Получение уведомления, если нагрузка на ЦП превышает этот порог (единица измерения:%)"
"timeZone" = "Часовой пояс"
"timeZoneDesc" = "Запланированные задания выполняются в соответствии со временем в данном часовом поясе."
"subSettings" = "Подписка"
"subEnable" = "Включить службу"
"subEnableDesc" = "Функция подписки с отдельной конфигурацией"
"subListen" = "Прослушивание IP"
"subListenDesc" = "Оставьте пустым по умолчанию, чтобы отслеживать все IP-адреса"
"subListen" = "Прослушиваемый IP"
"subListenDesc" = "Оставьте пустым, чтобы прослушивать все IP-адреса"
"subPort" = "Порт подписки"
"subPortDesc" = "Номер порта для обслуживания службы подписки не должен использоваться на сервере"
"subPortDesc" = "Номер порта для прослушивания службы подписки не должен использоваться на сервере"
"subCertPath" = "Путь к файлу открытого ключа сертификата подписки"
"subCertPathDesc" = "Введите абсолютный путь, начинающийся с '/'"
"subKeyPath" = "Путь к файлу закрытого ключа сертификата подписки"
"subKeyPathDesc" = "Введите абсолютный путь, начинающийся с '/'"
"subPath" = "Корневой путь URL-адреса подписки"
"subPathDesc" = "Должен начинаться с '/' и заканчиваться на '/'"
"subDomain" = "Домен прослушивания"
"subDomainDesc" = "Оставьте пустым по умолчанию, чтобы отслеживать все домены и IP-адреса"
"subDomain" = "Домен для прослушивания"
"subDomainDesc" = "Оставьте пустым, чтобы прослушивать все домены и IP-адреса"
"subUpdates" = "Интервалы обновления подписки"
"subUpdatesDesc" = "Часовой интервал между обновлениями в клиентском приложении"
"subEncrypt" = "Шифровать конфиги"
"subEncryptDesc" = "Шифровать возвращенные конфиги в подписке"
"subEncrypt" = "Шифрование конфигураций"
"subEncryptDesc" = "Шифрование возвращаемых конфигураций в подписке"
"subShowInfo" = "Показать информацию об использовании"
"subShowInfoDesc" = "Показывать восстановленный трафик и дату после имени конфигурации"
[pages.settings.templates]
"title" = "Шаблоны"
"basicTemplate" = "Базовые шаблоны"
"advancedTemplate" = "Расширенные шаблоны"
"completeTemplate" = "Конфигурация шаблона"
"completeTemplate" = "Итоговый шаблон"
"generalConfigs" = "Основные настройки"
"generalConfigsDesc" = "Эти параметры не позволят пользователям подключаться к определенным протоколам и веб-сайтам"
"blockConfigs" = "Блокировка конфигураций"
"generalConfigsDesc" = "Общие настройки"
"blockConfigs" = "Блокирующие конфигурации"
"blockConfigsDesc" = "Эти параметры не позволят пользователям подключаться к определенным протоколам и веб-сайтам."
"blockCountryConfigs" = "Заблокировать конфигурации страны"
"blockCountryConfigs" = "Конфигурация блокировки стран"
"blockCountryConfigsDesc" = "Эти параметры не позволят пользователям подключаться к доменам определенной страны."
"directCountryConfigs" = "Прямые настройки страны"
"directCountryConfigs" = "Прямые настройки стран"
"directCountryConfigsDesc" = "Эти параметры будут подключать пользователей напрямую к доменам определенной страны."
"ipv4Configs" = "Настройки IPv4 "
"ipv4Configs" = "Настройки IPv4"
"ipv4ConfigsDesc" = "Эти параметры будут маршрутизироваться к целевым доменам только через IPv4"
"xrayConfigTemplate" = "Шаблон конфигурации Xray"
"xrayConfigTemplateDesc" = "Создание файла конфигурации Xray на основе этого шаблона. Перезагрузите панель для применения настроек"
"xrayConfigTemplateDesc" = "Создание файла конфигурации Xray на основе этого шаблона."
"xrayConfigFreedomStrategy" = "Настроить стратегию протокола Freedom"
"xrayConfigFreedomStrategyDesc" = "Установить стратегию вывода сети в протоколе Freedom"
"xrayConfigRoutingStrategy" = "Настроить доменную стратегию маршрутизации"
"xrayConfigRoutingStrategyDesc" = "Установить общую стратегию маршрутизации разрешения DNS"
"xrayConfigTorrent" = "Запретить использование BitTorrent"
"xrayConfigTorrentDesc" = "Измените конфигурацию, чтобы пользователи не использовали BitTorrent. Перезагрузите панель для применения настроек"
"xrayConfigTorrentDesc" = "Измените конфигурацию, чтобы пользователи не использовали BitTorrent."
"xrayConfigPrivateIp" = "Запрет частных диапазонов IP-адресов для подключения"
"xrayConfigPrivateIpDesc" = "Измените конфигурацию, чтобы избежать подключения к диапазонам частных IP-адресов. Перезагрузите панель для применения настроек"
"xrayConfigAds" = "Бокировка рекламы"
"xrayConfigAdsDesc" = "Измените конфигурацию, чтобы заблокировать рекламу. Перезагрузите панель для применения настроек"
"xrayConfigPrivateIpDesc" = "Измените конфигурацию, чтобы избежать подключения к диапазонам частных IP-адресов."
"xrayConfigAds" = локировка рекламы"
"xrayConfigAdsDesc" = "Измените конфигурацию, чтобы заблокировать рекламу."
"xrayConfigFamily" = "Включить семейную конфигурацию"
"xrayConfigFamilyDesc" = "Избегайте подключения к небезопасным веб-сайтам для всей семьи"
"xrayConfigFamilyDesc" = "Избегать подключения к небезопасным веб-сайтам для всей семьи"
"xrayConfigIRIp" = "Отключить подключение к диапазонам IP-адресов Ирана"
"xrayConfigIRIpDesc" = "Измените конфигурацию, чтобы отключить подключение к диапазонам IP-адресов Ирана. Перезагрузите панель для применения настроек"
"xrayConfigIRIpDesc" = "Измените конфигурацию, чтобы отключить подключение к диапазонам IP-адресов Ирана."
"xrayConfigIRDomain" = "Отключить подключение к доменам Ирана"
"xrayConfigIRDomainDesc" = "Измените конфигурацию, чтобы отключить подключение к доменам Ирана. Перезагрузите панель для применения настроек"
"xrayConfigIRDomainDesc" = "Измените конфигурацию, чтобы отключить подключение к доменам Ирана."
"xrayConfigChinaIp" = "Отключить подключение к диапазонам IP-адресов Китая"
"xrayConfigChinaIpDesc" = "Измените конфигурацию, чтобы отключить подключение к диапазонам IP-адресов Китая. Перезагрузите панель для применения настроек"
"xrayConfigChinaIpDesc" = "Измените конфигурацию, чтобы отключить подключение к диапазонам IP-адресов Китая."
"xrayConfigChinaDomain" = "Отключить подключение к доменам Китая"
"xrayConfigChinaDomainDesc" = "Измените конфигурацию, чтобы отключить подключение к доменам Китая. Перезагрузите панель для применения настроек"
"xrayConfigChinaDomainDesc" = "Измените конфигурацию, чтобы отключить подключение к доменам Китая."
"xrayConfigRussiaIp" = "Отключить подключение к диапазонам IP-адресов России"
"xrayConfigRussiaIpDesc" = "Измените конфигурацию, чтобы отключить соединения с диапазонами IP-адресов России. Перезагрузите панель для применения настроек"
"xrayConfigRussiaIpDesc" = "Измените конфигурацию, чтобы отключить соединения с диапазонами IP-адресов России."
"xrayConfigRussiaDomain" = "Отключить подключение к доменам России"
"xrayConfigRussiaDomainDesc" = "Измените конфигурацию, чтобы избежать подключения к доменам России. Перезагрузите панель для применения настроек"
"xrayConfigRussiaDomainDesc" = "Измените конфигурацию, чтобы избежать подключения к доменам России."
"xrayConfigDirectIRIp" = "Прямое подключение к диапазонам IP-адресов Ирана"
"xrayConfigDirectIRIpDesc" = "Измените шаблон конфигурации для прямого подключения к диапазонам IP-адресов Ирана"
"xrayConfigDirectIRDomain" = "Прямое подключение к доменам Ирана"
@@ -323,16 +324,16 @@
"xrayConfigDirectRussiaDomain" = "Прямое подключение к доменам России"
"xrayConfigDirectRussiaDomainDesc" = "Изменить шаблон конфигурации для прямого подключения к доменам России"
"xrayConfigGoogleIPv4" = "Использовать IPv4 для Google"
"xrayConfigGoogleIPv4Desc" = "Применить маршрутизацию Google для подключения к IPv4. Перезагрузите панель для применения настроек"
"xrayConfigGoogleIPv4Desc" = "Применить маршрутизацию Google для подключения к IPv4."
"xrayConfigNetflixIPv4" = "Использовать IPv4 для Netflix"
"xrayConfigNetflixIPv4Desc" = "Применить маршрутизацию Netflix для подключения к IPv4. Перезагрузите панель для применения настроек"
"xrayConfigNetflixIPv4Desc" = "Применить маршрутизацию Netflix для подключения к IPv4."
"xrayConfigInbounds" = "Конфигурация подключений"
"xrayConfigInboundsDesc" = "Изменение шаблона конфигурации, для подключения определенных пользователей. Перезагрузите панель для применения настроек"
"xrayConfigInboundsDesc" = "Изменение шаблона конфигурации, для подключения определенных пользователей."
"xrayConfigOutbounds" = "Конфигурация исходящих"
"xrayConfigOutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить исходящие пути для этого сервера. Перезагрузите панель для применения настроек"
"xrayConfigOutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить исходящие пути для этого сервера."
"xrayConfigRoutings" = "Настройка правил маршрутизации"
"xrayConfigRoutingsDesc" = "Изменение шаблона конфигурации, для определения правил маршрутизации для этого сервера. Перезагрузите панель для применения настроек"
"manualLists" = "ручные списки"
"xrayConfigRoutingsDesc" = "Изменение шаблона конфигурации, для определения правил маршрутизации для этого сервера."
"manualLists" = "Пользовательские списки"
"manualListsDesc" = "Пожалуйста, используйте формат массива JSON"
"manualBlockedIPs" = "Список заблокированных IP-адресов"
"manualBlockedDomains" = "Список заблокированных доменов"
@@ -350,12 +351,12 @@
[tgbot]
"noResult" = "❗ Нет результатов!"
"wentWrong" = "❌ Что-то пошло не так!"
"noInbounds" = "❗ Входящих соединений не найдено!"
"noInbounds" = "❗ Подключений не найдено!"
"unlimited" = "♾ Неограниченно"
"day" = "День"
"days" = "Дней"
"unknown" = "Неизвестно"
"inbounds" = "Входящие"
"inbounds" = "Подключения"
"clients" = "Клиенты"
[tgbot.commands]
@@ -399,21 +400,21 @@
"upload" = "🔼 Загрузка↑: {{ .Upload }}\r\n"
"download" = "🔽 Скачивание↓: {{ .Download }}\r\n"
"total" = "🔄 Всего: {{ .UpDown }} / {{ .Total }}\r\n"
"exhaustedMsg" = "🚨 Исчерпаны {{ .Type }}:\r\n"
"exhaustedCount" = "🚨 Количество исчерпанных {{ .Type }}:\r\n"
"exhaustedMsg" = "🚨 Истекли {{ .Type }}:\r\n"
"exhaustedCount" = "🚨 Количество истекших {{ .Type }}:\r\n"
"disabled" = "🛑 Отключено: {{ .Disabled }}\r\n"
"depleteSoon" = "🔜 Скоро исчерпание: {{ .Deplete }}\r\n \r\n"
"depleteSoon" = "🔜 Скоро отключатся: {{ .Deplete }}\r\n \r\n"
"backupTime" = "🗄 Время резервного копирования: {{ .Time }}\r\n"
[tgbot.buttons]
"dbBackup" = "Получить резервную копию DB"
"dbBackup" = "Получить резервную копию базы данных"
"serverUsage" = "Использование сервера"
"getInbounds" = "Получить входящие потоки"
"depleteSoon" = "Скоро исчерпание"
"clientUsage" = "Получить использование"
"getInbounds" = "Получить список подключений"
"depleteSoon" = "Скоро отключатся"
"clientUsage" = "Получить статистику"
"commands" = "Команды"
[tgbot.answers]
"getInboundsFailed" = "❌ Не удалось получить входящие потоки."
"askToAddUser" = "Конфигурация не найдена!\r\nВы должны настроить свое телеграм-имя пользователя и попросить вашего администратора добавить его в вашу конфигурацию."
"askToAddUserName" = "Конфигурация не найдена!\r\nПожалуйста, попросите вашего администратора использовать ваше телеграм-имя пользователя в вашей конфигурации(ях).\r\n\r\nВаше имя пользователя: <b>@{{ .TgUserName }}</b>"
"getInboundsFailed" = "❌ Не удалось получить подключения."
"askToAddUser" = "Конфигурация не найдена!\r\nВы должны настроить свое имя пользователя Telegram и попросить вашего администратора добавить его в вашу конфигурацию."
"askToAddUserName" = "Конфигурация не найдена!\r\nПожалуйста, попросите вашего администратора использовать ваше имя пользователя Telegram в вашей конфигурации(ях).\r\n\r\nВаше имя пользователя: <b>@{{ .TgUserName }}</b>"

View File

@@ -127,7 +127,6 @@
"network" = "网络"
"destinationPort" = "目标端口"
"targetAddress" = "目标地址"
"disableInsecureEncryption" = "禁用不安全加密"
"monitorDesc" = "默认留空即可"
"meansNoLimit" = "表示不限制"
"totalFlow" = "总流量"
@@ -268,6 +267,8 @@
"subUpdatesDesc" = "客户端应用程序更新之间的间隔时间"
"subEncrypt" = "加密配置"
"subEncryptDesc" = "在订阅中加密返回的配置"
"subShowInfo" = "显示使用信息"
"subShowInfoDesc" = "在配置名称后显示剩余流量和日期"
[pages.settings.templates]
"title" = "模板"

View File

@@ -244,9 +244,6 @@ func (s *Server) startTask() {
s.cron.AddJob("@every 10s", job.NewXrayTrafficJob())
}()
// Check the inbound traffic every 30 seconds that the traffic exceeds and expires
s.cron.AddJob("@every 30s", job.NewCheckInboundJob())
// Make a traffic condition every day, 8:30
var entry cron.EntryID
isTgbotenabled, err := s.settingService.GetTgbotenabled()

161
x-ui.sh
View File

@@ -403,7 +403,139 @@ show_xray_status() {
fi
}
install_acme() {
cd ~
LOGI "install acme..."
curl https://get.acme.sh | sh
if [ $? -ne 0 ]; then
LOGE "install acme failed"
return 1
else
LOGI "install acme succeed"
fi
return 0
}
ssl_cert_issue_main() {
echo -e "${green}\t1.${plain} Get SSL"
echo -e "${green}\t2.${plain} Revoke"
echo -e "${green}\t3.${plain} Force Renew"
read -p "Choose an option: " choice
case "$choice" in
1) ssl_cert_issue ;;
2)
local domain=""
read -p "Please enter your domain name to revoke the certificate: " domain
~/.acme.sh/acme.sh --revoke -d ${domain}
LOGI "Certificate revoked"
;;
3)
local domain=""
read -p "Please enter your domain name to forcefully renew an SSL certificate: " domain
~/.acme.sh/acme.sh --renew -d ${domain} --force ;;
*) echo "Invalid choice" ;;
esac
}
ssl_cert_issue() {
# check for acme.sh first
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
echo "acme.sh could not be found. we will install it"
install_acme
if [ $? -ne 0 ]; then
LOGE "install acme failed, please check logs"
exit 1
fi
fi
# install socat second
case "${release}" in
ubuntu|debian)
apt update && apt install socat -y ;;
centos)
yum -y update && yum -y install socat ;;
fedora)
dnf -y update && dnf -y install socat ;;
*)
echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n"
exit 1 ;;
esac
if [ $? -ne 0 ]; then
LOGE "install socat failed, please check logs"
exit 1
else
LOGI "install socat succeed..."
fi
# get the domain here,and we need verify it
local domain=""
read -p "Please enter your domain name:" domain
LOGD "your domain is:${domain},check it..."
# here we need to judge whether there exists cert already
local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}')
if [ ${currentCert} == ${domain} ]; then
local certInfo=$(~/.acme.sh/acme.sh --list)
LOGE "system already has certs here,can not issue again,current certs details:"
LOGI "$certInfo"
exit 1
else
LOGI "your domain is ready for issuing cert now..."
fi
# create a directory for install cert
certPath="/root/cert/${domain}"
if [ ! -d "$certPath" ]; then
mkdir -p "$certPath"
else
rm -rf "$certPath"
mkdir -p "$certPath"
fi
# get needed port here
local WebPort=80
read -p "please choose which port do you use,default will be 80 port:" WebPort
if [[ ${WebPort} -gt 65535 || ${WebPort} -lt 1 ]]; then
LOGE "your input ${WebPort} is invalid,will use default port"
fi
LOGI "will use port:${WebPort} to issue certs,please make sure this port is open..."
# NOTE:This should be handled by user
# open the port and kill the occupied progress
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt
~/.acme.sh/acme.sh --issue -d ${domain} --standalone --httpport ${WebPort}
if [ $? -ne 0 ]; then
LOGE "issue certs failed,please check logs"
rm -rf ~/.acme.sh/${domain}
exit 1
else
LOGE "issue certs succeed,installing certs..."
fi
# install cert
~/.acme.sh/acme.sh --installcert -d ${domain} \
--key-file /root/cert/${domain}/privkey.pem \
--fullchain-file /root/cert/${domain}/fullchain.pem
if [ $? -ne 0 ]; then
LOGE "install certs failed,exit"
rm -rf ~/.acme.sh/${domain}
exit 1
else
LOGI "install certs succeed,enable auto renew..."
fi
~/.acme.sh/acme.sh --upgrade --auto-upgrade
if [ $? -ne 0 ]; then
LOGE "auto renew failed, certs details:"
ls -lah cert/*
chmod 755 $certPath/*
exit 1
else
LOGI "auto renew succeed, certs details:"
ls -lah cert/*
chmod 755 $certPath/*
fi
}
ssl_cert_issue_CF() {
echo -E ""
LOGD "******Instructions for use******"
LOGI "This Acme script requires the following data:"
@@ -413,12 +545,14 @@ ssl_cert_issue() {
LOGI "4.The script applies for a certificate. The default installation path is /root/cert "
confirm "Confirmed?[y/n]" "y"
if [ $? -eq 0 ]; then
cd ~
LOGI "Install Acme-Script"
curl https://get.acme.sh | sh
if [ $? -ne 0 ]; then
LOGE "Failed to install acme script"
exit 1
# check for acme.sh first
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
echo "acme.sh could not be found. we will install it"
install_acme
if [ $? -ne 0 ]; then
LOGE "install acme failed, please check logs"
exit 1
fi
fi
CF_Domain=""
CF_GlobalKey=""
@@ -489,7 +623,6 @@ show_usage() {
echo "x-ui enable - Enable x-ui on system startup"
echo "x-ui disable - Disable x-ui on system startup"
echo "x-ui log - Check x-ui logs"
echo "x-ui v2-ui - Migrate v2-ui Account data to x-ui"
echo "x-ui update - Update x-ui"
echo "x-ui install - Install x-ui"
echo "x-ui uninstall - Uninstall x-ui"
@@ -511,19 +644,20 @@ show_menu() {
${green}7.${plain} View current panel settings
————————————————
${green}8.${plain} Start x-ui
${green}9.${plain} stop x-ui
${green}9.${plain} Stop x-ui
${green}10.${plain} Reboot x-ui
${green}11.${plain} Check x-ui state
${green}12.${plain} Check x-ui logs
————————————————
${green}13.${plain} set x-ui Autostart
${green}13.${plain} Set x-ui Autostart
${green}14.${plain} Cancel x-ui Autostart
————————————————
${green}15.${plain} 一A key installation bbr (latest kernel)
${green}16.${plain}Apply for a SSL certificate with one click(acme script)
${green}16.${plain} 一SSL Certificate Management
${green}17.${plain} 一Cloudflare SSL Certificate
"
show_status
echo && read -p "Please enter your selection [0-16]: " num
echo && read -p "Please enter your selection [0-17]: " num
case "${num}" in
0)
@@ -575,7 +709,10 @@ show_menu() {
install_bbr
;;
16)
ssl_cert_issue
ssl_cert_issue_main
;;
17)
ssl_cert_issue_CF
;;
*)
LOGE "Please enter the correct number [0-16]"

View File

@@ -12,6 +12,7 @@ import (
"runtime"
"strings"
"syscall"
"time"
"x-ui/config"
"x-ui/util/common"
@@ -58,16 +59,18 @@ type process struct {
version string
apiPort int
config *Config
lines *queue.Queue
exitErr error
config *Config
lines *queue.Queue
exitErr error
startTime time.Time
}
func newProcess(config *Config) *process {
return &process{
version: "Unknown",
config: config,
lines: queue.New(100),
version: "Unknown",
config: config,
lines: queue.New(100),
startTime: time.Now(),
}
}
@@ -111,6 +114,10 @@ func (p *Process) GetConfig() *Config {
return p.config
}
func (p *Process) GetUptime() uint64 {
return uint64(time.Since(p.startTime).Seconds())
}
func (p *process) refreshAPIPort() {
for _, inbound := range p.config.InboundConfigs {
if inbound.Tag == "api" {