Compare commits

..

247 Commits

Author SHA1 Message Date
MHSanaei
ff8c4b44a8 v1.7.7 2023-08-26 21:29:11 +03:30
MHSanaei
f3280b46fe pic v1.7.7 2023-08-26 21:06:14 +03:30
MHSanaei
0f91e4d5f6 update dependencies 2023-08-26 20:20:21 +03:30
MHSanaei
18617afd43 add tls ocspStapling
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-08-26 18:52:38 +03:30
MHSanaei
731e83a7da Clean legacy vmess
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-08-26 16:58:55 +03:30
MHSanaei
1c1f53267a Add encrypt subscription ON/OFF switch
Co-Authored-By: SudoSpace <79229394+sudospaes@users.noreply.github.com>
2023-08-26 16:54:01 +03:30
MHSanaei
8489f5f528 some changes
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-08-26 16:07:02 +03:30
MHSanaei
872974910c Show ALPN order
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-08-26 15:45:53 +03:30
MHSanaei
147999dd88 [sub] support client-side group name
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-08-26 15:41:52 +03:30
MHSanaei
fe22cbd0e5 Transparent Proxy with sockopt Stream Setting
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-08-26 15:39:18 +03:30
MHSanaei
1277285d08 [db] Enbancement add traffic fully transactional
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-08-26 15:19:51 +03:30
MHSanaei
75df8a05f1 [ss] add ietf methods
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-08-26 15:12:32 +03:30
MHSanaei
38e1d0f94e [sub] improve usage info in Remark
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-08-26 15:11:12 +03:30
dependabot[bot]
71c1a05386 Bump actions/checkout from 3.5.3 to 3.6.0 (#960)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3.5.3 to 3.6.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3.5.3...v3.6.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-25 14:51:41 +03:30
Hamidreza
4a6bd23873 daily clear logs #952 2023-08-24 17:14:51 +03:30
dependabot[bot]
8a1d647547 Bump gorm.io/gorm from 1.25.3 to 1.25.4 (#942)
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-21 14:32:07 +03:30
dependabot[bot]
fb5180a890 Bump github.com/mymmrac/telego from 0.26.0 to 0.26.1 (#943)
Bumps [github.com/mymmrac/telego](https://github.com/mymmrac/telego) from 0.26.0 to 0.26.1.
- [Release notes](https://github.com/mymmrac/telego/releases)
- [Commits](https://github.com/mymmrac/telego/compare/v0.26.0...v0.26.1)

---
updated-dependencies:
- dependency-name: github.com/mymmrac/telego
  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-21 14:31:51 +03:30
quydang
d2c75b94cf Added Vietnamese Language (#939) 2023-08-21 11:32:01 +03:30
MHSanaei
edfc2d8d93 fix switch enable function for clients
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-08-17 15:26:32 +03:30
dependabot[bot]
dd177b19f7 Bump gorm.io/driver/sqlite from 1.5.2 to 1.5.3 (#919)
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 15:21:15 +03:30
MHSanaei
ae7283fc73 fix finding client issue #884
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-08-12 17:18:24 +03:30
dependabot[bot]
51a7c56cae Bump gorm.io/gorm from 1.25.2 to 1.25.3 (#889)
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-12 12:52:09 +03:30
MasterKia
c812168d6e Attribution (#887) 2023-08-10 03:36:42 +03:30
MHSanaei
fa10ae14f7 v1.7.6 2023-08-09 15:30:32 +03:30
MHSanaei
ed53df5ef3 multi user HTTP & Socks inbounds
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-08-09 01:43:58 +03:30
MHSanaei
b541290ded [front] better info modal
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-08-09 01:03:52 +03:30
MHSanaei
792fe6d9ef [front] better info modal
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-08-09 00:58:53 +03:30
MHSanaei
eb0c1dabf1 bash - Cloudflare SSL Certificate
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-08-09 00:52:40 +03:30
MHSanaei
e00c3f1823 add panel usage to main page 2023-08-09 00:37:05 +03:30
MHSanaei
05bc655e16 update dependencies 2023-08-09 00:34:09 +03:30
dependabot[bot]
7cddada8b4 Bump actions/setup-go from 4.0.1 to 4.1.0 (#879)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 4.0.1 to 4.1.0.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v4.0.1...v4.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-08 22:23:08 +03:30
Hamidreza
24eb36715a Add iran.dat when updating xray (#870)
we can download iran.dat, because it's one of the required files in our settings
2023-08-08 22:21:02 +03:30
Hiradpi
22cf278ce2 add Arch Linux support to install.sh (#873)
* Update install.sh

Arch linux support added

* Update install.sh
2023-08-08 22:20:16 +03:30
dependabot[bot]
5ab5986bd0 Bump golang.org/x/text from 0.11.0 to 0.12.0 (#876)
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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-08 22:03:38 +03:30
MHSanaei
f0775abc67 [install] stop service after download
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-08-08 22:01:49 +03:30
MHSanaei
d424b4bc32 [ss] fix 2022 links #878
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-08-08 21:58:41 +03:30
MHSanaei
329889ec00 v1.7.5 2023-08-02 17:19:50 +03:30
MHSanaei
95268dbc61 update dependencies 2023-08-02 17:02:32 +03:30
dependabot[bot]
9a3200c9b5 Bump github.com/mymmrac/telego from 0.25.1 to 0.26.0 (#852)
Bumps [github.com/mymmrac/telego](https://github.com/mymmrac/telego) from 0.25.1 to 0.26.0.
- [Release notes](https://github.com/mymmrac/telego/releases)
- [Commits](https://github.com/mymmrac/telego/compare/v0.25.1...v0.26.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-02 14:23:19 +03:30
TeleDark
c213fb6216 Display 'terminated' in title when service is terminated (#839) 2023-08-02 00:28:51 +03:30
somebodywashere
dd0217b46b IP Limit Tweaks to reduce false bans (#850)
* IP Limit Tweaks to reduce false bans
1) Check IPs every 10s instead of 20s
2) F2B jail: maxretry 3 -> 4, findtime 100 -> 60

* USERS SHOULD UPDATE BANTIME ONCE AFTER UPDATE
to recreate jail for Ip Limit
2023-08-02 00:28:16 +03:30
MMR
b805bf6222 change bootmortis project to MasterKia fork (#849)
* Update x-ui.sh

change bootmortis project to MasterKia fork

* Update DockerInit.sh

replace bootmortis with MasterKia

* Update release.yml

Replace bootmortis with MasterKia
2023-08-02 00:27:12 +03:30
dependabot[bot]
07b9474212 Bump github.com/shirou/gopsutil/v3 from 3.23.6 to 3.23.7 (#845)
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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-01 13:18:29 +03:30
MHSanaei
bf971911d5 log level & syslog
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-07-31 20:11:47 +03:30
MHSanaei
c46ced0c4c fix logs in api
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-07-31 19:52:28 +03:30
MHSanaei
dd3bbbc4af [SS] fix bulk creation
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-07-31 19:49:27 +03:30
dependabot[bot]
a97f90d225 Bump svenstaro/upload-release-action from 2.6.1 to 2.7.0 (#834)
Bumps [svenstaro/upload-release-action](https://github.com/svenstaro/upload-release-action) from 2.6.1 to 2.7.0.
- [Release notes](https://github.com/svenstaro/upload-release-action/releases)
- [Changelog](https://github.com/svenstaro/upload-release-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/svenstaro/upload-release-action/compare/2.6.1...2.7.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-29 16:01:33 +03:30
MHSanaei
6066edd510 sockopt acceptProxyProtocol for h2 , gRPC #773 2023-07-29 15:52:02 +03:30
MHSanaei
eaec9e54ad random password button for kcp , quic 2023-07-28 18:27:04 +03:30
dependabot[bot]
fafcb2e8e7 Bump google.golang.org/grpc from 1.56.2 to 1.57.0 (#823)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.56.2 to 1.57.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.56.2...v1.57.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-27 17:55:28 +03:30
MHSanaei
145ea1e6f1 full multiuser shadowsocks
full multiuser shadowsocks +
fix logs after api changes

Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-07-27 11:58:46 +03:30
MHSanaei
4cfed17650 [api] fix actions for shadowsocks
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-07-27 11:34:46 +03:30
MHSanaei
c2b1fb4855 because of #815 now we can use XTLS/Xray-core
change MHSanaei/Xray-core to XTLS/Xray-core
2023-07-27 11:06:27 +03:30
somebodywashere
09807b39aa No reason to write to BlockedIPs (#815) 2023-07-26 20:03:28 +03:30
Hamidreza
a0ec2f3972 Merge pull request #798 from hamid-gh98/main
Update README.md
2023-07-21 18:46:40 +03:30
Hamidreza Ghavami
e4b1dc20c3 Update README.md 2023-07-21 19:45:20 +04:30
Hamidreza
2f05d4960e silence allowipv6 warning in docker 2023-07-20 21:54:51 +03:30
Hamidreza
e63d2644bd Fix fail2ban inside DockerEntrypoint.sh 2023-07-20 21:48:55 +03:30
MHSanaei
56e4d13179 change date format to days remaining
example:
kkv4fhs4: 5.00GB📊- 6 Days
far6160p: 2.00GB📊- 23 Hours
Co-Authored-By: somebodywashere <68244480+somebodywashere@users.noreply.github.com>
2023-07-19 20:46:06 +03:30
MHSanaei
0dd0ba717f fail2ban status + apt-get purge 2023-07-19 15:06:55 +03:30
Hamidreza
8050330e2e Fix x-ui.sh bug #778 2023-07-19 12:34:38 +03:30
MHSanaei
65e35c1711 v1.7.1 2023-07-18 19:20:32 +03:30
MHSanaei
0311ae4d05 Revert
This reverts commit 9e433ea4c4.
2023-07-18 13:55:01 +03:30
MHSanaei
6f09fae28b new - fakedns
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-07-18 03:20:41 +03:30
MHSanaei
c2e9ee3665 update - shadowsocks
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-07-18 03:19:01 +03:30
MHSanaei
1f78842b70 [feature] using xray API for inbound
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-07-18 02:40:22 +03:30
MHSanaei
81a057d638 set flow to none when we change TCP thanks to @sudospaes
when we change transmission type from TCP to others

Co-Authored-By: SudoSpace <79229394+sudospaes@users.noreply.github.com>
2023-07-18 01:54:26 +03:30
TeleDark
64d17bee70 Show client info in the remark (#765) 2023-07-18 01:45:45 +03:30
MHSanaei
6beb19b945 update dependencies 2023-07-14 21:19:06 +03:30
dependabot[bot]
6b84d39700 Bump github.com/shirou/gopsutil/v3 from 3.23.5 to 3.23.6 (#692)
Bumps [github.com/shirou/gopsutil/v3](https://github.com/shirou/gopsutil) from 3.23.5 to 3.23.6.
- [Release notes](https://github.com/shirou/gopsutil/releases)
- [Commits](https://github.com/shirou/gopsutil/compare/v3.23.5...v3.23.6)

---
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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-04 12:06:21 +03:30
Mazdak Pakaghideh
5f3b91f3f1 changed PUSH to POST in README.md (#689) 2023-07-04 12:05:40 +03:30
MHSanaei
9e433ea4c4 revert x-ui changes
need update for this
2023-07-02 00:42:31 +03:30
MHSanaei
39537f6f44 change height 2023-07-02 00:10:39 +03:30
dependabot[bot]
44b34c437e Bump gorm.io/gorm from 1.25.2-0.20230530020048-26663ab9bf55 to 1.25.2 (#675)
Bumps [gorm.io/gorm](https://github.com/go-gorm/gorm) from 1.25.2-0.20230530020048-26663ab9bf55 to 1.25.2.
- [Release notes](https://github.com/go-gorm/gorm/releases)
- [Commits](https://github.com/go-gorm/gorm/commits/v1.25.2)

---
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-07-01 15:57:07 +03:30
dependabot[bot]
467c5f807e Bump docker/setup-buildx-action from 2.7.0 to 2.8.0 (#674)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2.7.0 to 2.8.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2.7.0...v2.8.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-01 15:56:56 +03:30
Hamidreza
1028319386 🚀 Some improvements for x-ui.sh and ip job (#665) 2023-07-01 15:56:43 +03:30
MHSanaei
f726474a5d v1.7.0 2023-06-25 02:20:41 +03:30
MHSanaei
cd7a790637 change gin-contrib to Calidity
its just a fork for gin-contrib
2023-06-25 01:10:41 +03:30
MHSanaei
4f74f5154b Update x-ui.sh 2023-06-25 01:07:34 +03:30
somebodywashere
6e22aa59e7 Added IP Limit Management to x-ui menu, Tweaked IP Limit to check every 20s (#615)
Co-authored-by: Hamidreza <70919649+hamid-gh98@users.noreply.github.com>
Co-authored-by: Ho3ein <ho3ein.sanaei@gmail.com>
2023-06-25 00:06:18 +03:30
dependabot[bot]
85df1301dc Bump google.golang.org/grpc from 1.56.0 to 1.56.1 (#636)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.56.0 to 1.56.1.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.56.0...v1.56.1)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  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-06-22 12:03:01 +03:30
dependabot[bot]
e92aba0179 Bump github.com/mymmrac/telego from 0.25.0 to 0.25.1 (#635)
Bumps [github.com/mymmrac/telego](https://github.com/mymmrac/telego) from 0.25.0 to 0.25.1.
- [Release notes](https://github.com/mymmrac/telego/releases)
- [Commits](https://github.com/mymmrac/telego/compare/v0.25.0...v0.25.1)

---
updated-dependencies:
- dependency-name: github.com/mymmrac/telego
  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-06-22 12:02:50 +03:30
MHSanaei
85fb2fda5e simplify with show remaining flow
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-06-21 14:31:09 +03:30
MHSanaei
f57e693023 better clients info tables
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-06-21 14:02:13 +03:30
dependabot[bot]
83f6f13b50 Bump github.com/xtls/xray-core from 1.8.1 to 1.8.3 (#621)
Bumps [github.com/xtls/xray-core](https://github.com/xtls/xray-core) from 1.8.1 to 1.8.3.
- [Release notes](https://github.com/xtls/xray-core/releases)
- [Commits](https://github.com/xtls/xray-core/compare/v1.8.1...v1.8.3)

---
updated-dependencies:
- dependency-name: github.com/xtls/xray-core
  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-06-19 14:14:54 +03:30
MHSanaei
b833ed7992 new - tg Login Notification #584 2023-06-17 19:11:16 +03:30
Ho3ein
5188d516e3 new - loglevel (#612) 2023-06-16 18:25:33 +03:30
MHSanaei
97925eeebe fix default language in initLocalizer
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-06-16 13:01:34 +03:30
MHSanaei
1328bb5aba gracefully shutdown xray-core
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-06-16 01:10:49 +03:30
MHSanaei
4cc755c883 fix warning when there is no access.log
after this
if limitip is 0 and there is no access.log on xray config you don't see this warning
access.log doesn't exist in your config.json
-------------
better view on ip log
-------------
update dependencies
2023-06-16 01:08:35 +03:30
somebodywashere
4e89c71095 Removed ipLimit from the panel, changed the code to work with fail2ban (#580)
* Logging for fail2ban service
Removed limitDevice and other unnecessary functions

* Logging for fail2ban service
Removed limitDevice and other unnecessary functions

* fixed shouldCleanLog

* last fix

* reduced ip limit detection frequency to 30 sec (less logging, more precise)
    changed maxretry in fail2ban jail config to 2 to fit above

* fixed check delay

* added 5 seconds delay before cleaning logs
2023-06-15 22:45:34 +03:30
MHSanaei
d40e61fc45 fix and improve 2023-06-14 19:50:19 +03:30
MHSanaei
c0f1a926e5 update - security "none"
security set to none when tls, reality, xtls are not set
its because when there is no security none on trojan link some app like v2rayn set tls for your config
2023-06-14 17:06:56 +03:30
dependabot[bot]
970dd0915e Bump docker/build-push-action from 4.1.0 to 4.1.1 (#604)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 4.1.0 to 4.1.1.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v4.1.0...v4.1.1)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  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-06-14 15:38:57 +03:30
dependabot[bot]
526e2578b0 Bump docker/metadata-action from 4.5.0 to 4.6.0 (#603)
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 4.5.0 to 4.6.0.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](https://github.com/docker/metadata-action/compare/v4.5.0...v4.6.0)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-14 15:38:47 +03:30
MHSanaei
ad9134bc1a removed - alterId
https://github.com/XTLS/Xray-core/pull/2199
9112cfd39c
2023-06-13 17:26:50 +03:30
dependabot[bot]
b5657ab87d Bump golang.org/x/text from 0.9.0 to 0.10.0 (#595)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.9.0 to 0.10.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.9.0...v0.10.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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-13 15:36:33 +03:30
dependabot[bot]
2c7ec3dc8a Bump docker/setup-buildx-action from 2.6.0 to 2.7.0 (#594)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2.6.0 to 2.7.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2.6.0...v2.7.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-13 15:36:21 +03:30
dependabot[bot]
0fe35fde49 Bump docker/build-push-action from 4.0.0 to 4.1.0 (#589)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 4.0.0 to 4.1.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v4.0.0...v4.1.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-12 13:50:16 +03:30
dependabot[bot]
c7658973c3 Bump github.com/mymmrac/telego from 0.24.0 to 0.25.0 (#588)
Bumps [github.com/mymmrac/telego](https://github.com/mymmrac/telego) from 0.24.0 to 0.25.0.
- [Release notes](https://github.com/mymmrac/telego/releases)
- [Commits](https://github.com/mymmrac/telego/compare/v0.24.0...v0.25.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-12 13:50:02 +03:30
MHSanaei
dea61b839c new - acceptProxyProtocol for H2 and gRPC #581 2023-06-09 18:53:57 +03:30
MHSanaei
9063336778 new - Reject Unknown SNI - thanks to @sudospaes 2023-06-09 16:48:50 +03:30
MHSanaei
7fc3d37851 random password button for SS & more
crypto.getRandomValues for uuid
change name randomText to randomLowerAndNum
2023-06-09 16:04:57 +03:30
MHSanaei
81aa3ed10e mtproto removed 2023-06-09 15:58:47 +03:30
dependabot[bot]
ca10623e40 Bump gorm.io/driver/sqlite from 1.5.1 to 1.5.2 (#579)
Bumps [gorm.io/driver/sqlite](https://github.com/go-gorm/sqlite) from 1.5.1 to 1.5.2.
- [Commits](https://github.com/go-gorm/sqlite/compare/v1.5.1...v1.5.2)

---
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-06-09 14:03:09 +03:30
MHSanaei
547e38079f fix error - speedtest
The update command takes no arguments
2023-06-08 16:08:08 +03:30
MHSanaei
29e40a0bce access.log #571
access.log will be clear only when limitip is not 0
2023-06-08 13:50:35 +03:30
dependabot[bot]
4c1fa59453 Bump docker/metadata-action from 4.4.0 to 4.5.0 (#575)
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 4.4.0 to 4.5.0.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](https://github.com/docker/metadata-action/compare/v4.4.0...v4.5.0)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-08 13:35:10 +03:30
dependabot[bot]
84b0471ca0 Bump docker/setup-qemu-action from 2.1.0 to 2.2.0 (#574)
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 2.1.0 to 2.2.0.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v2.1.0...v2.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-08 13:34:55 +03:30
dependabot[bot]
1c785b930f Bump docker/setup-buildx-action from 2.5.0 to 2.6.0 (#573)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2.5.0 to 2.6.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2.5.0...v2.6.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-08 13:34:46 +03:30
dependabot[bot]
c8ea9e43ec Bump docker/login-action from 2.1.0 to 2.2.0 (#572)
Bumps [docker/login-action](https://github.com/docker/login-action) from 2.1.0 to 2.2.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v2.1.0...v2.2.0)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-08 13:34:36 +03:30
MHSanaei
594d682e20 random sub Id
optimize utils
trojan short id fix
index page better view
2023-06-07 12:45:58 +03:30
MHSanaei
70f250dfe1 [feature] using xray api and more
Improve DB performance
[api] backward compatibility: add client by update

Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-06-05 00:32:19 +03:30
MHSanaei
1030bcf321 update dependencies 2023-06-03 19:00:06 +03:30
MHSanaei
fdc1124ea4 some changes
ip limit method back to v1.6.0 method
remove event on getDBClientIps
better show ip on log (",\n")
2023-06-03 18:59:32 +03:30
Tara Rostami
33a598366b Update custom.css (#559) 2023-06-02 23:48:26 +03:30
dependabot[bot]
459c19eee2 Bump github.com/gin-gonic/gin from 1.9.0 to 1.9.1 (#555)
Bumps [github.com/gin-gonic/gin](https://github.com/gin-gonic/gin) from 1.9.0 to 1.9.1.
- [Release notes](https://github.com/gin-gonic/gin/releases)
- [Changelog](https://github.com/gin-gonic/gin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/gin-gonic/gin/compare/v1.9.0...v1.9.1)

---
updated-dependencies:
- dependency-name: github.com/gin-gonic/gin
  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-06-01 13:45:02 +03:30
dependabot[bot]
d73a995257 Bump github.com/shirou/gopsutil/v3 from 3.23.4 to 3.23.5 (#554)
Bumps [github.com/shirou/gopsutil/v3](https://github.com/shirou/gopsutil) from 3.23.4 to 3.23.5.
- [Release notes](https://github.com/shirou/gopsutil/releases)
- [Commits](https://github.com/shirou/gopsutil/compare/v3.23.4...v3.23.5)

---
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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-01 13:44:17 +03:30
MHSanaei
e4e0deeed4 removed - v2-ui migrate 2023-06-01 01:14:54 +03:30
MHSanaei
48dce38e14 forgot linux tag name
Co-Authored-By: somebodywashere <68244480+somebodywashere@users.noreply.github.com>
2023-05-31 23:50:37 +03:30
dependabot[bot]
3eebd7c3e5 Bump svenstaro/upload-release-action from 2.6.0 to 2.6.1 (#552)
Bumps [svenstaro/upload-release-action](https://github.com/svenstaro/upload-release-action) from 2.6.0 to 2.6.1.
- [Release notes](https://github.com/svenstaro/upload-release-action/releases)
- [Changelog](https://github.com/svenstaro/upload-release-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/svenstaro/upload-release-action/compare/2.6.0...2.6.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-31 23:41:29 +03:30
Hamidreza
2fa151d52e Merge pull request #546 from somebodywashere/main
[tgbot] - small bugfix
2023-05-31 15:02:29 +03:30
somebodywashere
c182f48079 [tgbot] - bugfix 2023-05-31 08:37:03 +00:00
Ho3ein
94fad02737 Merge pull request #545 from hamid-gh98/main
🔀 New Feature + Fix URLs + Some Improvements 🛠️🌐
2023-05-31 09:47:02 +03:30
Hamidreza
d694e6eafc FIX tgbot adminIds 2023-05-31 05:01:20 +03:30
Hamidreza Ghavami
2a9cb6d29e Update README.md 2023-05-31 04:25:13 +04:30
Hamidreza Ghavami
c565a429af fix setting 2023-05-31 03:29:59 +04:30
Hamidreza Ghavami
572d912858 Update README.md 2023-05-31 02:57:01 +04:30
Hamidreza Ghavami
6ae80fc992 add /id command to tgbot to get user id 2023-05-31 02:31:00 +04:30
Hamidreza Ghavami
ef7b979d53 move manual list to new tab 2023-05-31 01:59:41 +04:30
Hamidreza Ghavami
b203067dfd fix urls + use the new buildURL func 2023-05-31 01:59:41 +04:30
Hamidreza Ghavami
1c9fc9422e add buildURL func 2023-05-31 01:59:41 +04:30
Hamidreza Ghavami
6c26e40aea fix qrModal.client is null 2023-05-31 01:59:12 +04:30
Hamidreza Ghavami
fe963d91ef some improvements 2023-05-31 01:31:56 +04:30
Hamidreza Ghavami
c9461f1647 Update translations 2023-05-31 01:26:29 +04:30
Hamidreza Ghavami
ea7fe09c27 use the middlewares 2023-05-31 01:24:18 +04:30
Hamidreza Ghavami
8170b65db4 add an option for webDomain 2023-05-31 01:21:14 +04:30
Hamidreza Ghavami
a2d8c98b0d create and move middlewares to seperate folder 2023-05-31 01:13:46 +04:30
somebodywashere
8442836512 [tgbot] - some fixes and improvments (#542) 2023-05-30 15:07:23 +03:30
Ho3ein
331f2adb1c Update README.md 2023-05-29 17:56:13 +03:30
Ho3ein
45a8d6c95e better view 2023-05-29 17:42:49 +03:30
Ho3ein
1f2eb2ca1a Manual install & Upgrade , docker 2023-05-29 17:07:03 +03:30
Ho3ein
de571a2da4 Workflow with Matrix Strategy 2023-05-29 15:57:12 +03:30
Tara Rostami
2c233dffa5 Animated shadow (#540)
* Update antd.min.css

* Update settings.html

* Update custom.css

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

* Update settings.html

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

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

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

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

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

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

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

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

References:
https://github.com/v2fly/domain-list-community/blob/master/data/category-ads
https://github.com/v2fly/domain-list-community/blob/master/data/category-ads-all
2023-05-18 00:48:37 +03:30
MHSanaei
85d42ce94f bug fix - expiry Time 2023-05-17 18:57:34 +03:30
MHSanaei
2f3c3d0ed2 v1.5.0 2023-05-17 18:19:39 +03:30
MHSanaei
7debf96610 fix bug - mobile view 2023-05-17 18:17:12 +03:30
Ho3ein
d8b60c3cd5 Update docker.yml
always use the latest version :D
2023-05-17 16:34:44 +03:30
MHSanaei
f8eb548376 [feature] SpiderX for Reality
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-17 16:10:11 +03:30
MHSanaei
88fc4f81d4 [feature] filter inbound clients
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-17 15:57:47 +03:30
MHSanaei
911f2b0bb5 adguard Family protection 2023-05-17 03:05:36 +03:30
MHSanaei
2d16eabc6e [feature] interactive deplete soon
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-17 02:01:56 +03:30
MHSanaei
be50be75fe [sub] add more headers
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-17 01:07:35 +03:30
MHSanaei
70e7987df5 [feature] multi cert per inbound
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-17 00:55:19 +03:30
MHSanaei
8ceeb454ee random - trojan password 2023-05-17 00:09:32 +03:30
MHSanaei
f311bf1dbf update - ui 2023-05-16 23:40:27 +03:30
MHSanaei
837d7f77a1 update - xray configuration
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-16 23:39:59 +03:30
Ho3ein
d0213ce50b Merge pull request #452 from MHSanaei/dependabot/github_actions/actions/setup-go-4.0.1
Bump actions/setup-go from 4.0.0 to 4.0.1
2023-05-16 14:36:51 +03:30
dependabot[bot]
622e440366 Bump actions/setup-go from 4.0.0 to 4.0.1
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 4.0.0 to 4.0.1.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v4.0.0...v4.0.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-16 10:04:13 +00:00
MHSanaei
1dc5452f1d security issue - CVE-2023-29401
Gin Web Framework does not properly sanitize filename parameter of Context.FileAttachment function

References
gin-gonic/gin#3555
gin-gonic/gin#3556
https://pkg.go.dev/vuln/GO-2023-1737
2023-05-15 15:59:27 +03:30
Ho3ein
a0daf2fae2 Merge pull request #438 from masoud-hidden/main
[tgbot] Some new changes in the bot
2023-05-14 23:58:54 +03:30
Masoud Hidden
a21bdc9396 [tgbot] Fix reply keyboard height 2023-05-14 22:55:01 +03:30
Masoud Hidden
4cf7f75749 [tgbot] Ability to remove telegram user from the client in the bot 2023-05-14 22:43:23 +03:30
Masoud Hidden
bcdac5aad6 [tgbot] Ability to select telegram user for client from bot 2023-05-14 22:07:49 +03:30
Masoud Hidden
3d5f851fce [tgbot] Changed to telego 2023-05-14 18:50:01 +03:30
Ho3ein
5487dc41cc Merge pull request #434 from hamid-gh98/main
[FIX] siderDrawer button functionality + [Update] redirect restart panel
2023-05-14 10:49:30 +03:30
Hamidreza Ghavami
5a908b9f58 FIX redirect after restart panel 2023-05-14 06:19:18 +04:30
Hamidreza Ghavami
61288db11e FIX siderDrawer button function 2023-05-14 06:08:49 +04:30
Hamidreza Ghavami
317f7fe9da FIX sideBar style 2023-05-14 06:08:34 +04:30
Ho3ein
7b5dd2d0ee Merge pull request #432 from hamid-gh98/main
[HOTFIX] Add basePath to Redirect Middleware
2023-05-14 01:51:02 +03:30
Hamidreza Ghavami
b1302c70fb Merge branch 'main' of https://github.com/hamid-gh98/3x-ui into main 2023-05-14 02:32:17 +04:30
Hamidreza Ghavami
addedb1adf HOTFIX redirect middleware to add basePath 2023-05-14 02:31:23 +04:30
MHSanaei
62bb42cfab v1.4.6 2023-05-14 01:10:51 +03:30
MHSanaei
f4be9f234a lang show 2023-05-14 01:09:31 +03:30
MHSanaei
947129a62a update UI - calendar 2023-05-14 01:08:29 +03:30
Ho3ein
66f0a13145 Merge pull request #431 from hamid-gh98/main
[HOTFIX] Redirect `/xui` to `/panel`
2023-05-14 01:07:17 +03:30
Hamidreza Ghavami
9626379731 Update README.md 2023-05-14 01:42:29 +04:30
Hamidreza Ghavami
c2c61cdd5b Add Redirect Middleware for Router 2023-05-14 01:42:08 +04:30
Hamidreza Ghavami
b5ae580d12 Update '/xui/API' to new path '/panel/api' 2023-05-14 01:41:18 +04:30
101 changed files with 7633 additions and 2994 deletions

View File

@@ -11,15 +11,15 @@ jobs:
steps:
- name: Check out the code
uses: actions/checkout@v3
uses: actions/checkout@v3.6.0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v2.2.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v2.9.1
- name: Log in to GitHub Container Registry
uses: docker/login-action@v2
uses: docker/login-action@v2.2.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -27,15 +27,15 @@ jobs:
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@v4.6.0
with:
images: ghcr.io/${{ github.repository }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
uses: docker/build-push-action@v4.1.1
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
platforms: linux/amd64, linux/arm64
platforms: linux/amd64, linux/arm64/v8
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -1,4 +1,5 @@
name: Release X-ui
name: Release 3X-ui
on:
push:
tags:
@@ -6,85 +7,70 @@ on:
workflow_dispatch:
jobs:
linuxamd64build:
name: build x-ui amd64 version
build:
strategy:
matrix:
platform: [amd64, arm64]
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3.5.2
- name: Set up Go
uses: actions/setup-go@v4.0.0
- name: Checkout repository
uses: actions/checkout@v3.6.0
- name: Setup Go
uses: actions/setup-go@v4.1.0
with:
go-version: "stable"
- name: build linux amd64 version
run: |
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o xui-release -v main.go
mkdir x-ui
cp xui-release x-ui/xui-release
cp x-ui.service x-ui/x-ui.service
cp x-ui.sh x-ui/x-ui.sh
cd x-ui
mv xui-release x-ui
mkdir bin
cd bin
wget https://github.com/mhsanaei/Xray-core/releases/latest/download/Xray-linux-64.zip
unzip Xray-linux-64.zip
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
mv xray xray-linux-amd64
cd ..
cd ..
- name: package
run: tar -zcvf x-ui-linux-amd64.tar.gz x-ui
- name: upload
uses: svenstaro/upload-release-action@2.5.0
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref }}
file: x-ui-linux-amd64.tar.gz
asset_name: x-ui-linux-amd64.tar.gz
prerelease: true
overwrite: true
linuxarm64build:
name: build x-ui arm64 version
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3.5.2
- name: Set up Go
uses: actions/setup-go@v4.0.0
with:
go-version: "stable"
- name: build linux arm64 version
go-version: '1.20'
- name: Install dependencies for arm64
if: matrix.platform == 'arm64'
run: |
sudo apt-get update
sudo apt install gcc-aarch64-linux-gnu
CGO_ENABLED=1 GOOS=linux GOARCH=arm64 CC=aarch64-linux-gnu-gcc go build -o xui-release -v main.go
- name: Build x-ui
run: |
export CGO_ENABLED=1
export GOOS=linux
export GOARCH=${{ matrix.platform }}
if [ "${{ matrix.platform }}" == "arm64" ]; then
export CC=aarch64-linux-gnu-gcc
fi
go build -o xui-release -v main.go
mkdir x-ui
cp xui-release x-ui/xui-release
cp x-ui.service x-ui/x-ui.service
cp x-ui.sh x-ui/x-ui.sh
cd x-ui
mv xui-release x-ui
mkdir bin
cd bin
wget https://github.com/mhsanaei/xray-core/releases/latest/download/Xray-linux-arm64-v8a.zip
unzip Xray-linux-arm64-v8a.zip
rm -f Xray-linux-arm64-v8a.zip geoip.dat geosite.dat iran.dat
cp xui-release x-ui/
cp x-ui.service x-ui/
cp x-ui.sh x-ui/
mv x-ui/xui-release x-ui/x-ui
mkdir x-ui/bin
cd x-ui/bin
# Download dependencies
if [ "${{ matrix.platform }}" == "amd64" ]; then
wget https://github.com/XTLS/Xray-core/releases/download/v1.8.1/Xray-linux-64.zip
unzip Xray-linux-64.zip
rm -f Xray-linux-64.zip
else
wget https://github.com/XTLS/Xray-core/releases/download/v1.8.1/Xray-linux-arm64-v8a.zip
unzip Xray-linux-arm64-v8a.zip
rm -f Xray-linux-arm64-v8a.zip
fi
rm -f 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
mv xray xray-linux-arm64
cd ..
cd ..
- name: package
run: tar -zcvf x-ui-linux-arm64.tar.gz x-ui
- name: upload
uses: svenstaro/upload-release-action@2.5.0
wget https://github.com/MasterKia/iran-hosted-domains/releases/latest/download/iran.dat
mv xray xray-linux-${{ matrix.platform }}
cd ../..
- name: Package
run: tar -zcvf x-ui-linux-${{ matrix.platform }}.tar.gz x-ui
- name: Upload
uses: svenstaro/upload-release-action@2.7.0
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref }}
file: x-ui-linux-arm64.tar.gz
asset_name: x-ui-linux-arm64.tar.gz
file: x-ui-linux-${{ matrix.platform }}.tar.gz
asset_name: x-ui-linux-${{ matrix.platform }}.tar.gz
prerelease: true
overwrite: true

16
.gitignore vendored
View File

@@ -1,15 +1,15 @@
.idea
.vscode
.cache
.sync*
*.tar.gz
access.log
error.log
tmp
main
backup/
bin/
dist/
x-ui-*.tar.gz
/x-ui
/release.sh
.sync*
main
release/
access.log
error.log
.cache
/release.sh
/x-ui

7
DockerEntrypoint.sh Normal file
View File

@@ -0,0 +1,7 @@
#!/bin/sh
# Start fail2ban
fail2ban-client -x start
# Run x-ui
exec /app/x-ui

View File

@@ -1,22 +1,28 @@
#!/bin/sh
if [ $1 == "amd64" ]; then
ARCH="64";
FNAME="amd64";
elif [ $1 == "arm64" ]; then
ARCH="arm64-v8a"
FNAME="arm64";
else
ARCH="64";
FNAME="amd64";
fi
case $1 in
amd64)
ARCH="64"
FNAME="amd64"
;;
arm64)
ARCH="arm64-v8a"
FNAME="arm64"
;;
*)
ARCH="64"
FNAME="amd64"
;;
esac
mkdir -p build/bin
cd build/bin
wget "https://github.com/mhsanaei/xray-core/releases/latest/download/Xray-linux-${ARCH}.zip"
wget "https://github.com/XTLS/Xray-core/releases/download/v1.8.1/Xray-linux-${ARCH}.zip"
unzip "Xray-linux-${ARCH}.zip"
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat 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"

View File

@@ -1,20 +1,49 @@
#Build latest x-ui from source
# ========================================================
# Stage: Builder
# ========================================================
FROM --platform=$BUILDPLATFORM golang:1.20.4-alpine AS builder
WORKDIR /app
ARG TARGETARCH
RUN apk --no-cache --update add build-base gcc wget unzip
ARG TARGETARCH
ENV CGO_ENABLED=1
RUN apk --no-cache --update add \
build-base \
gcc \
wget \
unzip
COPY . .
RUN env CGO_ENABLED=1 go build -o build/x-ui main.go
RUN go build -o build/x-ui main.go
RUN ./DockerInit.sh "$TARGETARCH"
#Build app image using latest x-ui
# ========================================================
# Stage: Final Image of 3x-ui
# ========================================================
FROM alpine
ENV TZ=Asia/Tehran
WORKDIR /app
RUN apk add ca-certificates tzdata
RUN apk add --no-cache --update \
ca-certificates \
tzdata \
fail2ban
COPY --from=builder /app/build/ /app/
COPY --from=builder /app/DockerEntrypoint.sh /app/
COPY --from=builder /app/x-ui.sh /usr/bin/x-ui
# Configure fail2ban
RUN rm -f /etc/fail2ban/jail.d/alpine-ssh.conf \
&& cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local \
&& sed -i "s/^\[ssh\]$/&\nenabled = false/" /etc/fail2ban/jail.local \
&& sed -i "s/^\[sshd\]$/&\nenabled = false/" /etc/fail2ban/jail.local \
&& sed -i "s/#allowipv6 = auto/allowipv6 = auto/g" /etc/fail2ban/fail2ban.conf
RUN chmod +x \
/app/DockerEntrypoint.sh \
/app/x-ui \
/usr/bin/x-ui
VOLUME [ "/etc/x-ui" ]
ENTRYPOINT [ "/app/x-ui" ]
ENTRYPOINT [ "/app/DockerEntrypoint.sh" ]

304
README.md
View File

@@ -8,8 +8,7 @@
[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](#)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html)
3x-ui panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese,Russian)**
3x-ui panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese,Russian,Vietnamese)**
**If you think this project is helpful to you, you may wish to give a** :star2:
**Buy Me a Coffee :**
@@ -22,12 +21,12 @@
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
```
## Install custom version
# Install custom version
To install your desired version you can add the version to the end of install command. Example for ver `v1.4.5`:
To install your desired version you can add the version to the end of install command. Example for ver `v1.7.7`:
```
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v1.4.5
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v1.7.7
```
# SSL
@@ -38,97 +37,7 @@ certbot certonly --standalone --agree-tos --register-unsafely-without-email -d y
certbot renew --dry-run
```
or you can use x-ui menu then number '16' (Apply for an SSL Certificate)
# Default settings
- Port: 2053
- username and password will be generated randomly if you skip to modify your own security(x-ui "7")
- database path: /etc/x-ui/x-ui.db
- xray config path: /usr/local/x-ui/bin/config.json
Before you set ssl on settings
- http://ip:2053/panel
- http://domain:2053/panel
After you set ssl on settings
- https://yourdomain:2053/panel
# Environment Variables
| Variable | Type | Default |
| -------------- | :--------------------------------------------: | :------------ |
| XUI_LOG_LEVEL | `"debug"` \| `"info"` \| `"warn"` \| `"error"` | `"info"` |
| XUI_DEBUG | `boolean` | `false` |
| XUI_BIN_FOLDER | `string` | `"bin"` |
| XUI_DB_FOLDER | `string` | `"/etc/x-ui"` |
Example:
```sh
XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
```
# Install with Docker
1. Install Docker:
```sh
bash <(curl -sSL https://get.docker.com)
```
2. Run 3x-ui:
```sh
docker compose up -d
```
OR
```sh
docker run -itd \
-e XRAY_VMESS_AEAD_FORCED=false \
-v $PWD/db/:/etc/x-ui/ \
-v $PWD/cert/:/root/cert/ \
--network=host \
--restart=unless-stopped \
--name 3x-ui \
ghcr.io/mhsanaei/3x-ui:latest
```
# Xray Configurations:
**copy and paste to xray Configuration :** (you don't need to do this if you have a fresh install)
- [traffic](./media/configs/traffic.json)
- [traffic + Block all Iran IP address](./media/configs/traffic+block-iran-ip.json)
- [traffic + Block all Iran Domains](./media/configs/traffic+block-iran-domains.json)
- [traffic + Block Ads + Use IPv4 for Google](./media/configs/traffic+block-ads+ipv4-google.json)
- [traffic + Block Ads + Route Google + Netflix + Spotify + OpenAI (ChatGPT) to WARP](./media/configs/traffic+block-ads+warp.json)
# [WARP Configuration](https://github.com/fscarmen/warp) (Optional)
If you want to use routing to WARP follow steps as below:
1. If you already installed warp, you can uninstall using below command:
```sh
warp u
```
2. Install WARP on **socks proxy mode**:
```sh
curl -fsSL https://gist.githubusercontent.com/hamid-gh98/dc5dd9b0cc5b0412af927b1ccdb294c7/raw/install_warp_proxy.sh | bash
```
3. Turn on the config you need in panel or [Copy and paste this file to Xray Configuration](./media/configs/traffic+block-ads+warp.json)
Config Features:
- Block Ads
- Route Google + Netflix + Spotify + OpenAI (ChatGPT) to WARP
- Fix Google 403 error
You also can use `x-ui` menu then select `SSL Certificate Management`
# Features
@@ -147,7 +56,166 @@ If you want to use routing to WARP follow steps as below:
- Support to change configs by different items provided in panel
- Support export/import database from panel
# Tg robot use
# Manual Install & Upgrade
<details>
<summary>Click for Manual Install details</summary>
1. To download the latest version of the compressed package directly to your server, run the following command:
```sh
ARCH=$(uname -m)
[[ "${ARCH}" == "aarch64" || "${ARCH}" == "arm64" ]] && XUI_ARCH="arm64" || XUI_ARCH="amd64"
wget https://github.com/MHSanaei/3x-ui/releases/latest/download/x-ui-linux-${XUI_ARCH}.tar.gz
```
2. Once the compressed package is downloaded, execute the following commands to install or upgrade x-ui:
```sh
ARCH=$(uname -m)
[[ "${ARCH}" == "aarch64" || "${ARCH}" == "arm64" ]] && XUI_ARCH="arm64" || XUI_ARCH="amd64"
cd /root/
rm -rf x-ui/ /usr/local/x-ui/ /usr/bin/x-ui
tar zxvf x-ui-linux-${XUI_ARCH}.tar.gz
chmod +x x-ui/x-ui x-ui/bin/xray-linux-* x-ui/x-ui.sh
cp x-ui/x-ui.sh /usr/bin/x-ui
cp -f x-ui/x-ui.service /etc/systemd/system/
mv x-ui/ /usr/local/
systemctl daemon-reload
systemctl enable x-ui
systemctl restart x-ui
```
</details>
# Install with Docker
<details>
<summary>Click for Docker details</summary>
1. Install Docker:
```sh
bash <(curl -sSL https://get.docker.com)
```
2. Clone the Project Repository:
```sh
git clone https://github.com/MHSanaei/3x-ui.git
cd 3x-ui
```
3. Start the Service
```sh
docker compose up -d
```
OR
```sh
docker run -itd \
-e XRAY_VMESS_AEAD_FORCED=false \
-v $PWD/db/:/etc/x-ui/ \
-v $PWD/cert/:/root/cert/ \
--network=host \
--restart=unless-stopped \
--name 3x-ui \
ghcr.io/mhsanaei/3x-ui:latest
```
</details>
# Default settings
<details>
<summary>Click for Default settings details</summary>
- Port: 2053
- username and password will be generated randomly if you skip to modify your own security(x-ui "7")
- database path: /etc/x-ui/x-ui.db
- xray config path: /usr/local/x-ui/bin/config.json
Before you set ssl on settings
- http://ip:2053/panel
- http://domain:2053/panel
After you set ssl on settings
- https://yourdomain:2053/panel
</details>
# Xray Configurations:
<details>
<summary>Click for Xray Configurations details</summary>
**copy and paste to xray Configuration :** (you don't need to do this if you have a fresh install)
- [traffic](./media/configs/traffic.json)
- [traffic + Block all Iran IP address](./media/configs/traffic+block-iran-ip.json)
- [traffic + Block all Iran Domains](./media/configs/traffic+block-iran-domains.json)
- [traffic + Block Ads + Use IPv4 for Google](./media/configs/traffic+block-ads+ipv4-google.json)
- [traffic + Block Ads + Route Google + Netflix + Spotify + OpenAI (ChatGPT) to WARP](./media/configs/traffic+block-ads+warp.json)
</details>
# [WARP Configuration](https://github.com/fscarmen/warp) (Optional)
<details>
<summary>Click for WARP Configuration details</summary>
If you want to use routing to WARP follow steps as below:
1. If you already installed warp, you can uninstall using below command:
```sh
warp u
```
2. Install WARP on **socks proxy mode**:
```sh
bash <(curl -sSL https://raw.githubusercontent.com/hamid-gh98/x-ui-scripts/main/install_warp_proxy.sh)
```
3. Turn on the config you need in panel or [Copy and paste this file to Xray Configuration](./media/configs/traffic+block-ads+warp.json)
Config Features:
- Block Ads
- Route Google + Netflix + Spotify + OpenAI (ChatGPT) to WARP
- Fix Google 403 error
</details>
# IP Limit
<details>
<summary>Click for IP Limit details</summary>
**Note: IP Limit won't work correctly when using IP Tunnel**
- For versions up to `v1.6.1`:
- IP limit is built-in into the panel.
- For versions `v1.7.0` and newer:
- To make IP Limit work properly, you need to install fail2ban and its required files by following these steps:
1. Use the `x-ui` command inside the shell.
2. Select `IP Limit Management`.
3. Choose the appropriate options based on your needs.
</details>
# Telegram Bot
<details>
<summary>Click for Telegram Bot details</summary>
X-UI supports daily traffic notification, panel login reminder and other functions through the Tg robot. To use the Tg robot, you need to apply for the specific application tutorial. You can refer to the [blog](https://coderfan.net/how-to-use-telegram-bot-to-alarm-you-when-someone-login-into-your-vps.html)
Set the robot-related parameters in the panel background, including:
@@ -176,24 +244,30 @@ Reference syntax:
- CPU threshold notification
- Threshold for Expiration time and Traffic to report in advance
- Support client report menu if client's telegram username added to the user's configurations
- Support telegram traffic report searched with UID (VMESS/VLESS) or Password (TROJAN) - anonymously
- Support telegram traffic report searched with UUID (VMESS/VLESS) or Password (TROJAN) - anonymously
- Menu based bot
- Search client by email ( only admin )
- Check all inbounds
- Check server status
- Check depleted users
- Receive backup by request and in periodic reports
- Multi language bot
</details>
## API routes
# API routes
- `/login` with `PUSH` user data: `{username: '', password: ''}` for login
- `/xui/API/inbounds` base for following actions:
<details>
<summary>Click for API routes details</summary>
- `/login` with `POST` user data: `{username: '', password: ''}` for login
- `/panel/api/inbounds` base for following actions:
| Method | Path | Action |
| :----: | ---------------------------------- | ------------------------------------------- |
| `GET` | `"/list"` | Get all inbounds |
| `GET` | `"/get/:id"` | Get inbound with inbound.id |
| `GET` | `"/getClientTraffics/:email"` | Get Client Traffics with email |
| `GET` | `"/createbackup"` | Telegram bot sends backup to admins |
| `POST` | `"/add"` | Add inbound |
| `POST` | `"/del/:id"` | Delete Inbound |
| `POST` | `"/update/:id"` | Update Inbound |
@@ -214,17 +288,45 @@ Reference syntax:
- `client.email` for Shadowsocks
- [Postman Collection](https://gist.github.com/mehdikhody/9a862801a2e41f6b5fb6bbc7e1326044)
</details>
# Environment Variables
<details>
<summary>Click for Environment Variables details</summary>
| Variable | Type | Default |
| -------------- | :--------------------------------------------: | :------------ |
| XUI_LOG_LEVEL | `"debug"` \| `"info"` \| `"warn"` \| `"error"` | `"info"` |
| XUI_DEBUG | `boolean` | `false` |
| XUI_BIN_FOLDER | `string` | `"bin"` |
| XUI_DB_FOLDER | `string` | `"/etc/x-ui"` |
| XUI_LOG_FOLDER | `string` | `"/var/log"` |
Example:
```sh
XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
```
</details>
# A Special Thanks To
- [alireza0](https://github.com/alireza0/)
# 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._
# Suggestion System
- Ubuntu 20.04+
- Debian 10+
- CentOS 8+
- Fedora 36+
- Arch Linux
# Pictures

View File

@@ -16,10 +16,11 @@ var name string
type LogLevel string
const (
Debug LogLevel = "debug"
Info LogLevel = "info"
Warn LogLevel = "warn"
Error LogLevel = "error"
Debug LogLevel = "debug"
Info LogLevel = "info"
Notice LogLevel = "notice"
Warn LogLevel = "warn"
Error LogLevel = "error"
)
func GetVersion() string {
@@ -64,3 +65,11 @@ func GetDBFolderPath() string {
func GetDBPath() string {
return fmt.Sprintf("%s/%s.db", GetDBFolderPath(), GetName())
}
func GetLogFolder() string {
logFolderPath := os.Getenv("XUI_LOG_FOLDER")
if logFolderPath == "" {
logFolderPath = "/var/log"
}
return logFolderPath
}

View File

@@ -1 +1 @@
1.4.5
1.7.7

View File

@@ -6,6 +6,7 @@ import (
"io/fs"
"os"
"path"
"x-ui/config"
"x-ui/database/model"
"x-ui/xray"
@@ -17,6 +18,14 @@ import (
var db *gorm.DB
var initializers = []func() error{
initUser,
initInbound,
initSetting,
initInboundClientIps,
initClientTraffic,
}
func initUser() error {
err := db.AutoMigrate(&model.User{})
if err != nil {
@@ -45,16 +54,18 @@ func initInbound() error {
func initSetting() error {
return db.AutoMigrate(&model.Setting{})
}
func initInboundClientIps() error {
return db.AutoMigrate(&model.InboundClientIps{})
}
func initClientTraffic() error {
return db.AutoMigrate(&xray.ClientTraffic{})
}
func InitDB(dbPath string) error {
dir := path.Dir(dbPath)
err := os.MkdirAll(dir, fs.ModeDir)
err := os.MkdirAll(dir, fs.ModePerm)
if err != nil {
return err
}
@@ -75,25 +86,10 @@ func InitDB(dbPath string) error {
return err
}
err = initUser()
if err != nil {
return err
}
err = initInbound()
if err != nil {
return err
}
err = initSetting()
if err != nil {
return err
}
err = initInboundClientIps()
if err != nil {
return err
}
err = initClientTraffic()
if err != nil {
return err
for _, initialize := range initializers {
if err := initialize(); err != nil {
return err
}
}
return nil
@@ -107,10 +103,10 @@ func IsNotFound(err error) bool {
return err == gorm.ErrRecordNotFound
}
func IsSQLiteDB(file io.Reader) (bool, error) {
func IsSQLiteDB(file io.ReaderAt) (bool, error) {
signature := []byte("SQLite format 3\x00")
buf := make([]byte, len(signature))
_, err := file.Read(buf)
_, err := file.ReadAt(buf, 0)
if err != nil {
return false, err
}

View File

@@ -76,7 +76,6 @@ type Client struct {
ID string `json:"id"`
Password string `json:"password"`
Flow string `json:"flow"`
AlterIds uint16 `json:"alterId"`
Email string `json:"email"`
LimitIP int `json:"limitIp"`
TotalGB int64 `json:"totalGB" form:"totalGB"`

95
go.mod
View File

@@ -3,61 +3,96 @@ module x-ui
go 1.20
require (
github.com/Workiva/go-datastructures v1.0.53
github.com/gin-contrib/sessions v0.0.4
github.com/gin-gonic/gin v1.9.0
github.com/go-cmd/cmd v1.4.1
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
github.com/Calidity/gin-sessions v1.3.1
github.com/Workiva/go-datastructures v1.1.0
github.com/gin-gonic/gin v1.9.1
github.com/goccy/go-json v0.10.2
github.com/mymmrac/telego v0.26.1
github.com/nicksnyder/go-i18n/v2 v2.2.1
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pelletier/go-toml/v2 v2.0.7
github.com/pelletier/go-toml/v2 v2.0.9
github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v3 v3.23.4
github.com/xtls/xray-core v1.8.1
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.9.0
google.golang.org/grpc v1.55.0
gorm.io/driver/sqlite v1.5.0
gorm.io/gorm v1.25.1
golang.org/x/text v0.12.0
google.golang.org/grpc v1.57.0
gorm.io/driver/sqlite v1.5.3
gorm.io/gorm v1.25.4
)
require (
github.com/BurntSushi/toml v1.2.1 // indirect
github.com/bytedance/sonic v1.8.8 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/BurntSushi/toml v1.3.2 // indirect
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/bytedance/sonic v1.10.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.0 // indirect
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect
github.com/fasthttp/router v1.4.20 // indirect
github.com/francoispqt/gojay v1.2.13 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gaukas/godicttls v0.0.4 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.13.0 // indirect
github.com/go-playground/validator/v10 v10.15.1 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 // indirect
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/gorilla/sessions v1.2.1 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/klauspost/compress v1.16.7 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect
github.com/mattn/go-isatty v0.0.18 // indirect
github.com/mattn/go-sqlite3 v1.14.16 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/onsi/ginkgo/v2 v2.11.0 // indirect
github.com/pires/go-proxyproto v0.7.0 // indirect
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect
github.com/shoenig/go-m1cpu v0.1.5 // indirect
github.com/tklauser/go-sysconf v0.3.11 // indirect
github.com/tklauser/numcpus v0.6.0 // indirect
github.com/quic-go/qtls-go1-19 v0.3.2 // indirect
github.com/quic-go/qtls-go1-20 v0.2.2 // indirect
github.com/quic-go/quic-go v0.35.1 // indirect
github.com/refraction-networking/utls v1.3.3 // indirect
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/sagernet/sing v0.2.7 // indirect
github.com/sagernet/sing-shadowsocks v0.2.2 // indirect
github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c // indirect
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.8.0 // indirect
golang.org/x/net v0.9.0 // indirect
golang.org/x/sys v0.7.0 // indirect
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
google.golang.org/protobuf v1.30.0 // indirect
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.48.0 // indirect
github.com/xtls/reality v0.0.0-20230613075828-e07c3b04b983 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
golang.org/x/arch v0.4.0 // indirect
golang.org/x/crypto v0.12.0 // indirect
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
golang.org/x/mod v0.11.0 // indirect
golang.org/x/net v0.14.0 // indirect
golang.org/x/sys v0.11.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-20230822172742-b8732ec3820d // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gvisor.dev/gvisor v0.0.0-20220901235040-6ca97ef2ce1c // indirect
lukechampine.com/blake3 v1.2.1 // indirect
)

405
go.sum
View File

@@ -1,259 +1,446 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/Workiva/go-datastructures v1.0.53 h1:J6Y/52yX10Xc5JjXmGtWoSSxs3mZnGSaq37xZZh7Yig=
github.com/Workiva/go-datastructures v1.0.53/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A=
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/Calidity/gin-sessions v1.3.1 h1:nF3dCBWa7TZ4j26iYLwGRmzZy9YODhWoOS3fmi+snyE=
github.com/Calidity/gin-sessions v1.3.1/go.mod h1:I0+QE6qkO50TeN/n6If6novvxHk4Isvr23U8EdvPdns=
github.com/Workiva/go-datastructures v1.1.0 h1:hu20UpgZneBhQ3ZvwiOGlqJSKIosin2Rd5wAKUHEO/k=
github.com/Workiva/go-datastructures v1.1.0/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/antonlindstrom/pgstore v0.0.0-20200229204646-b08ebf1105e0/go.mod h1:2Ti6VUHVxpC0VSmTZzEvpzysnaGAfGBOoMIz5ykPyyw=
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.8.8 h1:Kj4AYbZSeENfyXicsYppYKO0K2YWab+i2UTSY7Ukz9Q=
github.com/bytedance/sonic v1.8.8/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.10.0 h1:qtNZduETEIWJVIyDl01BeNxur2rW9OwTQ/yBqFRkKEk=
github.com/bytedance/sonic v1.10.0/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo=
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 h1:y7y0Oa6UawqTFPCDw9JG6pdKt4F9pAhHv0B7FMGaGD0=
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/fasthttp/router v1.4.20 h1:yPeNxz5WxZGojzolKqiP15DTXnxZce9Drv577GBrDgU=
github.com/fasthttp/router v1.4.20/go.mod h1:um867yNQKtERxBm+C+yzgWxjspTiQoA8z86Ec3fK/tc=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
github.com/gaukas/godicttls v0.0.3 h1:YNDIf0d9adcxOijiLrEzpfZGAkNwLRzPaG6OjU7EITk=
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
github.com/gin-contrib/sessions v0.0.4 h1:gq4fNa1Zmp564iHP5G6EBuktilEos8VKhe2sza1KMgo=
github.com/gin-contrib/sessions v0.0.4/go.mod h1:pQ3sIyviBBGcxgyR8mkeJuXbeV3h3NYmhJADQTq5+Vo=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8=
github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k=
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/go-cmd/cmd v1.4.1 h1:JUcEIE84v8DSy02XTZpUDeGKExk2oW3DA10hTjbQwmc=
github.com/go-cmd/cmd v1.4.1/go.mod h1:tbBenttXtZU4c5djS1o7PWL5pd2xAr5sIqH1kGdNiRc=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-playground/validator/v10 v10.13.0 h1:cFRQdfaSMCOSfGCCLB20MHvuoHb/s5G8L5pu2ppK5AQ=
github.com/go-playground/validator/v10 v10.13.0/go.mod h1:dwu7+CG8/CtBiJFZDz4e+5Upb6OLw04gtBYw0mcG/z4=
github.com/go-playground/validator/v10 v10.15.1 h1:BSe8uhN+xQ4r5guV/ywQI4gO59C2raYcGffYWZEjZzM=
github.com/go-playground/validator/v10 v10.15.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20230406165453-00490a63f317 h1:hFhpt7CTmR3DX+b4R19ydQFtofxT0Sv3QsKNMVQYTMQ=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 h1:hR7/MlvK23p6+lIw9SN1TigNLn9ZnF3W4SYRKq2gAHs=
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA=
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw=
github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a h1:N9zuLhTvBSRt0gWSiJswwQ2HqDmtX/ZCDJURnKUt1Ik=
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc=
github.com/miekg/dns v1.1.53 h1:ZBkuHr5dxHtB1caEOlZTLPo7D3L3TWckgUUs/RHfDxw=
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
github.com/miekg/dns v1.1.54 h1:5jon9mWcb0sFJGpnI99tOMhCPyJ+RPVz5b63MQG0VWI=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mymmrac/telego v0.26.1 h1:BzWWzDOkov0SMVnSF+mLJ5ChYcTqmhTIyBWBGyi51hw=
github.com/mymmrac/telego v0.26.1/go.mod h1:kizipjY3MhxmkcGvyz8jiw/26vEKAhR2V7YTE69iqvw=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
github.com/nicksnyder/go-i18n/v2 v2.2.1 h1:aOzRCdwsJuoExfZhoiXHy4bjruwCMdt5otbYojM/PaA=
github.com/nicksnyder/go-i18n/v2 v2.2.1/go.mod h1:fF2++lPHlo+/kPaj3nB0uxtPwzlPm+BlgwGX7MkeGj0=
github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU=
github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU=
github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM=
github.com/onsi/gomega v1.27.8 h1:gegWiwZjBsf2DgiSbf5hpokZ98JVDMcWkUiigk6/KXc=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml/v2 v2.0.7 h1:muncTPStnKRos5dpVKULv2FVd4bMOhNePj9CjgDb8Us=
github.com/pelletier/go-toml/v2 v2.0.7/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0=
github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3gMACTjAbMZBjXAqTOzOwFaj2Ld6cjeQ7Rig=
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U=
github.com/quic-go/qtls-go1-19 v0.3.2/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI=
github.com/quic-go/qtls-go1-20 v0.2.2 h1:WLOPx6OY/hxtTxKV1Zrq20FtXtDEkeY00CGQm8GEa3E=
github.com/quic-go/quic-go v0.33.0 h1:ItNoTDN/Fm/zBlq769lLJc8ECe9gYaW40veHCCco7y0=
github.com/refraction-networking/utls v1.3.2 h1:o+AkWB57mkcoW36ET7uJ002CpBWHu0KPxi6vzxvPnv8=
github.com/quic-go/qtls-go1-20 v0.2.2/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM=
github.com/quic-go/quic-go v0.35.1 h1:b0kzj6b/cQAf05cT0CkQubHM31wiA+xH3IBkxP62poo=
github.com/quic-go/quic-go v0.35.1/go.mod h1:+4CVgVppm0FNjpG3UcX8Joi/frKOH7/ciD5yGcwOO1g=
github.com/refraction-networking/utls v1.3.3 h1:f/TBLX7KBciRyFH3bwupp+CE4fzoYKCirhdRcC490sw=
github.com/refraction-networking/utls v1.3.3/go.mod h1:DlecWW1LMlMJu+9qpzzQqdHDT/C2LAe03EdpLUz/RL8=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/sagernet/sing v0.2.3 h1:V50MvZ4c3Iij2lYFWPlzL1PyipwSzjGeN9x+Ox89vpk=
github.com/sagernet/sing-shadowsocks v0.2.1 h1:FvdLQOqpvxHBJUcUe4fvgiYP2XLLwH5i1DtXQviVEPw=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/sagernet/sing v0.2.7 h1:cOy0FfPS8q7m0aJ51wS7LRQAGc9wF+fWhHtBDj99wy8=
github.com/sagernet/sing v0.2.7/go.mod h1:Ta8nHnDLAwqySzKhGoKk4ZIB+vJ3GTKj7UPrWYvM+4w=
github.com/sagernet/sing-shadowsocks v0.2.2 h1:ezSdVhrmIcwDXmCZF3bOJVMuVtTQWpda+1Op+Ie2TA4=
github.com/sagernet/sing-shadowsocks v0.2.2/go.mod h1:JIBWG6a7orB2HxBxYElViQFLUQxFVG7DuqIj8gD7uCQ=
github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c h1:vK2wyt9aWYHHvNLWniwijBu/n4pySypiKRhN32u/JGo=
github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c/go.mod h1:euOmN6O5kk9dQmgSS8Df4psAl3TCjxOz0NW60EWkSaI=
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U=
github.com/shirou/gopsutil/v3 v3.23.4 h1:hZwmDxZs7Ewt75DV81r4pFMqbq+di2cbt9FsQBqLD2o=
github.com/shirou/gopsutil/v3 v3.23.4/go.mod h1:ZcGxyfzAMRevhUR2+cfhXDH6gQdFYE/t8j1nsU4mPI8=
github.com/shoenig/go-m1cpu v0.1.5 h1:LF57Z/Fpb/WdGLjt2HZilNnmZOxg/q2bSKTQhgbrLrQ=
github.com/shoenig/go-m1cpu v0.1.5/go.mod h1:Wwvst4LR89UxjeFtLRMrpgRiyY4xPsejnVZym39dbAQ=
github.com/shoenig/test v0.6.3 h1:GVXWJFk9PiOjN0KoJ7VrJGH6uLPnqxR7/fe3HUPfE0c=
github.com/shoenig/test v0.6.3/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
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.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=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/tinylib/msgp v1.1.5/go.mod h1:eQsjooMTnV42mHu917E26IogZ2930nFyBQdofk10Udg=
github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM=
github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31/go.mod h1:onvgF043R+lC5RZ8IT9rBXDaEDnpnw/Cl+HFiw+v/7Q=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI=
github.com/xtls/reality v0.0.0-20230331223127-176a94313eda h1:psRJD2RrZbnI0OWyHvXfgYCPqlRM5q5SPDcjDoDBWhE=
github.com/xtls/xray-core v1.8.1 h1:iSTTqXj82ZdwC1ah+eV331X4JTcnrDz+WuKuB/EB3P4=
github.com/xtls/xray-core v1.8.1/go.mod h1:AXxSso0MZwUE4NhRocCfHCg73BtJ+T2dSpQVo1Cg9VM=
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.48.0 h1:oJWvHb9BIZToTQS3MuQ2R3bJZiNSa2KiNdeI8A+79Tc=
github.com/valyala/fasthttp v1.48.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
github.com/xtls/reality v0.0.0-20230613075828-e07c3b04b983 h1:AMyzgjkh54WocjQSlCnT1LhDc/BKiUqtNOv40AkpURs=
github.com/xtls/reality v0.0.0-20230613075828-e07c3b04b983/go.mod h1:rkuAY1S9F8eI8gDiPDYvACE8e2uwkyg8qoOTuwWov7Y=
github.com/xtls/xray-core v1.8.3 h1:lxaVklPjLKqUU4ua4qH8SBaRcAaNHlH+LmXOx0U/Ejg=
github.com/xtls/xray-core v1.8.3/go.mod h1:i7t4JFnq828P2+XK0XjGQ8W9x78iu+EJ7jI4l3sonIw=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 h1:Ss6D3hLXTM0KobyBYEAygXzFfGcjnmfEJOBgSbemCtg=
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/mock v0.2.0 h1:TaP3xedm7JaAgScZO7tlvlKrqT0p7I6OsdGB5YNSMDU=
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc=
golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
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.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
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=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y=
golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg=
golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag=
google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw=
google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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.0 h1:zKYbzRCpBrT1bNijRnxLDJWPjVfImGEn0lSnUY5gZ+c=
gorm.io/driver/sqlite v1.5.0/go.mod h1:kDMDfntV9u/vuMmz8APHtHF0b4nyBB7sfCieC6G8k8I=
gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.1 h1:nsSALe5Pr+cM3V1qwwQ7rOkw+6UeLrX5O4v3llhHa64=
gorm.io/gorm v1.25.1/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=
lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0=
gvisor.dev/gvisor v0.0.0-20220901235040-6ca97ef2ce1c/go.mod h1:TIvkJD0sxe8pIob3p6T8IzxXunlp6yfgktvTNp+DGNM=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI=
lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=

View File

@@ -8,7 +8,7 @@ plain='\033[0m'
cur_dir=$(pwd)
# check root
[[ $EUID -ne 0 ]] && echo -e "${red}Fatal error${plain} Please run this script with root privilege \n " && exit 1
[[ $EUID -ne 0 ]] && echo -e "${red}Fatal error: ${plain} Please run this script with root privilege \n " && exit 1
# Check OS and set release variable
if [[ -f /etc/os-release ]]; then
@@ -41,34 +41,41 @@ if [[ "${release}" == "centos" ]]; then
fi
elif [[ "${release}" == "ubuntu" ]]; then
if [[ ${os_version} -lt 20 ]]; then
echo -e "${red}please use Ubuntu 20 or higher version${plain}\n" && exit 1
echo -e "${red}please use Ubuntu 20 or higher version!${plain}\n" && exit 1
fi
elif [[ "${release}" == "fedora" ]]; then
if [[ ${os_version} -lt 36 ]]; then
echo -e "${red}please use Fedora 36 or higher version${plain}\n" && exit 1
echo -e "${red}please use Fedora 36 or higher version!${plain}\n" && exit 1
fi
elif [[ "${release}" == "debian" ]]; then
if [[ ${os_version} -lt 10 ]]; then
echo -e "${red} Please use Debian 10 or higher ${plain}\n" && exit 1
fi
elif [[ "${release}" == "arch" ]]; then
echo "OS is ArchLinux"
else
echo -e "${red}Failed to check the OS version, please contact the author!${plain}" && exit 1
fi
install_base() {
case "${release}" in
centos | fedora)
yum install -y -q wget curl tar
;;
*)
apt install -y -q wget curl tar
;;
centos|fedora)
yum install -y -q wget curl tar
;;
arch)
pacman -Syu --noconfirm wget curl tar
;;
*)
apt install -y -q wget curl tar
;;
esac
}
#This function will be called when user installed x-ui out of sercurity
# This function will be called when user installed x-ui out of sercurity
config_after_install() {
echo -e "${yellow}Install/update finished! For security it's recommended to modify panel settings ${plain}"
read -p "Do you want to continue with the modification [y/n]? ": config_confirm
@@ -104,7 +111,6 @@ config_after_install() {
}
install_x-ui() {
systemctl stop x-ui
cd /usr/local/
if [ $# == 0 ]; then
@@ -131,6 +137,7 @@ install_x-ui() {
fi
if [[ -e /usr/local/x-ui/ ]]; then
systemctl stop x-ui
rm /usr/local/x-ui/ -rf
fi

View File

@@ -1,26 +1,47 @@
package logger
import (
"fmt"
"os"
"time"
"github.com/op/go-logging"
)
var logger *logging.Logger
var logBuffer []struct {
time string
level logging.Level
log string
}
func init() {
InitLogger(logging.INFO)
}
func InitLogger(level logging.Level) {
format := logging.MustStringFormatter(
`%{time:2006/01/02 15:04:05} %{level} - %{message}`,
)
newLogger := logging.MustGetLogger("x-ui")
backend := logging.NewLogBackend(os.Stderr, "", 0)
var err error
var backend logging.Backend
var format logging.Formatter
ppid := os.Getppid()
if ppid == 1 {
backend, err = logging.NewSyslogBackend("")
format = logging.MustStringFormatter(
`%{level} - %{message}`,
)
}
if err != nil || ppid != 1 {
backend = logging.NewLogBackend(os.Stderr, "", 0)
format = logging.MustStringFormatter(
`%{time:2006/01/02 15:04:05} %{level} - %{message}`,
)
}
backendFormatter := logging.NewBackendFormatter(backend, format)
backendLeveled := logging.AddModuleLevel(backendFormatter)
backendLeveled.SetLevel(level, "")
backendLeveled.SetLevel(level, "x-ui")
newLogger.SetBackend(backendLeveled)
logger = newLogger
@@ -28,32 +49,70 @@ func InitLogger(level logging.Level) {
func Debug(args ...interface{}) {
logger.Debug(args...)
addToBuffer("DEBUG", fmt.Sprint(args...))
}
func Debugf(format string, args ...interface{}) {
logger.Debugf(format, args...)
addToBuffer("DEBUG", fmt.Sprintf(format, args...))
}
func Info(args ...interface{}) {
logger.Info(args...)
addToBuffer("INFO", fmt.Sprint(args...))
}
func Infof(format string, args ...interface{}) {
logger.Infof(format, args...)
addToBuffer("INFO", fmt.Sprintf(format, args...))
}
func Warning(args ...interface{}) {
logger.Warning(args...)
addToBuffer("WARNING", fmt.Sprint(args...))
}
func Warningf(format string, args ...interface{}) {
logger.Warningf(format, args...)
addToBuffer("WARNING", fmt.Sprintf(format, args...))
}
func Error(args ...interface{}) {
logger.Error(args...)
addToBuffer("ERROR", fmt.Sprint(args...))
}
func Errorf(format string, args ...interface{}) {
logger.Errorf(format, args...)
addToBuffer("ERROR", fmt.Sprintf(format, args...))
}
func addToBuffer(level string, newLog string) {
t := time.Now()
if len(logBuffer) >= 10240 {
logBuffer = logBuffer[1:]
}
logLevel, _ := logging.LogLevel(level)
logBuffer = append(logBuffer, struct {
time string
level logging.Level
log string
}{
time: t.Format("2006/01/02 15:04:05"),
level: logLevel,
log: newLog,
})
}
func GetLogs(c int, level string) []string {
var output []string
logLevel, _ := logging.LogLevel(level)
for i := len(logBuffer) - 1; i >= 0 && len(output) <= c; i-- {
if logBuffer[i].level <= logLevel {
output = append(output, fmt.Sprintf("%s %s - %s", logBuffer[i].time, logBuffer[i].level, logBuffer[i].log))
}
}
return output
}

49
main.go
View File

@@ -11,7 +11,7 @@ import (
"x-ui/config"
"x-ui/database"
"x-ui/logger"
"x-ui/v2ui"
"x-ui/sub"
"x-ui/web"
"x-ui/web/global"
"x-ui/web/service"
@@ -27,6 +27,8 @@ func runWebServer() {
logger.InitLogger(logging.DEBUG)
case config.Info:
logger.InitLogger(logging.INFO)
case config.Notice:
logger.InitLogger(logging.NOTICE)
case config.Warn:
logger.InitLogger(logging.WARNING)
case config.Error:
@@ -50,6 +52,16 @@ func runWebServer() {
return
}
var subServer *sub.Server
subServer = sub.NewServer()
global.SetSubServer(subServer)
err = subServer.Start()
if err != nil {
log.Println(err)
return
}
sigCh := make(chan os.Signal, 1)
// Trap shutdown signals
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM)
@@ -62,6 +74,11 @@ func runWebServer() {
if err != nil {
logger.Warning("stop server err:", err)
}
err = subServer.Stop()
if err != nil {
logger.Warning("stop server err:", err)
}
server = web.NewServer()
global.SetWebServer(server)
err = server.Start()
@@ -69,8 +86,18 @@ func runWebServer() {
log.Println(err)
return
}
subServer = sub.NewServer()
global.SetSubServer(subServer)
err = subServer.Start()
if err != nil {
log.Println(err)
return
}
default:
server.Stop()
subServer.Stop()
return
}
}
@@ -133,7 +160,6 @@ func updateTgbotEnableSts(status bool) {
logger.Infof("SetTgbotenabled[%v] success", status)
}
}
return
}
func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime string) {
@@ -245,10 +271,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
@@ -276,7 +298,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")
}
@@ -297,16 +318,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 {
@@ -331,12 +342,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()
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View File

@@ -6,7 +6,11 @@
},
"api": {
"tag": "api",
"services": ["HandlerService", "LoggerService", "StatsService"]
"services": [
"HandlerService",
"LoggerService",
"StatsService"
]
},
"inbounds": [
{
@@ -54,35 +58,41 @@
"rules": [
{
"type": "field",
"inboundTag": ["api"],
"inboundTag": [
"api"
],
"outboundTag": "api"
},
{
"type": "field",
"outboundTag": "blocked",
"ip": ["geoip:private"]
"ip": [
"geoip:private"
]
},
{
"type": "field",
"outboundTag": "blocked",
"protocol": ["bittorrent"]
"protocol": [
"bittorrent"
]
},
{
"type": "field",
"outboundTag": "blocked",
"domain": [
"geosite:category-ads-all",
"geosite:category-ads",
"geosite:google-ads",
"geosite:spotify-ads"
"ext:iran.dat:ads"
]
},
{
"type": "field",
"outboundTag": "IPv4",
"domain": ["geosite:google"]
"domain": [
"geosite:google"
]
}
]
},
"stats": {}
}
}

View File

@@ -6,7 +6,11 @@
},
"api": {
"tag": "api",
"services": ["HandlerService", "LoggerService", "StatsService"]
"services": [
"HandlerService",
"LoggerService",
"StatsService"
]
},
"inbounds": [
{
@@ -59,40 +63,44 @@
"rules": [
{
"type": "field",
"inboundTag": ["api"],
"inboundTag": [
"api"
],
"outboundTag": "api"
},
{
"type": "field",
"outboundTag": "blocked",
"ip": ["geoip:private"]
"ip": [
"geoip:private"
]
},
{
"type": "field",
"outboundTag": "blocked",
"protocol": ["bittorrent"]
"protocol": [
"bittorrent"
]
},
{
"type": "field",
"outboundTag": "blocked",
"domain": [
"geosite:category-ads-all",
"geosite:category-ads",
"geosite:google-ads",
"geosite:spotify-ads"
"ext:iran.dat:ads"
]
},
{
"type": "field",
"outboundTag": "WARP",
"domain": [
"geosite:google",
"geosite:netflix",
"geosite:spotify",
"geosite:openai"
"geosite:netflix",
"geosite:openai",
"geosite:google"
]
}
]
},
"stats": {}
}
}

View File

@@ -6,7 +6,11 @@
},
"api": {
"tag": "api",
"services": ["HandlerService", "LoggerService", "StatsService"]
"services": [
"HandlerService",
"LoggerService",
"StatsService"
]
},
"inbounds": [
{
@@ -47,18 +51,24 @@
"rules": [
{
"type": "field",
"inboundTag": ["api"],
"inboundTag": [
"api"
],
"outboundTag": "api"
},
{
"type": "field",
"outboundTag": "blocked",
"ip": ["geoip:private"]
"ip": [
"geoip:private"
]
},
{
"type": "field",
"outboundTag": "blocked",
"protocol": ["bittorrent"]
"protocol": [
"bittorrent"
]
},
{
"type": "field",
@@ -73,4 +83,4 @@
]
},
"stats": {}
}
}

View File

@@ -6,7 +6,11 @@
},
"api": {
"tag": "api",
"services": ["HandlerService", "LoggerService", "StatsService"]
"services": [
"HandlerService",
"LoggerService",
"StatsService"
]
},
"inbounds": [
{
@@ -47,30 +51,27 @@
"rules": [
{
"type": "field",
"inboundTag": ["api"],
"inboundTag": [
"api"
],
"outboundTag": "api"
},
{
"type": "field",
"outboundTag": "blocked",
"ip": ["geoip:private"]
"ip": [
"geoip:private",
"geoip:ir"
]
},
{
"type": "field",
"outboundTag": "blocked",
"protocol": ["bittorrent"]
},
{
"type": "field",
"outboundTag": "blocked",
"ip": ["geoip:private"]
},
{
"type": "field",
"outboundTag": "blocked",
"ip": ["geoip:ir"]
"protocol": [
"bittorrent"
]
}
]
},
"stats": {}
}
}

162
sub/sub.go Normal file
View File

@@ -0,0 +1,162 @@
package sub
import (
"context"
"crypto/tls"
"io"
"net"
"net/http"
"strconv"
"x-ui/config"
"x-ui/logger"
"x-ui/util/common"
"x-ui/web/middleware"
"x-ui/web/network"
"x-ui/web/service"
"github.com/gin-gonic/gin"
)
type Server struct {
httpServer *http.Server
listener net.Listener
sub *SUBController
settingService service.SettingService
ctx context.Context
cancel context.CancelFunc
}
func NewServer() *Server {
ctx, cancel := context.WithCancel(context.Background())
return &Server{
ctx: ctx,
cancel: cancel,
}
}
func (s *Server) initRouter() (*gin.Engine, error) {
if config.IsDebug() {
gin.SetMode(gin.DebugMode)
} else {
gin.DefaultWriter = io.Discard
gin.DefaultErrorWriter = io.Discard
gin.SetMode(gin.ReleaseMode)
}
engine := gin.Default()
subPath, err := s.settingService.GetSubPath()
if err != nil {
return nil, err
}
subDomain, err := s.settingService.GetSubDomain()
if err != nil {
return nil, err
}
if subDomain != "" {
engine.Use(middleware.DomainValidatorMiddleware(subDomain))
}
g := engine.Group(subPath)
s.sub = NewSUBController(g)
return engine, nil
}
func (s *Server) Start() (err error) {
//This is an anonymous function, no function name
defer func() {
if err != nil {
s.Stop()
}
}()
subEnable, err := s.settingService.GetSubEnable()
if err != nil {
return err
}
if !subEnable {
return nil
}
engine, err := s.initRouter()
if err != nil {
return err
}
certFile, err := s.settingService.GetSubCertFile()
if err != nil {
return err
}
keyFile, err := s.settingService.GetSubKeyFile()
if err != nil {
return err
}
listen, err := s.settingService.GetSubListen()
if err != nil {
return err
}
port, err := s.settingService.GetSubPort()
if err != nil {
return err
}
listenAddr := net.JoinHostPort(listen, strconv.Itoa(port))
listener, err := net.Listen("tcp", listenAddr)
if err != nil {
return err
}
if certFile != "" || keyFile != "" {
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
listener.Close()
return err
}
c := &tls.Config{
Certificates: []tls.Certificate{cert},
}
listener = network.NewAutoHttpsListener(listener)
listener = tls.NewListener(listener, c)
}
if certFile != "" || keyFile != "" {
logger.Info("Sub server run https on", listener.Addr())
} else {
logger.Info("Sub server run http on", listener.Addr())
}
s.listener = listener
s.httpServer = &http.Server{
Handler: engine,
}
go func() {
s.httpServer.Serve(listener)
}()
return nil
}
func (s *Server) Stop() error {
s.cancel()
var err1 error
var err2 error
if s.httpServer != nil {
err1 = s.httpServer.Shutdown(s.ctx)
}
if s.listener != nil {
err2 = s.listener.Close()
}
return common.Combine(err1, err2)
}
func (s *Server) GetCtx() context.Context {
return s.ctx
}

View File

@@ -1,4 +1,4 @@
package controller
package sub
import (
"encoding/base64"
@@ -9,9 +9,8 @@ import (
)
type SUBController struct {
BaseController
subService service.SubService
subService SubService
settingService service.SettingService
}
func NewSUBController(g *gin.RouterGroup) *SUBController {
@@ -21,15 +20,17 @@ func NewSUBController(g *gin.RouterGroup) *SUBController {
}
func (a *SUBController) initRouter(g *gin.RouterGroup) {
g = g.Group("/sub")
g = g.Group("/")
g.GET("/:subid", a.subs)
}
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, header, 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 {
@@ -38,9 +39,15 @@ func (a *SUBController) subs(c *gin.Context) {
result += sub + "\n"
}
// Add subscription-userinfo
c.Writer.Header().Set("Subscription-Userinfo", header)
// Add headers
c.Writer.Header().Set("Subscription-Userinfo", headers[0])
c.Writer.Header().Set("Profile-Update-Interval", headers[1])
c.Writer.Header().Set("Profile-Title", headers[2])
c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
if subEncrypt {
c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
} else {
c.String(200, result)
}
}
}

View File

@@ -1,42 +1,62 @@
package service
package sub
import (
"encoding/base64"
"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"
"github.com/goccy/go-json"
"gorm.io/gorm"
)
type SubService struct {
address string
inboundService InboundService
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 header string
var headers []string
var traffic xray.ClientTraffic
var clientTraffics []xray.ClientTraffic
inbounds, err := s.getInboundsBySubId(subId)
if err != nil {
return nil, "", err
return nil, nil, err
}
for _, inbound := range inbounds {
clients, err := s.inboundService.getClients(inbound)
clients, err := s.inboundService.GetClients(inbound)
if err != nil {
logger.Error("SubService - GetSub: Unable to get clients from inbound")
}
if clients == nil {
continue
}
if len(inbound.Listen) > 0 && inbound.Listen[0] == '@' {
fallbackMaster, err := s.getFallbackMaster(inbound.Listen)
if err == nil {
inbound.Listen = fallbackMaster.Listen
inbound.Port = fallbackMaster.Port
var stream map[string]interface{}
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
var masterStream map[string]interface{}
json.Unmarshal([]byte(fallbackMaster.StreamSettings), &masterStream)
stream["security"] = masterStream["security"]
stream["tlsSettings"] = masterStream["tlsSettings"]
modifiedStream, _ := json.MarshalIndent(stream, "", " ")
inbound.StreamSettings = string(modifiedStream)
}
}
for _, client := range clients {
if client.Enable && client.SubID == subId {
link := s.getLink(inbound, client.Email)
@@ -66,15 +86,18 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, string, error
}
}
}
header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
return result, header, nil
headers = append(headers, fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000))
updateInterval, _ := s.settingServics.GetSubUpdates()
headers = append(headers, fmt.Sprintf("%d", updateInterval))
headers = append(headers, subId)
return result, headers, nil
}
func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
db := database.GetDB()
var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Preload("ClientStats").Where("settings like ? and enable = ?", fmt.Sprintf(`%%"subId": "%s"%%`, subId), true).Find(&inbounds).Error
if err != nil && err != gorm.ErrRecordNotFound {
if err != nil {
return nil, err
}
return inbounds, nil
@@ -89,6 +112,19 @@ func (s *SubService) getClientTraffics(traffics []xray.ClientTraffic, email stri
return xray.ClientTraffic{}
}
func (s *SubService) getFallbackMaster(dest string) (*model.Inbound, error) {
db := database.GetDB()
var inbound *model.Inbound
err := db.Model(model.Inbound{}).
Where("JSON_TYPE(settings, '$.fallbacks') = 'array'").
Where("EXISTS (SELECT * FROM json_each(settings, '$.fallbacks') WHERE json_extract(value, '$.dest') = ?)", dest).
Find(&inbound).Error
if err != nil {
return nil, err
}
return inbound, nil
}
func (s *SubService) getLink(inbound *model.Inbound, email string) string {
switch inbound.Protocol {
case "vmess":
@@ -107,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",
@@ -162,6 +196,7 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
}
security, _ := stream["security"].(string)
var domains []interface{}
obj["tls"] = security
if security == "tls" {
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
@@ -184,6 +219,9 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
obj["allowInsecure"], _ = insecure.(bool)
}
if domainSettings, ok := searchKey(tlsSettings, "domains"); ok {
domains, _ = domainSettings.([]interface{})
}
}
serverName, _ := tlsSetting["serverName"].(string)
if serverName != "" {
@@ -191,7 +229,7 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
}
}
clients, _ := s.inboundService.getClients(inbound)
clients, _ := s.inboundService.GetClients(inbound)
clientIndex := -1
for i, client := range clients {
if client.Email == email {
@@ -200,7 +238,23 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
}
}
obj["id"] = clients[clientIndex].ID
obj["aid"] = clients[clientIndex].AlterIds
if len(domains) > 0 {
links := ""
for index, d := range domains {
domain := d.(map[string]interface{})
obj["ps"] = s.genRemark(inbound, email, domain["remark"].(string))
obj["add"] = domain["domain"].(string)
if index > 0 {
links += "\n"
}
jsonStr, _ := json.MarshalIndent(obj, "", " ")
links += "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
}
return links
}
obj["ps"] = s.genRemark(inbound, email, "")
jsonStr, _ := json.MarshalIndent(obj, "", " ")
return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
@@ -213,7 +267,7 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
}
var stream map[string]interface{}
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
clients, _ := s.inboundService.getClients(inbound)
clients, _ := s.inboundService.GetClients(inbound)
clientIndex := -1
for i, client := range clients {
if client.Email == email {
@@ -269,6 +323,7 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
}
security, _ := stream["security"].(string)
var domains []interface{}
if security == "tls" {
params["security"] = "tls"
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
@@ -293,6 +348,9 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
params["allowInsecure"] = "1"
}
}
if domainSettings, ok := searchKey(tlsSettings, "domains"); ok {
domains, _ = domainSettings.([]interface{})
}
}
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
@@ -326,6 +384,11 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
params["fp"] = fp
}
}
if spxValue, ok := searchKey(realitySettings, "spiderX"); ok {
if spx, ok := spxValue.(string); ok && len(spx) > 0 {
params["spx"] = spx
}
}
if serverName, ok := searchKey(realitySettings, "serverName"); ok {
if sname, ok := serverName.(string); ok && len(sname) > 0 {
address = sname
@@ -375,6 +438,10 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
}
}
if security != "tls" && security != "reality" && security != "xtls" {
params["security"] = "none"
}
link := fmt.Sprintf("vless://%s@%s:%d", uuid, address, port)
url, _ := url.Parse(link)
q := url.Query()
@@ -386,8 +453,21 @@ 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)
url.Fragment = remark
if len(domains) > 0 {
links := ""
for index, d := range domains {
domain := d.(map[string]interface{})
url.Fragment = s.genRemark(inbound, email, domain["remark"].(string))
url.Host = fmt.Sprintf("%s:%d", domain["domain"].(string), port)
if index > 0 {
links += "\n"
}
links += url.String()
}
return links
}
url.Fragment = s.genRemark(inbound, email, "")
return url.String()
}
@@ -398,7 +478,7 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
}
var stream map[string]interface{}
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
clients, _ := s.inboundService.getClients(inbound)
clients, _ := s.inboundService.GetClients(inbound)
clientIndex := -1
for i, client := range clients {
if client.Email == email {
@@ -454,6 +534,7 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
}
security, _ := stream["security"].(string)
var domains []interface{}
if security == "tls" {
params["security"] = "tls"
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
@@ -478,6 +559,9 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
params["allowInsecure"] = "1"
}
}
if domainSettings, ok := searchKey(tlsSettings, "domains"); ok {
domains, _ = domainSettings.([]interface{})
}
}
serverName, _ := tlsSetting["serverName"].(string)
@@ -498,7 +582,7 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
if pbkValue, ok := searchKey(realitySettings, "publicKey"); ok {
params["pbk"], _ = pbkValue.(string)
}
if sidValue, ok := searchKey(realitySettings, "shortIds"); ok {
if sidValue, ok := searchKey(realitySetting, "shortIds"); ok {
shortIds, _ := sidValue.([]interface{})
params["sid"], _ = shortIds[0].(string)
}
@@ -507,6 +591,11 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
params["fp"] = fp
}
}
if spxValue, ok := searchKey(realitySettings, "spiderX"); ok {
if spx, ok := spxValue.(string); ok && len(spx) > 0 {
params["spx"] = spx
}
}
if serverName, ok := searchKey(realitySettings, "serverName"); ok {
if sname, ok := serverName.(string); ok && len(sname) > 0 {
address = sname
@@ -556,6 +645,10 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
}
}
if security != "tls" && security != "reality" && security != "xtls" {
params["security"] = "none"
}
link := fmt.Sprintf("trojan://%s@%s:%d", password, address, port)
url, _ := url.Parse(link)
@@ -568,8 +661,21 @@ 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)
url.Fragment = remark
if len(domains) > 0 {
links := ""
for index, d := range domains {
domain := d.(map[string]interface{})
url.Fragment = s.genRemark(inbound, email, domain["remark"].(string))
url.Host = fmt.Sprintf("%s:%d", domain["domain"].(string), port)
if index > 0 {
links += "\n"
}
links += url.String()
}
return links
}
url.Fragment = s.genRemark(inbound, email, "")
return url.String()
}
@@ -578,7 +684,9 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
if inbound.Protocol != model.Shadowsocks {
return ""
}
clients, _ := s.inboundService.getClients(inbound)
var stream map[string]interface{}
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
clients, _ := s.inboundService.GetClients(inbound)
var settings map[string]interface{}
json.Unmarshal([]byte(inbound.Settings), &settings)
@@ -591,8 +699,112 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
break
}
}
encPart := fmt.Sprintf("%s:%s:%s", method, inboundPassword, clients[clientIndex].Password)
return fmt.Sprintf("ss://%s@%s:%d#%s", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port, clients[clientIndex].Email)
streamNetwork := stream["network"].(string)
params := make(map[string]string)
params["type"] = streamNetwork
switch streamNetwork {
case "tcp":
tcp, _ := stream["tcpSettings"].(map[string]interface{})
header, _ := tcp["header"].(map[string]interface{})
typeStr, _ := header["type"].(string)
if typeStr == "http" {
request := header["request"].(map[string]interface{})
requestPath, _ := request["path"].([]interface{})
params["path"] = requestPath[0].(string)
headers, _ := request["headers"].(map[string]interface{})
params["host"] = searchHost(headers)
params["headerType"] = "http"
}
case "kcp":
kcp, _ := stream["kcpSettings"].(map[string]interface{})
header, _ := kcp["header"].(map[string]interface{})
params["headerType"] = header["type"].(string)
params["seed"] = kcp["seed"].(string)
case "ws":
ws, _ := stream["wsSettings"].(map[string]interface{})
params["path"] = ws["path"].(string)
headers, _ := ws["headers"].(map[string]interface{})
params["host"] = searchHost(headers)
case "http":
http, _ := stream["httpSettings"].(map[string]interface{})
params["path"] = http["path"].(string)
params["host"] = searchHost(http)
case "quic":
quic, _ := stream["quicSettings"].(map[string]interface{})
params["quicSecurity"] = quic["security"].(string)
params["key"] = quic["key"].(string)
header := quic["header"].(map[string]interface{})
params["headerType"] = header["type"].(string)
case "grpc":
grpc, _ := stream["grpcSettings"].(map[string]interface{})
params["serviceName"] = grpc["serviceName"].(string)
if grpc["multiMode"].(bool) {
params["mode"] = "multi"
}
}
encPart := fmt.Sprintf("%s:%s", method, clients[clientIndex].Password)
if method[0] == '2' {
encPart = fmt.Sprintf("%s:%s:%s", method, inboundPassword, clients[clientIndex].Password)
}
link := fmt.Sprintf("ss://%s@%s:%d", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port)
url, _ := url.Parse(link)
q := url.Query()
for k, v := range params {
q.Add(k, v)
}
// Set the new query values on the URL
url.RawQuery = q.Encode()
url.Fragment = 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) {

View File

@@ -24,8 +24,8 @@ func getLinesNum(filename string) (int, error) {
var buffPosition int
for {
i := bytes.IndexByte(buf[buffPosition:], '\n')
if i < 0 || n == buffPosition {
i := bytes.IndexByte(buf[buffPosition:n], '\n')
if i < 0 {
break
}
buffPosition += i + 1
@@ -33,11 +33,12 @@ func getLinesNum(filename string) (int, error) {
}
if err == io.EOF {
return sum, nil
break
} else if err != nil {
return sum, err
return 0, err
}
}
return sum, nil
}
func GetTCPCount() (int, error) {
@@ -45,11 +46,11 @@ func GetTCPCount() (int, error) {
tcp4, err := getLinesNum(fmt.Sprintf("%v/net/tcp", root))
if err != nil {
return tcp4, err
return 0, err
}
tcp6, err := getLinesNum(fmt.Sprintf("%v/net/tcp6", root))
if err != nil {
return tcp4 + tcp6, nil
return 0, err
}
return tcp4 + tcp6, nil
@@ -60,11 +61,11 @@ func GetUDPCount() (int, error) {
udp4, err := getLinesNum(fmt.Sprintf("%v/net/udp", root))
if err != nil {
return udp4, err
return 0, err
}
udp6, err := getLinesNum(fmt.Sprintf("%v/net/udp6", root))
if err != nil {
return udp4 + udp6, nil
return 0, err
}
return udp4 + udp6, nil

View File

@@ -4,21 +4,27 @@
package sys
import (
"errors"
"github.com/shirou/gopsutil/v3/net"
)
func GetTCPCount() (int, error) {
stats, err := net.Connections("tcp")
func GetConnectionCount(proto string) (int, error) {
if proto != "tcp" && proto != "udp" {
return 0, errors.New("invalid protocol")
}
stats, err := net.Connections(proto)
if err != nil {
return 0, err
}
return len(stats), nil
}
func GetUDPCount() (int, error) {
stats, err := net.Connections("udp")
if err != nil {
return 0, err
}
return len(stats), nil
func GetTCPCount() (int, error) {
return GetConnectionCount("tcp")
}
func GetUDPCount() (int, error) {
return GetConnectionCount("udp")
}

View File

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

@@ -529,7 +529,7 @@ to{transform:scaleY(0);transform-origin:0 0;opacity:0}
.ant-select-selection-selected-value{float:left;max-width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
.ant-select-no-arrow .ant-select-selection-selected-value{padding-right:0}
.ant-select-disabled{color:rgba(0,0,0,.25)}
.ant-select-disabled .ant-select-selection{background:rgb(0 150 112);cursor:not-allowed}
.ant-select-disabled .ant-select-selection{background:rgb(221, 221, 221);cursor:not-allowed}
.ant-select-disabled .ant-select-selection:active,.ant-select-disabled .ant-select-selection:focus,.ant-select-disabled .ant-select-selection:hover{border-color:#d9d9d9;box-shadow:none}
.ant-select-disabled .ant-select-selection__clear{display:none;visibility:hidden;pointer-events:none}
.ant-select-disabled .ant-select-selection--multiple .ant-select-selection__choice{padding-right:10px;color:rgba(0,0,0,.33);background:#f5f5f5}
@@ -996,7 +996,8 @@ to{transform:scale(0) translate(50%,-50%);opacity:0}
.ant-menu-horizontal .ant-menu-item,.ant-menu-horizontal .ant-menu-submenu{margin-top:-1px}
.ant-menu-horizontal>.ant-menu-item-active,.ant-menu-horizontal>.ant-menu-item:hover,.ant-menu-horizontal>.ant-menu-submenu .ant-menu-submenu-title:hover{background-color:transparent}
.ant-menu-item-selected,.ant-menu-item-selected>a,.ant-menu-item-selected>a:hover{color:#1890ff}
.ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected{background: linear-gradient(90deg,#009670 0,#026247 100%);color: #fff;border-radius: 0.5rem}
.ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected{background-color: #0a7557;background-image: linear-gradient( 270deg, rgba(123, 199, 77, 0) 30%, #00ab80, rgba(123, 199, 77, 0) 100% );background-repeat: no-repeat;animation: ma-bg-move linear 6.6s infinite;/*background: linear-gradient(90deg,#009670 0,#026247 100%);*/color: #fff;border-radius: 0.5rem}
@-webkit-keyframes ma-bg-move {0% {background-position: -500px 0;}100% {background-position: 1000px 0;}}@keyframes ma-bg-move {0% {background-position: -500px 0;}50% {background-position: 1000px 0;}100% {background-position: 1000px 0;}}
.ant-menu-vertical-right{border-left:1px solid #e8e8e8}
.ant-menu-vertical-left.ant-menu-sub,.ant-menu-vertical-right.ant-menu-sub,.ant-menu-vertical.ant-menu-sub{min-width:160px;padding:0;border-right:0;transform-origin:0 0}
.ant-menu-vertical-left.ant-menu-sub .ant-menu-item,.ant-menu-vertical-right.ant-menu-sub .ant-menu-item,.ant-menu-vertical.ant-menu-sub .ant-menu-item{left:0;margin-left:0;border-right:0}
@@ -1081,8 +1082,8 @@ to{transform:scale(0) translate(50%,-50%);opacity:0}
.ant-menu-dark .ant-menu-item-selected{color:#fff;border-right:0}
.ant-menu-dark .ant-menu-item-selected:after{border-right:0}
.ant-menu-dark .ant-menu-item-selected .anticon,.ant-menu-dark .ant-menu-item-selected .anticon+span,.ant-menu-dark .ant-menu-item-selected>a,.ant-menu-dark .ant-menu-item-selected>a:hover{color:#ffffff}
.ant-menu-submenu-popup.ant-menu-dark .ant-menu-item-selected,.ant-menu.ant-menu-dark .ant-menu-item-selected{background-color:#15223a}
.ant-menu-submenu-popup.ant-menu-dark .ant-menu-item-active,.ant-menu.ant-menu-dark .ant-menu-item-active{background-color:#38383800}
.ant-menu-submenu-popup.ant-menu-dark .ant-menu-item-selected,.ant-menu.ant-menu-dark .ant-menu-item-selected{background-color:#0a7557}
/*.ant-menu-submenu-popup.ant-menu-dark .ant-menu-item-active,.ant-menu.ant-menu-dark .ant-menu-item-active{background-color:#0a7557}*/
.ant-menu-dark .ant-menu-item-disabled,.ant-menu-dark .ant-menu-item-disabled>a,.ant-menu-dark .ant-menu-submenu-disabled,.ant-menu-dark .ant-menu-submenu-disabled>a{color:hsla(0,0%,100%,.35)!important;opacity:.8}
.ant-menu-dark .ant-menu-item-disabled>.ant-menu-submenu-title,.ant-menu-dark .ant-menu-submenu-disabled>.ant-menu-submenu-title{color:hsla(0,0%,100%,.35)!important}
.ant-menu-dark .ant-menu-item-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-item-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:before,.ant-menu-dark .ant-menu-submenu-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-submenu-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:before{background:hsla(0,0%,100%,.35)!important}
@@ -3231,7 +3232,7 @@ textarea.ant-input-number{max-width:100%;height:auto;min-height:32px;line-height
.ant-input-number-handler-down-inner:before,.ant-input-number-handler-up-inner:before{display:none}
.ant-input-number-handler-down-inner .ant-input-number-handler-down-inner-icon,.ant-input-number-handler-down-inner .ant-input-number-handler-up-inner-icon,.ant-input-number-handler-up-inner .ant-input-number-handler-down-inner-icon,.ant-input-number-handler-up-inner .ant-input-number-handler-up-inner-icon{display:block}
.ant-input-number-focused,.ant-input-number:hover{border-color:rgb(0, 150, 112) !important;border-right-width:1px!important}
.ant-input-number-focused{outline:0;box-shadow:0 0 0 2px rgba(24,144,255,.2)}
.ant-input-number-focused{outline:0;box-shadow:rgba(0, 150, 112, 0.2) 0px 0px 0px 2px}
.ant-input-number-disabled{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}
.ant-input-number-disabled:hover{border-color:#d9d9d9;border-right-width:1px!important}
.ant-input-number-disabled .ant-input-number-input{cursor:not-allowed}

View File

@@ -9,7 +9,6 @@ body {
#app {
height: 100%;
min-height: 100vh;
position: fixed;
top: 0;
left: 0;
@@ -28,6 +27,12 @@ body {
display: none;
}
@media (max-width: 768px) {
.ant-layout-sider {
display: none;
}
}
.ant-card {
border-radius: 1.5rem;
}
@@ -197,9 +202,25 @@ body {
}
.ant-card-dark:hover {
border-color: #e8e8e8;
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 70%);
/*border-color: #e8e8e8;
animation:light-shadow ease-in 3s infinite;*/
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 80%);
}
/*
@keyframes light-shadow {
0% {
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 60%);
}
20% {
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 60%);
}
60% {
box-shadow: 0 1px 11px 2px rgb(154 175 238 / 70%);
}
100% {
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 60%);
}
}*/
.ant-setting-textarea {
margin-top: 1.5rem;
@@ -208,6 +229,7 @@ body {
}
.ant-card-dark-box-nohover{
margin-top: .5rem;
padding: 0 20px 20px !important;
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 0%) !important;
}
@@ -240,6 +262,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;
@@ -267,14 +290,17 @@ body {
color: hsla(0,0%,100%,.65);
}
.ant-card-dark .ant-table-tbody>tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)>td,
.ant-card-dark .ant-select-dropdown-menu-item:hover:not(.ant-select-dropdown-menu-item-disabled),
.ant-card-dark .ant-calendar-date:hover,
.ant-card-dark .ant-select-dropdown-menu-item-active,
.ant-card-dark li.ant-calendar-time-picker-select-option-selected {
.ant-card-dark .ant-table-tbody>tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)>td {
background-color: #11314d;
}
.ant-card-dark .ant-select-dropdown-menu-item:hover:not(.ant-select-dropdown-menu-item-disabled),
.ant-card-dark .ant-select-dropdown-menu-item-active,
.ant-card-dark .ant-calendar-date:hover,
.ant-card-dark li.ant-calendar-time-picker-select-option-selected {
background-color: rgb(4, 119, 90);
}
.ant-card-dark tbody .ant-table-expanded-row,
.ant-card-dark .ant-calendar-time-picker-inner {
color: hsla(0,0%,100%,.65);

View File

@@ -5,8 +5,8 @@ const supportLangs = [
icon: '🇺🇸',
},
{
name: 'Farsi',
value: 'fa_IR',
name: 'فارسی',
value: 'fa-IR',
icon: '🇮🇷',
},
{
@@ -15,10 +15,15 @@ const supportLangs = [
icon: '🇨🇳',
},
{
name: 'Russian',
value: 'ru_RU',
name: 'Русский',
value: 'ru-RU',
icon: '🇷🇺',
},
{
name: 'Tiếng Việt',
value: 'vi-VN',
icon: '🇻🇳',
},
];
function getLang() {

View File

@@ -153,9 +153,9 @@ class DBInbound {
}
}
genLink(clientIndex) {
genLink(address=this.address, remark=this.remark, clientIndex=0) {
const inbound = this.toInbound();
return inbound.genLink(this.address, this.remark, clientIndex);
return inbound.genLink(address, remark, clientIndex);
}
get genInboundLinks() {
@@ -168,6 +168,7 @@ class AllSetting {
constructor(data) {
this.webListen = "";
this.webDomain = "";
this.webPort = 2053;
this.webCertFile = "";
this.webKeyFile = "";
@@ -180,9 +181,21 @@ class AllSetting {
this.tgBotChatId = "";
this.tgRunTime = "@daily";
this.tgBotBackup = false;
this.tgBotLoginNotify = true;
this.tgCpu = "";
this.tgLang = "en-US";
this.xrayTemplateConfig = "";
this.secretEnable = false;
this.subEnable = false;
this.subListen = "";
this.subPort = "2096";
this.subPath = "/sub/";
this.subDomain = "";
this.subCertFile = "";
this.subKeyFile = "";
this.subUpdates = 0;
this.subEncrypt = true;
this.subShowInfo = true;
this.timeLocation = "Asia/Tehran";

View File

@@ -4,7 +4,6 @@ const Protocols = {
TROJAN: 'trojan',
SHADOWSOCKS: 'shadowsocks',
DOKODEMO: 'dokodemo-door',
MTPROTO: 'mtproto',
SOCKS: 'socks',
HTTP: 'http',
};
@@ -17,8 +16,15 @@ const VmessMethods = {
};
const SSMethods = {
BLAKE3_AES_128_GCM: '2022-blake3-aes-128-gcm',
BLAKE3_AES_256_GCM: '2022-blake3-aes-256-gcm',
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',
};
const XTLS_FLOW_CONTROL = {
@@ -72,15 +78,16 @@ const UTLS_FINGERPRINT = {
};
const ALPN_OPTION = {
HTTP1: "http/1.1",
H2: "h2",
H3: "h3",
H2: "h2",
HTTP1: "http/1.1",
};
const SNIFFING_OPTION = {
HTTP: "http",
TLS: "tls",
QUIC: "quic",
FAKEDNS: "fakedns"
};
Object.freeze(Protocols);
@@ -379,7 +386,10 @@ class WsStreamSettings extends XrayCommonClass {
}
class HttpStreamSettings extends XrayCommonClass {
constructor(path='/', host=['']) {
constructor(
path='/',
host=[''],
) {
super();
this.path = path;
this.host = host.length === 0 ? [''] : host;
@@ -411,9 +421,10 @@ class HttpStreamSettings extends XrayCommonClass {
}
}
class QuicStreamSettings extends XrayCommonClass {
constructor(security=VmessMethods.NONE,
key='', type='none') {
key=RandomUtil.randomSeq(10), type='none') {
super();
this.security = security;
this.key = key;
@@ -442,7 +453,7 @@ class QuicStreamSettings extends XrayCommonClass {
class GrpcStreamSettings extends XrayCommonClass {
constructor(
serviceName="",
multiMode=false
multiMode=false,
) {
super();
this.serviceName = serviceName;
@@ -459,16 +470,18 @@ class GrpcStreamSettings extends XrayCommonClass {
toJson() {
return {
serviceName: this.serviceName,
multiMode: this.multiMode
multiMode: this.multiMode,
}
}
}
class TlsStreamSettings extends XrayCommonClass {
constructor(serverName='',
minVersion = TLS_VERSION_OPTION.TLS12,
maxVersion = TLS_VERSION_OPTION.TLS13,
cipherSuites = '',
rejectUnknownSni = false,
certificates=[new TlsStreamSettings.Cert()],
alpn=[ALPN_OPTION.H2,ALPN_OPTION.HTTP1],
settings=new TlsStreamSettings.Settings()) {
@@ -477,13 +490,14 @@ class TlsStreamSettings extends XrayCommonClass {
this.minVersion = minVersion;
this.maxVersion = maxVersion;
this.cipherSuites = cipherSuites;
this.rejectUnknownSni = rejectUnknownSni;
this.certs = certificates;
this.alpn = alpn;
this.settings = settings;
}
addCert(cert) {
this.certs.push(cert);
addCert() {
this.certs.push(new TlsStreamSettings.Cert());
}
removeCert(index) {
@@ -498,13 +512,14 @@ class TlsStreamSettings extends XrayCommonClass {
}
if (!ObjectUtil.isEmpty(json.settings)) {
settings = new TlsStreamSettings.Settings(json.settings.allowInsecure , json.settings.fingerprint, json.settings.serverName);
settings = new TlsStreamSettings.Settings(json.settings.allowInsecure , json.settings.fingerprint, json.settings.serverName, json.settings.domains);
}
return new TlsStreamSettings(
json.serverName,
json.minVersion,
json.maxVersion,
json.cipherSuites,
json.rejectUnknownSni,
certs,
json.alpn,
settings,
@@ -517,6 +532,7 @@ class TlsStreamSettings extends XrayCommonClass {
minVersion: this.minVersion,
maxVersion: this.maxVersion,
cipherSuites: this.cipherSuites,
rejectUnknownSni: this.rejectUnknownSni,
certificates: TlsStreamSettings.toJsonArray(this.certs),
alpn: this.alpn,
settings: this.settings,
@@ -525,13 +541,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={}) {
@@ -539,13 +556,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,
);
}
}
@@ -555,28 +574,32 @@ 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,
};
}
}
};
TlsStreamSettings.Settings = class extends XrayCommonClass {
constructor(allowInsecure = false, fingerprint = '', serverName = '') {
constructor(allowInsecure = false, fingerprint = '', serverName = '', domains = []) {
super();
this.allowInsecure = allowInsecure;
this.fingerprint = fingerprint;
this.serverName = serverName;
this.domains = domains;
}
static fromJson(json = {}) {
return new TlsStreamSettings.Settings(
json.allowInsecure,
json.fingerprint,
json.servername,
json.serverName,
json.domains,
);
}
toJson() {
@@ -584,6 +607,7 @@ TlsStreamSettings.Settings = class extends XrayCommonClass {
allowInsecure: this.allowInsecure,
fingerprint: this.fingerprint,
serverName: this.serverName,
domains: this.domains,
};
}
};
@@ -599,8 +623,8 @@ class XtlsStreamSettings extends XrayCommonClass {
this.settings = settings;
}
addCert(cert) {
this.certs.push(cert);
addCert() {
this.certs.push(new XtlsStreamSettings.Cert());
}
removeCert(index) {
@@ -706,7 +730,7 @@ class RealityStreamSettings extends XrayCommonClass {
minClient = '',
maxClient = '',
maxTimediff = 0,
shortIds = RandomUtil.randowShortId(),
shortIds = RandomUtil.randomShortId(),
settings= new RealityStreamSettings.Settings()
){
super();
@@ -725,7 +749,7 @@ class RealityStreamSettings extends XrayCommonClass {
static fromJson(json = {}) {
let settings;
if (!ObjectUtil.isEmpty(json.settings)) {
settings = new RealityStreamSettings.Settings(json.settings.publicKey , json.settings.fingerprint, json.settings.serverName);
settings = new RealityStreamSettings.Settings(json.settings.publicKey , json.settings.fingerprint, json.settings.serverName, json.settings.spiderX);
}
return new RealityStreamSettings(
json.show,
@@ -758,17 +782,19 @@ class RealityStreamSettings extends XrayCommonClass {
}
RealityStreamSettings.Settings = class extends XrayCommonClass {
constructor(publicKey = '', fingerprint = UTLS_FINGERPRINT.UTLS_FIREFOX, serverName = '') {
constructor(publicKey = '', fingerprint = UTLS_FINGERPRINT.UTLS_FIREFOX, serverName = '', spiderX= '/') {
super();
this.publicKey = publicKey;
this.fingerprint = fingerprint;
this.serverName = serverName;
this.spiderX = spiderX;
}
static fromJson(json = {}) {
return new RealityStreamSettings.Settings(
json.publicKey,
json.fingerprint,
json.serverName,
json.spiderX,
);
}
toJson() {
@@ -776,10 +802,37 @@ RealityStreamSettings.Settings = class extends XrayCommonClass {
publicKey: this.publicKey,
fingerprint: this.fingerprint,
serverName: this.serverName,
spiderX: this.spiderX,
};
}
};
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 = {}) {
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',
@@ -792,6 +845,7 @@ class StreamSettings extends XrayCommonClass {
httpSettings=new HttpStreamSettings(),
quicSettings=new QuicStreamSettings(),
grpcSettings=new GrpcStreamSettings(),
sockopt = undefined,
) {
super();
this.network = network;
@@ -805,6 +859,7 @@ class StreamSettings extends XrayCommonClass {
this.http = httpSettings;
this.quic = quicSettings;
this.grpc = grpcSettings;
this.sockopt = sockopt;
}
get isTls() {
@@ -844,6 +899,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(
@@ -858,6 +921,7 @@ class StreamSettings extends XrayCommonClass {
HttpStreamSettings.fromJson(json.httpSettings),
QuicStreamSettings.fromJson(json.quicSettings),
GrpcStreamSettings.fromJson(json.grpcSettings),
SockoptStreamSettings.fromJson(json.sockopt),
);
}
@@ -875,12 +939,13 @@ 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,
};
}
}
class Sniffing extends XrayCommonClass {
constructor(enabled=true, destOverride=['http', 'tls', 'quic']) {
constructor(enabled=true, destOverride=['http', 'tls', 'quic', 'fakedns']) {
super();
this.enabled = enabled;
this.destOverride = destOverride;
@@ -890,7 +955,7 @@ class Sniffing extends XrayCommonClass {
let destOverride = ObjectUtil.clone(json.destOverride);
if (!ObjectUtil.isEmpty(destOverride) && !ObjectUtil.isArrEmpty(destOverride)) {
if (ObjectUtil.isEmpty(destOverride[0])) {
destOverride = ['http', 'tls', 'quic'];
destOverride = ['http', 'tls', 'quic', 'fakedns'];
}
}
return new Sniffing(
@@ -960,7 +1025,6 @@ class Inbound extends XrayCommonClass {
}
}
//for Reality
get reality() {
return this.stream.security === 'reality';
}
@@ -1014,6 +1078,12 @@ class Inbound extends XrayCommonClass {
return "";
}
}
get isSSMultiUser() {
return this.method != SSMethods.BLAKE3_CHACHA20_POLY1305;
}
get isSS2022(){
return this.method.substring(0,4) === "2022";
}
get serverName() {
if (this.stream.isTls || this.stream.isXtls || this.stream.isReality) {
@@ -1039,7 +1109,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;
}
@@ -1083,7 +1153,7 @@ class Inbound extends XrayCommonClass {
return this.settings.trojans[index].expiryTime < new Date().getTime();
return false
case Protocols.SHADOWSOCKS:
if(this.settings.shadowsockses[index].expiryTime > 0)
if(this.settings.shadowsockses.length > 0 && this.settings.shadowsockses[index].expiryTime > 0)
return this.settings.shadowsockses[index].expiryTime < new Date().getTime();
return false
default:
@@ -1164,6 +1234,7 @@ class Inbound extends XrayCommonClass {
case Protocols.VMESS:
case Protocols.VLESS:
case Protocols.TROJAN:
case Protocols.SHADOWSOCKS:
return true;
default:
return false;
@@ -1202,7 +1273,6 @@ class Inbound extends XrayCommonClass {
add: address,
port: this.port,
id: this.settings.vmesses[clientIndex].id,
aid: this.settings.vmesses[clientIndex].alterId,
net: this.stream.network,
type: 'none',
tls: this.stream.security,
@@ -1339,7 +1409,7 @@ class Inbound extends XrayCommonClass {
}
}
if (this.xtls) {
else if (this.xtls) {
params.set("security", "xtls");
params.set("alpn", this.stream.xtls.alpn);
if(this.stream.xtls.settings.allowInsecure){
@@ -1354,7 +1424,7 @@ class Inbound extends XrayCommonClass {
params.set("flow", this.settings.vlesses[clientIndex].flow);
}
if (this.reality) {
else if (this.reality) {
params.set("security", "reality");
params.set("fp", this.stream.reality.settings.fingerprint);
params.set("pbk", this.stream.reality.settings.publicKey);
@@ -1370,6 +1440,13 @@ class Inbound extends XrayCommonClass {
if (!ObjectUtil.isEmpty(this.stream.reality.settings.serverName)) {
address = this.stream.reality.settings.serverName;
}
if (!ObjectUtil.isEmpty(this.stream.reality.settings.spiderX)) {
params.set("spx", this.stream.reality.settings.spiderX);
}
}
else {
params.set("security", "none");
}
const link = `vless://${uuid}@${address}:${port}`;
@@ -1384,8 +1461,68 @@ class Inbound extends XrayCommonClass {
genSSLink(address='', remark='', clientIndex = 0) {
let settings = this.settings;
const port = this.port;
const type = this.stream.network;
const params = new Map();
params.set("type", this.stream.network);
switch (type) {
case "tcp":
const tcp = this.stream.tcp;
if (tcp.type === 'http') {
const request = tcp.request;
params.set("path", request.path.join(','));
const index = request.headers.findIndex(header => header.name.toLowerCase() === 'host');
if (index >= 0) {
const host = request.headers[index].value;
params.set("host", host);
}
params.set("headerType", 'http');
}
break;
case "kcp":
const kcp = this.stream.kcp;
params.set("headerType", kcp.type);
params.set("seed", kcp.seed);
break;
case "ws":
const ws = this.stream.ws;
params.set("path", ws.path);
const index = ws.headers.findIndex(header => header.name.toLowerCase() === 'host');
if (index >= 0) {
const host = ws.headers[index].value;
params.set("host", host);
}
break;
case "http":
const http = this.stream.http;
params.set("path", http.path);
params.set("host", http.host);
break;
case "quic":
const quic = this.stream.quic;
params.set("quicSecurity", quic.security);
params.set("key", quic.key);
params.set("headerType", quic.type);
break;
case "grpc":
const grpc = this.stream.grpc;
params.set("serviceName", grpc.serviceName);
if(grpc.multiMode){
params.set("mode", "multi");
}
break;
}
return 'ss://' + safeBase64(settings.method + ':' + settings.password + ':' +settings.shadowsockses[clientIndex].password) + '@' + address + ':' + this.port + '#' + encodeURIComponent(remark);
let password = new Array();
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);
for (const [key, value] of params) {
url.searchParams.set(key, value)
}
url.hash = encodeURIComponent(remark);
return url.toString();
}
genTrojanLink(address = '', remark = '', clientIndex = 0) {
@@ -1457,7 +1594,7 @@ class Inbound extends XrayCommonClass {
}
}
if (this.reality) {
else if (this.reality) {
params.set("security", "reality");
params.set("fp", this.stream.reality.settings.fingerprint);
params.set("pbk", this.stream.reality.settings.publicKey);
@@ -1470,9 +1607,12 @@ class Inbound extends XrayCommonClass {
if (!ObjectUtil.isEmpty(this.stream.reality.settings.serverName)) {
address = this.stream.reality.settings.serverName;
}
if (!ObjectUtil.isEmpty(this.stream.reality.settings.spiderX)) {
params.set("spx", this.stream.reality.settings.spiderX);
}
}
if (this.xtls) {
else if (this.xtls) {
params.set("security", "xtls");
params.set("alpn", this.stream.xtls.alpn);
if(this.stream.xtls.settings.allowInsecure){
@@ -1487,6 +1627,10 @@ class Inbound extends XrayCommonClass {
params.set("flow", this.settings.trojans[clientIndex].flow);
}
else {
params.set("security", "none");
}
const link = `trojan://${settings.trojans[clientIndex].password}@${address}:${this.port}`;
const url = new URL(link);
for (const [key, value] of params) {
@@ -1498,25 +1642,13 @@ class Inbound extends XrayCommonClass {
genLink(address='', remark='', clientIndex=0) {
switch (this.protocol) {
case Protocols.VMESS:
if (this.settings.vmesses[clientIndex].email != ""){
remark += '-' + this.settings.vmesses[clientIndex].email
}
case Protocols.VMESS:
return this.genVmessLink(address, remark, clientIndex);
case Protocols.VLESS:
if (this.settings.vlesses[clientIndex].email != ""){
remark += '-' + this.settings.vlesses[clientIndex].email
}
return this.genVLESSLink(address, remark, clientIndex);
case Protocols.SHADOWSOCKS:
if (this.settings.shadowsockses[clientIndex].email != ""){
remark = this.settings.shadowsockses[clientIndex].email
}
return this.genSSLink(address, remark, clientIndex);
case Protocols.TROJAN:
if (this.settings.trojans[clientIndex].email != ""){
remark += '-' + this.settings.trojans[clientIndex].email
}
return this.genTrojanLink(address, remark, clientIndex);
default: return '';
}
@@ -1528,12 +1660,17 @@ class Inbound extends XrayCommonClass {
case Protocols.VMESS:
case Protocols.VLESS:
case Protocols.TROJAN:
JSON.parse(this.settings).clients.forEach((_,index) => {
link += this.genLink(address, remark, index) + '\r\n';
case Protocols.SHADOWSOCKS:
JSON.parse(this.settings).clients.forEach((client,index) => {
if(this.tls && !ObjectUtil.isArrEmpty(this.stream.tls.settings.domains)){
this.stream.tls.settings.domains.forEach((domain) => {
link += this.genLink(domain.domain, [remark, client.email, domain.remark].filter(x => x.length > 0).join('-'), index) + '\r\n';
});
} else {
link += this.genLink(address, [remark, client.email].filter(x => x.length > 0).join('-'), index) + '\r\n';
}
});
return link;
case Protocols.SHADOWSOCKS:
return (this.genSSLink(address, remark) + '\r\n');
default: return '';
}
}
@@ -1582,7 +1719,6 @@ Inbound.Settings = class extends XrayCommonClass {
case Protocols.TROJAN: return new Inbound.TrojanSettings(protocol);
case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings(protocol);
case Protocols.DOKODEMO: return new Inbound.DokodemoSettings(protocol);
case Protocols.MTPROTO: return new Inbound.MtprotoSettings(protocol);
case Protocols.SOCKS: return new Inbound.SocksSettings(protocol);
case Protocols.HTTP: return new Inbound.HttpSettings(protocol);
default: return null;
@@ -1596,7 +1732,6 @@ Inbound.Settings = class extends XrayCommonClass {
case Protocols.TROJAN: return Inbound.TrojanSettings.fromJson(json);
case Protocols.SHADOWSOCKS: return Inbound.ShadowsocksSettings.fromJson(json);
case Protocols.DOKODEMO: return Inbound.DokodemoSettings.fromJson(json);
case Protocols.MTPROTO: return Inbound.MtprotoSettings.fromJson(json);
case Protocols.SOCKS: return Inbound.SocksSettings.fromJson(json);
case Protocols.HTTP: return Inbound.HttpSettings.fromJson(json);
default: return null;
@@ -1610,11 +1745,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) {
@@ -1639,22 +1772,19 @@ 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(), alterId=0, email=RandomUtil.randomText(),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId='') {
constructor(id=RandomUtil.randomUUID(), email=RandomUtil.randomLowerAndNum(8),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16)) {
super();
this.id = id;
this.alterId = alterId;
this.email = email;
this.limitIp = limitIp;
this.totalGB = totalGB;
@@ -1667,7 +1797,6 @@ Inbound.VmessSettings.Vmess = class extends XrayCommonClass {
static fromJson(json={}) {
return new Inbound.VmessSettings.Vmess(
json.id,
json.alterId,
json.email,
json.limitIp,
json.totalGB,
@@ -1743,7 +1872,7 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
};
Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
constructor(id=RandomUtil.randomUUID(), flow='', email=RandomUtil.randomText(),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId='') {
constructor(id=RandomUtil.randomUUID(), flow='', email=RandomUtil.randomLowerAndNum(8),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16)) {
super();
this.id = id;
this.flow = flow;
@@ -1866,7 +1995,7 @@ Inbound.TrojanSettings = class extends Inbound.Settings {
}
};
Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
constructor(password=RandomUtil.randomSeq(10), flow='', email=RandomUtil.randomText(),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId='') {
constructor(password=RandomUtil.randomSeq(10), flow='', email=RandomUtil.randomLowerAndNum(8),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16)) {
super();
this.password = password;
this.flow = flow;
@@ -2008,8 +2137,9 @@ Inbound.ShadowsocksSettings = class extends Inbound.Settings {
};
Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
constructor(password=RandomUtil.randomShadowsocksPassword(), email=RandomUtil.randomText(),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId='') {
constructor(method='', password=RandomUtil.randomShadowsocksPassword(), email=RandomUtil.randomLowerAndNum(8),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16)) {
super();
this.method = method;
this.password = password;
this.email = email;
this.limitIp = limitIp;
@@ -2022,6 +2152,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
toJson() {
return {
method: this.method,
password: this.password,
email: this.email,
limitIp: this.limitIp,
@@ -2035,6 +2166,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
static fromJson(json = {}) {
return new Inbound.ShadowsocksSettings.Shadowsocks(
json.method,
json.password,
json.email,
json.limitIp,
@@ -2102,36 +2234,6 @@ Inbound.DokodemoSettings = class extends Inbound.Settings {
}
};
Inbound.MtprotoSettings = class extends Inbound.Settings {
constructor(protocol, users=[new Inbound.MtprotoSettings.MtUser()]) {
super(protocol);
this.users = users;
}
static fromJson(json={}) {
return new Inbound.MtprotoSettings(
Protocols.MTPROTO,
json.users.map(user => Inbound.MtprotoSettings.MtUser.fromJson(user)),
);
}
toJson() {
return {
users: XrayCommonClass.toJsonArray(this.users),
};
}
};
Inbound.MtprotoSettings.MtUser = class extends XrayCommonClass {
constructor(secret=RandomUtil.randomMTSecret()) {
super();
this.secret = secret;
}
static fromJson(json={}) {
return new Inbound.MtprotoSettings.MtUser(json.secret);
}
};
Inbound.SocksSettings = class extends Inbound.Settings {
constructor(protocol, auth='password', accounts=[new Inbound.SocksSettings.SocksAccount()], udp=false, ip='127.0.0.1') {
super(protocol);

View File

@@ -5,7 +5,9 @@ const ONE_TB = ONE_GB * 1024;
const ONE_PB = ONE_TB * 1024;
function sizeFormat(size) {
if (size < ONE_KB) {
if (size < 0) {
return "0 B";
} else if (size < ONE_KB) {
return size.toFixed(0) + " B";
} else if (size < ONE_MB) {
return (size / ONE_KB).toFixed(2) + " KB";
@@ -20,6 +22,23 @@ function sizeFormat(size) {
}
}
function cpuSpeedFormat(speed) {
if (speed > 1000) {
const GHz = speed / 1000;
return GHz.toFixed(2) + " GHz";
} else {
return speed.toFixed(2) + " MHz";
}
}
function cpuCoreFormat(cores) {
if (cores === 1) {
return "1 Core";
} else {
return cores + " Cores";
}
}
function base64(str) {
return Base64.encode(str);
}
@@ -70,23 +89,67 @@ function debounce(fn, delay) {
function getCookie(cname) {
let name = cname + '=';
let decodedCookie = decodeURIComponent(document.cookie);
let ca = decodedCookie.split(';');
let ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
// decode cookie value only
return decodeURIComponent(c.substring(name.length, c.length));
}
}
return '';
}
function setCookie(cname, cvalue, exdays) {
const d = new Date();
d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000);
let expires = 'expires=' + d.toUTCString();
document.cookie = cname + '=' + cvalue + ';' + expires + ';path=/';
// encode cookie value
document.cookie = cname + '=' + encodeURIComponent(cvalue) + ';' + expires + ';path=/';
}
function usageColor(data, threshold, total) {
switch (true) {
case data === null:
return 'blue';
case total <= 0:
return 'blue';
case data < total - threshold:
return 'cyan';
case data < total:
return 'orange';
default:
return 'red';
}
}
function doAllItemsExist(array1, array2) {
for (let i = 0; i < array1.length; i++) {
if (!array2.includes(array1[i])) {
return false;
}
}
return true;
}
function buildURL({ host, port, isTLS, base, path }) {
if (!host || host.length === 0) host = window.location.hostname;
if (!port || port.length === 0) port = window.location.port;
if (isTLS === undefined) isTLS = window.location.protocol === "https:";
const protocol = isTLS ? "https:" : "http:";
port = String(port);
if (port === "" || (isTLS && port === "443") || (!isTLS && port === "80")) {
port = "";
} else {
port = `:${port}`;
}
return `${protocol}//${host}${port}${base}${path}`;
}

View File

@@ -75,26 +75,11 @@ 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 shortIdSeq = [
'a', 'b', 'c', 'd', 'e', 'f',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
];
const seq = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
class RandomUtil {
static randomIntRange(min, max) {
return parseInt(Math.random() * (max - min) + min, 10);
return Math.floor(Math.random() * (max - min) + min);
}
static randomInt(n) {
@@ -109,59 +94,33 @@ class RandomUtil {
return str;
}
static randomShortIdSeq(count) {
static randomShortId() {
let str = '';
for (let i = 0; i < count; ++i) {
str += shortIdSeq[this.randomInt(16)];
for (let i = 0; i < 8; ++i) {
str += seq[this.randomInt(16)];
}
return str;
}
static randomLowerAndNum(count) {
static randomLowerAndNum(len) {
let str = '';
for (let i = 0; i < count; ++i) {
for (let i = 0; i < len; ++i) {
str += seq[this.randomInt(36)];
}
return str;
}
static randomMTSecret() {
let str = '';
for (let i = 0; i < 32; ++i) {
let index = this.randomInt(16);
if (index <= 9) {
str += index;
} else {
str += seq[index - 10];
}
}
return str;
}
static randomUUID() {
let d = new Date().getTime();
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
let r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);
return (c === 'x' ? r : (r & 0x7 | 0x8)).toString(16);
const template = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx';
return template.replace(/[xy]/g, function (c) {
const randomValues = new Uint8Array(1);
crypto.getRandomValues(randomValues);
let randomValue = randomValues[0] % 16;
let calculatedValue = (c === 'x') ? randomValue : (randomValue & 0x3 | 0x8);
return calculatedValue.toString(16);
});
}
static randomText() {
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
var string = '';
for (var ii = 0; ii < 8; ii++) {
string += chars[Math.floor(Math.random() * chars.length)];
}
return string;
}
static randowShortId() {
let str = '';
str += this.randomShortIdSeq(8);
return str;
}
static randomShadowsocksPassword() {
let array = new Uint8Array(32);
window.crypto.getRandomValues(array);

View File

@@ -1,10 +1,15 @@
package controller
import "github.com/gin-gonic/gin"
import (
"x-ui/web/service"
"github.com/gin-gonic/gin"
)
type APIController struct {
BaseController
inboundController *InboundController
Tgbot service.Tgbot
}
func NewAPIController(g *gin.RouterGroup) *APIController {
@@ -14,7 +19,7 @@ func NewAPIController(g *gin.RouterGroup) *APIController {
}
func (a *APIController) initRouter(g *gin.RouterGroup) {
g = g.Group("/xui/API/inbounds")
g = g.Group("/panel/api/inbounds")
g.Use(a.checkLogin)
g.GET("/list", a.getAllInbounds)
@@ -32,24 +37,30 @@ func (a *APIController) initRouter(g *gin.RouterGroup) {
g.POST("/resetAllTraffics", a.resetAllTraffics)
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
g.POST("/delDepletedClients/:id", a.delDepletedClients)
g.GET("/createbackup", a.createBackup)
a.inboundController = NewInboundController(g)
}
func (a *APIController) getAllInbounds(c *gin.Context) {
a.inboundController.getInbounds(c)
}
func (a *APIController) getSingleInbound(c *gin.Context) {
a.inboundController.getInbound(c)
}
func (a *APIController) getClientTraffics(c *gin.Context) {
a.inboundController.getClientTraffics(c)
}
func (a *APIController) addInbound(c *gin.Context) {
a.inboundController.addInbound(c)
}
func (a *APIController) delInbound(c *gin.Context) {
a.inboundController.delInbound(c)
}
func (a *APIController) updateInbound(c *gin.Context) {
a.inboundController.updateInbound(c)
}
@@ -61,24 +72,35 @@ func (a *APIController) getClientIps(c *gin.Context) {
func (a *APIController) clearClientIps(c *gin.Context) {
a.inboundController.clearClientIps(c)
}
func (a *APIController) addInboundClient(c *gin.Context) {
a.inboundController.addInboundClient(c)
}
func (a *APIController) delInboundClient(c *gin.Context) {
a.inboundController.delInboundClient(c)
}
func (a *APIController) updateInboundClient(c *gin.Context) {
a.inboundController.updateInboundClient(c)
}
func (a *APIController) resetClientTraffic(c *gin.Context) {
a.inboundController.resetClientTraffic(c)
}
func (a *APIController) resetAllTraffics(c *gin.Context) {
a.inboundController.resetAllTraffics(c)
}
func (a *APIController) resetAllClientTraffics(c *gin.Context) {
a.inboundController.resetAllClientTraffics(c)
}
func (a *APIController) delDepletedClients(c *gin.Context) {
a.inboundController.delDepletedClients(c)
}
func (a *APIController) createBackup(c *gin.Context) {
a.Tgbot.SendBackupToAdmins()
}

View File

@@ -2,6 +2,8 @@ package controller
import (
"net/http"
"x-ui/logger"
"x-ui/web/locale"
"x-ui/web/session"
"github.com/gin-gonic/gin"
@@ -13,7 +15,7 @@ type BaseController struct {
func (a *BaseController) checkLogin(c *gin.Context) {
if !session.IsLogin(c) {
if isAjax(c) {
pureJsonMsg(c, false, I18n(c, "pages.login.loginAgain"))
pureJsonMsg(c, false, I18nWeb(c, "pages.login.loginAgain"))
} else {
c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
}
@@ -23,11 +25,13 @@ func (a *BaseController) checkLogin(c *gin.Context) {
}
}
func I18n(c *gin.Context, name string) string {
anyfunc, _ := c.Get("I18n")
i18n, _ := anyfunc.(func(key string, params ...string) (string, error))
message, _ := i18n(name)
return message
func I18nWeb(c *gin.Context, name string, params ...string) string {
anyfunc, funcExists := c.Get("I18n")
if !funcExists {
logger.Warning("I18n function not exists in gin context!")
return ""
}
i18nFunc, _ := anyfunc.(func(i18nType locale.I18nType, key string, keyParams ...string) string)
msg := i18nFunc(locale.Web, name, params...)
return msg
}

View File

@@ -40,7 +40,6 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
g.POST("/resetAllTraffics", a.resetAllTraffics)
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
g.POST("/delDepletedClients/:id", a.delDepletedClients)
}
func (a *InboundController) startTask() {
@@ -60,20 +59,21 @@ func (a *InboundController) getInbounds(c *gin.Context) {
user := session.GetLoginUser(c)
inbounds, err := a.inboundService.GetInbounds(user.Id)
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.toasts.obtain"), err)
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
return
}
jsonObj(c, inbounds, nil)
}
func (a *InboundController) getInbound(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
jsonMsg(c, I18n(c, "get"), err)
jsonMsg(c, I18nWeb(c, "get"), err)
return
}
inbound, err := a.inboundService.GetInbound(id)
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.toasts.obtain"), err)
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
return
}
jsonObj(c, inbound, nil)
@@ -93,16 +93,17 @@ func (a *InboundController) addInbound(c *gin.Context) {
inbound := &model.Inbound{}
err := c.ShouldBind(inbound)
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.create"), err)
jsonMsg(c, I18nWeb(c, "pages.inbounds.create"), err)
return
}
user := session.GetLoginUser(c)
inbound.UserId = user.Id
inbound.Enable = true
inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
inbound, err = a.inboundService.AddInbound(inbound)
jsonMsgObj(c, I18n(c, "pages.inbounds.create"), inbound, err)
if err == nil {
needRestart := false
inbound, needRestart, err = a.inboundService.AddInbound(inbound)
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.create"), inbound, err)
if err == nil && needRestart {
a.xrayService.SetToNeedRestart()
}
}
@@ -110,12 +111,13 @@ func (a *InboundController) addInbound(c *gin.Context) {
func (a *InboundController) delInbound(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
jsonMsg(c, I18n(c, "delete"), err)
jsonMsg(c, I18nWeb(c, "delete"), err)
return
}
err = a.inboundService.DelInbound(id)
jsonMsgObj(c, I18n(c, "delete"), id, err)
if err == nil {
needRestart := true
needRestart, err = a.inboundService.DelInbound(id)
jsonMsgObj(c, I18nWeb(c, "delete"), id, err)
if err == nil && needRestart {
a.xrayService.SetToNeedRestart()
}
}
@@ -123,7 +125,7 @@ func (a *InboundController) delInbound(c *gin.Context) {
func (a *InboundController) updateInbound(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
return
}
inbound := &model.Inbound{
@@ -131,12 +133,13 @@ func (a *InboundController) updateInbound(c *gin.Context) {
}
err = c.ShouldBind(inbound)
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
return
}
inbound, err = a.inboundService.UpdateInbound(inbound)
jsonMsgObj(c, I18n(c, "pages.inbounds.update"), inbound, err)
if err == nil {
needRestart := true
inbound, needRestart, err = a.inboundService.UpdateInbound(inbound)
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.update"), inbound, err)
if err == nil && needRestart {
a.xrayService.SetToNeedRestart()
}
}
@@ -145,12 +148,14 @@ func (a *InboundController) getClientIps(c *gin.Context) {
email := c.Param("email")
ips, err := a.inboundService.GetInboundClientIps(email)
if err != nil {
if err != nil || ips == "" {
jsonObj(c, "No IP Record", nil)
return
}
jsonObj(c, ips, nil)
}
func (a *InboundController) clearClientIps(c *gin.Context) {
email := c.Param("email")
@@ -161,21 +166,24 @@ func (a *InboundController) clearClientIps(c *gin.Context) {
}
jsonMsg(c, "Log Cleared", nil)
}
func (a *InboundController) addInboundClient(c *gin.Context) {
data := &model.Inbound{}
err := c.ShouldBind(data)
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
return
}
err = a.inboundService.AddInboundClient(data)
needRestart := true
needRestart, err = a.inboundService.AddInboundClient(data)
if err != nil {
jsonMsg(c, "Something went wrong!", err)
return
}
jsonMsg(c, "Client(s) added", nil)
if err == nil {
if err == nil && needRestart {
a.xrayService.SetToNeedRestart()
}
}
@@ -183,18 +191,20 @@ func (a *InboundController) addInboundClient(c *gin.Context) {
func (a *InboundController) delInboundClient(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
return
}
clientId := c.Param("clientId")
err = a.inboundService.DelInboundClient(id, clientId)
needRestart := true
needRestart, err = a.inboundService.DelInboundClient(id, clientId)
if err != nil {
jsonMsg(c, "Something went wrong!", err)
return
}
jsonMsg(c, "Client deleted", nil)
if err == nil {
if err == nil && needRestart {
a.xrayService.SetToNeedRestart()
}
}
@@ -205,17 +215,19 @@ func (a *InboundController) updateInboundClient(c *gin.Context) {
inbound := &model.Inbound{}
err := c.ShouldBind(inbound)
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
return
}
err = a.inboundService.UpdateInboundClient(inbound, clientId)
needRestart := true
needRestart, err = a.inboundService.UpdateInboundClient(inbound, clientId)
if err != nil {
jsonMsg(c, "Something went wrong!", err)
return
}
jsonMsg(c, "Client updated", nil)
if err == nil {
if err == nil && needRestart {
a.xrayService.SetToNeedRestart()
}
}
@@ -223,18 +235,20 @@ func (a *InboundController) updateInboundClient(c *gin.Context) {
func (a *InboundController) resetClientTraffic(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
return
}
email := c.Param("email")
err = a.inboundService.ResetClientTraffic(id, email)
needRestart := true
needRestart, err = a.inboundService.ResetClientTraffic(id, email)
if err != nil {
jsonMsg(c, "Something went wrong!", err)
return
}
jsonMsg(c, "traffic reseted", nil)
if err == nil {
if err == nil && needRestart {
a.xrayService.SetToNeedRestart()
}
}
@@ -244,6 +258,8 @@ func (a *InboundController) resetAllTraffics(c *gin.Context) {
if err != nil {
jsonMsg(c, "Something went wrong!", err)
return
} else {
a.xrayService.SetToNeedRestart()
}
jsonMsg(c, "All traffics reseted", nil)
}
@@ -251,7 +267,7 @@ func (a *InboundController) resetAllTraffics(c *gin.Context) {
func (a *InboundController) resetAllClientTraffics(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
return
}
@@ -259,6 +275,8 @@ func (a *InboundController) resetAllClientTraffics(c *gin.Context) {
if err != nil {
jsonMsg(c, "Something went wrong!", err)
return
} else {
a.xrayService.SetToNeedRestart()
}
jsonMsg(c, "All traffics of client reseted", nil)
}
@@ -266,7 +284,7 @@ func (a *InboundController) resetAllClientTraffics(c *gin.Context) {
func (a *InboundController) delDepletedClients(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
return
}
err = a.inboundService.DelDepletedClients(id)

View File

@@ -49,44 +49,45 @@ func (a *IndexController) login(c *gin.Context) {
var form LoginForm
err := c.ShouldBind(&form)
if err != nil {
pureJsonMsg(c, false, I18n(c, "pages.login.toasts.invalidFormData"))
pureJsonMsg(c, false, I18nWeb(c, "pages.login.toasts.invalidFormData"))
return
}
if form.Username == "" {
pureJsonMsg(c, false, I18n(c, "pages.login.toasts.emptyUsername"))
pureJsonMsg(c, false, I18nWeb(c, "pages.login.toasts.emptyUsername"))
return
}
if form.Password == "" {
pureJsonMsg(c, false, I18n(c, "pages.login.toasts.emptyPassword"))
pureJsonMsg(c, false, I18nWeb(c, "pages.login.toasts.emptyPassword"))
return
}
user := a.userService.CheckUser(form.Username, form.Password, form.LoginSecret)
timeStr := time.Now().Format("2006-01-02 15:04:05")
if user == nil {
logger.Warningf("wrong username or password: \"%s\" \"%s\"", form.Username, form.Password)
a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0)
logger.Infof("wrong username or password: \"%s\" \"%s\"", form.Username, form.Password)
pureJsonMsg(c, false, I18n(c, "pages.login.toasts.wrongUsernameOrPassword"))
pureJsonMsg(c, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword"))
return
} else {
logger.Infof("%s login success,Ip Address:%s\n", form.Username, getRemoteIp(c))
logger.Infof("%s login success, Ip Address: %s\n", form.Username, getRemoteIp(c))
a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 1)
}
sessionMaxAge, err := a.settingService.GetSessionMaxAge()
if err != nil {
logger.Infof("Unable to get session's max age from DB")
logger.Warningf("Unable to get session's max age from DB")
}
if sessionMaxAge > 0 {
err = session.SetMaxAge(c, sessionMaxAge*60)
if err != nil {
logger.Infof("Unable to set session's max age")
logger.Warningf("Unable to set session's max age")
}
}
err = session.SetLoginUser(c, user)
logger.Info("user", user.Id, "login success")
jsonMsg(c, I18n(c, "pages.login.toasts.successLogin"), err)
jsonMsg(c, I18nWeb(c, "pages.login.toasts.successLogin"), err)
}
func (a *IndexController) logout(c *gin.Context) {

View File

@@ -1,6 +1,9 @@
package controller
import (
"fmt"
"net/http"
"regexp"
"time"
"x-ui/web/global"
"x-ui/web/service"
@@ -8,6 +11,8 @@ import (
"github.com/gin-gonic/gin"
)
var filenameRegex = regexp.MustCompile(`^[a-zA-Z0-9_\-.]+$`)
type ServerController struct {
BaseController
@@ -76,7 +81,7 @@ func (a *ServerController) getXrayVersion(c *gin.Context) {
versions, err := a.serverService.GetXrayVersions()
if err != nil {
jsonMsg(c, I18n(c, "getVersion"), err)
jsonMsg(c, I18nWeb(c, "getVersion"), err)
return
}
@@ -89,7 +94,7 @@ func (a *ServerController) getXrayVersion(c *gin.Context) {
func (a *ServerController) installXray(c *gin.Context) {
version := c.Param("version")
err := a.serverService.UpdateXray(version)
jsonMsg(c, I18n(c, "install")+" xray", err)
jsonMsg(c, I18nWeb(c, "install")+" xray", err)
}
func (a *ServerController) stopXrayService(c *gin.Context) {
@@ -113,11 +118,9 @@ func (a *ServerController) restartXrayService(c *gin.Context) {
func (a *ServerController) getLogs(c *gin.Context) {
count := c.Param("count")
logs, err := a.serverService.GetLogs(count)
if err != nil {
jsonMsg(c, "getLogs", err)
return
}
level := c.PostForm("level")
syslog := c.PostForm("syslog")
logs := a.serverService.GetLogs(count, level, syslog)
jsonObj(c, logs, nil)
}
@@ -136,14 +139,27 @@ func (a *ServerController) getDb(c *gin.Context) {
jsonMsg(c, "get Database", err)
return
}
filename := "x-ui.db"
if !isValidFilename(filename) {
c.AbortWithError(http.StatusBadRequest, fmt.Errorf("invalid filename"))
return
}
// Set the headers for the response
c.Header("Content-Type", "application/octet-stream")
c.Header("Content-Disposition", "attachment; filename=x-ui.db")
c.Header("Content-Disposition", "attachment; filename="+filename)
// Write the file contents to the response
c.Writer.Write(db)
}
func isValidFilename(filename string) bool {
// Validate that the filename only contains allowed characters
return filenameRegex.MatchString(filename)
}
func (a *ServerController) importDB(c *gin.Context) {
// Get the file from the request body
file, _, err := c.Request.FormFile("db")

View File

@@ -49,7 +49,7 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) {
func (a *SettingController) getAllSetting(c *gin.Context) {
allSetting, err := a.settingService.GetAllSetting()
if err != nil {
jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return
}
jsonObj(c, allSetting, nil)
@@ -58,39 +58,51 @@ func (a *SettingController) getAllSetting(c *gin.Context) {
func (a *SettingController) getDefaultJsonConfig(c *gin.Context) {
defaultJsonConfig, err := a.settingService.GetDefaultJsonConfig()
if err != nil {
jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return
}
jsonObj(c, defaultJsonConfig, nil)
}
func (a *SettingController) getDefaultSettings(c *gin.Context) {
expireDiff, err := a.settingService.GetExpireDiff()
if err != nil {
jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
return
type settingFunc func() (interface{}, error)
settings := map[string]settingFunc{
"expireDiff": func() (interface{}, error) { return a.settingService.GetExpireDiff() },
"trafficDiff": func() (interface{}, error) { return a.settingService.GetTrafficDiff() },
"defaultCert": func() (interface{}, error) { return a.settingService.GetCertFile() },
"defaultKey": func() (interface{}, error) { return a.settingService.GetKeyFile() },
"tgBotEnable": func() (interface{}, error) { return a.settingService.GetTgbotenabled() },
"subEnable": func() (interface{}, error) { return a.settingService.GetSubEnable() },
"subPort": func() (interface{}, error) { return a.settingService.GetSubPort() },
"subPath": func() (interface{}, error) { return a.settingService.GetSubPath() },
"subDomain": func() (interface{}, error) { return a.settingService.GetSubDomain() },
"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() },
}
trafficDiff, err := a.settingService.GetTrafficDiff()
if err != nil {
jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
return
result := make(map[string]interface{})
for key, fn := range settings {
value, err := fn()
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return
}
result[key] = value
}
defaultCert, err := a.settingService.GetCertFile()
if err != nil {
jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
return
}
defaultKey, err := a.settingService.GetKeyFile()
if err != nil {
jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
return
}
result := map[string]interface{}{
"expireDiff": expireDiff,
"trafficDiff": trafficDiff,
"defaultCert": defaultCert,
"defaultKey": defaultKey,
subTLS := false
if result["subKeyFile"] != "" || result["subCertFile"] != "" {
subTLS = true
}
result["subTLS"] = subTLS
delete(result, "subKeyFile")
delete(result, "subCertFile")
jsonObj(c, result, nil)
}
@@ -98,27 +110,27 @@ func (a *SettingController) updateSetting(c *gin.Context) {
allSetting := &entity.AllSetting{}
err := c.ShouldBind(allSetting)
if err != nil {
jsonMsg(c, I18n(c, "pages.settings.toasts.modifySettings"), err)
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
return
}
err = a.settingService.UpdateAllSetting(allSetting)
jsonMsg(c, I18n(c, "pages.settings.toasts.modifySettings"), err)
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
}
func (a *SettingController) updateUser(c *gin.Context) {
form := &updateUserForm{}
err := c.ShouldBind(form)
if err != nil {
jsonMsg(c, I18n(c, "pages.settings.toasts.modifySettings"), err)
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
return
}
user := session.GetLoginUser(c)
if user.Username != form.OldUsername || user.Password != form.OldPassword {
jsonMsg(c, I18n(c, "pages.settings.toasts.modifyUser"), errors.New(I18n(c, "pages.settings.toasts.originalUserPassIncorrect")))
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), errors.New(I18nWeb(c, "pages.settings.toasts.originalUserPassIncorrect")))
return
}
if form.NewUsername == "" || form.NewPassword == "" {
jsonMsg(c, I18n(c, "pages.settings.toasts.modifyUser"), errors.New(I18n(c, "pages.settings.toasts.userPassMustBeNotEmpty")))
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), errors.New(I18nWeb(c, "pages.settings.toasts.userPassMustBeNotEmpty")))
return
}
err = a.userService.UpdateUser(user.Id, form.NewUsername, form.NewPassword)
@@ -127,19 +139,19 @@ func (a *SettingController) updateUser(c *gin.Context) {
user.Password = form.NewPassword
session.SetLoginUser(c, user)
}
jsonMsg(c, I18n(c, "pages.settings.toasts.modifyUser"), err)
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err)
}
func (a *SettingController) restartPanel(c *gin.Context) {
err := a.panelService.RestartPanel(time.Second * 3)
jsonMsg(c, I18n(c, "pages.settings.restartPanel"), err)
jsonMsg(c, I18nWeb(c, "pages.settings.restartPanel"), err)
}
func (a *SettingController) updateSecret(c *gin.Context) {
form := &updateSecretForm{}
err := c.ShouldBind(form)
if err != nil {
jsonMsg(c, I18n(c, "pages.settings.toasts.modifySettings"), err)
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
}
user := session.GetLoginUser(c)
err = a.userService.UpdateUserSecret(user.Id, form.LoginSecret)
@@ -147,7 +159,7 @@ func (a *SettingController) updateSecret(c *gin.Context) {
user.LoginSecret = form.LoginSecret
session.SetLoginUser(c, user)
}
jsonMsg(c, I18n(c, "pages.settings.toasts.modifyUser"), err)
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err)
}
func (a *SettingController) getUserSecret(c *gin.Context) {

View File

@@ -38,12 +38,12 @@ func jsonMsgObj(c *gin.Context, msg string, obj interface{}, err error) {
if err == nil {
m.Success = true
if msg != "" {
m.Msg = msg + I18n(c, "success")
m.Msg = msg + I18nWeb(c, "success")
}
} else {
m.Success = false
m.Msg = msg + I18n(c, "fail") + ": " + err.Error()
logger.Warning(msg+I18n(c, "fail")+": ", err)
m.Msg = msg + I18nWeb(c, "fail") + ": " + err.Error()
logger.Warning(msg+I18nWeb(c, "fail")+": ", err)
}
c.JSON(http.StatusOK, m)
}

View File

@@ -28,6 +28,7 @@ type Pager struct {
type AllSetting struct {
WebListen string `json:"webListen" form:"webListen"`
WebDomain string `json:"webDomain" form:"webDomain"`
WebPort int `json:"webPort" form:"webPort"`
WebCertFile string `json:"webCertFile" form:"webCertFile"`
WebKeyFile string `json:"webKeyFile" form:"webKeyFile"`
@@ -40,10 +41,22 @@ type AllSetting struct {
TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"`
TgRunTime string `json:"tgRunTime" form:"tgRunTime"`
TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"`
TgBotLoginNotify bool `json:"tgBotLoginNotify" form:"tgBotLoginNotify"`
TgCpu int `json:"tgCpu" form:"tgCpu"`
TgLang string `json:"tgLang" form:"tgLang"`
XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"`
TimeLocation string `json:"timeLocation" form:"timeLocation"`
SecretEnable bool `json:"secretEnable" form:"secretEnable"`
SubEnable bool `json:"subEnable" form:"subEnable"`
SubListen string `json:"subListen" form:"subListen"`
SubPort int `json:"subPort" form:"subPort"`
SubPath string `json:"subPath" form:"subPath"`
SubDomain string `json:"subDomain" form:"subDomain"`
SubCertFile string `json:"subCertFile" form:"subCertFile"`
SubKeyFile string `json:"subKeyFile" form:"subKeyFile"`
SubUpdates int `json:"subUpdates" form:"subUpdates"`
SubEncrypt bool `json:"subEncrypt" form:"subEncrypt"`
SubShowInfo bool `json:"subShowInfo" form:"subShowInfo"`
}
func (s *AllSetting) CheckValid() error {
@@ -54,10 +67,25 @@ func (s *AllSetting) CheckValid() error {
}
}
if s.SubListen != "" {
ip := net.ParseIP(s.SubListen)
if ip == nil {
return common.NewError("Sub listen is not valid ip:", s.SubListen)
}
}
if s.WebPort <= 0 || s.WebPort > 65535 {
return common.NewError("web port is not a valid port:", s.WebPort)
}
if s.SubPort <= 0 || s.SubPort > 65535 {
return common.NewError("Sub port is not a valid port:", s.SubPort)
}
if s.SubPort == s.WebPort {
return common.NewError("Sub and Web could not use same port:", s.SubPort)
}
if s.WebCertFile != "" || s.WebKeyFile != "" {
_, err := tls.LoadX509KeyPair(s.WebCertFile, s.WebKeyFile)
if err != nil {
@@ -65,6 +93,13 @@ func (s *AllSetting) CheckValid() error {
}
}
if s.SubCertFile != "" || s.SubKeyFile != "" {
_, err := tls.LoadX509KeyPair(s.SubCertFile, s.SubKeyFile)
if err != nil {
return common.NewErrorf("cert file <%v> or key file <%v> invalid: %v", s.SubCertFile, s.SubKeyFile, err)
}
}
if !strings.HasPrefix(s.WebBasePath, "/") {
s.WebBasePath = "/" + s.WebBasePath
}

View File

@@ -8,12 +8,17 @@ import (
)
var webServer WebServer
var subServer SubServer
type WebServer interface {
GetCron() *cron.Cron
GetCtx() context.Context
}
type SubServer interface {
GetCtx() context.Context
}
func SetWebServer(s WebServer) {
webServer = s
}
@@ -21,3 +26,11 @@ func SetWebServer(s WebServer) {
func GetWebServer() WebServer {
return webServer
}
func SetSubServer(s SubServer) {
subServer = s
}
func GetSubServer() SubServer {
return subServer
}

80
web/global/hashStorage.go Normal file
View File

@@ -0,0 +1,80 @@
package global
import (
"crypto/md5"
"encoding/hex"
"regexp"
"sync"
"time"
)
type HashEntry struct {
Hash string
Value string
Timestamp time.Time
}
type HashStorage struct {
sync.RWMutex
Data map[string]HashEntry
Expiration time.Duration
}
func NewHashStorage(expiration time.Duration) *HashStorage {
return &HashStorage{
Data: make(map[string]HashEntry),
Expiration: expiration,
}
}
func (h *HashStorage) SaveHash(query string) string {
h.Lock()
defer h.Unlock()
md5Hash := md5.Sum([]byte(query))
md5HashString := hex.EncodeToString(md5Hash[:])
entry := HashEntry{
Hash: md5HashString,
Value: query,
Timestamp: time.Now(),
}
h.Data[md5HashString] = entry
return md5HashString
}
func (h *HashStorage) GetValue(hash string) (string, bool) {
h.RLock()
defer h.RUnlock()
entry, exists := h.Data[hash]
return entry.Value, exists
}
func (h *HashStorage) IsMD5(hash string) bool {
match, _ := regexp.MatchString("^[a-f0-9]{32}$", hash)
return match
}
func (h *HashStorage) RemoveExpiredHashes() {
h.Lock()
defer h.Unlock()
now := time.Now()
for hash, entry := range h.Data {
if now.Sub(entry.Timestamp) > h.Expiration {
delete(h.Data, hash)
}
}
}
func (h *HashStorage) Reset() {
h.Lock()
defer h.Unlock()
h.Data = make(map[string]HashEntry)
}

View File

@@ -7,47 +7,55 @@
<a-tag color="green" style="margin-bottom: 10px;display: block;text-align: center;">
{{ i18n "pages.inbounds.clickOnQRcode" }}
</a-tag>
<a-tag v-if="qrModal.clientName" color="orange" style="margin-bottom: 10px;display: block;text-align: center;">
{{ i18n "pages.inbounds.email" }}: "[[ qrModal.clientName ]]"
</a-tag>
<canvas @click="copyToClipboard()" id="qrCode" style="width: 100%; height: 100%; margin-top: 10px;"></canvas>
<template v-if="app.subSettings.enable && qrModal.subId">
<a-divider>Subscription</a-divider>
<canvas @click="copyToClipboard('qrCode-sub',genSubLink(qrModal.client.subId))" id="qrCode-sub" style="width: 100%; height: 100%;"></canvas>
</template>
<a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
<template v-for="(row, index) in qrModal.qrcodes">
<a-tag color="orange" style="margin-top: 10px;display: block;text-align: center;">[[ row.remark ]]</a-tag>
<canvas @click="copyToClipboard('qrCode-'+index, row.link)" :id="'qrCode-'+index" style="width: 100%; height: 100%;"></canvas>
</template>
</a-modal>
<script>
const qrModal = {
title: '',
content: '',
clientIndex: 0,
inbound: new Inbound(),
dbInbound: new DBInbound(),
copyText: '',
clientName: null,
qrcode: null,
client: null,
qrcodes: [],
clipboard: null,
visible: false,
show: function (title = '', content = '', dbInbound = new DBInbound(), copyText = '', clientName = null) {
subId: '',
show: function (title = '', dbInbound = new DBInbound(), clientIndex = 0) {
this.title = title;
this.content = content;
this.clientIndex = clientIndex;
this.dbInbound = dbInbound;
this.inbound = dbInbound.toInbound();
this.clientName = clientName;
if (ObjectUtil.isEmpty(copyText)) {
this.copyText = content;
settings = JSON.parse(this.inbound.settings);
this.client = settings.clients[clientIndex];
remark = [this.dbInbound.remark, ( this.client ? 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: remarkText,
link: this.inbound.genLink(domain.domain, remarkText, clientIndex)
});
});
} else {
this.copyText = copyText;
this.qrcodes.push({
remark: remark,
link: this.inbound.genLink(address, remark, clientIndex)
});
}
this.visible = true;
qrModalApp.$nextTick(() => {
if (this.qrcode === null) {
this.qrcode = new QRious({
element: document.querySelector('#qrCode'),
size: 260,
value: content,
});
} else {
this.qrcode.value = content;
}
});
},
close: function () {
this.visible = false;
@@ -61,16 +69,36 @@
qrModal: qrModal,
},
methods: {
copyToClipboard() {
this.qrModal.clipboard = new ClipboardJS('#qrCode', {
text: () => this.qrModal.copyText,
copyToClipboard(elmentId, content) {
this.qrModal.clipboard = new ClipboardJS('#' + elmentId, {
text: () => content,
});
this.qrModal.clipboard.on('success', () => {
app.$message.success('{{ i18n "copied" }}')
this.qrModal.clipboard.destroy();
});
},
setQrCode(elmentId, content) {
new QRious({
element: document.querySelector('#' + elmentId),
size: 260,
value: content,
});
},
genSubLink(subID) {
const { domain: host, port, tls: isTLS, path: base } = app.subSettings;
return buildURL({ host, port, isTLS, base, path: subID+'?name='+subID });
}
},
updated() {
if (qrModal.client && qrModal.client.subId) {
qrModal.subId = qrModal.client.subId;
this.setQrCode("qrCode-sub", this.genSubLink(qrModal.subId));
}
qrModal.qrcodes.forEach((element, index) => {
this.setQrCode("qrCode-" + index, element.link);
});
}
});
</script>

View File

@@ -86,8 +86,8 @@
<a-row justify="center" class="centered">
<a-col :span="12">
<a-select ref="selectLang" v-model="lang" @change="setLang(lang)" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option :value="l.value" label="English" v-for="l in supportLangs">
<span role="img" aria-label="l.name" v-text="l.icon"></span>
<a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs">
<span role="img" :aria-label="l.name" v-text="l.icon"></span>
&nbsp;&nbsp;<span v-text="l.name"></span>
</a-select-option>
</a-select>

View File

@@ -33,7 +33,7 @@
<span slot="label">{{ i18n "pages.client.clientCount" }}</span>
<a-input-number v-model="clientsBulkModal.quantity" :min="1" :max="100"></a-input-number>
</a-form-item>
<a-form-item>
<a-form-item v-if="app.subSettings.enable">
<span slot="label">
Subscription
<a-tooltip>
@@ -45,7 +45,7 @@
</span>
<a-input v-model.trim="clientsBulkModal.subId"></a-input>
</a-form-item>
<a-form-item>
<a-form-item v-if="app.tgBotEnable">
<span slot="label">
Telegram ID
<a-tooltip>
@@ -57,6 +57,7 @@
</span>
<a-input v-model.trim="clientsBulkModal.tgId"></a-input>
</a-form-item>
<br>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.IPLimit" }}</span>
@@ -158,8 +159,8 @@
newClient = clientsBulkModal.newClient(clientsBulkModal.dbInbound.protocol);
if (method == 4) newClient.email = "";
newClient.email += useNum ? prefix + i.toString() + postfix : prefix + postfix;
newClient.subId = clientsBulkModal.subId;
newClient.tgId = clientsBulkModal.tgId;
if (clientsBulkModal.subId.length > 0) newClient.subId = clientsBulkModal.subId;
if (clientsBulkModal.tgId.length > 0) newClient.tgId = clientsBulkModal.tgId;
newClient.limitIp = clientsBulkModal.limitIp;
newClient._totalGB = clientsBulkModal.totalGB;
newClient._expiryTime = clientsBulkModal.expiryTime;
@@ -199,19 +200,12 @@
this.inbound = dbInbound.toInbound();
this.delayedStart = false;
},
getClients(protocol, clientSettings) {
switch (protocol) {
case Protocols.VMESS: return clientSettings.vmesses;
case Protocols.VLESS: return clientSettings.vlesses;
case Protocols.TROJAN: return clientSettings.trojans;
default: return null;
}
},
newClient(protocol) {
switch (protocol) {
case Protocols.VMESS: return new Inbound.VmessSettings.Vmess();
case Protocols.VLESS: return new Inbound.VLESSSettings.VLESS();
case Protocols.TROJAN: return new Inbound.TrojanSettings.Trojan();
case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings.Shadowsocks(clientsBulkModal.inbound.settings.shadowsockses[0].method);
default: return null;
}
},

View File

@@ -20,7 +20,6 @@
oldClientId: "",
index: null,
clientIps: null,
isExpired: false,
delayedStart: false,
ok() {
if (clientModal.isEdit) {
@@ -38,7 +37,6 @@
this.inbound = dbInbound.toInbound();
this.clients = this.getClients(this.inbound.protocol, this.inbound.settings);
this.index = index === null ? this.clients.length : index;
this.isExpired = isEdit ? this.inbound.isExpiry(this.index) : false;
this.delayedStart = false;
if (isEdit) {
if (this.clients[index].expiryTime < 0) {
@@ -72,7 +70,7 @@
case Protocols.VMESS: return clients.push(new Inbound.VmessSettings.Vmess());
case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS());
case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan());
case Protocols.SHADOWSOCKS: return clients.push(new Inbound.ShadowsocksSettings.Shadowsocks());
case Protocols.SHADOWSOCKS: return clients.push(new Inbound.ShadowsocksSettings.Shadowsocks(clients[0].method));
default: return null;
}
},
@@ -109,13 +107,10 @@
return true
},
get isExpiry() {
return this.clientModal.isExpired
return this.clientModal.isEdit && this.client.expiryTime >0 ? (this.client.expiryTime < new Date().getTime()) : false;
},
get statsColor() {
if (!clientStats) return 'blue'
if (clientStats.total <= 0) return 'blue'
else if (clientStats.total > 0 && (clientStats.down + clientStats.up) < clientStats.total) return 'cyan'
else return 'red'
return usageColor(clientStats.up + clientStats.down, app.trafficDiff, this.client.totalGB);
},
get delayedExpireDays() {
return this.client && this.client.expiryTime < 0 ? this.client.expiryTime / -86400000 : 0;
@@ -125,26 +120,32 @@
},
},
methods: {
async getDBClientIps(email, event) {
const msg = await HttpUtil.post('/panel/inbound/clientIps/' + email);
async getDBClientIps(email) {
const msg = await HttpUtil.post(`/panel/inbound/clientIps/${email}`);
if (!msg.success) {
document.getElementById("clientIPs").value = msg.obj;
return;
}
try {
ips = JSON.parse(msg.obj)
ips = ips.join(",")
event.target.value = ips
} catch (error) {
// text
event.target.value = msg.obj
let ips = msg.obj;
if (typeof ips === 'string' && ips.startsWith('[') && ips.endsWith(']')) {
try {
ips = JSON.parse(ips);
ips = Array.isArray(ips) ? ips.join("\n") : ips;
} catch (e) {
console.error('Error parsing JSON:', e);
}
}
document.getElementById("clientIPs").value = ips;
},
async clearDBClientIps(email) {
const msg = await HttpUtil.post('/panel/inbound/clearClientIps/' + email);
if (!msg.success) {
return;
try {
const msg = await HttpUtil.post(`/panel/inbound/clearClientIps/${email}`);
if (!msg.success) {
return;
}
document.getElementById("clientIPs").value = "";
} catch (error) {
}
document.getElementById("clientIPs").value = ""
},
resetClientTraffic(email, dbInboundId, iconElement) {
this.$confirm({

View File

@@ -18,23 +18,20 @@
</template>
</a-tooltip>
</span>
<a-icon @click="client.email = RandomUtil.randomText()" type="sync"> </a-icon>
<a-icon @click="client.email = RandomUtil.randomLowerAndNum(8)" type="sync"> </a-icon>
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item>
<a-form-item label="Password" v-if="inbound.protocol === Protocols.TROJAN || inbound.protocol === Protocols.SHADOWSOCKS">
<a-icon v-if="inbound.protocol === Protocols.SHADOWSOCKS"
@click="client.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
<a-icon v-if="inbound.protocol === Protocols.SHADOWSOCKS" @click="client.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
<a-icon v-if="inbound.protocol === Protocols.TROJAN" @click="client.password = RandomUtil.randomSeq(10)" type="sync"> </a-icon>
<a-input v-model.trim="client.password" style="width: 300px;"></a-input>
</a-form-item>
<br>
<a-form-item label='{{ i18n "additional" }} ID' v-if="inbound.protocol === Protocols.VMESS">
<a-input-number v-model="client.alterId"></a-input-number>
</a-form-item>
<a-form-item label="ID" v-if="inbound.protocol === Protocols.VMESS || inbound.protocol === Protocols.VLESS">
<a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"> </a-icon>
<a-input v-model.trim="client.id" style="width: 300px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email">
<a-form-item v-if="client.email && app.subSettings.enable">
<span slot="label">
Subscription
<a-tooltip>
@@ -44,10 +41,10 @@
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-icon @click="client.subId = RandomUtil.randomText()" type="sync"> </a-icon>
<a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"> </a-icon>
<a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email">
<a-form-item v-if="client.email && app.tgBotEnable" >
<span slot="label">
Telegram ID
<a-tooltip>
@@ -72,31 +69,32 @@
<a-input-number v-model="client.limitIp" min="0"></a-input-number>
</a-form-item>
<a-form-item v-if="client.email && client.limitIp > 0 && isEdit">
<span slot="label">
<span>{{ i18n "pages.inbounds.IPLimitlog" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.IPLimitlogDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.IPLimitlogclear" }}</span>
</template>
<span style="color: #FF4D4F">
<a-icon type="delete" @click="clearDBClientIps(client.email)"></a-icon>
</span>
</a-tooltip>
</span>
<a-form layout="block">
<a-textarea id="clientIPs" readonly
@click="getDBClientIps(client.email,$event)"
placeholder="Click To Get IPs"
:auto-size="{ minRows: 2, maxRows: 10 }">
</a-textarea>
</a-form>
</a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.IPLimitlog" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.IPLimitlogDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.IPLimitlogclear" }}</span>
</template>
<span style="color: #FF4D4F">
<a-icon type="delete" @click="clearDBClientIps(client.email)"></a-icon>
</span>
</a-tooltip>
</span>
<a-form layout="block">
<a-textarea id="clientIPs" readonly
@click="getDBClientIps(client.email)"
placeholder="Click To Get IPs"
:auto-size="{ minRows: 5, maxRows: 10 }"
>
</a-textarea>
</a-form>
</a-form-item>
<br>
<a-form-item v-if="inbound.xtls" label="Flow">
<a-select v-model="client.flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass">

View File

@@ -1,12 +1,13 @@
{{define "form/inbound"}}
<!-- base -->
<a-form layout="inline">
<a-form-item label='{{ i18n "remark" }}'>
<a-input v-model.trim="dbInbound.remark"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "enable" }}'>
<a-switch v-model="dbInbound.enable"></a-switch>
</a-form-item>
<br>
<a-form-item label='{{ i18n "remark" }}'>
<a-input v-model.trim="dbInbound.remark"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "protocol" }}'>
<a-select v-model="inbound.protocol" style="width: 160px;" :disabled="isEdit" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p ]]</a-select-option>
@@ -53,7 +54,7 @@
</span>
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.darkCardClass"
v-model="dbInbound._expiryTime" style="width: 300px;"></a-date-picker>
v-model="dbInbound._expiryTime" style="width: 250px;"></a-date-picker>
</a-form-item>
</a-form>

View File

@@ -1,10 +1,19 @@
{{define "form/http"}}
<a-form layout="inline">
<a-form-item label='{{ i18n "username"}}'>
<a-input v-model.trim="inbound.settings.accounts[0].user"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "password" }}'>
<a-input v-model.trim="inbound.settings.accounts[0].pass"></a-input>
<a-form-item>
<a-row>
<a-button type="primary" 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 type="primary" size="small" @click="inbound.settings.delAccount(index)">-</a-button>
</template>
</a-input>
</a-input-group>
</a-form-item>
</a-form>
{{end}}

View File

@@ -1,5 +1,6 @@
{{define "form/shadowsocks"}}
<a-form layout="inline" style="padding: 10px 0px;">
<template v-if="inbound.isSSMultiUser">
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.shadowsockses.slice(0,1)" v-if="!isEdit">
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
<a-form-item>
@@ -11,14 +12,14 @@
</template>
</a-tooltip>
</span>
<a-icon @click="client.email = RandomUtil.randomText()" type="sync"> </a-icon>
<a-icon @click="client.email = RandomUtil.randomLowerAndNum(8)" type="sync"> </a-icon>
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item>
<a-form-item label="Password">
<a-icon @click="client.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
<a-input v-model.trim="client.password" style="width: 250px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email">
<a-form-item v-if="client.email && app.subSettings.enable">
<span slot="label">
Subscription
<a-tooltip>
@@ -28,10 +29,10 @@
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-icon @click="client.subId = RandomUtil.randomText()" type="sync"> </a-icon>
<a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"> </a-icon>
<a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email">
<a-form-item v-if="client.email && app.tgBotEnable">
<span slot="label">
Telegram ID
<a-tooltip>
@@ -96,22 +97,26 @@
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.shadowsockses.length">
<table width="100%">
<tr class="client-table-header">
<th v-for="col in Object.keys(inbound.settings.shadowsockses[0]).slice(0, 3)">[[ col ]]</th>
<th>{{ i18n "pages.inbounds.email" }}</th>
<th>Password</th>
</tr>
<tr v-for="(client, index) in inbound.settings.shadowsockses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
<td>[[ client.email ]]</td>
<td>[[ client.password ]]</td>
</tr>
</table>
</a-collapse-panel>
</a-collapse>
</template>
</a-form>
<a-form layout="inline">
<a-form-item label='{{ i18n "encryption" }}'>
<a-select v-model="inbound.settings.method" style="width: 250px;" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select v-model="inbound.settings.method" style="width: 250px;" :dropdown-class-name="themeSwitcher.darkCardClass" @change="SSMethodChange">
<a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='{{ i18n "password" }}'>
<a-form-item v-if="inbound.isSS2022" label='{{ i18n "password" }}'>
<a-icon @click="inbound.settings.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
<a-input v-model.trim="inbound.settings.password" style="width: 250px;"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.network" }}'>

View File

@@ -6,11 +6,20 @@
</a-form-item>
<br>
<template v-if="inbound.settings.auth === 'password'">
<a-form-item label='{{ i18n "username" }}'>
<a-input v-model.trim="inbound.settings.accounts[0].user"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "password" }}'>
<a-input v-model.trim="inbound.settings.accounts[0].pass"></a-input>
<a-form-item>
<a-row>
<a-button type="primary" 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 type="primary" size="small" @click="inbound.settings.delAccount(index)">-</a-button>
</template>
</a-input>
</a-input-group>
</a-form-item>
</template>
<br>

View File

@@ -11,13 +11,14 @@
</template>
</a-tooltip>
</span>
<a-icon @click="client.email = RandomUtil.randomText()" type="sync"> </a-icon>
<a-icon @click="client.email = RandomUtil.randomLowerAndNum(8)" type="sync"> </a-icon>
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item>
<a-form-item label="Password">
<a-icon @click="client.password = RandomUtil.randomSeq(10)" type="sync"> </a-icon>
<a-input v-model.trim="client.password" style="width: 150px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email">
<a-form-item v-if="client.email && app.subSettings.enable">
<span slot="label">
Subscription
<a-tooltip>
@@ -27,10 +28,10 @@
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-icon @click="client.subId = RandomUtil.randomText()" type="sync"> </a-icon>
<a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"> </a-icon>
<a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email">
<a-form-item v-if="client.email && app.tgBotEnable">
<span slot="label">
Telegram ID
<a-tooltip>
@@ -101,10 +102,12 @@
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.trojans.length">
<table width="100%">
<tr class="client-table-header">
<th v-for="col in Object.keys(inbound.settings.trojans[0]).slice(0, 3)">[[ col ]]</th>
<th>{{ i18n "pages.inbounds.email" }}</th>
<th>Password</th>
</tr>
<tr v-for="(client, index) in inbound.settings.trojans" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
<td>[[ client.email ]]</td>
<td>[[ client.password ]]</td>
</tr>
</table>
</a-collapse-panel>

View File

@@ -11,14 +11,14 @@
</template>
</a-tooltip>
</span>
<a-icon @click="client.email = RandomUtil.randomText()" type="sync"> </a-icon>
<a-icon @click="client.email = RandomUtil.randomLowerAndNum(8)" type="sync"> </a-icon>
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item>
<a-form-item label="ID">
<a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"> </a-icon>
<a-input v-model.trim="client.id" style="width: 300px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email">
<a-form-item v-if="client.email && app.subSettings.enable">
<span slot="label">
Subscription
<a-tooltip>
@@ -28,10 +28,10 @@
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-icon @click="client.subId = RandomUtil.randomText()" type="sync"> </a-icon>
<a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"> </a-icon>
<a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email">
<a-form-item v-if="client.email && app.tgBotEnable">
<span slot="label">
Telegram ID
<a-tooltip>
@@ -108,10 +108,14 @@
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.vlesses.length">
<table width="100%">
<tr class="client-table-header">
<th v-for="col in Object.keys(inbound.settings.vlesses[0]).slice(0, 3)">[[ col ]]</th>
<th>{{ i18n "pages.inbounds.email" }}</th>
<th>Flow</th>
<th>ID</th>
</tr>
<tr v-for="(client, index) in inbound.settings.vlesses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
<td>[[ client.email ]]</td>
<td>[[ client.flow ]]</td>
<td>[[ client.id ]]</td>
</tr>
</table>
</a-collapse-panel>

View File

@@ -11,19 +11,15 @@
</template>
</a-tooltip>
</span>
<a-icon @click="client.email = RandomUtil.randomText()" type="sync"> </a-icon>
<a-icon @click="client.email = RandomUtil.randomLowerAndNum(8)" type="sync"> </a-icon>
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item>
<br>
<a-form-item label='{{ i18n "additional" }} ID'>
<a-input-number v-model="client.alterId"></a-input-number>
</a-form-item>
<br>
<a-form-item label="ID">
<a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"> </a-icon>
<a-input v-model.trim="client.id" style="width: 300px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email">
<a-form-item v-if="client.email && app.subSettings.enable">
<span slot="label">
Subscription
<a-tooltip>
@@ -33,10 +29,10 @@
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-icon @click="client.subId = RandomUtil.randomText()" type="sync"> </a-icon>
<a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"> </a-icon>
<a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email">
<a-form-item v-if="client.email && app.tgBotEnable">
<span slot="label">
Telegram ID
<a-tooltip>
@@ -101,18 +97,15 @@
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount" }}: ' + inbound.settings.vmesses.length">
<table width="100%">
<tr class="client-table-header">
<th v-for="col in Object.keys(inbound.settings.vmesses[0]).slice(0, 3)">[[ col ]]</th>
<th>{{ i18n "pages.inbounds.email" }}</th>
<th>ID</th>
</tr>
<tr v-for="(client, index) in inbound.settings.vmesses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
<td>[[ client.email ]]</td>
<td>[[ client.id ]]</td>
</tr>
</table>
</a-collapse-panel>
</a-collapse>
</a-form>
<a-form layout="inline">
<a-form-item label='{{ i18n "pages.inbounds.disableInsecureEncryption" }}'>
<a-switch v-model.number="inbound.settings.disableInsecure"></a-switch>
</a-form-item>
</a-form>
{{end}}

View File

@@ -12,7 +12,8 @@
</a-form-item>
<br>
<a-form-item label='{{ i18n "password" }}'>
<a-input v-model="inbound.stream.kcp.seed"></a-input>
<a-icon @click="inbound.stream.kcp.seed = RandomUtil.randomSeq(10)" type="sync"> </a-icon>
<a-input v-model="inbound.stream.kcp.seed" style="width: 150px;" ></a-input>
</a-form-item>
<br>
<a-form-item label="MTU">

View File

@@ -8,7 +8,8 @@
</a-select>
</a-form-item>
<a-form-item label='{{ i18n "password" }}'>
<a-input v-model.trim="inbound.stream.quic.key"></a-input>
<a-icon @click="inbound.stream.quic.key = RandomUtil.randomSeq(10)" type="sync"> </a-icon>
<a-input v-model.trim="inbound.stream.quic.key" style="width: 150px;"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "camouflage" }}'>
<a-select v-model="inbound.stream.quic.type" style="width: 280px;" :dropdown-class-name="themeSwitcher.darkCardClass">

View File

@@ -42,4 +42,9 @@
<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

@@ -25,11 +25,11 @@
<a-input v-model.trim="inbound.stream.tcp.request.path[index]"></a-input>
</a-row>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.general.requestHeader" }}'>
<br>
<a-form-item>
<a-row>
<a-button size="small" @click="inbound.stream.tcp.request.addHeader('Host', 'xxx.com')">
+
</a-button>
<span>{{ i18n "pages.inbounds.stream.general.requestHeader" }}:</span>
<a-button type="primary" size="small" style="margin-left: 10px" @click="inbound.stream.tcp.request.addHeader('Host', 'xxx.com')">+</a-button>
</a-row>
<a-input-group v-for="(header, index) in inbound.stream.tcp.request.headers">
<a-input style="width: 50%" v-model.trim="header.name"
@@ -37,13 +37,12 @@
<a-input style="width: 50%" v-model.trim="header.value"
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
<template slot="addonAfter">
<a-button size="small" @click="inbound.stream.tcp.request.removeHeader(index)">
-
</a-button>
<a-button type="primary" size="small" style="margin-left: 10px" @click="inbound.stream.tcp.request.removeHeader(index)">-</a-button>
</template>
</a-input>
</a-input-group>
</a-form-item>
</a-form>
<!-- tcp response -->
@@ -57,11 +56,10 @@
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseStatusDescription" }}'>
<a-input v-model.trim="inbound.stream.tcp.response.reason"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseHeader" }}'>
<a-form-item>
<a-row>
<a-button size="small" @click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')">
+
</a-button>
<span>{{ i18n "pages.inbounds.stream.tcp.responseHeader" }}:</span>
<a-button type="primary" size="small" style="margin-left: 10px" @click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')">+</a-button>
</a-row>
<a-input-group v-for="(header, index) in inbound.stream.tcp.response.headers">
<a-input style="width: 50%" v-model.trim="header.name"
@@ -69,9 +67,7 @@
<a-input style="width: 50%" v-model.trim="header.value"
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
<template slot="addonAfter">
<a-button size="small" @click="inbound.stream.tcp.response.removeHeader(index)">
-
</a-button>
<a-button type="primary" size="small" style="margin-left: 10px" @click="inbound.stream.tcp.response.removeHeader(index)">-</a-button>
</template>
</a-input>
</a-input-group>

View File

@@ -3,16 +3,15 @@
<a-form-item label="AcceptProxyProtocol">
<a-switch v-model="inbound.stream.ws.acceptProxyProtocol"></a-switch>
</a-form-item>
</a-form>
<a-form layout="inline">
<br>
<a-form-item label='{{ i18n "path" }}'>
<a-input v-model.trim="inbound.stream.ws.path"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.general.requestHeader" }}'>
<br>
<a-form-item>
<a-row>
<a-button size="small" @click="inbound.stream.ws.addHeader('Host', '')">
+
</a-button>
<span>{{ i18n "pages.inbounds.stream.general.requestHeader" }}:</span>
<a-button type="primary" size="small" style="margin-left: 10px" @click="inbound.stream.ws.addHeader('Host', '')">+</a-button>
</a-row>
<a-input-group v-for="(header, index) in inbound.stream.ws.headers">
<a-input style="width: 50%" v-model.trim="header.name"
@@ -20,9 +19,7 @@
<a-input style="width: 50%" v-model.trim="header.value"
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
<template slot="addonAfter">
<a-button size="small" @click="inbound.stream.ws.removeHeader(index)">
-
</a-button>
<a-button type="primary" size="small" style="margin-left: 10px" @click="inbound.stream.ws.removeHeader(index)">-</a-button>
</template>
</a-input>
</a-input-group>

View File

@@ -10,7 +10,7 @@
Reality
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.realityDesc" }}</span>
<span>{{ i18n "pages.inbounds.realityDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
@@ -22,7 +22,7 @@
XTLS
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.xtlsDesc" }}</span>
<span>{{ i18n "pages.inbounds.xtlsDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
@@ -33,7 +33,24 @@
<!-- tls settings -->
<a-form v-if="inbound.tls" layout="inline">
<a-form-item label='{{ i18n "domainName" }}'>
<a-form-item label='Multi Domain'>
<a-switch v-model="multiDomain"></a-switch>
</a-form-item>
<a-form-item v-if="multiDomain">
<a-row>
<span>Domains:</span>
<a-button v-if="multiDomain" type="primary" size="small" @click="inbound.stream.tls.settings.domains.push({remark: '', domain: ''})" style="margin-left: 10px">+</a-button>
</a-row>
<a-input-group v-for="(row, index) in inbound.stream.tls.settings.domains">
<a-input style="width: 40%" v-model.trim="row.remark" addon-before='{{ i18n "remark" }}'></a-input>
<a-input style="width: 60%" v-model.trim="row.domain" addon-before='{{ i18n "host" }}'>
<template slot="addonAfter">
<a-button type="primary" size="small" style="margin-left: 10px" @click="inbound.stream.tls.settings.domains.splice(index, 1)">-</a-button>
</template>
</a-input>
</a-input-group>
</a-form-item>
<a-form-item v-else label='{{ i18n "domainName" }}'>
<a-input v-model.trim="inbound.stream.tls.server" style="width: 250px"></a-input>
</a-form-item>
<a-form-item label="CipherSuites">
@@ -63,40 +80,57 @@
</a-select>
</a-form-item>
<a-form-item label="Alpn">
<a-checkbox-group v-model="inbound.stream.tls.alpn" style="width:200px">
<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>
<br>
<a-form-item label="Allow insecure">
<a-switch v-model="inbound.stream.tls.settings.allowInsecure"></a-switch>
</a-form-item>
<a-form-item label='{{ i18n "certificate" }}'>
<a-radio-group v-model="inbound.stream.tls.certs[0].useFile" button-style="solid">
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
</a-radio-group>
<br>
<a-form-item label="Reject Unknown SNI">
<a-switch v-model="inbound.stream.tls.rejectUnknownSni"></a-switch>
</a-form-item>
<template v-if="inbound.stream.tls.certs[0].useFile">
<a-form-item label='{{ i18n "pages.inbounds.publicKeyPath" }}'>
<a-input v-model.trim="inbound.stream.tls.certs[0].certFile" style="width:300px;"></a-input>
<template v-for="cert,index in inbound.stream.tls.certs">
<a-form-item label='{{ i18n "certificate" }}'>
<a-radio-group v-model="cert.useFile" button-style="solid">
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
</a-radio-group>
<a-button v-if="index === 0" type="primary" size="small" @click="inbound.stream.tls.addCert()" style="margin-left: 10px">+</a-button>
<a-button v-if="inbound.stream.tls.certs.length>1" type="primary" size="small" @click="inbound.stream.tls.removeCert(index)" style="margin-left: 10px">-</a-button>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
<a-input v-model.trim="inbound.stream.tls.certs[0].keyFile" style="width:300px;"></a-input>
</a-form-item>
<a-button type="primary" icon="import" @click="setDefaultCertData">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
</template>
<template v-else>
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
<a-input type="textarea" :rows="3" style="width:300px;" v-model="inbound.stream.tls.certs[0].cert"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.keyContent" }}'>
<a-input type="textarea" :rows="3" style="width:300px;" v-model="inbound.stream.tls.certs[0].key"></a-input>
<template v-if="cert.useFile">
<a-form-item label='{{ i18n "pages.inbounds.publicKeyPath" }}'>
<a-input v-model.trim="cert.certFile" style="width:300px;"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
<a-input v-model.trim="cert.keyFile" style="width:300px;"></a-input>
</a-form-item>
<a-button type="primary" icon="import" @click="setDefaultCertData(index)">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
</template>
<template v-else>
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
<a-input type="textarea" :rows="3" style="width:300px;" v-model="cert.cert"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.keyContent" }}'>
<a-input type="textarea" :rows="3" style="width:300px;" v-model="cert.key"></a-input>
</a-form-item>
</template>
<br>
<a-form-item label="ocspStapling">
<a-input-number v-model.number="cert.ocspStapling" :min="0"></a-input-number>
</a-form-item>
</template>
</a-form>
<!-- xtls settings -->
<a-form v-if="inbound.xtls" layout="inline">
<a-form v-else-if="inbound.xtls" layout="inline">
<a-form-item label='{{ i18n "domainName" }}'>
<a-input v-model.trim="inbound.stream.xtls.server"></a-input>
</a-form-item>
@@ -111,28 +145,32 @@
<a-form-item label="Allow insecure">
<a-switch v-model="inbound.stream.xtls.settings.allowInsecure"></a-switch>
</a-form-item>
<a-form-item label='{{ i18n "certificate" }}'>
<a-radio-group v-model="inbound.stream.xtls.certs[0].useFile" button-style="solid">
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
</a-radio-group>
</a-form-item>
<template v-if="inbound.stream.xtls.certs[0].useFile">
<a-form-item label='{{ i18n "pages.inbounds.publicKeyPath" }}'>
<a-input v-model.trim="inbound.stream.xtls.certs[0].certFile" style="width:300px;"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
<a-input v-model.trim="inbound.stream.xtls.certs[0].keyFile" style="width:300px;"></a-input>
</a-form-item>
<a-button type="primary" icon="import" @click="setDefaultCertXtls">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
</template>
<template v-else>
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
<a-input type="textarea" :rows="3" style="width:300px;" v-model="inbound.stream.xtls.certs[0].cert"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.keyContent" }}'>
<a-input type="textarea" :rows="3" style="width:300px;" v-model="inbound.stream.xtls.certs[0].key"></a-input>
<template v-for="cert,index in inbound.stream.xtls.certs">
<a-form-item label='{{ i18n "certificate" }}'>
<a-radio-group v-model="cert.useFile" button-style="solid">
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
</a-radio-group>
<a-button v-if="index === 0" type="primary" size="small" @click="inbound.stream.xtls.addCert()" style="margin-left: 10px">+</a-button>
<a-button v-if="inbound.stream.xtls.certs.length>1" type="primary" size="small" @click="inbound.stream.xtls.removeCert(index)" style="margin-left: 10px">-</a-button>
</a-form-item>
<template v-if="cert.useFile">
<a-form-item label='{{ i18n "pages.inbounds.publicKeyPath" }}'>
<a-input v-model.trim="cert.certFile" style="width:300px;"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
<a-input v-model.trim="cert.keyFile" style="width:300px;"></a-input>
</a-form-item>
<a-button type="primary" icon="import" @click="setDefaultCertXtls(index)">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
</template>
<template v-else>
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
<a-input type="textarea" :rows="3" style="width:300px;" v-model="cert.cert"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.keyContent" }}'>
<a-input type="textarea" :rows="3" style="width:300px;" v-model="cert.key"></a-input>
</a-form-item>
</template>
</template>
</a-form>
@@ -154,14 +192,19 @@
<a-form-item label='{{ i18n "domainName" }}'>
<a-input v-model.trim="inbound.stream.reality.settings.serverName" style="width: 250px"></a-input>
</a-form-item>
<a-form-item label="dest">
<a-form-item label="Dest">
<a-input v-model.trim="inbound.stream.reality.dest" style="width: 300px"></a-input>
</a-form-item>
<a-form-item label="Server Names">
<a-input v-model.trim="inbound.stream.reality.serverNames" style="width: 300px"></a-input>
</a-form-item>
<a-form-item label="ShortIds">
<a-input v-model.trim="inbound.stream.reality.shortIds"></a-input>
<a-icon @click="inbound.stream.reality.shortIds = RandomUtil.randomShortId()" type="sync"> </a-icon>
<a-input v-model.trim="inbound.stream.reality.shortIds" style="width: 150px;"></a-input>
</a-form-item>
<br>
<a-form-item label="SpiderX">
<a-input v-model.trim="inbound.stream.reality.settings.spiderX" style="width: 150px;"></a-input>
</a-form-item>
<a-form-item label="Private Key">
<a-input v-model.trim="inbound.stream.reality.privateKey" style="width: 300px"></a-input>

View File

@@ -29,18 +29,31 @@
<a-tag v-if="!isClientEnabled(record, client.email)" color="red">{{ i18n "depleted" }}</a-tag>
</template>
<template slot="traffic" slot-scope="text, client">
<a-tag color="blue">
[[ sizeFormat(getUpStats(record, client.email)) ]] / [[ sizeFormat(getDownStats(record, client.email)) ]]
</a-tag>
<template v-if="client._totalGB > 0">
<a-tag v-if="isTrafficExhausted(record, client.email)" color="red">[[client._totalGB]] GB</a-tag>
<a-tag v-else color="cyan">[[client._totalGB]] GB</a-tag>
</template>
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
<a-popover :overlay-class-name="themeSwitcher.darkClass">
<template slot="content" v-if="client.email">
<table cellpadding="2" width="100%">
<tr>
<td>↑[[ sizeFormat(getUpStats(record, client.email)) ]]</td>
<td>↓[[ sizeFormat(getDownStats(record, client.email)) ]]</td>
</tr>
<tr v-if="client.totalGB > 0">
<td>{{ i18n "remained" }}</td>
<td>[[ sizeFormat(client.totalGB - getUpStats(record, client.email) - getDownStats(record, client.email)) ]]</td>
</tr>
</table>
</template>
<a-tag :color="statsColor(record, client.email)">
[[ sizeFormat(getUpStats(record, client.email) + getDownStats(record, client.email)) ]] /
<template v-if="client.totalGB > 0">[[client._totalGB]]GB</template>
<template v-else>
<svg style="fill: currentColor; height: 10px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z"/></svg>
</template>
</a-tag>
</a-popover>
</template>
<template slot="expiryTime" slot-scope="text, client, index">
<template v-if="client.expiryTime > 0">
<a-tag :color="isExpiry(record, index)? 'red' : 'blue'">
<a-tag :color="usageColor(new Date().getTime(), app.expireDiff, client.expiryTime)">
[[ DateUtil.formatMillis(client._expiryTime) ]]
</a-tag>
</template>

View File

@@ -8,8 +8,7 @@
width="600px"
>
<table style="margin-bottom: 10px; width: 100%;">
<tr>
<td>
<tr><td>
<table>
<tr><td>{{ i18n "protocol" }}</td><td><a-tag color="green">[[ dbInbound.protocol ]]</a-tag></td></tr>
<tr><td>{{ i18n "pages.inbounds.address" }}</td><td><a-tag color="blue">[[ dbInbound.address ]]</a-tag></td></tr>
@@ -21,7 +20,6 @@
<tr>
<td>{{ i18n "transmission" }}</td><td><a-tag color="green">[[ inbound.network ]]</a-tag></td>
</tr>
<template v-if="inbound.isTcp || inbound.isWs || inbound.isH2">
<tr v-if="inbound.host"><td>{{ i18n "host" }}</td><td><a-tag color="green">[[ inbound.host ]]</a-tag></td></tr>
<tr v-else><td>{{ i18n "host" }}</td><td><a-tag color="orange">{{ i18n "none" }}</a-tag></td></tr>
@@ -46,8 +44,7 @@
<tr><td>grpc multiMode</td><td><a-tag color="green">[[ inbound.stream.grpc.multiMode ]]</a-tag></td></tr>
</template>
</table>
</td>
</tr>
</td></tr>
<tr colspan="2" v-if="dbInbound.hasLink()">
<td v-if="inbound.tls">
tls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
@@ -59,49 +56,79 @@
</td>
<td v-else-if="inbound.reality">
reality: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
reality {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
</td>
<td v-else>
tls: <a-tag color="red">{{ i18n "disabled" }}</a-tag>
reality Destination: <a-tag :color="inbound.stream.reality.dest ? 'green' : 'orange'">[[ inbound.stream.reality.dest ]]</a-tag>
</td>
<td v-else>tls: <a-tag color="red">{{ i18n "disabled" }}</a-tag></td>
</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">
<a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
<table style="margin-bottom: 10px;">
<tr v-for="col,index in Object.keys(infoModal.clientSettings).slice(0, 3)">
<td>[[ col ]]</td>
<td><a-tag color="green">[[ infoModal.clientSettings[col] ]]</a-tag></td>
</tr>
<tr>
<td>{{ i18n "status" }}</td>
<td>
<a-tag v-if="isEnable" color="blue">{{ i18n "enabled" }}</a-tag>
<a-tag v-else color="red">{{ i18n "disabled" }}</a-tag>
<a-tag v-if="!isActive" color="red">{{ i18n "depleted" }}</a-tag>
</td>
</tr>
</table>
<table style="margin-bottom: 10px; width: 100%;">
<a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
<table style="margin-bottom: 10px;">
<tr>
<th>{{ i18n "usage" }}</th>
<td>{{ i18n "pages.inbounds.email" }}</td>
<td><a-tag color="green">[[ infoModal.clientSettings.email ]]</a-tag></td>
</tr>
<tr v-if="infoModal.clientSettings.id">
<td>ID</td>
<td><a-tag color="green">[[ infoModal.clientSettings.id ]]</a-tag></td>
</tr>
<tr v-if="infoModal.inbound.canEnableTlsFlow()">
<td>Flow</td>
<td><a-tag color="green">[[ infoModal.clientSettings.flow ]]</a-tag></td>
</tr>
<tr v-if="infoModal.clientSettings.password">
<td>Password</td>
<td><a-tag color="green">[[ infoModal.clientSettings.password ]]</a-tag></td>
</tr>
<tr>
<td>{{ i18n "status" }}</td>
<td>
<a-tag v-if="isEnable" color="blue">{{ i18n "enabled" }}</a-tag>
<a-tag v-else color="red">{{ i18n "disabled" }}</a-tag>
<a-tag v-if="!isActive" color="red">{{ i18n "depleted" }}</a-tag>
</td>
</tr>
<tr v-if="infoModal.clientStats">
<td>{{ i18n "usage" }}</td>
<td>
<a-tag color="green">[[ sizeFormat(infoModal.clientStats.up + infoModal.clientStats.down) ]]</a-tag>
<a-tag color="blue">↑ [[ sizeFormat(infoModal.clientStats.up) ]] / [[ sizeFormat(infoModal.clientStats.down) ]] ↓</a-tag>
</td>
</tr>
</table>
<table style="margin-bottom: 10px; width: 100%;">
<tr>
<th>{{ i18n "remained" }}</th>
<th>{{ i18n "pages.inbounds.totalFlow" }}</th>
<th>{{ i18n "pages.inbounds.expireDate" }}</th>
</tr>
<tr>
<td>
<a-tag v-if="infoModal.clientStats" :color="statsColor(infoModal.clientStats)">
[[ sizeFormat(infoModal.clientStats['up']) ]] /
[[ sizeFormat(infoModal.clientStats['down']) ]]
([[ sizeFormat(infoModal.clientStats['up'] + infoModal.clientStats['down']) ]])
<a-tag v-if="infoModal.clientStats && infoModal.clientSettings.totalGB > 0" :color="statsColor(infoModal.clientStats)">
[[ sizeFormat(infoModal.clientSettings.totalGB - infoModal.clientStats.up - infoModal.clientStats.down) ]]
</a-tag>
</td>
<td>
<a-tag v-if="infoModal.clientSettings.totalGB > 0" :color="statsColor(infoModal.clientStats)">[[ sizeFormat(infoModal.clientSettings.totalGB) ]]</a-tag>
<a-tag v-if="infoModal.clientSettings.totalGB > 0" :color="statsColor(infoModal.clientStats)">
[[ sizeFormat(infoModal.clientSettings.totalGB) ]]
</a-tag>
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
</td>
<td>
<template v-if="infoModal.clientSettings.expiryTime > 0">
<a-tag :color="infoModal.isExpired ? 'red' : 'blue'">
<a-tag :color="usageColor(new Date().getTime(), app.expireDiff, infoModal.clientSettings.expiryTime)">
[[ DateUtil.formatMillis(infoModal.clientSettings.expiryTime) ]]
</a-tag>
</template>
@@ -110,48 +137,74 @@
</td>
</tr>
</table>
<table v-if="infoModal.clientSettings.subId + infoModal.clientSettings.tgId" style="margin-bottom: 10px;">
<tr v-if="infoModal.clientSettings.subId">
<td>Subscription link</td>
<td><a :href="[[ subBase + infoModal.clientSettings.subId ]]" target="_blank">[[ subBase + infoModal.clientSettings.subId ]]</a></td>
<td><a-icon id="copy-sub-link" type="snippets" @click="copyToClipboard('copy-sub-link', subBase + infoModal.clientSettings.subId)"></a-icon></td>
</tr>
<tr v-if="infoModal.clientSettings.tgId">
<td>Telegram ID</td>
<td><a :href="[[ tgBase + infoModal.clientSettings.tgId ]]" target="_blank">@[[ infoModal.clientSettings.tgId ]]</a></td>
</tr>
</table>
<template v-if="app.subSettings.enable && infoModal.clientSettings.subId">
<a-divider>Subscription link</a-divider>
<a-row>
<a-col :span="22"><a :href="[[ infoModal.subLink ]]" target="_blank">[[ infoModal.subLink ]]</a></a-col>
<a-col :span="2">
<a-tooltip title='{{ i18n "copy" }}'>
<button class="ant-btn ant-btn-primary" id="copy-sub-link" @click="copyToClipboard('copy-sub-link', infoModal.subLink)">
<a-icon type="snippets"></a-icon>
</button>
</a-tooltip>
</a-col>
</a-row>
</template>
<template v-if="app.tgBotEnable && infoModal.clientSettings.tgId">
<a-divider>Telegram Username</a-divider>
<a-row>
<a-col :span="22"><a :href="[[ infoModal.tgLink ]]" target="_blank">@[[ infoModal.clientSettings.tgId ]]</a></a-col>
<a-col :span="2">
<a-tooltip title='{{ i18n "copy" }}'>
<button class="ant-btn ant-btn-primary" id="copy-tg-link" @click="copyToClipboard('copy-tg-link', '@' + infoModal.clientSettings.tgId)">
<a-icon type="snippets"></a-icon>
</button>
</a-tooltip>
</a-col>
</a-row>
</template>
<template v-if="dbInbound.hasLink()">
<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>
<a-col :span="2" style="text-align: right;">
<a-tooltip title='{{ i18n "copy" }}'>
<button class="ant-btn ant-btn-primary" :id="'copy-url-link-'+index" @click="copyToClipboard('copy-url-link-'+index, link.link)">
<a-icon type="snippets"></a-icon>
</button>
</a-tooltip>
</a-col>
</a-row>
</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="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>
<a-col :span="2" style="text-align: right;">
<a-tooltip title='{{ i18n "copy" }}'>
<button class="ant-btn ant-btn-primary" :id="'copy-url-link-'+index" @click="copyToClipboard('copy-url-link-'+index, link.link)">
<a-icon type="snippets"></a-icon>
</button>
</a-tooltip>
</a-col>
</a-row>
</template>
<table v-if="inbound.protocol == Protocols.DOKODEMO" style="margin-bottom: 10px; width: 100%;">
<tr>
<th>{{ i18n "pages.inbounds.targetAddress" }}</th>
<th>{{ i18n "pages.inbounds.destinationPort" }}</th>
<th>{{ i18n "pages.inbounds.network" }}</th>
<th>FollowRedirect</th>
</tr>
<tr>
</tr><tr>
<td><a-tag color="green">[[ inbound.settings.address ]]</a-tag></td>
<td><a-tag color="blue">[[ inbound.settings.port ]]</a-tag></td>
<td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td>
<td><a-tag color="blue">[[ inbound.settings.followRedirect ]]</a-tag></td>
</tr>
</table>
</table>
<table v-if="inbound.protocol == Protocols.SOCKS" style="margin-bottom: 10px; width: 100%;">
<table v-if="dbInbound.isSocks" style="margin-bottom: 10px; width: 100%;">
<tr>
<th>{{ i18n "password" }} Auth</th>
<th>{{ i18n "pages.inbounds.enable" }} udp</th>
@@ -162,40 +215,32 @@
<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'">
<template v-if="inbound.settings.auth == 'password'">
<tr>
<td> </td>
<td>{{ i18n "username" }}</td>
<td>{{ i18n "password" }}</td>
</tr>
<tr v-for="account,index in inbound.settings.accounts">
</tr><tr v-for="account,index in inbound.settings.accounts">
<td><a-tag color="green">[[ index ]]</a-tag></td>
<td><a-tag color="blue">[[ account.user ]]</a-tag></td>
<td><a-tag color="green">[[ account.pass ]]</a-tag></td>
</tr>
</table>
</table>
<table v-if="inbound.protocol == Protocols.HTTP" style="margin-bottom: 10px; width: 100%;">
</template>
</table>
<table v-if="dbInbound.isHTTP" style="margin-bottom: 10px; width: 100%;">
<tr>
<th> </th>
<th>{{ i18n "username" }}</th>
<th>{{ i18n "password" }}</th>
</tr>
<tr v-for="account,index in inbound.settings.accounts">
</tr><tr v-for="account,index in inbound.settings.accounts">
<td><a-tag color="green">[[ index ]]</a-tag></td>
<td><a-tag color="blue">[[ account.user ]]</a-tag></td>
<td><a-tag color="green">[[ account.pass ]]</a-tag></td>
</tr>
</table>
</table>
</template>
<div v-if="dbInbound.hasLink()">
<a-divider>URL</a-divider>
<p>[[ infoModal.link ]]</p>
<button class="ant-btn ant-btn-primary" id="copy-url-link" @click="copyToClipboard('copy-url-link', infoModal.link)"><a-icon type="snippets"></a-icon>{{ i18n "copy" }}</button>
</div>
</a-modal>
<script>
const infoModal = {
visible: false,
inbound: new Inbound(),
@@ -206,23 +251,53 @@
upStats: 0,
downStats: 0,
clipboard: null,
link: null,
links: [],
index: null,
isExpired: false,
subLink: '',
tgLink: '',
show(dbInbound, index) {
this.index = index;
this.inbound = dbInbound.toInbound();
this.dbInbound = new DBInbound(dbInbound);
this.link = dbInbound.genLink(index);
this.settings = JSON.parse(this.inbound.settings);
this.clientSettings = this.settings.clients ? Object.values(this.settings.clients)[index] : null;
this.isExpired = this.inbound.isExpiry(index);
this.clientStats = this.settings.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : [];
remark = [this.dbInbound.remark, ( this.clientSettings ? 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: remarkText,
link: this.inbound.genLink(domain.domain, remarkText, index)
});
});
} else {
this.links.push({
remark: remark,
link: this.inbound.genLink(address, remark, index)
});
}
if (this.clientSettings) {
if (this.clientSettings.subId) {
this.subLink = this.genSubLink(this.clientSettings.subId);
}
if (this.clientSettings.tgId) {
this.tgLink = "https://t.me/" + this.clientSettings.tgId;
}
}
this.visible = true;
},
close() {
infoModal.visible = false;
},
genSubLink(subID) {
const { domain: host, port, tls: isTLS, path: base } = app.subSettings;
return buildURL({ host, port, isTLS, base, path: subID+'?name='+subID });
}
};
const infoModalApp = new Vue({
@@ -248,12 +323,6 @@
}
return infoModal.dbInbound.isEnable;
},
get subBase() {
return window.location.protocol + "//" + window.location.hostname + (window.location.port ? ":" + window.location.port : "") + basePath + "sub/";
},
get tgBase() {
return "https://t.me/"
},
},
methods: {
copyToClipboard(elmentId, content) {
@@ -266,10 +335,7 @@
});
},
statsColor(stats) {
if (!stats) return 'blue'
if (stats['total'] === 0) return 'blue'
else if (stats['total'] > 0 && (stats['down'] + stats['up']) < stats['total']) return 'cyan'
else return 'red'
return usageColor(stats.up + stats.down, app.trafficDiff, stats.total);
}
},
});

View File

@@ -54,23 +54,11 @@
},
};
const protocols = {
VMESS: Protocols.VMESS,
VLESS: Protocols.VLESS,
TROJAN: Protocols.TROJAN,
SHADOWSOCKS: Protocols.SHADOWSOCKS,
DOKODEMO: Protocols.DOKODEMO,
SOCKS: Protocols.SOCKS,
HTTP: Protocols.HTTP,
};
new Vue({
delimiters: ['[[', ']]'],
el: '#inbound-modal',
data: {
inModal: inModal,
Protocols: protocols,
SSMethods: SSMethods,
delayedStart: false,
get inbound() {
return inModal.inbound;
@@ -90,6 +78,18 @@
set delayedExpireDays(days) {
this.client.expiryTime = -86400000 * days;
},
get multiDomain() {
return this.inbound.stream.tls.settings.domains.length > 0;
},
set multiDomain(value) {
if (value) {
inModal.inbound.stream.tls.server = "";
inModal.inbound.stream.tls.settings.domains = [{ remark: "", domain: window.location.hostname }];
} else {
inModal.inbound.stream.tls.server = "";
inModal.inbound.stream.tls.settings.domains = [];
}
}
},
methods: {
streamNetworkChange() {
@@ -99,14 +99,39 @@
if (!inModal.inbound.canEnableReality()) {
this.inModal.inbound.reality = false;
}
if (this.inModal.inbound.protocol == Protocols.VLESS && !inModal.inbound.canEnableTlsFlow()) {
this.inModal.inbound.settings.vlesses.forEach(client => {
client.flow = "";
});
}
},
setDefaultCertData() {
inModal.inbound.stream.tls.certs[0].certFile = app.defaultCert;
inModal.inbound.stream.tls.certs[0].keyFile = app.defaultKey;
SSMethodChange() {
if (this.inModal.inbound.isSSMultiUser) {
if (this.inModal.inbound.settings.shadowsockses.length ==0){
this.inModal.inbound.settings.shadowsockses = [new Inbound.ShadowsocksSettings.Shadowsocks()];
}
if (!this.inModal.inbound.isSS2022) {
this.inModal.inbound.settings.shadowsockses.forEach(client => {
client.method = this.inModal.inbound.settings.method;
})
} else {
this.inModal.inbound.settings.shadowsockses.forEach(client => {
client.method = "";
})
}
} else {
if (this.inModal.inbound.settings.shadowsockses.length > 0){
this.inModal.inbound.settings.shadowsockses = [];
}
}
},
setDefaultCertXtls() {
inModal.inbound.stream.xtls.certs[0].certFile = app.defaultCert;
inModal.inbound.stream.xtls.certs[0].keyFile = app.defaultKey;
setDefaultCertData(index) {
inModal.inbound.stream.tls.certs[index].certFile = app.defaultCert;
inModal.inbound.stream.tls.certs[index].keyFile = app.defaultKey;
},
setDefaultCertXtls(index) {
inModal.inbound.stream.xtls.certs[index].certFile = app.defaultCert;
inModal.inbound.stream.xtls.certs[index].keyFile = app.defaultKey;
},
async getNewX25519Cert() {
inModal.loading(true);

View File

@@ -11,6 +11,9 @@
.ant-col-sm-24 {
margin-top: 10px;
}
tr.hideExpandIcon .ant-table-row-expand-icon {
display: none;
}
</style>
<body>
@@ -105,11 +108,25 @@
</a-col>
</a-row>
</div>
<a-input v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus style="max-width: 300px"></a-input>
<a-switch v-model="enableFilter"
checked-children='{{ i18n "search" }}' un-checked-children='{{ i18n "filter" }}'
@change="toggleFilter" style="margin-right: 10px;">
</a-switch>
<a-input v-if="!enableFilter" v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus style="max-width: 300px"></a-input>
<a-radio-group v-if="enableFilter" v-model="filterBy" @change="filterInbounds" button-style="solid">
<a-radio-button value="">{{ i18n "none" }}</a-radio-button>
<a-radio-button value="deactive">{{ i18n "disabled" }}</a-radio-button>
<a-radio-button value="depleted">{{ i18n "depleted" }}</a-radio-button>
<a-radio-button value="expiring">{{ i18n "depletingSoon" }}</a-radio-button>
</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">
@@ -121,7 +138,11 @@
<a-icon type="edit"></a-icon>
{{ i18n "edit" }}
</a-menu-item>
<template v-if="dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess || dbInbound.isSS">
<a-menu-item key="qrcode" v-if="dbInbound.isSS && !dbInbound.toInbound().isSSMultiUser">
<a-icon type="qrcode"></a-icon>
{{ i18n "qrCode" }}
</a-menu-item>
<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"}}
@@ -165,7 +186,7 @@
</template>
<template slot="protocol" slot-scope="text, dbInbound">
<a-tag style="margin:0;" color="blue">[[ dbInbound.protocol ]]</a-tag>
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan">
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
<a-tag style="margin:0;" color="green">[[ dbInbound.toInbound().stream.network ]]</a-tag>
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isTls" color="cyan">TLS</a-tag>
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isXtls" color="cyan">XTLS</a-tag>
@@ -196,12 +217,29 @@
</template>
</template>
<template slot="traffic" slot-scope="text, dbInbound">
<a-tag color="blue">[[ sizeFormat(dbInbound.up) ]] / [[ sizeFormat(dbInbound.down) ]]</a-tag>
<template v-if="dbInbound.total > 0">
<a-tag v-if="dbInbound.up + dbInbound.down < dbInbound.total" color="cyan">[[ sizeFormat(dbInbound.total) ]]</a-tag>
<a-tag v-else color="red">[[ sizeFormat(dbInbound.total) ]]</a-tag>
</template>
<a-tag v-else color="green">{{ i18n "unlimited" }}</a-tag>
<a-popover :overlay-class-name="themeSwitcher.darkClass">
<template slot="content">
<table cellpadding="2" width="100%">
<tr>
<td>↑[[ sizeFormat(dbInbound.up) ]]</td>
<td>↓[[ sizeFormat(dbInbound.down) ]]</td>
</tr>
<tr v-if="dbInbound.total > 0 && dbInbound.up + dbInbound.down < dbInbound.total">
<td>{{ i18n "remained" }}</td>
<td>[[ sizeFormat(dbInbound.total - dbInbound.up - dbInbound.down) ]]</td>
</tr>
</table>
</template>
<a-tag :color="dbInbound.total == 0 ? 'green' : dbInbound.up + dbInbound.down < dbInbound.total ? 'cyan' : 'red'">
[[ sizeFormat(dbInbound.up + dbInbound.down) ]] /
<template v-if="dbInbound.total > 0">
[[ sizeFormat(dbInbound.total) ]]
</template>
<template v-else>
<svg style="fill: currentColor; height: 10px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z"/></svg>
</template>
</a-tag>
</a-popover>
</template>
<template slot="enable" slot-scope="text, dbInbound">
<a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound.id)"></a-switch>
@@ -224,15 +262,17 @@
:columns="innerColumns"
:data-source="getInboundClients(record)"
:pagination="false"
style="margin-left: 20px;"
>
{{template "client_table"}}
</a-table>
<a-table
v-else-if="record.protocol === Protocols.TROJAN || record.protocol === Protocols.SHADOWSOCKS"
v-else-if="record.protocol === Protocols.TROJAN || record.toInbound().isSSMultiUser"
:row-key="client => client.id"
:columns="innerTrojanColumns"
:data-source="getInboundClients(record)"
:pagination="false"
style="margin-left: 20px;"
>
{{template "client_table"}}
</a-table>
@@ -247,22 +287,21 @@
{{template "js" .}}
{{template "component/themeSwitcher" .}}
<script>
const columns = [{
title: "ID",
align: 'right',
dataIndex: "id",
width: 30,
}, {
title: '{{ i18n "pages.inbounds.operate" }}',
align: 'center',
width: 60,
width: 40,
scopedSlots: { customRender: 'action' },
}, {
title: '{{ i18n "pages.inbounds.enable" }}',
align: 'center',
width: 40,
scopedSlots: { customRender: 'enable' },
}, {
title: "ID",
align: 'center',
dataIndex: "id",
width: 40,
}, {
title: '{{ i18n "pages.inbounds.remark" }}',
align: 'center',
@@ -281,12 +320,12 @@
}, {
title: '{{ i18n "clients" }}',
align: 'left',
width: 50,
width: 40,
scopedSlots: { customRender: 'clients' },
}, {
title: '{{ i18n "pages.inbounds.traffic" }}↑|↓',
title: '{{ i18n "pages.inbounds.traffic" }}',
align: 'center',
width: 120,
width: 60,
scopedSlots: { customRender: 'traffic' },
}, {
title: '{{ i18n "pages.inbounds.expireDate" }}',
@@ -299,38 +338,49 @@
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } },
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 40, scopedSlots: { customRender: 'enable' } },
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 120, scopedSlots: { customRender: 'traffic' } },
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
{ title: 'UID', width: 120, dataIndex: "id" },
{ title: '{{ i18n "pages.inbounds.traffic" }}', width: 50, scopedSlots: { customRender: 'traffic' } },
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 50, scopedSlots: { customRender: 'expiryTime' } },
{ title: 'UUID', width: 120, dataIndex: "id" },
];
const innerTrojanColumns = [
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } },
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 40, scopedSlots: { customRender: 'enable' } },
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 120, scopedSlots: { customRender: 'traffic' } },
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
{ title: 'Password', width: 170, dataIndex: "password" },
{ title: '{{ i18n "pages.inbounds.traffic" }}', width: 50, scopedSlots: { customRender: 'traffic' } },
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 50, scopedSlots: { customRender: 'expiryTime' } },
{ title: '{{ i18n "password" }}', width: 170, dataIndex: "password" },
];
const app = new Vue({
delimiters: ['[[', ']]'],
el: '#app',
data: {
siderDrawer,
themeSwitcher,
spinning: false,
inbounds: [],
dbInbounds: [],
searchKey: '',
enableFilter: false,
filterBy: '',
searchedInbounds: [],
expireDiff: 0,
trafficDiff: 0,
defaultCert: '',
defaultKey: '',
clientCount: {},
clientCount: [],
isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false,
refreshing: false,
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
subSettings: {
enable : false,
port: 0,
path: '',
domain: '',
tls: false
},
tgBotEnable: false
},
methods: {
loading(spinning = true) {
@@ -340,6 +390,7 @@
this.refreshing = true;
const msg = await HttpUtil.post('/panel/inbound/list');
if (!msg.success) {
this.refreshing = false;
return;
}
this.setInbounds(msg.obj);
@@ -352,24 +403,42 @@
if (!msg.success) {
return;
}
this.expireDiff = msg.obj.expireDiff * 86400000;
this.trafficDiff = msg.obj.trafficDiff * 1073741824;
this.defaultCert = msg.obj.defaultCert;
this.defaultKey = msg.obj.defaultKey;
with(msg.obj){
this.expireDiff = expireDiff * 86400000;
this.trafficDiff = trafficDiff * 1073741824;
this.defaultCert = defaultCert;
this.defaultKey = defaultKey;
this.tgBotEnable = tgBotEnable;
this.subSettings = {
enable : subEnable,
port: subPort,
path: subPath,
domain: subDomain,
tls: subTLS
};
}
},
setInbounds(dbInbounds) {
this.inbounds.splice(0);
this.dbInbounds.splice(0);
this.clientCount.splice(0);
for (const inbound of dbInbounds) {
const dbInbound = new DBInbound(inbound);
to_inbound = dbInbound.toInbound()
this.inbounds.push(to_inbound);
this.dbInbounds.push(dbInbound);
if ([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN].includes(inbound.protocol)) {
if ([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(inbound.protocol)) {
if (inbound.protocol === Protocols.SHADOWSOCKS && (!to_inbound.isSSMultiUser)) {
continue;
}
this.clientCount[inbound.id] = this.getClientCounts(inbound, to_inbound);
}
}
this.searchInbounds(this.searchKey);
if(this.enableFilter){
this.filterInbounds();
} else {
this.searchInbounds(this.searchKey);
}
},
getClientCounts(dbInbound, inbound) {
let clientCount = 0, active = [], deactive = [], depleted = [], expiring = [];
@@ -427,6 +496,38 @@
});
}
},
filterInbounds() {
if (ObjectUtil.isEmpty(this.filterBy)) {
this.searchedInbounds = this.dbInbounds.slice();
} else {
this.searchedInbounds.splice(0, this.searchedInbounds.length);
this.dbInbounds.forEach(inbound => {
const newInbound = new DBInbound(inbound);
const inboundSettings = JSON.parse(inbound.settings);
if (this.clientCount[inbound.id] && this.clientCount[inbound.id].hasOwnProperty(this.filterBy)){
const list = this.clientCount[inbound.id][this.filterBy];
if (list.length > 0) {
const filteredSettings = { "clients": [] };
inboundSettings.clients.forEach(client => {
if (list.includes(client.email)) {
filteredSettings.clients.push(client);
}
});
newInbound.settings = Inbound.Settings.fromJson(inbound.protocol, filteredSettings);
this.searchedInbounds.push(newInbound);
}
}
});
}
},
toggleFilter(){
if(this.enableFilter) {
this.searchKey = '';
} else {
this.filterBy = '';
this.searchedInbounds = this.dbInbounds.slice();
}
},
generalActions(action) {
switch (action.key) {
case "export":
@@ -625,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 = {
@@ -696,13 +796,32 @@
default: return client.id;
}
},
checkFallback(dbInbound) {
newDbInbound = new DBInbound(dbInbound);
if (dbInbound.listen.startsWith("@")){
rootInbound = this.inbounds.find((i) =>
i.tls &&
['trojan','vless'].includes(i.protocol) &&
i.settings.fallbacks.find(f => f.dest === dbInbound.listen)
);
if (rootInbound) {
newDbInbound.listen = rootInbound.listen;
newDbInbound.port = rootInbound.port;
newInbound = newDbInbound.toInbound();
newInbound.stream.security = 'tls';
newInbound.stream.tls = rootInbound.stream.tls;
newDbInbound.streamSettings = newInbound.stream.toString();
}
}
return newDbInbound;
},
showQrcode(dbInbound, clientIndex) {
const clientName = JSON.parse(dbInbound.settings).clients[clientIndex].email;
const link = dbInbound.genLink(clientIndex);
qrModal.show('{{ i18n "qrCode"}}', link, dbInbound, '', clientName);
newDbInbound = this.checkFallback(dbInbound);
qrModal.show('{{ i18n "qrCode"}}', newDbInbound, clientIndex);
},
showInfo(dbInbound, index) {
infoModal.show(dbInbound, index);
newDbInbound = this.checkFallback(dbInbound);
infoModal.show(newDbInbound, index);
},
switchEnable(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
@@ -713,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]);
@@ -789,10 +909,10 @@
clientStats = dbInbound.clientStats.find(stats => stats.email === email)
return clientStats ? clientStats.down : 0
},
isTrafficExhausted(dbInbound, email) {
if (email.length == 0) return false
clientStats = dbInbound.clientStats.find(stats => stats.email === email)
return clientStats ? clientStats.down + clientStats.up > clientStats.total : false
statsColor(dbInbound, email) {
if(email.length == 0) return 'blue';
clientStats = dbInbound.clientStats.find(stats => stats.email === email);
return usageColor(clientStats.down + clientStats.up, this.trafficDiff, clientStats.total);
},
isClientEnabled(dbInbound, email) {
clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null
@@ -803,7 +923,8 @@
},
inboundLinks(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
txtModal.show('{{ i18n "pages.inbounds.export"}}', dbInbound.genInboundLinks, dbInbound.remark);
newDbInbound = this.checkFallback(dbInbound);
txtModal.show('{{ i18n "pages.inbounds.export"}}', newDbInbound.genInboundLinks, newDbInbound.remark);
},
exportAllLinks() {
let copyText = '';

View File

@@ -34,7 +34,8 @@
:stroke-color="status.cpu.color"
:class="themeSwitcher.darkCardClass"
:percent="status.cpu.percent"></a-progress>
<div>CPU</div>
<div>CPU: [[ cpuCoreFormat(status.cpuCores) ]]</div>
<div>Speed: [[ cpuSpeedFormat(status.cpuSpeedMhz) ]]</div>
</a-col>
<a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal"
@@ -77,21 +78,17 @@
<a-row>
<a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass">
3x-ui: <a href="https://github.com/MHSanaei/3x-ui/releases" target="_blank"><a-tag color="green">v{{ .cur_ver }}</a-tag></a>
3X: <a href="https://github.com/MHSanaei/3x-ui/releases" target="_blank"><a-tag color="green">v{{ .cur_ver }}</a-tag></a>
Xray: <a-tag color="green" style="cursor: pointer;" @click="openSelectV2rayVersion">v[[ status.xray.version ]]</a-tag>
Telegram: <a href="https://t.me/panel3xui" target="_blank"><a-tag color="green">@panel3xui</a-tag></a>
<a href="https://t.me/panel3xui" target="_blank"><a-tag color="green">@panel3xui</a-tag></a>
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass">
{{ i18n "pages.index.operationHours" }}:
<a-tag color="green">[[ formatSecond(status.uptime) ]]</a-tag>
<a-tooltip>
<template slot="title">
{{ i18n "pages.index.operationHoursDesc" }}
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
{{ i18n "menu.link" }}:
<a-tag color="blue" style="cursor: pointer;" @click="openLogs()">{{ i18n "pages.index.logs" }}</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="openConfig">{{ i18n "pages.index.config" }}</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="openBackup">{{ i18n "pages.index.backup" }}</a-tag>
</a-card>
</a-col>
<a-col :sm="24" :md="12">
@@ -111,34 +108,86 @@
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass">
{{ i18n "menu.link" }}:
<a-tag color="blue" style="cursor: pointer;" @click="openLogs(20)">{{ i18n "pages.index.logs" }}</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="openConfig">{{ i18n "pages.index.config" }}</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="openBackup">{{ i18n "pages.index.backup" }}</a-tag>
{{ i18n "pages.index.operationHours" }}:
Xray:
<a-tag color="green">[[ formatSecond(status.appStats.uptime) ]]</a-tag>
OS:
<a-tag color="green">[[ formatSecond(status.uptime) ]]</a-tag>
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass">
{{ i18n "pages.index.systemLoad" }}: [[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass">
TCP / UDP {{ i18n "pages.index.connectionCount" }}: [[ status.tcpCount ]] / [[ status.udpCount ]]
<a-tooltip>
<template slot="title">
{{ i18n "pages.index.connectionCountDesc" }}
{{ i18n "pages.index.systemLoadDesc" }}
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass">
{{ 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">
<a-row>
<a-col :span="12">
IPv4:
<a-tooltip>
<template slot="title">
[[ status.publicIP.ipv4 ]]
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</a-col>
<a-col :span="12">
IPv6:
<a-tooltip>
<template slot="title">
[[ status.publicIP.ipv6 ]]
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</a-col>
</a-row>
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass">
<a-row>
<a-col :span="12">
TCP: [[ status.tcpCount ]]
<a-tooltip>
<template slot="title">
{{ i18n "pages.index.connectionTcpCountDesc" }}
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</a-col>
<a-col :span="12">
UDP: [[ status.udpCount ]]
<a-tooltip>
<template slot="title">
{{ i18n "pages.index.connectionUdpCountDesc" }}
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</a-col>
</a-row>
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass">
<a-row>
<a-col :span="12">
<a-icon type="arrow-up"></a-icon>
[[ sizeFormat(status.netIO.up) ]] / S
[[ sizeFormat(status.netIO.up) ]]/S
<a-tooltip>
<template slot="title">
{{ i18n "pages.index.upSpeed" }}
@@ -148,7 +197,7 @@
</a-col>
<a-col :span="12">
<a-icon type="arrow-down"></a-icon>
[[ sizeFormat(status.netIO.down) ]] / S
[[ sizeFormat(status.netIO.down) ]]/S
<a-tooltip>
<template slot="title">
{{ i18n "pages.index.downSpeed" }}
@@ -213,7 +262,7 @@
<a-form-item label="Count">
<a-select v-model="logModal.rows"
style="width: 80px"
@change="openLogs(logModal.rows)"
@change="openLogs()"
:dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="10">10</a-select-option>
<a-select-option value="20">20</a-select-option>
@@ -221,8 +270,23 @@
<a-select-option value="100">100</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Log Level">
<a-select v-model="logModal.level"
style="width: 120px"
@change="openLogs()"
:dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="debug">Debug</a-select-option>
<a-select-option value="info">Info</a-select-option>
<a-select-option value="notice">Notice</a-select-option>
<a-select-option value="warning">Warning</a-select-option>
<a-select-option value="err">Error</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="SysLog">
<a-checkbox v-model="logModal.syslog" @change="openLogs()"></a-checkbox>
</a-form-item>
<a-form-item>
<button class="ant-btn ant-btn-primary" @click="openLogs(logModal.rows)"><a-icon type="sync"></a-icon> Reload</button>
<button class="ant-btn ant-btn-primary" @click="openLogs()"><a-icon type="sync"></a-icon> Reload</button>
</a-form-item>
<a-form-item>
<a-button type="primary" style="margin-bottom: 10px;"
@@ -294,30 +358,40 @@
class Status {
constructor(data) {
this.cpu = new CurTotal(0, 0);
this.cpuCores = 0;
this.cpuSpeedMhz = 0;
this.disk = new CurTotal(0, 0);
this.loads = [0, 0, 0];
this.mem = new CurTotal(0, 0);
this.netIO = { up: 0, down: 0 };
this.netTraffic = { sent: 0, recv: 0 };
this.publicIP = { ipv4: 0, ipv6: 0 };
this.swap = new CurTotal(0, 0);
this.tcpCount = 0;
this.udpCount = 0;
this.uptime = 0;
this.appUptime = 0;
this.appStats = {threads: 0, mem: 0, uptime: 0};
this.xray = { state: State.Stop, errorMsg: "", version: "", color: "" };
if (data == null) {
return;
}
this.cpu = new CurTotal(data.cpu, 100);
this.cpuCores = data.cpuCores;
this.cpuSpeedMhz = data.cpuSpeedMhz;
this.disk = new CurTotal(data.disk.current, data.disk.total);
this.loads = data.loads.map(load => toFixed(load, 2));
this.mem = new CurTotal(data.mem.current, data.mem.total);
this.netIO = data.netIO;
this.netTraffic = data.netTraffic;
this.publicIP = data.publicIP;
this.swap = new CurTotal(data.swap.current, data.swap.total);
this.tcpCount = data.tcpCount;
this.udpCount = data.udpCount;
this.uptime = data.uptime;
this.appUptime = data.appUptime;
this.appStats = data.appStats;
this.xray = data.xray;
switch (this.xray.state) {
case State.Running:
@@ -351,10 +425,11 @@
visible: false,
logs: '',
rows: 20,
show(logs, rows) {
level: 'info',
syslog: false,
show(logs) {
this.visible = true;
this.rows = rows;
this.logs = logs.join("\n");
this.logs = logs? logs.join("\n"): "No Record...";
},
hide() {
this.visible = false;
@@ -388,6 +463,7 @@
delimiters: ['[[', ']]'],
el: '#app',
data: {
siderDrawer,
themeSwitcher,
status: new Status(),
versionModal,
@@ -402,9 +478,13 @@
this.loadingTip = tip;
},
async getStatus() {
const msg = await HttpUtil.post('/server/status');
if (msg.success) {
this.setStatus(msg.obj);
try {
const msg = await HttpUtil.post('/server/status');
if (msg.success) {
this.setStatus(msg.obj);
}
} catch (e) {
console.error("Failed to get status:", e);
}
},
setStatus(data) {
@@ -450,14 +530,14 @@
return;
}
},
async openLogs(rows) {
async openLogs(){
this.loading(true);
const msg = await HttpUtil.post('server/logs/' + rows);
const msg = await HttpUtil.post('server/logs/'+logModal.rows,{level: logModal.level, syslog: logModal.syslog});
this.loading(false);
if (!msg.success) {
return;
}
logModal.show(msg.obj, rows);
logModal.show(msg.obj);
},
async openConfig() {
this.loading(true);
@@ -513,11 +593,14 @@
},
},
async mounted() {
while (true) {
let retries = 0;
while (retries < 5) {
try {
await this.getStatus();
retries = 0;
} catch (e) {
console.error(e);
console.error("Error occurred while fetching status:", e);
retries++;
}
await PromiseUtil.sleep(2000);
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,28 +2,28 @@ package job
import (
"encoding/json"
"log"
"os"
"regexp"
"x-ui/database"
"x-ui/database/model"
"x-ui/logger"
"x-ui/web/service"
"x-ui/xray"
"net"
"sort"
"strings"
"time"
"github.com/go-cmd/cmd"
"x-ui/database"
"x-ui/database/model"
"x-ui/logger"
"x-ui/xray"
)
type CheckClientIpJob struct {
xrayService service.XrayService
}
type CheckClientIpJob struct{}
var job *CheckClientIpJob
var disAllowedIps []string
var ipFiles = []string{
xray.GetIPLimitLogPath(),
xray.GetIPLimitBannedLogPath(),
xray.GetAccessPersistentLogPath(),
}
func NewCheckClientIpJob() *CheckClientIpJob {
job = new(CheckClientIpJob)
@@ -31,31 +31,59 @@ func NewCheckClientIpJob() *CheckClientIpJob {
}
func (j *CheckClientIpJob) Run() {
logger.Debug("Check Client IP Job...")
processLogFile()
// disAllowedIps = []string{"192.168.1.183","192.168.1.197"}
blockedIps := []byte(strings.Join(disAllowedIps, ","))
err := os.WriteFile(xray.GetBlockedIPsPath(), blockedIps, 0755)
checkError(err)
// create files required for iplimit if not exists
for i := 0; i < len(ipFiles); i++ {
file, err := os.OpenFile(ipFiles[i], os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
j.checkError(err)
defer file.Close()
}
// check for limit ip
if j.hasLimitIp() {
j.processLogFile()
}
}
func processLogFile() {
accessLogPath := GetAccessLogPath()
func (j *CheckClientIpJob) hasLimitIp() bool {
db := database.GetDB()
var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Find(&inbounds).Error
if err != nil {
return false
}
for _, inbound := range inbounds {
if inbound.Settings == "" {
continue
}
settings := map[string][]model.Client{}
json.Unmarshal([]byte(inbound.Settings), &settings)
clients := settings["clients"]
for _, client := range clients {
limitIp := client.LimitIP
if limitIp > 0 {
return true
}
}
}
return false
}
func (j *CheckClientIpJob) processLogFile() {
accessLogPath := xray.GetAccessLogPath()
if accessLogPath == "" {
logger.Warning("xray log not init in config.json")
logger.Warning("access.log doesn't exist in your config.json")
return
}
data, err := os.ReadFile(accessLogPath)
InboundClientIps := make(map[string][]string)
checkError(err)
// clean log
if err := os.Truncate(GetAccessLogPath(), 0); err != nil {
checkError(err)
}
j.checkError(err)
lines := strings.Split(string(data), "\n")
for _, line := range lines {
@@ -73,10 +101,10 @@ func processLogFile() {
if matchesEmail == "" {
continue
}
matchesEmail = strings.Split(matchesEmail, "email: ")[1]
matchesEmail = strings.TrimSpace(strings.Split(matchesEmail, "email: ")[1])
if InboundClientIps[matchesEmail] != nil {
if contains(InboundClientIps[matchesEmail], ip) {
if j.contains(InboundClientIps[matchesEmail], ip) {
continue
}
InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail], ip)
@@ -85,54 +113,50 @@ func processLogFile() {
InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail], ip)
}
}
}
disAllowedIps = []string{}
shouldCleanLog := false
for clientEmail, ips := range InboundClientIps {
inboundClientIps, err := GetInboundClientIps(clientEmail)
inboundClientIps, err := j.getInboundClientIps(clientEmail)
sort.Strings(ips)
if err != nil {
addInboundClientIps(clientEmail, ips)
j.addInboundClientIps(clientEmail, ips)
} else {
updateInboundClientIps(inboundClientIps, clientEmail, ips)
shouldCleanLog = j.updateInboundClientIps(inboundClientIps, clientEmail, ips)
}
}
// check if inbound connection is more than limited ip and drop connection
LimitDevice := func() { LimitDevice() }
// added delay before cleaning logs to reduce chance of logging IP that already has been banned
time.Sleep(time.Second * 2)
stop := schedule(LimitDevice, 1000*time.Millisecond)
time.Sleep(10 * time.Second)
stop <- true
if shouldCleanLog {
// copy access log to persistent file
logAccessP, err := os.OpenFile(xray.GetAccessPersistentLogPath(), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
j.checkError(err)
input, err := os.ReadFile(accessLogPath)
j.checkError(err)
if _, err := logAccessP.Write(input); err != nil {
j.checkError(err)
}
defer logAccessP.Close()
}
func GetAccessLogPath() string {
config, err := os.ReadFile(xray.GetConfigPath())
checkError(err)
jsonConfig := map[string]interface{}{}
err = json.Unmarshal([]byte(config), &jsonConfig)
checkError(err)
if jsonConfig["log"] != nil {
jsonLog := jsonConfig["log"].(map[string]interface{})
if jsonLog["access"] != nil {
accessLogPath := jsonLog["access"].(string)
return accessLogPath
// clean access log
if err := os.Truncate(xray.GetAccessLogPath(), 0); err != nil {
j.checkError(err)
}
}
return ""
}
func checkError(e error) {
func (j *CheckClientIpJob) checkError(e error) {
if e != nil {
logger.Warning("client ip job err:", e)
}
}
func contains(s []string, str string) bool {
func (j *CheckClientIpJob) contains(s []string, str string) bool {
for _, v := range s {
if v == str {
return true
@@ -141,7 +165,8 @@ func contains(s []string, str string) bool {
return false
}
func GetInboundClientIps(clientEmail string) (*model.InboundClientIps, error) {
func (j *CheckClientIpJob) getInboundClientIps(clientEmail string) (*model.InboundClientIps, error) {
db := database.GetDB()
InboundClientIps := &model.InboundClientIps{}
err := db.Model(model.InboundClientIps{}).Where("client_email = ?", clientEmail).First(InboundClientIps).Error
@@ -150,13 +175,11 @@ func GetInboundClientIps(clientEmail string) (*model.InboundClientIps, error) {
}
return InboundClientIps, nil
}
func addInboundClientIps(clientEmail string, ips []string) error {
func (j *CheckClientIpJob) addInboundClientIps(clientEmail string, ips []string) error {
inboundClientIps := &model.InboundClientIps{}
jsonIps, err := json.Marshal(ips)
checkError(err)
// Trim any leading/trailing whitespace from clientEmail
clientEmail = strings.TrimSpace(clientEmail)
j.checkError(err)
inboundClientIps.ClientEmail = clientEmail
inboundClientIps.Ips = string(jsonIps)
@@ -178,37 +201,50 @@ func addInboundClientIps(clientEmail string, ips []string) error {
}
return nil
}
func updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmail string, ips []string) error {
func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmail string, ips []string) bool {
jsonIps, err := json.Marshal(ips)
checkError(err)
j.checkError(err)
inboundClientIps.ClientEmail = clientEmail
inboundClientIps.Ips = string(jsonIps)
// check inbound limitation
inbound, err := GetInboundByEmail(clientEmail)
checkError(err)
inbound, err := j.getInboundByEmail(clientEmail)
j.checkError(err)
if inbound.Settings == "" {
logger.Debug("wrong data ", inbound)
return nil
return false
}
settings := map[string][]model.Client{}
json.Unmarshal([]byte(inbound.Settings), &settings)
clients := settings["clients"]
shouldCleanLog := false
var disAllowedIps []string // initialize the slice
// create iplimit log file channel
logIpFile, err := os.OpenFile(xray.GetIPLimitLogPath(), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
if err != nil {
logger.Errorf("failed to create or open ip limit log file: %s", err)
}
defer logIpFile.Close()
log.SetOutput(logIpFile)
log.SetFlags(log.LstdFlags)
for _, client := range clients {
if client.Email == clientEmail {
limitIp := client.LimitIP
if limitIp < len(ips) && limitIp != 0 && inbound.Enable {
if limitIp != 0 {
shouldCleanLog = true
disAllowedIps = append(disAllowedIps, ips[limitIp:]...)
if limitIp < len(ips) && inbound.Enable {
disAllowedIps = append(disAllowedIps, ips[limitIp:]...)
for i := limitIp; i < len(ips); i++ {
log.Printf("[LIMIT_IP] Email = %s || SRC = %s", clientEmail, ips[i])
}
}
}
}
}
@@ -218,132 +254,19 @@ func updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmai
db := database.GetDB()
err = db.Save(inboundClientIps).Error
if err != nil {
return err
return shouldCleanLog
}
return nil
return shouldCleanLog
}
func DisableInbound(id int) error {
db := database.GetDB()
result := db.Model(model.Inbound{}).
Where("id = ? and enable = ?", id, true).
Update("enable", false)
err := result.Error
logger.Warning("disable inbound with id:", id)
if err == nil {
job.xrayService.SetToNeedRestart()
}
return err
}
func GetInboundByEmail(clientEmail string) (*model.Inbound, error) {
func (j *CheckClientIpJob) getInboundByEmail(clientEmail string) (*model.Inbound, error) {
db := database.GetDB()
var inbounds *model.Inbound
err := db.Model(model.Inbound{}).Where("settings LIKE ?", "%"+clientEmail+"%").Find(&inbounds).Error
if err != nil {
return nil, err
}
return inbounds, nil
}
func LimitDevice() {
var destIp, destPort, srcIp, srcPort string
localIp, err := LocalIP()
checkError(err)
c := cmd.NewCmd("bash", "-c", "ss --tcp | grep -E '"+IPsToRegex(localIp)+"'| awk '{if($1==\"ESTAB\") print $4,$5;}'", "| sort | uniq -c | sort -nr | head")
<-c.Start()
if len(c.Status().Stdout) > 0 {
ipRegx, _ := regexp.Compile(`[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+`)
portRegx, _ := regexp.Compile(`(?:(:))([0-9]..[^.][0-9]+)`)
for _, row := range c.Status().Stdout {
data := strings.Split(row, " ")
if len(data) < 2 {
continue // Skip this row if it doesn't have at least two elements
}
destIp = string(ipRegx.FindString(data[0]))
destPort = portRegx.FindString(data[0])
destPort = strings.Replace(destPort, ":", "", -1)
srcIp = string(ipRegx.FindString(data[1]))
srcPort = portRegx.FindString(data[1])
srcPort = strings.Replace(srcPort, ":", "", -1)
if contains(disAllowedIps, srcIp) {
dropCmd := cmd.NewCmd("bash", "-c", "ss -K dport = "+srcPort)
dropCmd.Start()
logger.Debug("request droped : ", srcIp, srcPort, "to", destIp, destPort)
}
}
}
}
func LocalIP() ([]string, error) {
// get machine ips
ifaces, err := net.Interfaces()
ips := []string{}
if err != nil {
return ips, err
}
for _, i := range ifaces {
addrs, err := i.Addrs()
if err != nil {
return ips, err
}
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}
ips = append(ips, ip.String())
}
}
logger.Debug("System IPs : ", ips)
return ips, nil
}
func IPsToRegex(ips []string) string {
regx := ""
for _, ip := range ips {
regx += "(" + strings.Replace(ip, ".", "\\.", -1) + ")"
}
regx = "(" + strings.Replace(regx, ")(", ")|(.", -1) + ")"
return regx
}
func schedule(LimitDevice func(), delay time.Duration) chan bool {
stop := make(chan bool)
go func() {
for {
LimitDevice()
select {
case <-time.After(delay):
case <-stop:
return
}
}
}()
return stop
}

View File

@@ -1,7 +1,7 @@
package job
import (
"fmt"
"strconv"
"time"
"x-ui/web/service"
@@ -24,7 +24,10 @@ func (j *CheckCpuJob) Run() {
// get latest status of server
percent, err := cpu.Percent(1*time.Second, false)
if err == nil && percent[0] > float64(threshold) {
msg := fmt.Sprintf("🔴 CPU usage %.2f%% is more than threshold %d%%", percent[0], threshold)
msg := j.tgbotService.I18nBot("tgbot.messages.cpuThreshold",
"Percent=="+strconv.FormatFloat(percent[0], 'f', 2, 64),
"Threshold=="+strconv.Itoa(threshold))
j.tgbotService.SendMsgToTgbotAdmins(msg)
}
}

View File

@@ -0,0 +1,19 @@
package job
import (
"x-ui/web/service"
)
type CheckHashStorageJob struct {
tgbotService service.Tgbot
}
func NewCheckHashStorageJob() *CheckHashStorageJob {
return new(CheckHashStorageJob)
}
// Here Run is an interface method of the Job interface
func (j *CheckHashStorageJob) Run() {
// Remove expired hashes from storage
j.tgbotService.GetHashStorage().RemoveExpiredHashes()
}

View File

@@ -1,33 +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() {
count, err := j.inboundService.DisableInvalidClients()
if err != nil {
logger.Warning("disable invalid Client err:", err)
} else if count > 0 {
logger.Debugf("disabled %v Client", count)
j.xrayService.SetToNeedRestart()
}
count, err = j.inboundService.DisableInvalidInbounds()
if err != nil {
logger.Warning("disable invalid inbounds err:", err)
} else if count > 0 {
logger.Debugf("disabled %v inbounds", count)
j.xrayService.SetToNeedRestart()
}
}

25
web/job/clear_logs_job.go Normal file
View File

@@ -0,0 +1,25 @@
package job
import (
"os"
"x-ui/logger"
"x-ui/xray"
)
type ClearLogsJob struct{}
func NewClearLogsJob() *ClearLogsJob {
return new(ClearLogsJob)
}
// Here Run is an interface method of the Job interface
func (j *ClearLogsJob) Run() {
logFiles := []string{xray.GetIPLimitLogPath(), xray.GetIPLimitBannedLogPath(), xray.GetAccessPersistentLogPath()}
// clear log files
for i := 0; i < len(logFiles); i++ {
if err := os.Truncate(logFiles[i], 0); err != nil {
logger.Warning("clear logs job err:", err)
}
}
}

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

144
web/locale/locale.go Normal file
View File

@@ -0,0 +1,144 @@
package locale
import (
"embed"
"io/fs"
"strings"
"x-ui/logger"
"github.com/gin-gonic/gin"
"github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/pelletier/go-toml/v2"
"golang.org/x/text/language"
)
var i18nBundle *i18n.Bundle
var LocalizerWeb *i18n.Localizer
var LocalizerBot *i18n.Localizer
type I18nType string
const (
Bot I18nType = "bot"
Web I18nType = "web"
)
type SettingService interface {
GetTgLang() (string, error)
}
func InitLocalizer(i18nFS embed.FS, settingService SettingService) error {
// set default bundle to english
i18nBundle = i18n.NewBundle(language.MustParse("en-US"))
i18nBundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
// parse files
if err := parseTranslationFiles(i18nFS, i18nBundle); err != nil {
return err
}
// setup bot locale
if err := initTGBotLocalizer(settingService); err != nil {
return err
}
return nil
}
func createTemplateData(params []string, seperator ...string) map[string]interface{} {
var sep string = "=="
if len(seperator) > 0 {
sep = seperator[0]
}
templateData := make(map[string]interface{})
for _, param := range params {
parts := strings.SplitN(param, sep, 2)
templateData[parts[0]] = parts[1]
}
return templateData
}
func I18n(i18nType I18nType, key string, params ...string) string {
var localizer *i18n.Localizer
switch i18nType {
case "bot":
localizer = LocalizerBot
case "web":
localizer = LocalizerWeb
default:
logger.Errorf("Invalid type for I18n: %s", i18nType)
return ""
}
templateData := createTemplateData(params)
msg, err := localizer.Localize(&i18n.LocalizeConfig{
MessageID: key,
TemplateData: templateData,
})
if err != nil {
logger.Errorf("Failed to localize message: %v", err)
return ""
}
return msg
}
func initTGBotLocalizer(settingService SettingService) error {
botLang, err := settingService.GetTgLang()
if err != nil {
return err
}
LocalizerBot = i18n.NewLocalizer(i18nBundle, botLang)
return nil
}
func LocalizerMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
var lang string
if cookie, err := c.Request.Cookie("lang"); err == nil {
lang = cookie.Value
} else {
lang = c.GetHeader("Accept-Language")
}
LocalizerWeb = i18n.NewLocalizer(i18nBundle, lang)
c.Set("localizer", LocalizerWeb)
c.Set("I18n", I18n)
c.Next()
}
}
func parseTranslationFiles(i18nFS embed.FS, i18nBundle *i18n.Bundle) error {
err := fs.WalkDir(i18nFS, "translation",
func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
data, err := i18nFS.ReadFile(path)
if err != nil {
return err
}
_, err = i18nBundle.ParseMessageFileBytes(data, path)
return err
})
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,21 @@
package middleware
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
func DomainValidatorMiddleware(domain string) gin.HandlerFunc {
return func(c *gin.Context) {
host := strings.Split(c.Request.Host, ":")[0]
if host != domain {
c.AbortWithStatus(http.StatusForbidden)
return
}
c.Next()
}
}

View File

@@ -0,0 +1,34 @@
package middleware
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
func RedirectMiddleware(basePath string) gin.HandlerFunc {
return func(c *gin.Context) {
// Redirect from old '/xui' path to '/panel'
redirects := map[string]string{
"panel/API": "panel/api",
"xui/API": "panel/api",
"xui": "panel",
}
path := c.Request.URL.Path
for from, to := range redirects {
from, to = basePath+from, basePath+to
if strings.HasPrefix(path, from) {
newPath := to + path[len(from):]
c.Redirect(http.StatusMovedPermanently, newPath)
c.Abort()
return
}
}
c.Next()
}
}

View File

@@ -6,7 +6,11 @@
},
"api": {
"tag": "api",
"services": ["HandlerService", "LoggerService", "StatsService"]
"services": [
"HandlerService",
"LoggerService",
"StatsService"
]
},
"inbounds": [
{
@@ -47,20 +51,26 @@
"rules": [
{
"type": "field",
"inboundTag": ["api"],
"inboundTag": [
"api"
],
"outboundTag": "api"
},
{
"type": "field",
"outboundTag": "blocked",
"ip": ["geoip:private"]
"ip": [
"geoip:private"
]
},
{
"type": "field",
"outboundTag": "blocked",
"protocol": ["bittorrent"]
"protocol": [
"bittorrent"
]
}
]
},
"stats": {}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -12,8 +12,10 @@ import (
"os"
"os/exec"
"runtime"
"strconv"
"strings"
"time"
"x-ui/config"
"x-ui/database"
"x-ui/logger"
@@ -38,9 +40,11 @@ const (
)
type Status struct {
T time.Time `json:"-"`
Cpu float64 `json:"cpu"`
Mem struct {
T time.Time `json:"-"`
Cpu float64 `json:"cpu"`
CpuCores int `json:"cpuCores"`
CpuSpeedMhz float64 `json:"cpuSpeedMhz"`
Mem struct {
Current uint64 `json:"current"`
Total uint64 `json:"total"`
} `json:"mem"`
@@ -69,6 +73,15 @@ type Status struct {
Sent uint64 `json:"sent"`
Recv uint64 `json:"recv"`
} `json:"netTraffic"`
PublicIP struct {
IPv4 string `json:"ipv4"`
IPv6 string `json:"ipv6"`
} `json:"publicIP"`
AppStats struct {
Threads uint32 `json:"threads"`
Mem uint64 `json:"mem"`
Uptime uint64 `json:"uptime"`
} `json:"appStats"`
}
type Release struct {
@@ -80,6 +93,26 @@ type ServerService struct {
inboundService InboundService
}
func getPublicIP(url string) string {
resp, err := http.Get(url)
if err != nil {
return "N/A"
}
defer resp.Body.Close()
ip, err := io.ReadAll(resp.Body)
if err != nil {
return "N/A"
}
ipString := string(ip)
if ipString == "" {
return "N/A"
}
return ipString
}
func (s *ServerService) GetStatus(lastStatus *Status) *Status {
now := time.Now()
status := &Status{
@@ -93,6 +126,21 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
status.Cpu = percents[0]
}
status.CpuCores, err = cpu.Counts(false)
if err != nil {
logger.Warning("get cpu cores count failed:", err)
}
cpuInfos, err := cpu.Info()
if err != nil {
logger.Warning("get cpu info failed:", err)
} else if len(cpuInfos) > 0 {
cpuInfo := cpuInfos[0]
status.CpuSpeedMhz = cpuInfo.Mhz // setting CPU speed in MHz
} else {
logger.Warning("could not find cpu info")
}
upTime, err := host.Uptime()
if err != nil {
logger.Warning("get uptime failed:", err)
@@ -161,6 +209,9 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
logger.Warning("get udp connections failed:", err)
}
status.PublicIP.IPv4 = getPublicIP("https://api.ipify.org")
status.PublicIP.IPv6 = getPublicIP("https://api6.ipify.org")
if s.xrayService.IsXrayRunning() {
status.Xray.State = Running
status.Xray.ErrorMsg = ""
@@ -174,12 +225,22 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
status.Xray.ErrorMsg = s.xrayService.GetXrayResult()
}
status.Xray.Version = s.xrayService.GetXrayVersion()
var rtm runtime.MemStats
runtime.ReadMemStats(&rtm)
status.AppStats.Mem = rtm.Sys
status.AppStats.Threads = uint32(runtime.NumGoroutine())
if p.IsRunning() {
status.AppStats.Uptime = p.GetUptime()
} else {
status.AppStats.Uptime = 0
}
return status
}
func (s *ServerService) GetXrayVersions() ([]string, error) {
url := "https://api.github.com/repos/MHSanaei/Xray-core/releases"
url := "https://api.github.com/repos/XTLS/Xray-core/releases"
resp, err := http.Get(url)
if err != nil {
return nil, err
@@ -198,15 +259,16 @@ func (s *ServerService) GetXrayVersions() ([]string, error) {
if err != nil {
return nil, err
}
versions := make([]string, 0, len(releases))
var versions []string
for _, release := range releases {
versions = append(versions, release.TagName)
if release.TagName >= "v1.7.5" {
versions = append(versions, release.TagName)
}
}
return versions, nil
}
func (s *ServerService) StopXrayService() (string error) {
err := s.xrayService.StopXray()
if err != nil {
logger.Error("stop xray failed:", err)
@@ -217,7 +279,6 @@ func (s *ServerService) StopXrayService() (string error) {
}
func (s *ServerService) RestartXrayService() (string error) {
s.xrayService.StopXray()
defer func() {
err := s.xrayService.RestartXray(true)
@@ -246,7 +307,7 @@ func (s *ServerService) downloadXRay(version string) (string, error) {
}
fileName := fmt.Sprintf("Xray-%s-%s.zip", osName, arch)
url := fmt.Sprintf("https://github.com/MHSanaei/Xray-core/releases/download/%s/%s", version, fileName)
url := fmt.Sprintf("https://github.com/XTLS/Xray-core/releases/download/%s/%s", version, fileName)
resp, err := http.Get(url)
if err != nil {
return "", err
@@ -315,66 +376,86 @@ func (s *ServerService) UpdateXray(version string) error {
return err
}
err = copyZipFile("xray", xray.GetBinaryPath())
if err != nil {
downloadFile := func(fileName string, url string) error {
os.Remove(fileName)
file, err := os.OpenFile(fileName, os.O_CREATE|os.O_RDWR|os.O_TRUNC, fs.ModePerm)
if err != nil {
return err
}
defer file.Close()
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("download file failed: %s", resp.Status)
}
_, err = io.Copy(file, resp.Body)
return err
}
err = copyZipFile("geosite.dat", xray.GetGeositePath())
if err != nil {
return err
copyFiles := map[string]string{
"xray": xray.GetBinaryPath(),
"geosite.dat": xray.GetGeositePath(),
"geoip.dat": xray.GetGeoipPath(),
}
err = copyZipFile("geoip.dat", xray.GetGeoipPath())
if err != nil {
return err
downloadFiles := map[string]string{
xray.GetIranPath(): "https://github.com/MasterKia/iran-hosted-domains/releases/latest/download/iran.dat",
}
err = copyZipFile("iran.dat", xray.GetIranPath())
if err != nil {
return err
for fileName, filePath := range copyFiles {
err := copyZipFile(fileName, filePath)
if err != nil {
return err
}
}
for fileName, filePath := range downloadFiles {
err := downloadFile(fileName, filePath)
if err != nil {
return err
}
}
return nil
}
func (s *ServerService) GetLogs(count string) ([]string, error) {
// Define the journalctl command and its arguments
var cmdArgs []string
if runtime.GOOS == "linux" {
cmdArgs = []string{"journalctl", "-u", "x-ui", "--no-pager", "-n", count}
func (s *ServerService) GetLogs(count string, level string, syslog string) []string {
c, _ := strconv.Atoi(count)
var lines []string
if syslog == "true" {
cmdArgs := []string{"journalctl", "-u", "x-ui", "--no-pager", "-n", count, "-p", level}
// Run the command
cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return []string{"Failed to run journalctl command!"}
}
lines = strings.Split(out.String(), "\n")
} else {
return []string{"Unsupported operating system"}, nil
lines = logger.GetLogs(c, level)
}
// Run the command
cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return nil, err
}
lines := strings.Split(out.String(), "\n")
return lines, nil
return lines
}
func (s *ServerService) GetConfigJson() (interface{}, error) {
// Open the file for reading
file, err := os.Open(xray.GetConfigPath())
config, err := s.xrayService.GetXrayConfig()
if err != nil {
return nil, err
}
defer file.Close()
// Read the file contents
fileContents, err := io.ReadAll(file)
contents, err := json.MarshalIndent(config, "", " ")
if err != nil {
return nil, err
}
var jsonData interface{}
err = json.Unmarshal(fileContents, &jsonData)
err = json.Unmarshal(contents, &jsonData)
if err != nil {
return nil, err
}

View File

@@ -24,6 +24,7 @@ var xrayTemplateConfig string
var defaultValueMap = map[string]string{
"xrayTemplateConfig": xrayTemplateConfig,
"webListen": "",
"webDomain": "",
"webPort": "2053",
"webCertFile": "",
"webKeyFile": "",
@@ -38,8 +39,20 @@ var defaultValueMap = map[string]string{
"tgBotChatId": "",
"tgRunTime": "@daily",
"tgBotBackup": "false",
"tgBotLoginNotify": "true",
"tgCpu": "0",
"tgLang": "en-US",
"secretEnable": "false",
"subEnable": "false",
"subListen": "",
"subPort": "2096",
"subPath": "/sub/",
"subDomain": "",
"subCertFile": "",
"subKeyFile": "",
"subUpdates": "12",
"subEncrypt": "true",
"subShowInfo": "true",
}
type SettingService struct {
@@ -216,6 +229,10 @@ func (s *SettingService) GetListen() (string, error) {
return s.getString("webListen")
}
func (s *SettingService) GetWebDomain() (string, error) {
return s.getString("webDomain")
}
func (s *SettingService) GetTgBotToken() (string, error) {
return s.getString("tgBotToken")
}
@@ -252,10 +269,18 @@ func (s *SettingService) GetTgBotBackup() (bool, error) {
return s.getBool("tgBotBackup")
}
func (s *SettingService) GetTgBotLoginNotify() (bool, error) {
return s.getBool("tgBotLoginNotify")
}
func (s *SettingService) GetTgCpu() (int, error) {
return s.getInt("tgCpu")
}
func (s *SettingService) GetTgLang() (string, error) {
return s.getString("tgLang")
}
func (s *SettingService) GetPort() (int, error) {
return s.getInt("webPort")
}
@@ -331,6 +356,56 @@ func (s *SettingService) GetTimeLocation() (*time.Location, error) {
return location, nil
}
func (s *SettingService) GetSubEnable() (bool, error) {
return s.getBool("subEnable")
}
func (s *SettingService) GetSubListen() (string, error) {
return s.getString("subListen")
}
func (s *SettingService) GetSubPort() (int, error) {
return s.getInt("subPort")
}
func (s *SettingService) GetSubPath() (string, error) {
subPath, err := s.getString("subPath")
if err != nil {
return "", err
}
if !strings.HasPrefix(subPath, "/") {
subPath = "/" + subPath
}
if !strings.HasSuffix(subPath, "/") {
subPath += "/"
}
return subPath, nil
}
func (s *SettingService) GetSubDomain() (string, error) {
return s.getString("subDomain")
}
func (s *SettingService) GetSubCertFile() (string, error) {
return s.getString("subCertFile")
}
func (s *SettingService) GetSubKeyFile() (string, error) {
return s.getString("subKeyFile")
}
func (s *SettingService) GetSubUpdates() (int, error) {
return s.getInt("subUpdates")
}
func (s *SettingService) 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

File diff suppressed because it is too large Load Diff

View File

@@ -18,6 +18,7 @@ var result string
type XrayService struct {
inboundService InboundService
settingService SettingService
xrayAPI xray.XrayAPI
}
func (s *XrayService) IsXrayRunning() bool {
@@ -68,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 {
@@ -115,7 +116,7 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
}
}
for key := range c {
if key != "email" && key != "id" && key != "password" && key != "flow" && key != "alterId" {
if key != "email" && key != "id" && key != "password" && key != "flow" && key != "method" {
delete(c, key)
}
if c["flow"] == "xtls-rprx-vision-udp443" {
@@ -143,7 +144,9 @@ func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, []*xray.ClientTraffic,
if !s.IsXrayRunning() {
return nil, nil, errors.New("xray is not running")
}
return p.GetTraffic(true)
s.xrayAPI.Init(p.GetAPIPort())
defer s.xrayAPI.Close()
return s.xrayAPI.GetTraffic(true)
}
func (s *XrayService) RestartXray(isForce bool) error {
@@ -158,7 +161,7 @@ func (s *XrayService) RestartXray(isForce bool) error {
if p != nil && p.IsRunning() {
if !isForce && p.GetConfig().Equals(xrayConfig) {
logger.Debug("not need to restart xray")
logger.Debug("It does not need to restart xray")
return nil
}
p.Stop()
@@ -166,7 +169,11 @@ func (s *XrayService) RestartXray(isForce bool) error {
p = xray.NewProcess(xrayConfig)
result = ""
return p.Start()
err = p.Start()
if err != nil {
return err
}
return nil
}
func (s *XrayService) StopXray() error {

View File

@@ -4,7 +4,7 @@ import (
"encoding/gob"
"x-ui/database/model"
"github.com/gin-contrib/sessions"
sessions "github.com/Calidity/gin-sessions"
"github.com/gin-gonic/gin"
)

View File

@@ -11,7 +11,7 @@
"enable" = "Enable"
"protocol" = "Protocol"
"search" = "Search"
"filter" = "Filter"
"loading" = "Loading"
"second" = "Second"
"minute" = "Minute"
@@ -39,7 +39,6 @@
"depleted" = "Depleted"
"depletingSoon" = "Depleting soon"
"domainName" = "Domain name"
"additional" = "Alter"
"monitor" = "Listening IP"
"certificate" = "Certificate"
"fail" = "Fail"
@@ -49,6 +48,7 @@
"clients" = "Clients"
"usage" = "Usage"
"secretToken" = "Secret Token"
"remained" = "Remained"
[menu]
"dashboard" = "System Status"
@@ -72,17 +72,18 @@
"title" = "System Status"
"memory" = "Memory"
"hard" = "Hard Disk"
"xrayStatus" = "Xray Status"
"xrayStatus" = "Status"
"stopXray" = "Stop"
"restartXray" = "Restart"
"xraySwitch" = "Switch Version"
"xraySwitch" = "SwitchV"
"xraySwitchClick" = "Choose the version you want to switch to."
"xraySwitchClickDesk" = "Choose wisely, as older versions may not be compatible with current configurations."
"operationHours" = "Operation Hours"
"operationHoursDesc" = "System uptime: time since startup."
"operationHours" = "Uptime"
"systemLoad" = "System Load"
"systemLoadDesc" = "system load average for the past 1, 5, and 15 minutes"
"connectionTcpCountDesc" = "Total TCP connections across all network cards."
"connectionUdpCountDesc" = "Total UDP connections across all network cards."
"connectionCount" = "Number of Connections"
"connectionCountDesc" = "Total connections across all network cards."
"upSpeed" = "Total upload speed for all network cards."
"downSpeed" = "Total download speed for all network cards."
"totalSent" = "Total upload traffic of all network cards since system startup."
@@ -126,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"
@@ -148,8 +148,6 @@
"resetAllTraffic" = "Reset All Inbounds Traffic"
"resetAllTrafficTitle" = "Reset all inbounds traffic"
"resetAllTrafficContent" = "Are you sure you want to reset all inbounds traffic?"
"resetAllTrafficOkText" = "Confirm"
"resetAllTrafficCancelText" = "Cancel"
"resetInboundClientTraffics" = "Reset Clients Traffic"
"resetInboundClientTrafficTitle" = "Reset all client traffic"
"resetInboundClientTrafficContent" = "Are you sure you want to reset all traffic for this inbound's clients?"
@@ -169,7 +167,7 @@
"setDefaultCert" = "Set cert from panel"
"xtlsDesc" = "Xray core needs to be 1.7.5"
"realityDesc" = "Xray core needs to be 1.8.0 or higher."
"telegramDesc" = "use Telegram ID without @ or chat IDs ( you can get it here @userinfobot )"
"telegramDesc" = "use Telegram ID without @ or chat IDs ( you can get it here @userinfobot or use '/id' command in bot )"
"subscriptionDesc" = "you can find your sub link on Details, also you can use the same name for several configurations"
[pages.client]
@@ -211,6 +209,7 @@
[pages.settings]
"title" = "Settings"
"save" = "Save"
"infoDesc" = "Every change made here needs to be saved. Please restart the panel to apply changes."
"restartPanel" = "Restart Panel "
"restartPanelDesc" = "Are you sure you want to restart the panel? Click OK to restart after 3 seconds. If you cannot access the panel after restarting, please view the panel log information on the server."
"actions" = "Actions"
@@ -220,29 +219,33 @@
"xrayConfiguration" = "Xray Configuration"
"TGBotSettings" = "Telegram Bot Settings"
"panelListeningIP" = "Panel Listening IP"
"panelListeningIPDesc" = "Leave blank by default to monitor all IPs. Restart the panel to apply changes."
"panelListeningIPDesc" = "Leave blank by default to monitor all IPs."
"panelListeningDomain" = "Panel Listening Domain"
"panelListeningDomainDesc" = "Leave blank by default to monitor all domains and IPs"
"panelPort" = "Panel Port"
"panelPortDesc" = "Restart the panel to apply changes."
"panelPortDesc" = "The port used to display this panel"
"publicKeyPath" = "Panel Certificate Public Key File Path"
"publicKeyPathDesc" = "Fill in an absolute path starting with '/'. Restart the panel to apply changes."
"publicKeyPathDesc" = "Fill in an absolute path starting with."
"privateKeyPath" = "Panel Certificate Private Key File Path"
"privateKeyPathDesc" = "Fill in an absolute path starting with '/'. Restart the panel to apply changes."
"privateKeyPathDesc" = "Fill in an absolute path starting with."
"panelUrlPath" = "Panel URL Root Path"
"panelUrlPathDesc" = "Must start with '/' and end with '/'. Restart the panel to apply changes."
"panelUrlPathDesc" = "Must start with '/' and end with."
"oldUsername" = "Current Username"
"currentPassword" = "Current Password"
"newUsername" = "New Username"
"newPassword" = "New Password"
"telegramBotEnable" = "Enable Telegram bot"
"telegramBotEnableDesc" = "Restart the panel to take effect."
"telegramBotEnableDesc" = "Connect to the features of this panel through the Telegram bot"
"telegramToken" = "Telegram Token"
"telegramTokenDesc" = "Restart the panel to take effect."
"telegramTokenDesc" = "You must get the token from the manager of Telegram bots @botfather"
"telegramChatId" = "Telegram Admin Chat IDs"
"telegramChatIdDesc" = "Multiple Chat IDs separated by comma. use @userinfobot to get your Chat IDs. Restart the panel to apply changes."
"telegramChatIdDesc" = "Multiple Chat IDs separated by comma. use @userinfobot or use '/id' command in bot to get your Chat IDs."
"telegramNotifyTime" = "Telegram bot notification time"
"telegramNotifyTimeDesc" = "Use Crontab timing format. Restart the panel to apply changes."
"telegramNotifyTimeDesc" = "Use Crontab timing format."
"tgNotifyBackup" = "Database Backup"
"tgNotifyBackupDesc" = "Include database backup file with report notification. Restart the panel to apply changes."
"tgNotifyBackupDesc" = "Include database backup file with report notification."
"tgNotifyLogin" = "Login Notification"
"tgNotifyLoginDesc" = "Displays the username, IP address, and time when someone tries to log into your panel."
"sessionMaxAge" = "Session maximum age"
"sessionMaxAgeDesc" = "The duration of a login session (unit: minute)"
"expireTimeDiff" = "Expiration threshold for notification"
@@ -252,7 +255,28 @@
"tgNotifyCpu" = "CPU percentage alert threshold"
"tgNotifyCpuDesc" = "Receive notification if CPU usage exceeds this threshold (unit: %)"
"timeZone" = "Time zone"
"timeZoneDesc" = "Scheduled tasks run according to the time in this time zone. Restart the panel to apply changes."
"timeZoneDesc" = "Scheduled tasks run according to the time in this time zone."
"subSettings" = "Subscription"
"subEnable" = "Enable service"
"subEnableDesc" = "Subscription feature with separate configuration"
"subListen" = "Listening IP"
"subListenDesc" = "Leave blank by default to monitor all IPs"
"subPort" = "Subscription Port"
"subPortDesc" = "Port number for serving the subscription service must be unused in server"
"subCertPath" = "Subscription Certificate Public Key File Path"
"subCertPathDesc" = "Fill in an absolute path starting with '/'"
"subKeyPath" = "Subscription Certificate Private Key File Path"
"subKeyPathDesc" = "Fill in an absolute path starting with '/'"
"subPath" = "Subscription URL Root Path"
"subPathDesc" = "Must start with '/' and end with '/'"
"subDomain" = "Listening Domain"
"subDomainDesc" = "Leave blank by default to monitor all domains and IPs"
"subUpdates" = "Subscription update intervals"
"subUpdatesDesc" = "Interval hours between updates in client application"
"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"
@@ -260,57 +284,85 @@
"advancedTemplate" = "Advanced Template"
"completeTemplate" = "Complete Template"
"generalConfigs" = "General Configs"
"generalConfigsDesc" = "These options will prevent users from connecting to specific protocols and websites."
"countryConfigs" = "Country Configs"
"countryConfigsDesc" = "These options will prevent users from connecting to specific country domains."
"generalConfigsDesc" = "These options will provide general adjustments."
"blockConfigs" = "Blocking Configs"
"blockConfigsDesc" = "These options will prevent users from connecting to specific protocols and websites."
"blockCountryConfigs" = "Block Country Configs"
"blockCountryConfigsDesc" = "These options will prevent users from connecting to specific country domains."
"directCountryConfigs" = "Direct Country Configs"
"directCountryConfigsDesc" = "These options will connect users directly to specific country domains."
"ipv4Configs" = "IPv4 Configs"
"ipv4ConfigsDesc" = "These options will route to target domains only via IPv4."
"warpConfigs" = "WARP Configs"
"warpConfigsDesc" = "Caution: Before using these options, install WARP in socks5 proxy mode on your server by following the steps on the panel's GitHub. WARP will route traffic to websites through Cloudflare servers."
"xrayConfigTemplate" = "Xray Configuration Template"
"xrayConfigTemplateDesc" = "Generate the final Xray configuration file based on this template. Restart the panel to apply changes."
"xrayConfigTemplateDesc" = "Generate the final Xray configuration file based on this template."
"xrayConfigFreedomStrategy" = "Configure Strategy for Freedom Protocol"
"xrayConfigFreedomStrategyDesc" = "Set the output strategy of the network in the Freedom Protocol."
"xrayConfigRoutingStrategy" = "Configure Domains Routing Strategy"
"xrayConfigRoutingStrategyDesc" = "Set the overall routing strategy for DNS resolving."
"xrayConfigTorrent" = "Ban BitTorrent Usage"
"xrayConfigTorrentDesc" = "Change the configuration template to avoid using BitTorrent by users. Restart the panel to apply changes."
"xrayConfigTorrentDesc" = "Change the configuration template to avoid using BitTorrent by users."
"xrayConfigPrivateIp" = "Ban Private IP Ranges to Connect"
"xrayConfigPrivateIpDesc" = "Change the configuration template to avoid connecting with private IP ranges. Restart the panel to apply changes."
"xrayConfigPrivateIpDesc" = "Change the configuration template to avoid connecting to private IP ranges."
"xrayConfigAds" = "Block Ads"
"xrayConfigAdsDesc" = "Change the configuration template to block ads. Restart the panel to apply changes."
"xrayConfigPorn" = "Block Porn Websites"
"xrayConfigPornDesc" = "Change the configuration template to avoid connecting to porn websites. Restart the panel to apply changes."
"xrayConfigAdsDesc" = "Change the configuration template to block ads."
"xrayConfigFamily" = "Block Malware and Adult Content"
"xrayConfigFamilyDesc" = "DNS resolvers to block malware and adult content for family protection."
"xrayConfigSpeedtest" = "Block Speedtest Websites"
"xrayConfigSpeedtestDesc" = "Change the configuration template to avoid connecting to speedtest websites. Restart the panel to apply changes."
"xrayConfigSpeedtestDesc" = "Change the configuration template to avoid connecting to speedtest websites."
"xrayConfigIRIp" = "Disable connection to Iran IP ranges"
"xrayConfigIRIpDesc" = "Change the configuration template to avoid connecting with Iran IP ranges. Restart the panel to apply changes."
"xrayConfigIRIpDesc" = "Change the configuration template to avoid connecting to Iran IP ranges."
"xrayConfigIRDomain" = "Disable connection to Iran domains"
"xrayConfigIRDomainDesc" = "Change the configuration template to avoid connecting with Iran domains. Restart the panel to apply changes."
"xrayConfigIRDomainDesc" = "Change the configuration template to avoid connecting to Iran domains."
"xrayConfigChinaIp" = "Disable connection to China IP ranges"
"xrayConfigChinaIpDesc" = "Change the configuration template to avoid connecting with China IP ranges. Restart the panel to apply changes."
"xrayConfigChinaIpDesc" = "Change the configuration template to avoid connecting to China IP ranges."
"xrayConfigChinaDomain" = "Disable connection to China domains"
"xrayConfigChinaDomainDesc" = "Change the configuration template to avoid connecting with China domains. Restart the panel to apply changes."
"xrayConfigChinaDomainDesc" = "Change the configuration template to avoid connecting to China domains."
"xrayConfigRussiaIp" = "Disable connection to Russia IP ranges"
"xrayConfigRussiaIpDesc" = "Change the configuration template to avoid connecting with Russia IP ranges. Restart the panel to apply changes."
"xrayConfigRussiaIpDesc" = "Change the configuration template to avoid connecting to Russia IP ranges."
"xrayConfigRussiaDomain" = "Disable connection to Russia domains"
"xrayConfigRussiaDomainDesc" = "Change the configuration template to avoid connecting with Russia domains. Restart the panel to apply changes."
"xrayConfigRussiaDomainDesc" = "Change the configuration template to avoid connecting to Russia domains."
"xrayConfigDirectIRIp" = "Direct connection to Iran IP ranges"
"xrayConfigDirectIRIpDesc" = "Change the configuration template for direct connecting to Iran IP ranges."
"xrayConfigDirectIRDomain" = "Direct connection to Iran domains"
"xrayConfigDirectIRDomainDesc" = "Change the configuration template for direct connecting to Iran domains."
"xrayConfigDirectChinaIp" = "Direct connection to China IP ranges"
"xrayConfigDirectChinaIpDesc" = "Change the configuration template for direct connecting to China IP ranges."
"xrayConfigDirectChinaDomain" = "Direct connection to China domains"
"xrayConfigDirectChinaDomainDesc" = "Change the configuration template for direct connecting to China domains."
"xrayConfigDirectRussiaIp" = "Direct connection to Russia IP ranges"
"xrayConfigDirectRussiaIpDesc" = "Change the configuration template for direct connecting to Russia IP ranges."
"xrayConfigDirectRussiaDomain" = "Direct connection to Russia domains"
"xrayConfigDirectRussiaDomainDesc" = "Change the configuration template for direct connecting to Russia domains."
"xrayConfigGoogleIPv4" = "Use IPv4 for Google"
"xrayConfigGoogleIPv4Desc" = "Add routing for Google to connect with IPv4. Restart the panel to apply changes."
"xrayConfigGoogleIPv4Desc" = "Add routing for Google to connect with IPv4."
"xrayConfigNetflixIPv4" = "Use IPv4 for Netflix"
"xrayConfigNetflixIPv4Desc" = "Add routing for Netflix to connect with IPv4. Restart the panel to apply changes."
"xrayConfigNetflixIPv4Desc" = "Add routing for Netflix to connect with IPv4."
"xrayConfigGoogleWARP" = "Route Google through WARP."
"xrayConfigGoogleWARPDesc" = "Add routing for Google via WARP. Restart the panel to apply changes."
"xrayConfigGoogleWARPDesc" = "Add routing for Google via WARP."
"xrayConfigOpenAIWARP" = "Route OpenAI (ChatGPT) through WARP."
"xrayConfigOpenAIWARPDesc" = "Add routing for OpenAI (ChatGPT) via WARP. Restart the panel to apply changes."
"xrayConfigOpenAIWARPDesc" = "Add routing for OpenAI (ChatGPT) via WARP."
"xrayConfigNetflixWARP" = "Route Netflix through WARP."
"xrayConfigNetflixWARPDesc" = "Add routing for Netflix via WARP. Restart the panel to apply changes."
"xrayConfigNetflixWARPDesc" = "Add routing for Netflix via WARP."
"xrayConfigSpotifyWARP" = "Route Spotify through WARP."
"xrayConfigSpotifyWARPDesc" = "Add routing for Spotify via WARP. Restart the panel to apply changes."
"xrayConfigSpotifyWARPDesc" = "Add routing for Spotify via WARP."
"xrayConfigIRWARP" = "Route Iran domains through WARP."
"xrayConfigIRWARPDesc" = "Add routing for Iran domains via WARP. Restart the panel to apply changes."
"xrayConfigIRWARPDesc" = "Add routing for Iran domains via WARP."
"xrayConfigInbounds" = "Configuration of Inbounds"
"xrayConfigInboundsDesc" = "Change the configuration template to accept specific clients. Restart the panel to apply changes."
"xrayConfigInboundsDesc" = "Change the configuration template to accept specific clients."
"xrayConfigOutbounds" = "Configuration of Outbounds"
"xrayConfigOutboundsDesc" = "Change the configuration template to define outgoing ways for this server. Restart the panel to apply changes."
"xrayConfigOutboundsDesc" = "Change the configuration template to define outgoing ways for this server."
"xrayConfigRoutings" = "Configuration of routing rules."
"xrayConfigRoutingsDesc" = "Change the configuration template to define routing rules for this server. Restart the panel to apply changes."
"xrayConfigRoutingsDesc" = "Change the configuration template to define routing rules for this server."
"manualLists" = "Manual Lists"
"manualListsDesc" = "Please use the JSON array format."
"manualBlockedIPs" = "List of Blocked IPs"
"manualBlockedDomains" = "List of Blocked Domains"
"manualDirectIPs" = "List of Direct IPs"
"manualDirectDomains" = "List of Direct Domains"
"manualIPv4Domains" = "List of IPv4 Domains"
"manualWARPDomains" = "List of WARP Domains"
[pages.settings.security]
"admin" = "Admin"
@@ -326,3 +378,118 @@
"modifyUser" = "Modify User "
"originalUserPassIncorrect" = "Incorrect original username or password"
"userPassMustBeNotEmpty" = "New username and new password cannot be empty"
[tgbot]
"keyboardClosed" = "❌ Custom keyboard closed!"
"noResult" = "❗ No result!"
"noQuery" = "❌ Query not found! Please use the command again!"
"wentWrong" = "❌ Something went wrong!"
"noIpRecord" = "❗ No IP Record!"
"noInbounds" = "❗ No inbound found!"
"unlimited" = "♾ Unlimited"
"month" = "Month"
"months" = "Months"
"day" = "Day"
"days" = "Days"
"hours" = "Hours"
"unknown" = "Unknown"
"inbounds" = "Inbounds"
"clients" = "Clients"
[tgbot.commands]
"unknown" = "❗ Unknown command"
"pleaseChoose" = "👇 Please choose:\r\n"
"help" = "🤖 Welcome to this bot! It's designed to offer you specific data from the server, and it allows you to make modifications as needed.\r\n\r\n"
"start" = "👋 Hello <i>{{ .Firstname }}</i>.\r\n"
"welcome" = "🤖 Welcome to <b>{{ .Hostname }}</b> management bot.\r\n"
"status" = "✅ Bot is ok!"
"usage" = "❗ Please provide a text to search!"
"getID" = "🆔 Your ID: <code>{{ .ID }}</code>"
"helpAdminCommands" = "Search for a client email:\r\n<code>/usage [Email]</code>\r\n \r\nSearch for inbounds (with client stats):\r\n<code>/inbound [Remark]</code>"
"helpClientCommands" = "To search for statistics, just use folowing command:\r\n \r\n<code>/usage [UUID|Password]</code>\r\n \r\nUse UUID for vmess/vless and Password for Trojan."
[tgbot.messages]
"cpuThreshold" = "🔴 The CPU usage {{ .Percent }}% is more than threshold {{ .Threshold }}%"
"selectUserFailed" = "❌ Error in user selection!"
"userSaved" = "✅ Telegram User saved."
"loginSuccess" = "✅ Successfully logged-in to the panel.\r\n"
"loginFailed" = "❗️ Login to the panel failed.\r\n"
"report" = "🕰 Scheduled Reports: {{ .RunTime }}\r\n"
"datetime" = "⏰ Date-Time: {{ .DateTime }}\r\n"
"hostname" = "💻 Hostname: {{ .Hostname }}\r\n"
"version" = "🚀 X-UI Version: {{ .Version }}\r\n"
"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n"
"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n"
"ip" = "🌐 IP: {{ .IP }}\r\n"
"ips" = "🔢 IPs: \r\n{{ .IPs }}\r\n"
"serverUpTime" = "⏳ Server Uptime: {{ .UpTime }} {{ .Unit }}\r\n"
"serverLoad" = "📈 Server Load: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
"serverMemory" = "📋 Server Memory: {{ .Current }}/{{ .Total }}\r\n"
"tcpCount" = "🔹 TcpCount: {{ .Count }}\r\n"
"udpCount" = "🔸 UdpCount: {{ .Count }}\r\n"
"traffic" = "🚦 Traffic: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
"xrayStatus" = " Xray Status: {{ .State }}\r\n"
"username" = "👤 Username: {{ .Username }}\r\n"
"time" = "⏰ Time: {{ .Time }}\r\n"
"inbound" = "📍 Inbound: {{ .Remark }}\r\n"
"port" = "🔌 Port: {{ .Port }}\r\n"
"expire" = "📅 Expire Date: {{ .Time }}\r\n"
"expireIn" = "📅 Expire In: {{ .Time }}\r\n"
"active" = "💡 Active: ✅ Yes\r\n"
"inactive" = "💡 Active: ❌ No\r\n"
"email" = "📧 Email: {{ .Email }}\r\n"
"upload" = "🔼 Upload: ↑{{ .Upload }}\r\n"
"download" = "🔽 Download: ↓{{ .Download }}\r\n"
"total" = "📊 Total: ↑↓{{ .UpDown }} / {{ .Total }}\r\n"
"TGUser" = "👤 Telegram User: {{ .TelegramID }}\r\n"
"exhaustedMsg" = "🚨 Exhausted {{ .Type }}:\r\n"
"exhaustedCount" = "🚨 Exhausted {{ .Type }} count:\r\n"
"disabled" = "🛑 Disabled: {{ .Disabled }}\r\n"
"depleteSoon" = "🔜 Deplete soon: {{ .Deplete }}\r\n \r\n"
"backupTime" = "🗄 Backup Time: {{ .Time }}\r\n"
"refreshedOn" = "\r\n📋🔄 Refreshed On: {{ .Time }}\r\n \r\n"
[tgbot.buttons]
"closeKeyboard" = "❌ Close Keyboard"
"cancel" = "❌ Cancel"
"cancelReset" = "❌ Cancel Reset"
"cancelIpLimit" = "❌ Cancel IP Limit"
"confirmResetTraffic" = "✅ Confirm Reset Traffic?"
"confirmClearIps" = "✅ Confirm Clear IPs?"
"confirmRemoveTGUser" = "✅ Confirm Remove Telegram User?"
"dbBackup" = "Get DB Backup"
"serverUsage" = "Server Usage"
"getInbounds" = "Get Inbounds"
"depleteSoon" = "Deplete soon"
"clientUsage" = "Get Usage"
"commands" = "Commands"
"refresh" = "🔄 Refresh"
"clearIPs" = "❌ Clear IPs"
"removeTGUser" = "❌ Remove Telegram User"
"selectTGUser" = "👤 Select Telegram User"
"selectOneTGUser" = "👤 Select a telegram user:"
"resetTraffic" = "📈 Reset Traffic"
"resetExpire" = "📅 Reset Expire Days"
"ipLog" = "🔢 IP Log"
"ipLimit" = "🔢 IP Limit"
"setTGUser" = "👤 Set Telegram User"
"toggle" = "🔘 Enable / Disable"
[tgbot.answers]
"errorOperation" = "❗ Error in Operation."
"getInboundsFailed" = "❌ Failed to get inbounds"
"canceled" = "❌ {{ .Email }} : Operation canceled."
"clientRefreshSuccess" = "✅ {{ .Email }} : Client refreshed successfully."
"IpRefreshSuccess" = "✅ {{ .Email }} : IPs refreshed successfully."
"TGIdRefreshSuccess" = "✅ {{ .Email }} : Client's Telegram User refreshed successfully."
"resetTrafficSuccess" = "✅ {{ .Email }} : Traffic reset successfully."
"expireResetSuccess" = "✅ {{ .Email }} : Expire days reset successfully."
"resetIpSuccess" = "✅ {{ .Email }} : IP limit {{ .Count }} saved successfully."
"clearIpSuccess" = "✅ {{ .Email }} : IPs cleared successfully."
"getIpLog" = "✅ {{ .Email }} : Get IP Log."
"getUserInfo" = "✅ {{ .Email }} : Get Telegram User Info."
"removedTGUserSuccess" = "✅ {{ .Email }} : Telegram User removed successfully."
"enableSuccess" = "✅ {{ .Email }} : Enabled successfully."
"disableSuccess" = "✅ {{ .Email }} : Disabled successfully."
"askToAddUserId" = "Your configuration is not found!\r\nPlease ask your Admin to use your telegram user id in your configuration(s).\r\n\r\nYour user id: <b>{{ .TgUserID }}</b>"
"askToAddUserName" = "Your configuration is not found!\r\nPlease ask your Admin to use your telegram username or user id in your configuration(s).\r\n\r\nYour username: <b>@{{ .TgUserName }}</b>\r\n\r\nYour user id: <b>{{ .TgUserID }}</b>"

View File

@@ -11,7 +11,7 @@
"enable" = "فعال"
"protocol" = "پروتکل"
"search" = "جستجو"
"filter" = "فیلتر"
"loading" = "در حال بروزرسانی..."
"second" = "ثانیه"
"minute" = "دقیقه"
@@ -39,7 +39,6 @@
"depleted" = "منقضی"
"depletingSoon" = "در حال انقضا"
"domainName" = "آدرس دامنه"
"additional" = "آی دی جایگزین"
"monitor" = "آی پی اتصال"
"certificate" = "گواهی دیجیتال"
"fail" = "خطا"
@@ -49,6 +48,7 @@
"clients" = "کاربران"
"usage" = "استفاده"
"secretToken" = "توکن امنیتی"
"remained" = "باقیمانده"
[menu]
"dashboard" = "وضعیت سیستم"
@@ -72,17 +72,18 @@
"title" = "وضعیت سیستم"
"memory" = "حافظه رم"
"hard" = "حافظه دیسک"
"xrayStatus" = "وضعیت Xray"
"xrayStatus" = "وضعیت"
"stopXray" = "توقف"
"restartXray" = "شروع مجدد"
"xraySwitch" = "تغییر ورژن"
"xraySwitchClick" = "ورژن مورد نظر را انتخاب کنید"
"xraySwitchClickDesk" = "لطفا با دقت انتخاب کنید ، در صورت انتخاب اشتباه امکان قطعی سیستم وجود دارد ."
"operationHours" = "مدت فعالیت"
"operationHoursDesc" = "مدت فعالیت سیستم بعد از روشن شدن"
"systemLoad" = "بار روی سیستم"
"xraySwitchClickDesk" = "لطفا با دقت انتخاب کنید ، در صورت انتخاب اشتباه امکان قطعی سیستم وجود دارد "
"operationHours" = "آپ تایم سیستم"
"systemLoad" = "بار سیستم"
"systemLoadDesc" = "میانگین بار سیستم برای 1، 5 و 15 دقیقه گذشته"
"connectionTcpCountDesc" = "مجموع اتصالات TCP در تمام کارت های شبکه"
"connectionUdpCountDesc" = "مجموع اتصالات UDP در تمام کارت های شبکه"
"connectionCount" = "تعداد کانکشن ها"
"connectionCountDesc" = "تعداد کانکشن ها برای کل شبکه"
"upSpeed" = "سرعت آپلود در حال حاضر سیستم"
"downSpeed" = "سرعت دانلود در حال حاضر سیستم"
"totalSent" = "جمع کل ترافیک آپلود مصرفی"
@@ -94,7 +95,7 @@
"config" = "تنظیمات"
"backup" = "پشتیبان گیری و بازیابی"
"backupTitle" = "پشتیبان گیری و بازیابی دیتابیس"
"backupDescription" = "به یاد داشته باشید که قبل از وارد کردن یک دیتابیس جدید، نسخه پشتیبان تهیه کنید."
"backupDescription" = "به یاد داشته باشید که قبل از وارد کردن یک دیتابیس جدید، نسخه پشتیبان تهیه کنید"
"exportDatabase" = "دانلود دیتابیس"
"importDatabase" = "آپلود دیتابیس"
@@ -126,7 +127,6 @@
"network" = "شبکه"
"destinationPort" = "پورت مقصد"
"targetAddress" = "آدرس مقصد"
"disableInsecureEncryption" = "رمزگذاری ناامن را غیرفعال کنید"
"monitorDesc" = "به طور پیش فرض خالی بگذارید"
"meansNoLimit" = "یعنی بدون محدودیت"
"totalFlow" = "کل ترافیک"
@@ -167,7 +167,7 @@
"setDefaultCert" = "استفاده از گواهی پنل"
"xtlsDesc" = "هسته Xray باید 1.7.5 باشد"
"realityDesc" = "هسته Xray باید 1.8.0 و بالاتر باشد"
"telegramDesc" = "از آیدی تلگرام بدون @ یا آیدی چت استفاده کنید (می توانید آن را از اینجا دریافت کنید @userinfobot)"
"telegramDesc" = "از آیدی تلگرام بدون @ یا آیدی چت استفاده کنید (می توانید آن را از اینجا دریافت کنید @userinfobot یا در ربات دستور '/id' را وارد کنید)"
"subscriptionDesc" = "می توانید ساب لینک خود را در جزئیات پیدا کنید، همچنین می توانید از همین نام برای چندین کانفیگ استفاده کنید"
[pages.client]
@@ -209,6 +209,7 @@
[pages.settings]
"title" = "تنظیمات"
"save" = "ذخیره"
"infoDesc" = "برای اعمال تغییرات در این بخش باید پس از ذخیره کردن، پنل را ریستارت کنید"
"restartPanel" = "ریستارت پنل"
"restartPanelDesc" = "آیا مطمئن هستید که می خواهید پنل را دوباره راه اندازی کنید؟ برای راه اندازی مجدد روی OK کلیک کنید. اگر بعد از 3 ثانیه نمی توانید به پنل دسترسی پیدا کنید، لطفاً برای مشاهده اطلاعات گزارش پانل به سرور برگردید"
"actions" = "عملیات ها"
@@ -218,29 +219,33 @@
"xrayConfiguration" = "تنظیمات Xray"
"TGBotSettings" = "تنظیمات ربات تلگرام"
"panelListeningIP" = "محدودیت آی پی پنل"
"panelListeningIPDesc" = "برای استفاده از تمام IP ها به طور پیش فرض خالی بگذارید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"panelListeningIPDesc" = "برای استفاده از تمام آی‌پیها به طور پیش فرض خالی بگذارید"
"panelListeningDomain" = "محدودیت دامین پنل"
"panelListeningDomainDesc" = "برای استفاده از تمام دامنه‌ها و آی‌پی‌ها به طور پیش فرض خالی بگذارید"
"panelPort" = "پورت پنل"
"panelPortDesc" = نل را مجدداً راه اندازی کنید تا اعمال شود"
"panelPortDesc" = ورت مورد استفاده برای نمایش این پنل"
"publicKeyPath" = "مسیر فایل گواهی کلید عمومی پنل"
"publicKeyPathDesc" = "باید یک مسیر مطلق باشد که با / شروع می شود . پنل را مجدداً راه اندازی کنید تا اعمال شود"
"publicKeyPathDesc" = "باید یک مسیر مطلق باشد که با / شروع می شود "
"privateKeyPath" = "مسیر فایل گواهی کلید خصوصی پنل"
"privateKeyPathDesc" = "باید یک مسیر مطلق باشد که با / شروع می شود . پنل را مجدداً راه اندازی کنید تا اعمال شود"
"privateKeyPathDesc" = "باید یک مسیر مطلق باشد که با / شروع می شود "
"panelUrlPath" = "آدرس روت پنل"
"panelUrlPathDesc" = "باید با '/' شروع شود و با '/' تمام شود. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"panelUrlPathDesc" = "باید با '/' شروع شود و با '/' تمام شود"
"oldUsername" = "نام کاربری فعلی"
"currentPassword" = "رمز عبور فعلی"
"newUsername" = "نام کاربری جدید"
"newPassword" = "رمز عبور جدید"
"telegramBotEnable" = "فعالسازی ربات تلگرام"
"telegramBotEnableDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
"telegramBotEnableDesc" = "از طریق بات تلگرام به امکانات این پنل متصل شوید"
"telegramToken" = "توکن تلگرام"
"telegramTokenDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
"telegramTokenDesc" = "توکن را باید از مدیر بات های تلگرام دریافت کنید @botfather"
"telegramChatId" = "آی دی تلگرام مدیریت"
"telegramChatIdDesc" = "از @userinfobot برای دریافت شناسه های چت خود استفاده کنید. با استفاده از کاما میتونید چند آی دی را از هم جدا کنید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"telegramChatIdDesc" = "از @userinfobot یا دستور '/id' در ربات برای دریافت شناسه های چت خود استفاده کنید. با استفاده از کاما میتونید چند آی دی را از هم جدا کنید. "
"telegramNotifyTime" = "مدت زمان نوتیفیکیشن ربات تلگرام"
"telegramNotifyTimeDesc" = "از فرمت زمان بندی لینوکس استفاده کنید . پنل را مجدداً راه اندازی کنید تا اعمال شود"
"telegramNotifyTimeDesc" = "از فرمت زمان بندی لینوکس استفاده کنید "
"tgNotifyBackup" = "پشتیبان گیری از پایگاه داده"
"tgNotifyBackupDesc" = "ارسال کپی فایل پایگاه داده به همراه گزارش دوره ای"
"tgNotifyLogin" = "اعلان ورود"
"tgNotifyLoginDesc" = "نام کاربری، آدرس ای پی، و زمان وقتی که فردی سعی می‌کند به پنل شما وارد شود نمایش میدهد"
"sessionMaxAge" = "بیشینه زمان جلسه وب"
"sessionMaxAgeDesc" = "بیشینه زمانی که میتوانید لاگین بمانید (واحد: دقیقه)"
"expireTimeDiff" = "آستانه زمان باقی مانده"
@@ -251,6 +256,27 @@
"tgNotifyCpuDesc" = "این ربات تلگرام در صورت استفاده پردازنده بیشتر از این درصد برای شما پیام ارسال می کند.(واحد: درصد)"
"timeZone" = "منظقه زمانی"
"timeZoneDesc" = "وظایف برنامه ریزی شده بر اساس این منطقه زمانی اجرا می شوند. پنل را مجدداً راه اندازی می کند تا اعمال شود"
"subSettings" = "سابسکریپشن"
"subEnable" = "فعال کردن سرویس"
"subEnableDesc" = "ویژگی سابسکریپشن با پیکربندی جداگانه"
"subListen" = "محدودیت آی‌پی"
"subListenDesc" = "برای استفاده از همه آی‌پی ها به طور پیش فرض خالی بگذارید"
"subPort" = "پورت سرویس"
"subPortDesc" = "شماره پورت برای ارائه خدمات سابسکریپشن باید خالی باشد"
"subCertPath" = "مسیر فایل کلید عمومی گواهی سابسکریپشن"
"subCertPathDesc" = "یک مسیر مطلق که با '/' شروع می شود را پر کنید."
"subKeyPath" = "مسیر فایل کلید خصوصی گواهی سابسکریپشن"
"subKeyPathDesc" = "یک مسیر مطلق که با '/' شروع می شود را پر کنید."
"subPath" = "مسیر ریشه سابسکریپشن"
"subPathDesc" = "باید با '/' شروع شود و با '/' ختم شود."
"subDomain" = "دامنه مخصوص سابسکریپشن"
"subDomainDesc" = "برای نظارت بر همه دامنه ها و آی‌پی ها به طور پیش فرض خالی بگذارید"
"subUpdates" = "فاصله به روز رسانی های سابسکریپشن"
"subUpdatesDesc" = "ساعت های فاصله بین به روز رسانی در برنامه کاربر"
"subEncrypt" = "رمزگذاری کانفیگ ها"
"subEncryptDesc" = "رمزگذاری کانفیگ های بازگشتی سابسکریپشن"
"subShowInfo" = "نمایش اطلاعات مصرف"
"subShowInfoDesc" = "ترافیک و زمان باقیمانده را در هر کانفیگ نمایش میدهد"
[pages.settings.templates]
"title" = "الگوها"
@@ -258,57 +284,85 @@
"advancedTemplate" = "بخش الگو پیشرفته"
"completeTemplate" = "بخش الگو کامل"
"generalConfigs" = "تنظیمات عمومی"
"generalConfigsDesc" = "این گزینه ها از اتصال کاربران به پروتکل ها و وب سایت های خاص جلوگیری می کند."
"countryConfigs" = "تنظیمات برای کشورها"
"countryConfigsDesc" = "این گزینه از اتصال کاربران به دامنه های کشوری خاص جلوگیری می کند."
"generalConfigsDesc" = "این تنظیمات میتواند ترافیک کلی سرویس را متاثر کند"
"blockConfigs" = "مسدود سازی"
"blockConfigsDesc" = "این گزینه ها از اتصال کاربران به پروتکل ها و وب سایت های خاص جلوگیری می کند"
"blockCountryConfigs" = "تنظیمات برای مسدودسازی کشورها"
"blockCountryConfigsDesc" = "این گزینه اتصال کاربران به دامنه های کشوری خاص را مسدود می کند"
"directCountryConfigs" = "تنظیمات برای اتصال مستقیم کشورها"
"directCountryConfigsDesc" = "این گزینه کاربران را به دامنه های کشوری خاص را به طور مستقیم، متصل می کند"
"ipv4Configs" = "تنظیمات برای IPv4"
"ipv4ConfigsDesc" = "این گزینه فقط از طریق آیپی ورژن 4 به دامنه های هدف هدایت می شود."
"ipv4ConfigsDesc" = "این گزینه فقط از طریق آیپی ورژن ۴ به دامنه های هدف هدایت می شود"
"warpConfigs" = "تنظیمات برای WARP"
"warpConfigsDesc" = "هشدار: قبل از استفاده از این گزینه، WARP را در حالت پراکسی socks5 با دنبال کردن مراحل در GitHub پنل روی سرور خود نصب کنید. WARP ترافیک را از طریق سرورهای Cloudflare به وب سایت ها هدایت می کند."
"warpConfigsDesc" = "هشدار: قبل از استفاده از این گزینه، WARP را در حالت پراکسی socks5 با دنبال کردن مراحل در GitHub پنل روی سرور خود نصب کنید. WARP ترافیک را از طریق سرورهای Cloudflare به وب سایت ها هدایت می کند"
"xrayConfigTemplate" = "تنظیمات الگو ایکس ری"
"xrayConfigTemplateDesc" = "فایل پیکربندی ایکس ری نهایی بر اساس این الگو ایجاد میشود. لطفاً این را تغییر ندهید مگر اینکه دقیقاً بدانید که چه کاری انجام می دهید! پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigTemplateDesc" = "فایل پیکربندی ایکس ری نهایی بر اساس این الگو ایجاد میشود. لطفاً این را تغییر ندهید مگر اینکه دقیقاً بدانید که چه کاری انجام می دهید!"
"xrayConfigFreedomStrategy" = "روش استفاده از شبکه خروجی مستقیم"
"xrayConfigFreedomStrategyDesc" = "تعیین روش استفاده از خروجی برای پرتکل مستقیم"
"xrayConfigRoutingStrategy" = "پیکربندی استراتژی حل دامنه در مسیریابی"
"xrayConfigRoutingStrategyDesc" = "تعیین استراتژی مسیریابی کلی برای پیدا کردن دامنه"
"xrayConfigTorrent" = "فیلتر کردن بیت تورنت"
"xrayConfigTorrentDesc" = "الگوی تنظیمات را برای فیلتر کردن پروتکل بیت تورنت برای کاربران تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigTorrentDesc" = "الگوی تنظیمات را برای فیلتر کردن پروتکل بیت تورنت برای کاربران تغییر میدهد"
"xrayConfigPrivateIp" = "جلوگیری از اتصال آیپی های خصوصی یا محلی"
"xrayConfigPrivateIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های خصوصی یا محلی و بسته های سرگردان تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigPrivateIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های خصوصی یا محلی و بسته های سرگردان تغییر میدهد"
"xrayConfigAds" = "مسدود کردن تبلیغات"
"xrayConfigAdsDesc" = "الگوی تنظیمات را برای مسدود کردن تبلیغات تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigPorn" = "جلوگیری از اتصال به سایت های پورن"
"xrayConfigPornDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال به سایت های پورن تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigAdsDesc" = "الگوی تنظیمات را برای مسدود کردن تبلیغات تغییر میدهد"
"xrayConfigFamily" = "فعال کردن حالت خانواده"
"xrayConfigFamilyDesc" = "برای جلوگیری از ارتباط با وبسایت های ناامن"
"xrayConfigSpeedtest" = "جلوگیری از اتصال به سایت های تست سرعت"
"xrayConfigSpeedtestDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال به سایت های تست سرعت تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigSpeedtestDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال به سایت های تست سرعت تغییر میدهد"
"xrayConfigIRIp" = "جلوگیری از اتصال آیپی های ایران"
"xrayConfigIRIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های ایران تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigIRIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های ایران تغییر میدهد"
"xrayConfigIRDomain" = "جلوگیری از اتصال دامنه های ایران"
"xrayConfigIRDomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های ایران تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigIRDomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های ایران تغییر میدهد"
"xrayConfigChinaIp" = "جلوگیری از اتصال آیپی های چین"
"xrayConfigChinaIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های چین تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigChinaIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های چین تغییر میدهد"
"xrayConfigChinaDomain" = "جلوگیری از اتصال دامنه های چین"
"xrayConfigChinaDomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های چین تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigChinaDomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های چین تغییر میدهد"
"xrayConfigRussiaIp" = "جلوگیری از اتصال آیپی های روسیه"
"xrayConfigRussiaIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های روسیه تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigRussiaIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های روسیه تغییر میدهد"
"xrayConfigRussiaDomain" = "جلوگیری از اتصال دامنه های روسیه"
"xrayConfigRussiaDomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های روسیه تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigRussiaDomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های روسیه تغییر میدهد"
"xrayConfigDirectIRIp" = "ارتباط مستقیم به آیپی های ایران"
"xrayConfigDirectIRIpDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به آیپی های ایران تغییر میدهد"
"xrayConfigDirectIRDomain" = "ارتباط مستقیم به دامنه های ایران"
"xrayConfigDirectIRDomainDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به دامنه های ایران تغییر میدهد"
"xrayConfigDirectChinaIp" = "ارتباط مستقیم به آیپی های چین"
"xrayConfigDirectChinaIpDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به آیپی های چین تغییر میدهد"
"xrayConfigDirectChinaDomain" = "ارتباط مستقیم به دامنه های چین"
"xrayConfigDirectChinaDomainDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به دامنه های چین تغییر میدهد"
"xrayConfigDirectRussiaIp" = "ارتباط مستقیم به آیپی های روسیه"
"xrayConfigDirectRussiaIpDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به آیپی های روسیه تغییر میدهد"
"xrayConfigDirectRussiaDomain" = "ارتباط مستقیم به دامنه های روسیه"
"xrayConfigDirectRussiaDomainDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به دامنه های روسیه تغییر میدهد"
"xrayConfigGoogleIPv4" = "استفاده از آیپی ورژن 4 برای اتصال به گوگل"
"xrayConfigGoogleIPv4Desc" = "مسیردهی جدید برای اتصال به گوگل با آیپی ورژن 4 اضافه میکند. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigGoogleIPv4Desc" = "مسیردهی جدید برای اتصال به گوگل با آیپی ورژن 4 اضافه میکند"
"xrayConfigNetflixIPv4" = "استفاده از آیپی ورژن 4 برای اتصال به نتفلیکس"
"xrayConfigNetflixIPv4Desc" = "مسیردهی جدید برای اتصال به نتفلیکس با آیپی ورژن 4 اضافه میکند. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigNetflixIPv4Desc" = "مسیردهی جدید برای اتصال به نتفلیکس با آیپی ورژن 4 اضافه میکند"
"xrayConfigGoogleWARP" = "مسیردهی گوگل به WARP"
"xrayConfigGoogleWARPDesc" = "مسیردهی جدید برای اتصال به گوگل به WARP اضافه میکند. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigGoogleWARPDesc" = "مسیردهی جدید برای اتصال به گوگل به WARP اضافه میکند"
"xrayConfigOpenAIWARP" = "مسیردهی OpenAI (ChatGPT) به WARP"
"xrayConfigOpenAIWARPDesc" = "مسیردهی جدید برای اتصال به OpenAI (ChatGPT) به WARP اضافه میکند. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigOpenAIWARPDesc" = "مسیردهی جدید برای اتصال به OpenAI (ChatGPT) به WARP اضافه میکند"
"xrayConfigNetflixWARP" = "مسیردهی نتفلیکس به WARP"
"xrayConfigNetflixWARPDesc" = "مسیردهی جدید برای اتصال به نتفلیکس به WARP اضافه میکند. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigNetflixWARPDesc" = "مسیردهی جدید برای اتصال به نتفلیکس به WARP اضافه میکند"
"xrayConfigSpotifyWARP" = "مسیردهی اسپاتیفای به WARP"
"xrayConfigSpotifyWARPDesc" = "مسیردهی جدید برای اتصال به اسپاتیفای به WARP اضافه میکند. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigSpotifyWARPDesc" = "مسیردهی جدید برای اتصال به اسپاتیفای به WARP اضافه میکند"
"xrayConfigIRWARP" = "مسیردهی دامنه های ایران به WARP"
"xrayConfigIRWARPDesc" = "مسیردهی جدید برای اتصال به دامنه های ایران به WARP اضافه میکند. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigIRWARPDesc" = "مسیردهی جدید برای اتصال به دامنه های ایران به WARP اضافه میکند"
"xrayConfigInbounds" = "تنظیمات ورودی"
"xrayConfigInboundsDesc" = "میتوانید الگوی تنظیمات را برای ورودی های خاص تنظیم نمایید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigInboundsDesc" = "میتوانید الگوی تنظیمات را برای ورودی های خاص تنظیم نمایید"
"xrayConfigOutbounds" = "تنظیمات خروجی"
"xrayConfigOutboundsDesc" = "میتوانید الگوی تنظیمات را برای خروجی اینترنت تنظیم نمایید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigOutboundsDesc" = "میتوانید الگوی تنظیمات را برای خروجی اینترنت تنظیم نمایید"
"xrayConfigRoutings" = "تنظیمات قوانین مسیریابی"
"xrayConfigRoutingsDesc" = "میتوانید الگوی تنظیمات را برای مسیریابی تنظیم نمایید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigRoutingsDesc" = "میتوانید الگوی تنظیمات را برای مسیریابی تنظیم نمایید"
"manualLists" = "لیست های دستی"
"manualListsDesc" = "فرمت: JSON Array"
"manualBlockedIPs" = "لیست آی‌پی های مسدود شده"
"manualBlockedDomains" = "لیست دامنه های مسدود شده"
"manualDirectIPs" = "لیست آی‌پی های مستقیم"
"manualDirectDomains" = "لیست دامنه های مستقیم"
"manualIPv4Domains" = "لیست دامنه‌های IPv4"
"manualWARPDomains" = "لیست دامنه های WARP"
[pages.settings.security]
"admin" = "مدیر"
@@ -322,5 +376,120 @@
"modifySettings" = "ویرایش تنظیمات"
"getSettings" = "دریافت تنظیمات"
"modifyUser" = "ویرایش کاربر"
"originalUserPassIncorrect" = "نام کاربری و رمز عبور فعلی اشتباه می باشد ."
"userPassMustBeNotEmpty" = "نام کاربری و رمز عبور جدید نمیتواند خالی باشد ."
"originalUserPassIncorrect" = "نام کاربری و رمز عبور فعلی اشتباه می باشد "
"userPassMustBeNotEmpty" = "نام کاربری و رمز عبور جدید نمیتواند خالی باشد "
[tgbot]
"keyboardClosed" = "❌ کیبورد سفارشی بسته شد!"
"noResult" = "❗ نتیجه‌ای یافت نشد!"
"noQuery" = "❌ کوئری یافت نشد! لطفاً دستور را مجدداً استفاده کنید!"
"wentWrong" = "❌ مشکلی رخ داده است!"
"noIpRecord" = "❗ رکورد IP یافت نشد!"
"noInbounds" = "❗ هیچ ورودی یافت نشد!"
"unlimited" = "♾ نامحدود"
"month" = "ماه"
"months" = "ماه‌ها"
"day" = "روز"
"days" = "روزها"
"hours" = "ساعت ها"
"unknown" = "نامشخص"
"inbounds" = "ورودی‌ها"
"clients" = "کلاینت‌ها"
[tgbot.commands]
"unknown" = "❗ دستور ناشناخته"
"pleaseChoose" = "👇 لطفاً انتخاب کنید:\r\n"
"help" = "🤖 به این ربات خوش آمدید! این ربات برای ارائه داده‌های خاص از سرور طراحی شده است و به شما امکان تغییرات لازم را می‌دهد.\r\n\r\n"
"start" = "👋 سلام <i>{{ .Firstname }}</i>.\r\n"
"welcome" = "🤖 به ربات مدیریت <b>{{ .Hostname }}</b> خوش آمدید.\r\n"
"status" = "✅ ربات در حالت عادی است!"
"usage" = "❗ لطفاً یک متن برای جستجو وارد کنید!"
"getID" = "🆔 شناسه شما: <code>{{ .ID }}</code>"
"helpAdminCommands" = "برای جستجوی ایمیل مشتری:\r\n<code>/usage [ایمیل]</code>\r\n \r\nبرای جستجوی ورودی‌ها (با آمار مشتری):\r\n<code>/inbound [توضیح]</code>"
"helpClientCommands" = "برای جستجوی آمار، فقط از دستور زیر استفاده کنید:\r\n \r\n<code>/usage [UUID|رمز عبور]</code>\r\n \r\nاز UUID برای vmess/vless و از رمز عبور برای Trojan استفاده کنید."
[tgbot.messages]
"cpuThreshold" = "🔴 میزان استفاده از CPU {{ .Percent }}% بیشتر از آستانه {{ .Threshold }}% است."
"selectUserFailed" = "❌ خطا در انتخاب کاربر!"
"userSaved" = "✅ کاربر تلگرام ذخیره شد."
"loginSuccess" = "✅ با موفقیت به پنل وارد شدید.\r\n"
"loginFailed" = "❗️ ورود به پنل ناموفق بود.\r\n"
"report" = "🕰 گزارشات زمان‌بندی شده: {{ .RunTime }}\r\n"
"datetime" = "⏰ تاریخ-زمان: {{ .DateTime }}\r\n"
"hostname" = "💻 نام میزبان: {{ .Hostname }}\r\n"
"version" = "🚀 نسخه X-UI: {{ .Version }}\r\n"
"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n"
"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n"
"ip" = "🌐 آدرس IP: {{ .IP }}\r\n"
"ips" = "🔢 آدرس‌های IP: \r\n{{ .IPs }}\r\n"
"serverUpTime" = "⏳ زمان کارکرد سرور: {{ .UpTime }} {{ .Unit }}\r\n"
"serverLoad" = "📈 بار سرور: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
"serverMemory" = "📋 حافظه سرور: {{ .Current }}/{{ .Total }}\r\n"
"tcpCount" = "🔹 تعداد ترافیک TCP: {{ .Count }}\r\n"
"udpCount" = "🔸 تعداد ترافیک UDP: {{ .Count }}\r\n"
"traffic" = "🚦 ترافیک: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
"xrayStatus" = " وضعیت Xray: {{ .State }}\r\n"
"username" = "👤 نام کاربری: {{ .Username }}\r\n"
"time" = "⏰ زمان: {{ .Time }}\r\n"
"inbound" = "📍 ورودی: {{ .Remark }}\r\n"
"port" = "🔌 پورت: {{ .Port }}\r\n"
"expire" = "📅 تاریخ انقضا: {{ .Time }}\r\n"
"expireIn" = "📅 باقیمانده از انقضا: {{ .Time }}\r\n"
"active" = "💡 فعال: ✅\r\n"
"inactive" = "💡 فعال: ❌\r\n"
"email" = "📧 ایمیل: {{ .Email }}\r\n"
"upload" = "🔼 آپلود↑: {{ .Upload }}\r\n"
"download" = "🔽 دانلود↓: {{ .Download }}\r\n"
"total" = "📊 کل: {{ .UpDown }} / {{ .Total }}\r\n"
"TGUser" = "👤 کاربر تلگرام: {{ .TelegramID }}\r\n"
"exhaustedMsg" = "🚨 {{ .Type }} به اتمام رسیده است:\r\n"
"exhaustedCount" = "🚨 تعداد {{ .Type }} به اتمام رسیده:\r\n"
"disabled" = "🛑 غیرفعال: {{ .Disabled }}\r\n"
"depleteSoon" = "🔜 به زودی به پایان خواهد رسید: {{ .Deplete }}\r\n \r\n"
"backupTime" = "🗄 زمان پشتیبان‌گیری: {{ .Time }}\r\n"
"refreshedOn" = "\r\n📋🔄 تازه‌سازی شده در: {{ .Time }}\r\n \r\n"
[tgbot.buttons]
"closeKeyboard" = "❌ بستن کیبورد"
"cancel" = "❌ لغو"
"cancelReset" = "❌ لغو تنظیم مجدد"
"cancelIpLimit" = "❌ لغو محدودیت IP"
"confirmResetTraffic" = "✅ تأیید تنظیم مجدد ترافیک؟"
"confirmClearIps" = "✅ تأیید پاک‌سازی آدرس‌های IP؟"
"confirmRemoveTGUser" = "✅ تأیید حذف کاربر تلگرام؟"
"dbBackup" = "دریافت پشتیبان پایگاه داده"
"serverUsage" = "استفاده از سرور"
"getInbounds" = "دریافت ورودی‌ها"
"depleteSoon" = "به زودی به پایان خواهد رسید"
"clientUsage" = "دریافت آمار کاربر"
"commands" = "دستورات"
"refresh" = "🔄 تازه‌سازی"
"clearIPs" = "❌ پاک‌سازی آدرس‌ها"
"removeTGUser" = "❌ حذف کاربر تلگرام"
"selectTGUser" = "👤 انتخاب کاربر تلگرام"
"selectOneTGUser" = "👤 یک کاربر تلگرام را انتخاب کنید:"
"resetTraffic" = "📈 تنظیم مجدد ترافیک"
"resetExpire" = "📅 تنظیم مجدد تاریخ انقضا"
"ipLog" = "🔢 لاگ آدرس‌های IP"
"ipLimit" = "🔢 محدودیت IP"
"setTGUser" = "👤 تنظیم کاربر تلگرام"
"toggle" = "🔘 فعال / غیرفعال"
[tgbot.answers]
"errorOperation" = "❗ خطا در عملیات."
"getInboundsFailed" = "❌ دریافت ورودی‌ها با خطا مواجه شد."
"canceled" = "❌ {{ .Email }} : عملیات لغو شد."
"clientRefreshSuccess" = "✅ {{ .Email }} : کلاینت با موفقیت تازه‌سازی شد."
"IpRefreshSuccess" = "✅ {{ .Email }} : آدرس‌ها با موفقیت تازه‌سازی شدند."
"TGIdRefreshSuccess" = "✅ {{ .Email }} : کاربر تلگرام کلاینت با موفقیت تازه‌سازی شد."
"resetTrafficSuccess" = "✅ {{ .Email }} : ترافیک با موفقیت تنظیم مجدد شد."
"expireResetSuccess" = "✅ {{ .Email }} : تاریخ انقضا با موفقیت تنظیم مجدد شد."
"resetIpSuccess" = "✅ {{ .Email }} : محدودیت آدرس IP {{ .Count }} با موفقیت ذخیره شد."
"clearIpSuccess" = "✅ {{ .Email }} : آدرس‌ها با موفقیت پاک‌سازی شدند."
"getIpLog" = "✅ {{ .Email }} : دریافت لاگ آدرس‌های IP."
"getUserInfo" = "✅ {{ .Email }} : دریافت اطلاعات کاربر تلگرام."
"removedTGUserSuccess" = "✅ {{ .Email }} : کاربر تلگرام با موفقیت حذف شد."
"enableSuccess" = "✅ {{ .Email }} : با موفقیت فعال شد."
"disableSuccess" = "✅ {{ .Email }} : با موفقیت غیرفعال شد."
"askToAddUserId" = "پیکربندی شما یافت نشد!\r\nلطفاً از مدیر خود بخواهید که شناسه کاربر تلگرام خود را در پیکربندی (های) خود استفاده کند.\r\n\r\nشناسه کاربری شما: <b>{{ .TgUserID }}</b>"
"askToAddUserName" = "پیکربندی شما یافت نشد!\r\nلطفاً از مدیر خود بخواهید که نام کاربری یا شناسه کاربر تلگرام خود را در پیکربندی (های) خود استفاده کند.\r\n\r\nنام کاربری شما: <b>@{{ .TgUserName }}</b>\r\n\r\nشناسه کاربری شما: <b>{{ .TgUserID }}</b>"

View File

@@ -1,136 +1,136 @@
"username" = "имя пользователя"
"password" = "пароль"
"login" = "логин"
"confirm" = "подтвердить"
"cancel" = "отмена"
"close" = "закрыть"
"copy" = "копировать"
"copied" = "скопировано"
"download" = "скачать"
"remark" = "примечание"
"enable" = "включить"
"protocol" = "протокол"
"search" = "поиск"
"loading" = "загрузка"
"second" = "секунда"
"minute" = "минута"
"hour" = "час"
"day" = "день"
"check" = "просмотр"
"indefinite" = "бессрочно"
"unlimited" = "безлимитно"
"none" = "пусто"
"qrCode" = "QR-код"
"info" = "больше информации"
"edit" = "изменить"
"delete" = "удалить"
"reset" = "обнулить"
"copySuccess" = "скопировано"
"sure" = "да"
"username" = "Имя пользователя"
"password" = "Пароль"
"login" = "Логин"
"confirm" = "Подтвердить"
"cancel" = "Отмена"
"close" = "Закрыть"
"copy" = "Копировать"
"copied" = "Скопировано"
"download" = "Скачать"
"remark" = "Примечание"
"enable" = "Включить"
"protocol" = "Протокол"
"search" = "Поиск"
"filter" = "Фильтр"
"loading" = "Загрузка"
"second" = "Секунда"
"minute" = "Минута"
"hour" = "Час"
"day" = "День"
"check" = "Проверить"
"indefinite" = "Бессрочно"
"unlimited" = "Безлимитно"
"none" = "Пусто"
"qrCode" = "QR код"
"info" = "Информация"
"edit" = "Изменить"
"delete" = "Удалить"
"reset" = "Сбросить"
"copySuccess" = "Скопировано"
"sure" = "Да"
"encryption" = "Шифрование"
"transmission" = "протокол передачи"
"host" = "хост"
"path" = "путь"
"camouflage" = "маскировка"
"status" = "статус"
"enabled" = "включено"
"disabled" = "отключено"
"depleted" = "исчерпано"
"depletingSoon" = "почти исчерпано"
"domainName" = "домен"
"additional" = "допольнительно"
"monitor" = "порт IP"
"certificate" = "сертификат"
"fail" = "неудача"
"success" = "успешно"
"getVersion" = "узнать версию"
"install" = "установка"
"clients" = "клиенты"
"usage" = "использование"
"secretToken" = "секретный токен"
"transmission" = "Протокол передачи"
"host" = "Хост"
"path" = "Путь"
"camouflage" = "Маскировка"
"status" = "Статус"
"enabled" = "Включено"
"disabled" = "Отключено"
"depleted" = "Исчерпано"
"depletingSoon" = "Почти исчерпано"
"domainName" = "Домен"
"monitor" = "Порт IP"
"certificate" = "Сертификат"
"fail" = "Неудачно"
"success" = "Успешно"
"getVersion" = "Узнать версию"
"install" = "Установка"
"clients" = "Клиенты"
"usage" = "Использование"
"secretToken" = "Секретный токен"
"remained" = "остались"
[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" = "успешный вход"
"invalidFormData" = "Недопустимый формат данных"
"emptyUsername" = "Введите имя пользователя"
"emptyPassword" = "Введите пароль"
"wrongUsernameOrPassword" = "Неверное имя пользователя или пароль"
"successLogin" = "Успешный вход"
[pages.index]
"title" = "статус системы"
"memory" = "память"
"hard" = "жесткий диск"
"xrayStatus" = "статус Xray"
"stopXray" = "стоп"
"restartXray" = "рестарт Xray"
"xraySwitch" = "переключить версию"
"xraySwitchClick" = "Выберите желаемую версию."
"xraySwitchClickDesk" = "Выбирайте внимательно, так как старые версии могут быть несовместимы с текущими конфигурациями."
"operationHours" = "Часы работы"
"operationHoursDesc" = "Аптайм системы: время системы в сети."
"title" = "Статус системы"
"memory" = "Память"
"hard" = "Жесткий диск"
"xrayStatus" = "Статус"
"stopXray" = "Остановить Xray"
"restartXray" = "Рестарт Xray"
"xraySwitch" = "Переключить версию"
"xraySwitchClick" = "Выберите желаемую версию"
"xraySwitchClickDesk" = "Выбирайте внимательно, так как старые версии могут быть несовместимы с текущими конфигурациями"
"operationHours" = "Время работы системы"
"systemLoad" = "Системная нагрузка"
"connectionCount" = "количество соединений"
"connectionCountDesc" = "Всего подключений по всем сетям»."
"upSpeed" = "Общая скорость upload."
"downSpeed" = "Общая скорость download."
"totalSent" = "Общий объем загруженных данных с момента запуска системы"
"totalReceive" = "Общий объем полученных данных с момента запуска системы.."
"xraySwitchVersionDialog" = "переключить версию Xray"
"systemLoadDesc" = "средняя загрузка системы за последние 1, 5 и 15 минут"
"connectionTcpCountDesc" = "Всего подключений TCP по всем сетевым картам."
"connectionUdpCountDesc" = "Общее количество подключений UDP по всем сетевым картам."
"connectionCount" = "Количество соединений"
"upSpeed" = "Общая скорость upload для всех сетей"
"downSpeed" = "Общая скорость download для всех сетей"
"totalSent" = "Общий объем загруженных данных для всех сетей с момента запуска системы"
"totalReceive" = "Общий объем полученных данных для всех сетей с момента запуска системы."
"xraySwitchVersionDialog" = "Переключить версию Xray"
"xraySwitchVersionDialogDesc" = "Вы точно хотите сменить версию Xray?"
"dontRefresh" = "Установка. Не обновляйте эту страницу."
"dontRefresh" = "Идёт установка, пожалуйста не обновляйте эту страницу"
"logs" = "Логи"
"config" = "Конфиг"
"backup" = екап и восстановление"
"backupTitle" = "База данных бекапа и восстановления"
"backupDescription" = "Не забудьте сделать резервную копию перед импортом новой базы данных."
"config" = "Конфигурация"
"backup" = экап и восстановление"
"backupTitle" = "База данных бэкапа и восстановления"
"backupDescription" = "Не забудьте сделать резервную копию перед импортом новой базы данных"
"exportDatabase" = "Экспорт базы данных"
"importDatabase" = "Импорт базы данных"
[pages.inbounds]
"title" = "пользователи"
"totalDownUp" = "Всего входящих/исходящих"
"title" = "Подключения"
"totalDownUp" = "Всего uploads/downloads"
"totalUsage" = "Всего использовано"
"inboundCount" = "Количество пользователей"
"inboundCount" = "Количество подключений"
"operate" = "Меню"
"enable" = "Включить"
"remark" = "Примечание"
"protocol" = "Протокол"
"port" = "Порт"
"traffic" = "Траффик"
"traffic" = "Трафик"
"details" = "Подробнее"
"transportConfig" = "Перенести"
"transportConfig" = "Транспорт"
"expireDate" = "Дата окончания"
"resetTraffic" = "Обнулить траффик"
"addInbound" = "Добавить пользователя"
"resetTraffic" = "Сбросить трафик"
"addInbound" = "Добавить подключение"
"generalActions" = "Общие действия"
"create" = "Создать"
"update" = "Обновить"
"modifyInbound" = "Изменить данные"
"deleteInbound" = "Удалить пользователя"
"deleteInboundContent" = "Подтвердите удаление пользователя?"
"resetTrafficContent" = "Подтвердите обнуление траффика?"
"modifyInbound" = "Изменить подключение"
"deleteInbound" = "Удалить подключение"
"deleteInboundContent" = "Подтвердите удаление подключения?"
"resetTrafficContent" = "Подтвердите сброс трафика?"
"copyLink" = "Копировать ключ"
"address" = "Адрес"
"network" = "Сеть"
"destinationPort" = "Порт назначения"
"targetAddress" = "Целевой адрес"
"disableInsecureEncryption" = "Отключить небезопасное шифрование"
"monitorDesc" = "Оставьте пустым по умолчанию"
"meansNoLimit" = "Значит без ограничений"
"totalFlow" = "Общий расход"
"leaveBlankToNeverExpire" = "Оставьте пустым = бессрочно"
"leaveBlankToNeverExpire" = "Оставьте пустым, чтобы не истекало"
"noRecommendKeepDefault" = "Нет требований для сохранения настроек по умолчанию"
"certificatePath" = "Путь файла сертификата"
"certificateContent" = "Содержимое файла сертификата"
@@ -138,39 +138,37 @@
"publicKeyContent" = "Содержимое публичного ключа"
"keyPath" = "Путь к приватному ключу"
"keyContent" = "Содержимое приватного ключа"
"clickOnQRcode" = "Нажмите на QR-код, чтобы скопировать"
"clickOnQRcode" = "Нажмите на QR код, чтобы скопировать"
"client" = "Клиент"
"export" = "Поделиться ключом"
"export" = "Экспорт ключей"
"clone" = "Клонировать"
"cloneInbound" = "Клонировать пользователя"
"cloneInboundContent" = "Все настройки этого пользователя, кроме порта, порт прослушки и клиентов, будут клонированы."
"cloneInbound" = "Клонировать"
"cloneInboundContent" = "Все настройки этого подключения, кроме порта, IP прослушки и клиентов, будут клонированы"
"cloneInboundOk" = "Клонировано"
"resetAllTraffic" = "Обнулить весь траффик"
"resetAllTrafficTitle" = "Обнуление всего траффика"
"resetAllTrafficContent" = "Подтверждаете обнуление всего траффика пользователей?"
"resetAllTrafficOkText" = "Подтвердить"
"resetAllTrafficCancelText" = "Отмена"
"resetInboundClientTraffics" = "Обнулить траффик пользователей"
"resetInboundClientTrafficTitle" = "Обнуление траффика пользователей"
"resetInboundClientTrafficContent" = "Вы уверены, что хотите обнулить весь трафик для этих пользователей?"
"resetAllClientTraffics" = "Обнулить весь траффик пользователей"
"resetAllClientTrafficTitle" = "Обнуление всего траффика пользователей"
"resetAllClientTrafficContent" = "Подтверждаете обнуление всего траффика пользователей?"
"resetAllTraffic" = "Сбросить трафик всех подключений"
"resetAllTrafficTitle" = "Сброс трафика всех подключений"
"resetAllTrafficContent" = "Подтверждаете сброс трафика всех подключений?"
"resetInboundClientTraffics" = "Сбросить трафик пользователей"
"resetInboundClientTrafficTitle" = "Сброс трафика пользователей"
"resetInboundClientTrafficContent" = "Вы уверены, что хотите сбросить весь трафик для этих пользователей?"
"resetAllClientTraffics" = "Сбросить трафик всех пользователей"
"resetAllClientTrafficTitle" = "Сброс трафика всех пользователей"
"resetAllClientTrafficContent" = "Подтверждаете сброс трафика всех пользователей?"
"delDepletedClients" = "Удалить отключенных пользователей"
"delDepletedClientsTitle" = "Удаление отключенных пользователей"
"delDepletedClientsContent" = "Подтверждаете удаление отключенных пользователей?"
"email" = "Email"
"emailDesc" = "Пожалуйста, укажите уникальный Email"
"IPLimit" = "ограничение по IP"
"IPLimitDesc" = "Отключить ключ, если подключено больше введенного значения (введите 0, чтобы отключить ограничение IP-адресов)."
"IPLimit" = "Ограничение по IP"
"IPLimitDesc" = "Сбросить подключение, если подключено больше введенного значения (введите 0, чтобы отключить ограничение IP адресов)"
"IPLimitlog" = "IP лог"
"IPLimitlogDesc" = "Лог IP-адресов (перед включением лога IP-адресов, вы должны очистить список)."
"IPLimitlogclear" = "Очистить список"
"IPLimitlogDesc" = "Лог IP адресов (перед включением лога IP адресов, вы должны очистить список)"
"IPLimitlogclear" = "Очистить лог"
"setDefaultCert" = "Установить сертификат с панели"
"xtlsDesc" = "Версия Xray должна быть не ниже 1.7.5"
"realityDesc" = "Версия Xray должна быть 1.8.0 или выше."
"telegramDesc" = "используйте Telegram ID (вы можете получить его у @userinfobot)"
"subscriptionDesc" = "вы можете найти свою ссылку подписки в разделе «Подробнее», также вы можете использовать одно и то же имя для нескольких конфигов."
"realityDesc" = "Версия Xray должна быть не ниже 1.8.0"
"telegramDesc" = "Используйте идентификатор Telegram без символа @ или идентификатора чата (можно получить его здесь @userinfobot или использовать команду '/id' в боте)"
"subscriptionDesc" = "Вы можете найти свою ссылку подписки в разделе «Подробнее», также вы можете использовать одно и то же имя для нескольких конфигураций"
[pages.client]
"add" = "Добавить пользователя"
@@ -184,7 +182,7 @@
"last" = "Последний"
"prefix" = "Префикс"
"postfix" = "Постфикс"
"delayedStart" = "Начать со времени первого подключения"
"delayedStart" = "Начать со момента первого подключения"
"expireDays" = "Срок действия"
"days" = "дней"
@@ -192,18 +190,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" = "Шифрование"
@@ -211,106 +209,160 @@
[pages.settings]
"title" = "Настройки"
"save" = "Сохранить"
"restartPanel" = "Рестарт панели"
"restartPanelDesc" = одтвердите рестарт панели? ОК для рестарта панели через 3 сек. Если вы не можете пользоваться панелью после рестарта, пожалуйста, посмотрите лог панели на сервере"
"infoDesc" = "Каждое сделанное здесь изменение необходимо сохранить. Пожалуйста, перезапустите панель, чтобы изменения вступили в силу"
"restartPanel" = ерезапуск панели"
"restartPanelDesc" = "Подтвердите перезапуск панели? ОК для перезапуска панели через 3 сек. Если вы не можете пользоваться панелью после перезапуска, пожалуйста, посмотрите лог панели на сервере"
"actions" = "Действия"
"resetDefaultConfig" = "Сбросить всё по-умолчанию"
"resetDefaultConfig" = "Сбросить на конфигурацию по-умолчанию"
"panelSettings" = "Настройки панели"
"securitySettings" = "Настройки безопасности"
"xrayConfiguration" = "Конфигурация Xray"
"TGBotSettings" = "Настройки Телеграм-бота"
"panelListeningIP" = "IP-порт панели"
"panelListeningIPDesc" = "Оставьте пустым для работы с любого IP. Перезагрузите панель для применения настроек."
"TGBotSettings" = "Настройки Telegram бота"
"panelListeningIP" = "IP адрес панели"
"panelListeningIPDesc" = "Оставьте пустым для подключения с любого IP"
"panelListeningDomain" = "Домен прослушивания панели"
"panelListeningDomainDesc" = "По умолчанию оставьте пустым, чтобы отслеживать все домены и IP-адреса"
"panelPort" = "Порт панели"
"panelPortDesc" = ерезагрузите панель для применения настроек."
"panelPortDesc" = орт, используемый для отображения этой панели"
"publicKeyPath" = "Путь к файлу публичного ключа сертификата панели"
"publicKeyPathDesc" = "Введите полный путь, начинающийся с «/». Перезагрузите панель для применения настроек."
"publicKeyPathDesc" = "Введите полный путь, начинающийся с "
"privateKeyPath" = "Путь к файлу приватного ключа сертификата панели"
"privateKeyPathDesc" = "Введите полный путь, начинающийся с «/». Перезагрузите панель для применения настроек."
"panelUrlPath" = "Корневой путь URL-адреса панели"
"panelUrlPathDesc" = "Должен начинаться с «/» и заканчиваться на «/». Перезагрузите панель для применения настроек."
"oldUsername" = "Имя пользователя сейчас"
"currentPassword" = "Пароль сейчас"
"privateKeyPathDesc" = "Введите полный путь, начинающийся с "
"panelUrlPath" = "Корневой путь URL адреса панели"
"panelUrlPathDesc" = "Должен начинаться с «/» и заканчиваться на "
"oldUsername" = "Текущее имя пользователя"
"currentPassword" = "Текущий пароль"
"newUsername" = "Новое имя пользователя"
"newPassword" = "Новый пароль"
"telegramBotEnable" = "Включить Телеграм-бота"
"telegramBotEnableDesc" = ерезагрузите панель для применения настроек."
"telegramToken" = "Токен Телеграм-бота"
"telegramTokenDesc" = "Перезагрузите панель для применения настроек."
"telegramChatId" = "Телеграм-ID админа бота"
"telegramChatIdDesc" = "Если несколько Телеграм-ID, разделить запятой. Используйте @userinfobot, чтобы получить Телеграм-ID. Перезагрузите панель для применения настроек."
"telegramNotifyTime" = "Частота уведомлений телеграм-бота"
"telegramNotifyTimeDesc" = "Используйте формат Crontab. Перезагрузите панель для применения настроек."
"telegramBotEnable" = "Включить Telegram бота"
"telegramBotEnableDesc" = одключайтесь к функциям этой панели через Telegram бота"
"telegramToken" = "Токен Telegram бота"
"telegramTokenDesc" = "Необходимо получить токен у менеджера ботов Telegram @botfather"
"telegramChatId" = "Telegram ID админа бота"
"telegramChatIdDesc" = "Множественные идентификаторы чата, разделенные запятыми. Чтобы получить свои идентификаторы чатов, используйте @userinfobot или команду '/id' в боте."
"telegramNotifyTime" = "Частота уведомлений бота Telegram"
"telegramNotifyTimeDesc" = "Используйте формат времени Crontab"
"tgNotifyBackup" = "Резервное копирование базы данных"
"tgNotifyBackupDesc" = "Включать файл резервной копии базы данных с уведомлением об отчете. Перезагрузите панель для применения настроек."
"tgNotifyBackupDesc" = "Включать файл резервной копии базы данных с уведомлением об отчете"
"tgNotifyLogin" = "Уведомление о входе"
"tgNotifyLoginDesc" = "Отображает имя пользователя, IP-адрес и время, когда кто-то пытается войти в вашу панель."
"sessionMaxAge" = "Продолжительность сессии"
"sessionMaxAgeDesc" = "Продолжительность сессии в системе (значение: минута)"
"expireTimeDiff" = "Порог истечения срока сессии для уведомления"
"expireTimeDiffDesc" = "Получение уведомления об истечении срока действия сессии до достижения порогового значения (значение: день)"
"trafficDiff" = "Порог траффика для уведомления"
"trafficDiffDesc" = "Получение уведомления об исчерпании трафика до достижения порога (значение: ГБ)"
"trafficDiff" = "Порог трафика для уведомления"
"trafficDiffDesc" = "Получение уведомления об исчерпании трафика до достижения порога (значение: Гб)"
"tgNotifyCpu" = "Порог нагрузки на ЦП для уведомления"
"tgNotifyCpuDesc" = "Получение уведомления, если нагрузка на ЦП превышает этот порог (значение:%)"
"timeZone" = "Временная зона"
"timeZoneDesc" = "Запланированные задачи выполняются в соответствии со временем в этом часовом поясе. Перезагрузите панель для применения настроек."
"tgNotifyCpuDesc" = "Получение уведомления, если нагрузка на ЦП превышает этот порог (значение: %)"
"timeZone" = "Часовой пояс"
"timeZoneDesc" = "Запланированные задачи выполняются в соответствии со временем в этом часовом поясе"
"subSettings" = "Подписка"
"subEnable" = "Включить службу"
"subEnableDesc" = "Функция подписки с отдельной конфигурацией"
"subListen" = "Прослушивание IP"
"subListenDesc" = "Оставьте пустым по умолчанию, чтобы отслеживать все IP-адреса"
"subPort" = "Порт подписки"
"subPortDesc" = "Номер порта для обслуживания службы подписки не должен использоваться на сервере"
"subCertPath" = "Путь к файлу открытого ключа сертификата подписки"
"subCertPathDesc" = "Введите абсолютный путь, начинающийся с '/'"
"subKeyPath" = "Путь к файлу закрытого ключа сертификата подписки"
"subKeyPathDesc" = "Введите абсолютный путь, начинающийся с '/'"
"subPath" = "Корневой путь URL-адреса подписки"
"subPathDesc" = "Должен начинаться с '/' и заканчиваться на '/'"
"subDomain" = "Домен прослушивания"
"subDomainDesc" = "Оставьте пустым по умолчанию, чтобы отслеживать все домены и IP-адреса"
"subUpdates" = "Интервалы обновления подписки"
"subUpdatesDesc" = "Часовой интервал между обновлениями в клиентском приложении"
"subEncrypt" = "Шифровать конфиги"
"subEncryptDesc" = "Шифровать возвращенные конфиги в подписке"
"subShowInfo" = "Показать информацию об использовании"
"subShowInfoDesc" = "Показывать восстановленный трафик и дату после имени конфигурации"
[pages.settings.templates]
"title" = "Шаблоны"
"basicTemplate" = "Базовые шаблоны"
"advancedTemplate" = "Расширенные шаблоны"
"completeTemplate" = "Конфигурация шаблона"
"basicTemplate" = "Базовый шаблон"
"advancedTemplate" = "Расширенный шаблон"
"completeTemplate" = "Полный шаблон"
"generalConfigs" = "Основные настройки"
"generalConfigsDesc" = "Эти параметры не позволят пользователям подключаться к определенным протоколам и веб-сайтам."
"countryConfigs" = "Настройки по гео"
"countryConfigsDesc" = "Эти параметры не позволят пользователям подключаться к доменам определенной страны."
"ipv4Configs" = "Настройки IPv4 "
"ipv4ConfigsDesc" = "Эти параметры будут маршрутизироваться к целевым доменам только через IPv4."
"warpConfigs" = "Настройки WARP"
"warpConfigsDesc" = "Внимание: перед использованием этих параметров установите WARP в режиме прокси-сервера socks5 на свой сервер, следуя инструкциям на GitHub панели. WARP будет направлять трафик на веб-сайты через серверы Cloudflare."
"generalConfigsDesc" = "Эти параметры описывают общие настройки"
"blockConfigs" = "Блокировка конфигураций"
"blockConfigsDesc" = "Эти параметры не позволят пользователям подключаться к определенным протоколам и веб-сайтам"
"blockCountryConfigs" = "Конфигурации блокировки страны"
"blockCountryConfigsDesc" = "Эти параметры не позволят пользователям подключаться к доменам определенной страны"
"directCountryConfigs" = "Настройки прямого подключения для страны"
"directCountryConfigsDesc" = "Эти параметры позволят пользователям подключаться напрямую к доменам определенной страны"
"ipv4Configs" = "Настройки IPv4"
"ipv4ConfigsDesc" = "Эти параметры позволят пользователям маршрутизироваться к целевым доменам только через IPv4"
"warpConfigs" = "Настройки WARP"
"warpConfigsDesc" = "Внимание: перед использованием этих параметров установите WARP в режиме прокси-сервера socks5 на свой сервер, следуя инструкциям на GitHub панели. WARP будет направлять трафик на веб-сайты через серверы Cloudflare"
"xrayConfigTemplate" = "Шаблон конфигурации Xray"
"xrayConfigTemplateDesc" = "Создание файла конфигурации Xray на основе этого шаблона. Перезагрузите панель для применения настроек."
"xrayConfigTorrent" = "Запретить использование BitTorrent"
"xrayConfigTorrentDesc" = "Измените конфигурацию, чтобы пользователи не использовали BitTorrent. Перезагрузите панель для применения настроек."
"xrayConfigPrivateIp" = "Запрет частных диапазонов IP-адресов для подключения"
"xrayConfigPrivateIpDesc" = "Измените конфигурацию, чтобы избежать подключения к диапазонам частных IP-адресов. Перезагрузите панель для применения настроек."
"xrayConfigAds" = "Бокировка рекламы"
"xrayConfigAdsDesc" = "Измените конфигурацию, чтобы заблокировать рекламу. Перезагрузите панель для применения настроек."
"xrayConfigPorn" = "Блокировка порносайтов"
"xrayConfigPornDesc" = "Измените конфигурацию, чтобы отключить подключения к порносайтам. Перезагрузите панель для применения настроек."
"xrayConfigTemplateDesc" = "Создание файла конфигурации Xray на основе этого шаблона"
"xrayConfigFreedomStrategy" = "Настройка стратегии протокола Freedom"
"xrayConfigFreedomStrategyDesc" = "Установка стратегию вывода сети в протоколе Freedom"
"xrayConfigRoutingStrategy" = "Настройка стратегии маршрутизации доменов"
"xrayConfigRoutingStrategyDesc" = "Установка общей стратегии маршрутизации разрешения DNS"
"xrayConfigTorrent" = "Запрет использования BitTorrent"
"xrayConfigTorrentDesc" = "Изменение шаблона конфигурации, для предупреждения использования BitTorrent пользователями"
"xrayConfigPrivateIp" = "Запрет частных диапазонов IP адресов для подключения"
"xrayConfigPrivateIpDesc" = "Изменение шаблона конфигурации, для предупреждения подключения к диапазонам частных IP адресов"
"xrayConfigAds" = "Блокировка рекламы"
"xrayConfigAdsDesc" = "Изменение конфигурации, для блокировки рекламы"
"xrayConfigFamily" = "Блокировать вредоносное ПО и контент для взрослых"
"xrayConfigFamilyDesc" = "Резольверы DNS для блокировки вредоносных программ и контента для взрослых для защиты семьи"
"xrayConfigSpeedtest" = "Блокировать сайты для проверки скорости"
"xrayConfigSpeedtestDesc" = "Измените шаблон конфигурации, чтобы избежать подключения к веб-сайтам для тестирования скорости. Перезапустите панель, чтобы применить изменения."
"xrayConfigIRIp" = "Отключить подключение к диапазонам IP-адресов Ирана"
"xrayConfigIRIpDesc" = "Измените конфигурацию, чтобы отключить подключение к диапазонам IP-адресов Ирана. Перезагрузите панель для применения настроек."
"xrayConfigIRDomain" = "Отключить подключение к доменам Ирана"
"xrayConfigIRDomainDesc" = "Измените конфигурацию, чтобы отключить подключение к доменам Ирана. Перезагрузите панель для применения настроек."
"xrayConfigChinaIp" = "Отключить подключение к диапазонам IP-адресов Китая"
"xrayConfigChinaIpDesc" = "Измените конфигурацию, чтобы отключить подключение к диапазонам IP-адресов Китая. Перезагрузите панель для применения настроек."
"xrayConfigChinaDomain" = "Отключить подключение к доменам Китая"
"xrayConfigChinaDomainDesc" = "Измените конфигурацию, чтобы отключить подключение к доменам Китая. Перезагрузите панель для применения настроек."
"xrayConfigRussiaIp" = "Отключить подключение к диапазонам IP-адресов России"
"xrayConfigRussiaIpDesc" = "Измените конфигурацию, чтобы отключить соединения с диапазонами IP-адресов России. Перезагрузите панель для применения настроек."
"xrayConfigRussiaDomain" = "Отключить подключение к доменам России"
"xrayConfigRussiaDomainDesc" = "Измените конфигурацию, чтобы избежать подключения к доменам России. Перезагрузите панель для применения настроек."
"xrayConfigSpeedtestDesc" = "Изменение шаблона конфигурации, для предупреждения подключения к веб-сайтам для тестирования скорости"
"xrayConfigIRIp" = "Заблокировать подключения к диапазонам IP адресов Ирана"
"xrayConfigIRIpDesc" = "Изменение конфигурации, чтобы заблокировать подключения к диапазонам IP адресов Ирана"
"xrayConfigIRDomain" = "Заблокировать подключения к доменам Ирана"
"xrayConfigIRDomainDesc" = "Изменение конфигурации, чтобы заблокировать подключения к доменам Ирана"
"xrayConfigChinaIp" = "Заблокировать подключения к диапазонам IP адресов Китая"
"xrayConfigChinaIpDesc" = "Изменение конфигурации, чтобы заблокировать подключения к диапазонам IP адресов Китая"
"xrayConfigChinaDomain" = "Заблокировать подключения к доменам Китая"
"xrayConfigChinaDomainDesc" = "Изменение конфигурации, чтобы заблокировать подключения к доменам Китая"
"xrayConfigRussiaIp" = "Заблокировать подключения к диапазонам IP адресов России"
"xrayConfigRussiaIpDesc" = "Изменение конфигурации, чтобы заблокировать подключения к диапазонами IP адресов России"
"xrayConfigRussiaDomain" = "Заблокировать подключения к доменам России"
"xrayConfigRussiaDomainDesc" = "Изменение конфигурации, чтобы заблокировать подключения к доменам России"
"xrayConfigDirectIRIp" = "Прямое подключения к диапазонам IP адресов Ирана"
"xrayConfigDirectIRIpDesc" = "Изменение шаблона конфигурации для прямого подключения к диапазонам IP адресов Ирана"
"xrayConfigDirectIRDomain" = "Прямое подключение к доменам Ирана"
"xrayConfigDirectIRDomainDesc" = "Изменение шаблон конфигурации для прямого подключения к доменам Ирана"
"xrayConfigDirectChinaIp" = "Прямое подключение к диапазонам IP адресов Китая"
"xrayConfigDirectChinaIpDesc" = "Изменение шаблона конфигурации для прямого подключения к диапазонам IP адресов Китая"
"xrayConfigDirectChinaDomain" = "Прямое подключение к доменам Китая"
"xrayConfigDirectChinaDomainDesc" = "Изменение шаблона конфигурации для прямого подключения к доменам Китая"
"xrayConfigDirectRussiaIp" = "Прямое подключение к диапазонам IP адресов России"
"xrayConfigDirectRussiaIpDesc" = "Изменение шаблона конфигурации для прямого подключения к диапазонам IP адресов России"
"xrayConfigDirectRussiaDomain" = "Прямое подключение к доменам России"
"xrayConfigDirectRussiaDomainDesc" = "Изменение шаблона конфигурации для прямого подключения к доменам России"
"xrayConfigGoogleIPv4" = "Использовать IPv4 для Google"
"xrayConfigGoogleIPv4Desc" = "Применить маршрутизацию Google для подключения к IPv4. Перезагрузите панель для применения настроек."
"xrayConfigGoogleIPv4Desc" = "Добавить маршрутизацию для Google для подключения к IPv4"
"xrayConfigNetflixIPv4" = "Использовать IPv4 для Netflix"
"xrayConfigNetflixIPv4Desc" = "Применить маршрутизацию Netflix для подключения к IPv4. Перезагрузите панель для применения настроек."
"xrayConfigGoogleWARP" = "Маршрутизация Google через WARP."
"xrayConfigGoogleWARPDesc" = "Применить маршрутизацию Google через WARP. Перезагрузите панель для применения настроек."
"xrayConfigOpenAIWARP" = "Маршрут OpenAI (ChatGPT) через WARP."
"xrayConfigOpenAIWARPDesc" = "Применить маршрутизацию для OpenAI (ChatGPT) через WARP. Перезагрузите панель для применения настроек."
"xrayConfigNetflixWARP" = "Маршрутизация Netflix через WARP."
"xrayConfigNetflixWARPDesc" = "Применить маршрутизацию Netflix через WARP. Перезагрузите панель для применения настроек."
"xrayConfigSpotifyWARP" = "Маршрутизация Spotify через WARP."
"xrayConfigSpotifyWARPDesc" = "Применить маршрутизацию Spotify через WARP. Перезагрузите панель для применения настроек."
"xrayConfigIRWARP" = "Маршрутизация доменов Ирана через WARP."
"xrayConfigIRWARPDesc" = "Применить маршрутизацию для доменов Ирана через WARP. Перезагрузите панель для применения настроек."
"xrayConfigNetflixIPv4Desc" = "Добавить маршрутизацию для Netflix для подключения к IPv4"
"xrayConfigGoogleWARP" = "Маршрутизация Google через WARP"
"xrayConfigGoogleWARPDesc" = "Добавить маршрутизацию для Google через WARP"
"xrayConfigOpenAIWARP" = "Маршрутизация OpenAI (ChatGPT) через WARP"
"xrayConfigOpenAIWARPDesc" = "Добавить маршрутизацию для OpenAI (ChatGPT) через WARP"
"xrayConfigNetflixWARP" = "Маршрутизация Netflix через WARP"
"xrayConfigNetflixWARPDesc" = "Добавить маршрутизацию для Netflix через WARP"
"xrayConfigSpotifyWARP" = "Маршрутизация Spotify через WARP"
"xrayConfigSpotifyWARPDesc" = "Добавить маршрутизацию для Spotify через WARP"
"xrayConfigIRWARP" = "Маршрутизация доменов Ирана через WARP"
"xrayConfigIRWARPDesc" = "Добавить маршрутизацию для доменов Ирана через WARP"
"xrayConfigInbounds" = "Конфигурация подключений"
"xrayConfigInboundsDesc" = "Изменение шаблона конфигурации, для подключения определенных пользователей. Перезагрузите панель для применения настроек."
"xrayConfigInboundsDesc" = "Изменение шаблона конфигурации, для подключения определенных пользователей"
"xrayConfigOutbounds" = "Конфигурация исходящих"
"xrayConfigOutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить исходящие пути для этого сервера. Перезагрузите панель для применения настроек."
"xrayConfigRoutings" = "Настройка правил маршрутизации."
"xrayConfigRoutingsDesc" = "Изменение шаблона конфигурации, для определения правил маршрутизации для этого сервера. Перезагрузите панель для применения настроек."
"xrayConfigOutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить исходящие пути для этого сервера"
"xrayConfigRoutings" = "Настройка правил маршрутизации"
"xrayConfigRoutingsDesc" = "Изменение шаблона конфигурации, для определения правил маршрутизации для этого сервера"
"manualLists" = "Ручные списки"
"manualListsDesc" = "Пожалуйста, используйте формат массива JSON"
"manualBlockedIPs" = "Список заблокированных IP адресов"
"manualBlockedDomains" = "Список заблокированных доменов"
"manualDirectIPs" = "Список прямых IP адресов"
"manualDirectDomains" = "Список прямых доменов"
"manualIPv4Domains" = "Список доменов IPv4"
"manualWARPDomains" = "Список доменов WARP"
[pages.settings.security]
"admin" = "Админ"
@@ -318,11 +370,126 @@
"loginSecurity" = "Безопасность входа"
"loginSecurityDesc" = "Включить дополнительные меры безопасности входа пользователя"
"secretToken" = "Секретный токен"
"secretTokenDesc" = "Пожалуйста, скопируйте и сохраните этот токен в безопасном месте. Этот токен необходим для входа в систему и не может быть восстановлен с помощью командного инструмента x-ui."
"secretTokenDesc" = "Пожалуйста, скопируйте и сохраните этот токен в безопасном месте. Этот токен необходим для входа в систему и не может быть восстановлен с помощью инструмента x-ui"
[pages.settings.toasts]
"modifySettings" = "Изменение настроек"
"getSettings" = "Просмотр настроек"
"modifyUser" = "Изменение пользователя "
"modifyUser" = "Изменение пользователя"
"originalUserPassIncorrect" = "Неверное имя пользователя или пароль"
"userPassMustBeNotEmpty" = "Новое имя пользователя и новый пароль должны быть заполнены"
[tgbot]
"keyboardClosed" = "❌ Закрыта настраиваемая клавиатура!"
"noResult" = "❗ Нет результатов!"
"noQuery" = "❌ Запрос не найден! Пожалуйста, повторите команду!"
"wentWrong" = "❌ Что-то пошло не так!"
"noIpRecord" = "❗ Нет записей об IP-адресе!"
"noInbounds" = "❗ Входящих соединений не найдено!"
"unlimited" = "♾ Неограниченно"
"month" = "Месяц"
"months" = "Месяцев"
"day" = "День"
"days" = "Дней"
"hours" = "Часов"
"unknown" = "Неизвестно"
"inbounds" = "Входящие"
"clients" = "Клиенты"
[tgbot.commands]
"unknown" = "❗ Неизвестная команда"
"pleaseChoose" = "👇 Пожалуйста, выберите:\r\n"
"help" = "🤖 Добро пожаловать в этого бота! Он предназначен для предоставления вам конкретных данных с сервера и позволяет вносить необходимые изменения.\r\n\r\n"
"start" = "👋 Привет, <i>{{ .Firstname }}</i>.\r\n"
"welcome" = "🤖 Добро пожаловать в бота управления <b>{{ .Hostname }}</b>.\r\n"
"status" = "✅ Бот работает нормально!"
"usage" = "❗ Пожалуйста, укажите текст для поиска!"
"getID" = "🆔 Ваш ID: <code>{{ .ID }}</code>"
"helpAdminCommands" = "Поиск по электронной почте клиента:\r\n<code>/usage [Email]</code>\r\n \r\nПоиск входящих соединений (со статистикой клиента):\r\n<code>/inbound [Remark]</code>"
"helpClientCommands" = "Для получения статистики используйте следующую команду:\r\n \r\n<code>/usage [UUID|Password]</code>\r\n \r\nИспользуйте UUID для vmess/vless и пароль для Trojan."
[tgbot.messages]
"cpuThreshold" = "🔴 Загрузка процессора составляет {{ .Percent }}%, что превышает пороговое значение {{ .Threshold }}%"
"selectUserFailed" = "❌ Ошибка при выборе пользователя!"
"userSaved" = "✅ Пользователь Telegram сохранен."
"loginSuccess" = "✅ Успешный вход в панель.\r\n"
"loginFailed" = "❗️ Ошибка входа в панель.\r\n"
"report" = "🕰 Запланированные отчеты: {{ .RunTime }}\r\n"
"datetime" = "⏰ Дата и время: {{ .DateTime }}\r\n"
"hostname" = "💻 Имя хоста: {{ .Hostname }}\r\n"
"version" = "🚀 Версия X-UI: {{ .Version }}\r\n"
"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n"
"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n"
"ip" = "🌐 IP: {{ .IP }}\r\n"
"ips" = "🔢 IP-адреса: \r\n{{ .IPs }}\r\n"
"serverUpTime" = "⏳ Время работы сервера: {{ .UpTime }} {{ .Unit }}\r\n"
"serverLoad" = "📈 Загрузка сервера: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
"serverMemory" = "📋 Память сервера: {{ .Current }}/{{ .Total }}\r\n"
"tcpCount" = "🔹 Количество TCP-соединений: {{ .Count }}\r\n"
"udpCount" = "🔸 Количество UDP-соединений: {{ .Count }}\r\n"
"traffic" = "🚦 Трафик: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
"xrayStatus" = " Состояние Xray: {{ .State }}\r\n"
"username" = "👤 Имя пользователя: {{ .Username }}\r\n"
"time" = "⏰ Время: {{ .Time }}\r\n"
"inbound" = "📍 Входящий поток: {{ .Remark }}\r\n"
"port" = "🔌 Порт: {{ .Port }}\r\n"
"expire" = "📅 Дата окончания: {{ .Time }}\r\n"
"expireIn" = "📅 Окончание через: {{ .Time }}\r\n"
"active" = "💡 Активен: ✅ Да\r\n"
"inactive" = "💡 Активен: ❌ Нет\r\n"
"email" = "📧 Email: {{ .Email }}\r\n"
"upload" = "🔼 Исходящий трафик: ↑{{ .Upload }}\r\n"
"download" = "🔽 Входящий трафик: ↓{{ .Download }}\r\n"
"total" = "📊 Всего: ↑↓{{ .UpDown }} из {{ .Total }}\r\n"
"TGUser" = "👤 Пользователь Telegram: {{ .TelegramID }}\r\n"
"exhaustedMsg" = "🚨 Исчерпаны {{ .Type }}:\r\n"
"exhaustedCount" = "🚨 Количество исчерпанных {{ .Type }}:\r\n"
"disabled" = "🛑 Отключено: {{ .Disabled }}\r\n"
"depleteSoon" = "🔜 Скоро исчерпание: {{ .Deplete }}\r\n \r\n"
"backupTime" = "🗄 Время резервного копирования: {{ .Time }}\r\n"
"refreshedOn" = "\r\n📋🔄 Обновлено: {{ .Time }}\r\n \r\n"
[tgbot.buttons]
"closeKeyboard" = "❌ Закрыть клавиатуру"
"cancel" = "❌ Отмена"
"cancelReset" = "❌ Отменить сброс"
"cancelIpLimit" = "❌ Отменить лимит IP"
"confirmResetTraffic" = "✅ Подтвердить сброс трафика?"
"confirmClearIps" = "✅ Подтвердить очистку IP?"
"confirmRemoveTGUser" = "✅ Подтвердить удаление пользователя Telegram?"
"dbBackup" = "Получить резервную копию DB"
"serverUsage" = "Использование сервера"
"getInbounds" = "Получить входящие потоки"
"depleteSoon" = "Скоро исчерпание"
"clientUsage" = "Получить использование"
"commands" = "Команды"
"refresh" = "🔄 Обновить"
"clearIPs" = "❌ Очистить IP"
"removeTGUser" = "❌ Удалить пользователя Telegram"
"selectTGUser" = "👤 Выбрать пользователя Telegram"
"selectOneTGUser" = "👤 Выберите пользователя Telegram:"
"resetTraffic" = "📈 Сбросить трафик"
"resetExpire" = "📅 Сбросить дату окончания"
"ipLog" = "🔢 Лог IP"
"ipLimit" = "🔢 Лимит IP"
"setTGUser" = "👤 Установить пользователя Telegram"
"toggle" = "🔘 Вкл./Выкл."
[tgbot.answers]
"errorOperation" = "❗ Ошибка в операции."
"getInboundsFailed" = "❌ Не удалось получить входящие потоки."
"canceled" = "❌ {{ .Email }}: Операция отменена."
"clientRefreshSuccess" = "✅ {{ .Email }}: Клиент успешно обновлен."
"IpRefreshSuccess" = "✅ {{ .Email }}: IP-адреса успешно обновлены."
"TGIdRefreshSuccess" = "✅ {{ .Email }}: Пользователь Telegram клиента успешно обновлен."
"resetTrafficSuccess" = "✅ {{ .Email }}: Трафик успешно сброшен."
"expireResetSuccess" = "✅ {{ .Email }}: Дни истечения успешно сброшены."
"resetIpSuccess" = "✅ {{ .Email }}: Лимит IP ({{ .Count }}) успешно сохранен."
"clearIpSuccess" = "✅ {{ .Email }}: IP-адреса успешно очищены."
"getIpLog" = "✅ {{ .Email }}: Получен лог IP."
"getUserInfo" = "✅ {{ .Email }}: Получена информация о пользователе Telegram."
"removedTGUserSuccess" = "✅ {{ .Email }}: Пользователь Telegram успешно удален."
"enableSuccess" = "✅ {{ .Email }}: Включено успешно."
"disableSuccess" = "✅ {{ .Email }}: Отключено успешно."
"askToAddUserId" = "Ваша конфигурация не найдена!\r\nПожалуйста, попросите администратора использовать ваш идентификатор пользователя Telegram в ваших конфигурациях.\r\n\r\nВаш идентификатор пользователя: <b>{{ .TgUserID }}</b>"
"askToAddUserName" = "Ваша конфигурация не найдена!\r\nПожалуйста, попросите администратора использовать ваше имя пользователя или идентификатор пользователя Telegram в ваших конфигурациях.\r\n\r\nВаше имя пользователя: <b>@{{ .TgUserName }}</b>\r\n\r\nВаш идентификатор пользователя: <b>{{ .TgUserID }}</b>"

View File

@@ -0,0 +1,495 @@
"username" = "Tên người dùng"
"password" = "Mật khẩu"
"login" = "Đăng nhập"
"confirm" = "Xác nhận"
"cancel" = "Hủy bỏ"
"close" = "Đóng"
"copy" = "Sao chép"
"copied" = "Đã sao chép"
"download" = "Tải xuống"
"remark" = "Ghi chú"
"enable" = "Kích hoạt"
"protocol" = "Giao thức"
"search" = "Tìm kiếm"
"filter" = "Bộ lọc"
"loading" = "Đang tải"
"second" = "Giây"
"minute" = "Phút"
"hour" = "Giờ"
"day" = "Ngày"
"check" = "Kiểm tra"
"indefinite" = "Không xác định"
"unlimited" = "Không giới hạn"
"none" = "Không có"
"qrCode" = "Mã QR"
"info" = "Thông tin thêm"
"edit" = "Chỉnh sửa"
"delete" = "Xóa"
"reset" = "Đặt lại"
"copySuccess" = "Đã sao chép thành công"
"sure" = "Chắc chắn"
"encryption" = "Mã hóa"
"transmission" = "Truyền tải"
"host" = "Máy chủ"
"path" = "Đường dẫn"
"camouflage" = "camouflage"
"status" = "Trạng thái"
"enabled" = "Đã kích hoạt"
"disabled" = "Đã tắt"
"depleted" = "Đã cạn kiệt"
"depletingSoon" = "Sắp cạn kiệt"
"domainName" = "Tên miền"
"monitor" = "Listening IP"
"certificate" = "Chứng chỉ"
"fail" = "Thất bại"
"success" = "Thành công"
"getVersion" = "Lấy phiên bản"
"install" = "Cài đặt"
"clients" = "Khách hàng"
"usage" = "Sử dụng"
"secretToken" = "secretToken"
"remained" = "Còn lại"
[menu]
"dashboard" = "Trạng thái hệ thống"
"inbounds" = "Inbounds"
"settings" = "Cài đặt bảng điều khiển"
"logout" = "Đăng xuất"
"link" = "Khác"
[pages.login]
"title" = "Đăng nhập"
"loginAgain" = "Thời hạn đăng nhập đã hết. Vui lòng đăng nhập lại."
[pages.login.toasts]
"invalidFormData" = "Dạng dữ liệu nhập không hợp lệ."
"emptyUsername" = "Vui lòng nhập tên người dùng."
"emptyPassword" = "Vui lòng nhập mật khẩu."
"wrongUsernameOrPassword" = "Tên người dùng hoặc mật khẩu không đúng."
"successLogin" = "Đăng nhập thành công."
[pages.index]
"title" = "Trạng thái hệ thống"
"memory" = "Bộ nhớ"
"hard" = "Ổ cứng"
"xrayStatus" = "Trạng thái của Xray"
"stopXray" = "Dừng Xray"
"restartXray" = "Khởi động lại Xray"
"xraySwitch" = "Chuyển đổi phiên bản"
"xraySwitchClick" = "Chọn phiên bản mà bạn muốn chuyển đổi sang."
"xraySwitchClickDesk" = "Hãy lựa chọn thận trọng, vì các phiên bản cũ có thể không tương thích với các cấu hình hiện tại."
"operationHours" = "Thời gian hoạt động"
"systemLoad" = "Tải hệ thống"
"systemLoadDesc" = "trung bình tải hệ thống trong 1, 5 và 15 phút qua"
"connectionTcpCountDesc" = "Tổng số kết nối TCP trên tất cả các card mạng."
"connectionUdpCountDesc" = "Tổng số kết nối UDP trên tất cả các card mạng."
"connectionCount" = "Số lượng kết nối"
"upSpeed" = "Tổng tốc độ tải lên cho tất cả các card mạng."
"downSpeed" = "Tổng tốc độ tải xuống cho tất cả các card mạng."
"totalSent" = "Tổng lưu lượng tải lên của tất cả các thẻ mạng kể từ khi hệ thống khởi động."
"totalReceive" = "Tổng lưu lượng tải xuống trên tất cả các thẻ mạng kể từ khi hệ thống khởi động."
"xraySwitchVersionDialog" = "Chuyển đổi Phiên bản Xray"
"xraySwitchVersionDialogDesc" = "Bạn có chắc chắn muốn chuyển đổi phiên bản Xray sang"
"dontRefresh" = "Đang tiến hành cài đặt, vui lòng không làm mới trang này."
"logs" = "Nhật ký"
"config" = "Cấu hình"
"backup" = "Sao lưu & Khôi phục"
"backupTitle" = "Sao lưu & Khôi phục Cơ sở dữ liệu"
"backupDescription" = "Hãy nhớ sao lưu trước khi nhập cơ sở dữ liệu mới."
"exportDatabase" = "Tải về Cơ sở dữ liệu"
"importDatabase" = "Tải lên Cơ sở dữ liệu"
[pages.inbounds]
"title" = "Điểm vào (Inbounds)"
"totalDownUp" = "Tổng tải lên/tải xuống"
"totalUsage" = "Tổng sử dụng"
"inboundCount" = "Số lượng điểm vào"
"operate" = "Thao tác"
"enable" = "Kích hoạt"
"remark" = "Chú thích"
"protocol" = "Giao thức"
"port" = "Cổng"
"traffic" = "Lưu lượng"
"details" = "Chi tiết"
"transportConfig" = "Giao vận"
"expireDate" = "Ngày hết hạn"
"resetTraffic" = "Đặt lại lưu lượng"
"addInbound" = "Thêm điểm vào"
"generalActions" = "Hành động chung"
"create" = "Tạo mới"
"update" = "Cập nhật"
"modifyInbound" = "Chỉnh sửa điểm vào (Inbound)"
"deleteInbound" = "Xóa điểm vào (Inbound)"
"deleteInboundContent" = "Xác nhận xóa điểm vào? (Inbound)"
"resetTrafficContent" = "Xác nhận đặt lại lưu lượng?"
"copyLink" = "Sao chép liên kết"
"address" = "Địa chỉ"
"network" = "Mạng"
"destinationPort" = "Cổng đích"
"targetAddress" = "Địa chỉ mục tiêu"
"monitorDesc" = "Mặc định để trống"
"meansNoLimit" = "Nghĩa là không giới hạn"
"totalFlow" = "Tổng lưu lượng"
"leaveBlankToNeverExpire" = "Để trống để không bao giờ hết hạn"
"noRecommendKeepDefault" = "Không yêu cầu đặc biệt để giữ nguyên cài đặt mặc định"
"certificatePath" = "Đường dẫn tập tin chứng chỉ"
"certificateContent" = "Nội dung tập tin chứng chỉ"
"publicKeyPath" = "Đường dẫn khóa công khai"
"publicKeyContent" = "Nội dung khóa công khai"
"keyPath" = "Đường dẫn khóa riêng tư"
"keyContent" = "Nội dung khóa riêng tư"
"clickOnQRcode" = "Nhấn vào Mã QR để sao chép"
"client" = "Client"
"export" = "Xuất liên kết"
"clone" = "Sao chép"
"cloneInbound" = "Sao chép điểm vào (Inbound)"
"cloneInboundContent" = "Tất cả cài đặt của điểm vào này, trừ Cổng, IP nghe và Clients, sẽ được áp dụng cho bản sao."
"cloneInboundOk" = "Sao chép"
"resetAllTraffic" = "Đặt lại lưu lượng cho tất cả điểm vào"
"resetAllTrafficTitle" = "Đặt lại lưu lượng cho tất cả điểm vào"
"resetAllTrafficContent" = "Bạn có chắc chắn muốn đặt lại lưu lượng cho tất cả điểm vào không?"
"resetInboundClientTraffics" = "Đặt lại lưu lượng cho các client của điểm vào"
"resetInboundClientTrafficTitle" = "Đặt lại lưu lượng cho tất cả lưu lượng của client"
"resetInboundClientTrafficContent" = "Bạn có chắc chắn muốn đặt lại tất cả lưu lượng cho các client của điểm vào này không?"
"resetAllClientTraffics" = "Đặt lại lưu lượng cho tất cả client"
"resetAllClientTrafficTitle" = "Đặt lại lưu lượng cho tất cả client"
"resetAllClientTrafficContent" = "Bạn có chắc chắn muốn đặt lại tất cả lưu lượng cho tất cả client không?"
"delDepletedClients" = "Xóa các client đã cạn kiệt"
"delDepletedClientsTitle" = "Xóa các client đã cạn kiệt"
"delDepletedClientsContent" = "Bạn có chắc chắn muốn xóa tất cả các client đã cạn kiệt không?"
"email" = "Email"
"emailDesc" = "Vui lòng cung cấp một địa chỉ email duy nhất."
"IPLimit" = "Giới hạn IP"
"IPLimitDesc" = "Vô hiệu hóa điểm vào nếu số lượng vượt quá giá trị đã nhập (nhập 0 để vô hiệu hóa giới hạn IP)."
"IPLimitlog" = "Lịch sử IP"
"IPLimitlogDesc" = "Lịch sử đăng nhập IP (trước khi kích hoạt điểm vào sau khi bị vô hiệu hóa bởi giới hạn IP, bạn nên xóa lịch sử)."
"IPLimitlogclear" = "Xóa Lịch sử"
"setDefaultCert" = "Đặt chứng chỉ từ bảng điều khiển"
"xtlsDesc" = "Xray core cần phiên bản 1.7.5"
"realityDesc" = "Xray core cần phiên bản 1.8.0 hoặc cao hơn."
"telegramDesc" = "Sử dụng Telegram ID mà không cần ký hiệu @ hoặc chat IDs (bạn có thể nhận được nó ở đây @userinfobot hoặc sử dụng lệnh '/id' trong bot)"
"subscriptionDesc" = "Bạn có thể tìm liên kết đăng ký của mình trong Chi tiết, cũng như bạn có thể sử dụng cùng tên cho nhiều cấu hình khác nhau"
[pages.client]
"add" = "Thêm Client"
"edit" = "Chỉnh sửa Client"
"submitAdd" = "Thêm Client"
"submitEdit" = "Lưu thay đổi"
"clientCount" = "Số lượng Client"
"bulk" = "Thêm hàng loạt"
"method" = "Phương pháp"
"first" = "Đầu tiên"
"last" = "Cuối cùng"
"prefix" = "Tiền tố"
"postfix" = "Hậu tố"
"delayedStart" = "Bắt đầu sau khi sử dụng lần đầu"
"expireDays" = "Số ngày hết hạn"
"days" = "ngày"
[pages.inbounds.toasts]
"obtain" = "Nhận"
[pages.inbounds.stream.general]
"requestHeader" = "Tiêu đề yêu cầu"
"name" = "Tên"
"value" = "Giá trị"
[pages.inbounds.stream.tcp]
"requestVersion" = "Phiên bản yêu cầu"
"requestMethod" = "Phương thức yêu cầu"
"requestPath" = "Đường dẫn yêu cầu"
"responseVersion" = "Phiên bản phản hồi"
"responseStatus" = "Trạng thái phản hồi"
"responseStatusDescription" = "Mô tả trạng thái phản hồi"
"responseHeader" = "Tiêu đề phản hồi"
[pages.inbounds.stream.quic]
"encryption" = "Mã hóa"
[pages.settings]
"title" = "Cài đặt"
"save" = "Lưu"
"infoDesc" = "Mọi thay đổi được thực hiện ở đây cần phải được lưu. Vui lòng khởi động lại bảng điều khiển để áp dụng các thay đổi."
"restartPanel" = "Khởi động lại Bảng điều khiển"
"restartPanelDesc" = "Bạn có chắc chắn muốn khởi động lại bảng điều khiển? Nhấn OK để khởi động lại sau 3 giây. Nếu bạn không thể truy cập bảng điều khiển sau khi khởi động lại, vui lòng xem thông tin nhật ký của bảng điều khiển trên máy chủ."
"actions" = "Hành động"
"resetDefaultConfig" = "Đặt lại Cấu hình Mặc định"
"panelSettings" = "Cài đặt Bảng điều khiển"
"securitySettings" = "Cài đặt Bảo mật"
"xrayConfiguration" = "Cấu hình Xray"
"TGBotSettings" = "Cài đặt Bot Telegram"
"panelListeningIP" = "IP Nghe của Bảng điều khiển"
"panelListeningIPDesc" = "Mặc định để trống để nghe tất cả các IP."
"panelListeningDomain" = "Tên miền của nghe Bảng điều khiển"
"panelListeningDomainDesc" = "Mặc định để trống để nghe tất cả các tên miền và IP"
"panelPort" = "Cổng Bảng điều khiển"
"panelPortDesc" = "Cổng được sử dụng để hiển thị bảng điều khiển này"
"publicKeyPath" = "Đường dẫn tập tin khóa công khai Chứng chỉ Bảng điều khiển"
"publicKeyPathDesc" = "Điền vào đường dẫn tuyệt đối bắt đầu với."
"privateKeyPath" = "Đường dẫn tập tin khóa riêng tư Chứng chỉ Bảng điều khiển"
"privateKeyPathDesc" = "Điền vào đường dẫn tuyệt đối bắt đầu với."
"panelUrlPath" = "Đường dẫn gốc URL Bảng điều khiển"
"panelUrlPathDesc" = "Phải bắt đầu bằng '/' và kết thúc bằng."
"oldUsername" = "Tên người dùng hiện tại"
"currentPassword" = "Mật khẩu hiện tại"
"newUsername" = "Tên người dùng mới"
"newPassword" = "Mật khẩu mới"
"telegramBotEnable" = "Bật Bot Telegram"
"telegramBotEnableDesc" = "Kết nối với các tính năng của bảng điều khiển này thông qua bot Telegram"
"telegramToken" = "Token Telegram"
"telegramTokenDesc" = "Bạn phải nhận token từ quản lý bot Telegram @botfather"
"telegramChatId" = "Chat ID Telegram của quản trị viên"
"telegramChatIdDesc" = "Nhiều Chat ID phân tách bằng dấu phẩy. Sử dụng @userinfobot hoặc sử dụng lệnh '/id' trong bot để lấy Chat ID của bạn."
"telegramNotifyTime" = "Thời gian thông báo của bot Telegram"
"telegramNotifyTimeDesc" = "Sử dụng định dạng thời gian Crontab."
"tgNotifyBackup" = "Sao lưu Cơ sở dữ liệu"
"tgNotifyBackupDesc" = "Bao gồm tệp sao lưu cơ sở dữ liệu với thông báo báo cáo."
"tgNotifyLogin" = "Thông báo Đăng nhập"
"tgNotifyLoginDesc" = "Hiển thị tên người dùng, địa chỉ IP và thời gian khi ai đó cố gắng đăng nhập vào bảng điều khiển của bạn."
"sessionMaxAge" = "Tuổi tối đa của phiên"
"sessionMaxAgeDesc" = "Thời gian của phiên đăng nhập (đơn vị: phút)"
"expireTimeDiff" = "Ngưỡng hết hạn cho thông báo"
"expireTimeDiffDesc" = "Nhận thông báo về việc hết hạn tài khoản trước ngưỡng này (đơn vị: ngày)"
"trafficDiff" = "Ngưỡng lưu lượng cho thông báo"
"trafficDiffDesc" = "Nhận thông báo về việc cạn kiệt lưu lượng trước khi đạt đến ngưỡng này (đơn vị: GB)"
"tgNotifyCpu" = "Ngưỡng cảnh báo tỷ lệ CPU"
"tgNotifyCpuDesc" = "Nhận thông báo nếu tỷ lệ sử dụng CPU vượt quá ngưỡng này (đơn vị: %)"
"timeZone" = "Múi giờ"
"timeZoneDesc" = "Các tác vụ được lên lịch chạy theo thời gian trong múi giờ này."
"subSettings" = "Đăng ký"
"subEnable" = "Bật dịch vụ"
"subEnableDesc" = "Tính năng đăng ký với cấu hình riêng"
"subListen" = "Listening IP"
"subListenDesc" = "Mặc định để trống để nghe tất cả các IP"
"subPort" = "Cổng Đăng ký"
"subPortDesc" = "Số cổng dịch vụ đăng ký phải chưa được sử dụng trên máy chủ"
"subCertPath" = "Đường dẫn tập tin khóa công khai Chứng chỉ Đăng ký"
"subCertPathDesc" = "Điền vào đường dẫn tuyệt đối bắt đầu với '/'"
"subKeyPath" = "Đường dẫn tập tin khóa riêng tư Chứng chỉ Đăng ký"
"subKeyPathDesc" = "Điền vào đường dẫn tuyệt đối bắt đầu với '/'"
"subPath" = "Đường dẫn gốc URL Đăng ký"
"subPathDesc" = "Phải bắt đầu bằng '/' và kết thúc bằng '/'"
"subDomain" = "Tên miền con"
"subDomainDesc" = "Mặc định để trống để nghe tất cả các tên miền và IP"
"subUpdates" = "Khoảng thời gian cập nhật đăng ký"
"subUpdatesDesc" = "Số giờ giữa các cập nhật trong ứng dụng khách"
"subEncrypt" = "Mã hóa cấu hình"
"subEncryptDesc" = "Mã hóa các cấu hình được trả về trong đăng ký"
"subShowInfo" = "Hiển thị thông tin sử dụng"
"subShowInfoDesc" = "Hiển thị lưu lượng truy cập còn lại và ngày sau tên cấu hình"
[pages.settings.templates]
"title" = "Mẫu"
"basicTemplate" = "Mẫu Cơ bản"
"advancedTemplate" = "Mẫu Nâng cao"
"completeTemplate" = "Mẫu Đầy đủ"
"generalConfigs" = "Cấu hình Chung"
"generalConfigsDesc" = "Những tùy chọn này sẽ cung cấp điều chỉnh tổng quát."
"blockConfigs" = "Cấu hình Chặn"
"blockConfigsDesc" = "Những tùy chọn này sẽ ngăn người dùng kết nối đến các giao thức và trang web cụ thể."
"blockCountryConfigs" = "Cấu hình Chặn Quốc gia"
"blockCountryConfigsDesc" = "Những tùy chọn này sẽ ngăn người dùng kết nối đến các tên miền quốc gia cụ thể."
"directCountryConfigs" = "Cấu hình Kết nối Trực tiếp Quốc gia"
"directCountryConfigsDesc" = "Những tùy chọn này sẽ kết nối người dùng trực tiếp đến các tên miền quốc gia cụ thể."
"ipv4Configs" = "Cấu hình IPv4"
"ipv4ConfigsDesc" = "Những tùy chọn này sẽ chỉ định kết nối đến các tên miền mục tiêu qua IPv4."
"warpConfigs" = "Cấu hình WARP"
"warpConfigsDesc" = "Cảnh báo: Trước khi sử dụng những tùy chọn này, hãy cài đặt WARP ở chế độ proxy socks5 trên máy chủ của bạn bằng cách làm theo các bước trên GitHub của bảng điều khiển. WARP sẽ định tuyến lưu lượng đến các trang web qua máy chủ Cloudflare."
"xrayConfigTemplate" = "Mẫu Cấu hình Xray"
"xrayConfigTemplateDesc" = "Tạo tệp cấu hình Xray cuối cùng dựa trên mẫu này."
"xrayConfigFreedomStrategy" = "Cấu hình Chiến lược cho Giao thức Freedom"
"xrayConfigFreedomStrategyDesc" = "Đặt chiến lược đầu ra của mạng trong Giao thức Freedom."
"xrayConfigRoutingStrategy" = "Cấu hình Chiến lược Định tuyến Tên miền"
"xrayConfigRoutingStrategyDesc" = "Đặt chiến lược định tuyến tổng thể cho việc giải quyết DNS."
"xrayConfigTorrent" = "Cấu hình sử dụng BitTorrent"
"xrayConfigTorrentDesc" = "Thay đổi mẫu cấu hình để tránh việc người dùng sử dụng BitTorrent."
"xrayConfigPrivateIp" = "Cấm kết nối đến dải IP Riêng tư"
"xrayConfigPrivateIpDesc" = "Thay đổi mẫu cấu hình để tránh kết nối đến dải IP riêng tư."
"xrayConfigAds" = "Chặn Quảng cáo"
"xrayConfigAdsDesc" = "Thay đổi mẫu cấu hình để chặn quảng cáo."
"xrayConfigFamily" = "Chặn Phần mềm độc hại và Nội dung cho Người lớn"
"xrayConfigFamilyDesc" = "Các trình giải quyết DNS để chặn phần mềm độc hại và nội dung cho bảo vệ gia đình."
"xrayConfigSpeedtest" = "Chặn Trang web Speedtest"
"xrayConfigSpeedtestDesc" = "Thay đổi mẫu cấu hình để tránh kết nối đến các trang web Speedtest."
"xrayConfigIRIp" = "Vô hiệu hóa kết nối đến dải IP của Iran"
"xrayConfigIRIpDesc" = "Thay đổi mẫu cấu hình để tránh kết nối đến dải IP của Iran."
"xrayConfigIRDomain" = "Vô hiệu hóa kết nối đến tên miền của Iran"
"xrayConfigIRDomainDesc" = "Thay đổi mẫu cấu hình để tránh kết nối đến các tên miền của Iran."
"xrayConfigChinaIp" = "Vô hiệu hóa kết nối đến dải IP của Trung Quốc"
"xrayConfigChinaIpDesc" = "Thay đổi mẫu cấu hình để tránh kết nối đến dải IP của Trung Quốc."
"xrayConfigChinaDomain" = "Vô hiệu hóa kết nối đến tên miền của Trung Quốc"
"xrayConfigChinaDomainDesc" = "Thay đổi mẫu cấu hình để tránh kết nối đến các tên miền của Trung Quốc."
"xrayConfigRussiaIp" = "Vô hiệu hóa kết nối đến dải IP của Nga"
"xrayConfigRussiaIpDesc" = "Thay đổi mẫu cấu hình để tránh kết nối đến dải IP của Nga."
"xrayConfigRussiaDomain" = "Vô hiệu hóa kết nối đến tên miền của Nga"
"xrayConfigRussiaDomainDesc" = "Thay đổi mẫu cấu hình để tránh kết nối đến các tên miền của Nga."
"xrayConfigDirectIRIp" = "Kết nối trực tiếp đến dải IP của Iran"
"xrayConfigDirectIRIpDesc" = "Thay đổi mẫu cấu hình cho kết nối trực tiếp đến dải IP của Iran."
"xrayConfigDirectIRDomain" = "Kết nối trực tiếp đến tên miền của Iran"
"xrayConfigDirectIRDomainDesc" = "Thay đổi mẫu cấu hình cho kết nối trực tiếp đến các tên miền của Iran."
"xrayConfigDirectChinaIp" = "Kết nối trực tiếp đến dải IP của Trung Quốc"
"xrayConfigDirectChinaIpDesc" = "Thay đổi mẫu cấu hình cho kết nối trực tiếp đến dải IP của Trung Quốc."
"xrayConfigDirectChinaDomain" = "Kết nối trực tiếp đến tên miền của Trung Quốc"
"xrayConfigDirectChinaDomainDesc" = "Thay đổi mẫu cấu hình cho kết nối trực tiếp đến các tên miền của Trung Quốc."
"xrayConfigDirectRussiaIp" = "Kết nối trực tiếp đến dải IP của Nga"
"xrayConfigDirectRussiaIpDesc" = "Thay đổi mẫu cấu hình cho kết nối trực tiếp đến dải IP của Nga."
"xrayConfigDirectRussiaDomain" = "Kết nối trực tiếp đến tên miền của Nga"
"xrayConfigDirectRussiaDomainDesc" = "Thay đổi mẫu cấu hình cho kết nối trực tiếp đến các tên miền của Nga."
"xrayConfigGoogleIPv4" = "Sử dụng IPv4 cho Google"
"xrayConfigGoogleIPv4Desc" = "Thêm định tuyến cho Google để kết nối qua IPv4."
"xrayConfigNetflixIPv4" = "Sử dụng IPv4 cho Netflix"
"xrayConfigNetflixIPv4Desc" = "Thêm định tuyến cho Netflix để kết nối qua IPv4."
"xrayConfigGoogleWARP" = "Định tuyến Google qua WARP."
"xrayConfigGoogleWARPDesc" = "Thêm định tuyến cho Google qua WARP."
"xrayConfigOpenAIWARP" = "Định tuyến OpenAI (ChatGPT) qua WARP."
"xrayConfigOpenAIWARPDesc" = "Thêm định tuyến cho OpenAI (ChatGPT) qua WARP."
"xrayConfigNetflixWARP" = "Định tuyến Netflix qua WARP."
"xrayConfigNetflixWARPDesc" = "Thêm định tuyến cho Netflix qua WARP."
"xrayConfigSpotifyWARP" = "Định tuyến Spotify qua WARP."
"xrayConfigSpotifyWARPDesc" = "Thêm định tuyến cho Spotify qua WARP."
"xrayConfigIRWARP" = "Định tuyến tên miền của Iran qua WARP."
"xrayConfigIRWARPDesc" = "Thêm định tuyến cho các tên miền của Iran qua WARP."
"xrayConfigInbounds" = "Cấu hình của Inbounds"
"xrayConfigInboundsDesc" = "Thay đổi mẫu cấu hình để chấp nhận các máy khách cụ thể."
"xrayConfigOutbounds" = "Cấu hình của Outbounds"
"xrayConfigOutboundsDesc" = "Thay đổi mẫu cấu hình để xác định các cách ra đi cho máy chủ này."
"xrayConfigRoutings" = "Cấu hình của Luật Định tuyến."
"xrayConfigRoutingsDesc" = "Thay đổi mẫu cấu hình để xác định luật định tuyến cho máy chủ này."
"manualLists" = "Danh sách Thủ công"
"manualListsDesc" = "Vui lòng sử dụng định dạng mảng JSON."
"manualBlockedIPs" = "Danh sách IP bị Chặn"
"manualBlockedDomains" = "Danh sách Tên miền bị Chặn"
"manualDirectIPs" = "Danh sách IP Trực tiếp"
"manualDirectDomains" = "Danh sách Tên miền Trực tiếp"
"manualIPv4Domains" = "Danh sách Tên miền IPv4"
"manualWARPDomains" = "Danh sách Tên miền WARP"
[pages.settings.security]
"admin" = "Quản trị viên"
"secret" = "Mã thông báo bí mật"
"loginSecurity" = "Bảo mật đăng nhập"
"loginSecurityDesc" = "Bật bước bảo mật đăng nhập bổ sung cho người dùng"
"secretToken" = "Mã thông báo bí mật"
"secretTokenDesc" = "Vui lòng sao chép và lưu trữ mã thông báo này một cách an toàn ở nơi an toàn. Mã thông báo này cần thiết để đăng nhập và không thể phục hồi từ công cụ lệnh x-ui."
[pages.settings.toasts]
"modifySettings" = "Chỉnh sửa cài đặt "
"getSettings" = "Lấy cài đặt "
"modifyUser" = "Chỉnh sửa người dùng "
"originalUserPassIncorrect" = "Tên người dùng hoặc mật khẩu gốc không đúng"
"userPassMustBeNotEmpty" = "Tên người dùng mới và mật khẩu mới không thể để trống"
[tgbot]
"keyboardClosed" = "❌ Bàn phím tùy chỉnh đã đóng!"
"noResult" = "❗ Không có kết quả!"
"noQuery" = "❌ Không tìm thấy truy vấn! Vui lòng sử dụng lệnh lại!"
"wentWrong" = "❌ Đã xảy ra lỗi!"
"noIpRecord" = "❗ Không có bản ghi IP!"
"noInbounds" = "❗ Không tìm thấy inbound!"
"unlimited" = "♾ Không giới hạn"
"month" = "Tháng"
"months" = "Tháng"
"day" = "Ngày"
"days" = "Ngày"
"hours" = "Giờ"
"unknown" = "Không rõ"
"inbounds" = "Inbounds"
"clients" = "Khách hàng"
[tgbot.commands]
"unknown" = "❗ Lệnh không rõ"
"pleaseChoose" = "👇 Vui lòng chọn:\r\n"
"help" = "🤖 Chào mừng bạn đến với bot này! Bot được thiết kế để cung cấp cho bạn dữ liệu cụ thể từ máy chủ và cho phép bạn thực hiện các thay đổi cần thiết.\r\n\r\n"
"start" = "👋 Xin chào <i>{{ .Firstname }}</i>.\r\n"
"welcome" = "🤖 Chào mừng đến với bot quản lý của <b>{{ .Hostname }}</b>.\r\n"
"status" = "✅ Bot hoạt động bình thường!"
"usage" = "❗ Vui lòng cung cấp văn bản để tìm kiếm!"
"getID" = "🆔 ID của bạn: <code>{{ .ID }}</code>"
"helpAdminCommands" = "Tìm kiếm email của khách hàng:\r\n<code>/usage [Email]</code>\r\n \r\nTìm kiếm inbounds (với thống kê của khách hàng):\r\n<code>/inbound [Ghi chú]</code>"
"helpClientCommands" = "Để tìm kiếm thống kê, hãy sử dụng lệnh sau:\r\n \r\n<code>/usage [UUID|Mật khẩu]</code>\r\n \r\nSử dụng UUID cho vmess/vless và Mật khẩu cho Trojan."
[tgbot.messages]
"cpuThreshold" = "🔴 Sử dụng CPU {{ .Percent }}% vượt quá ngưỡng {{ .Threshold }}%"
"selectUserFailed" = "❌ Lỗi khi chọn người dùng!"
"userSaved" = "✅ Người dùng Telegram đã được lưu."
"loginSuccess" = "✅ Đăng nhập thành công vào bảng điều khiển.\r\n"
"loginFailed" = "❗️ Đăng nhập vào bảng điều khiển thất bại.\r\n"
"report" = "🕰 Báo cáo định kỳ: {{ .RunTime }}\r\n"
"datetime" = "⏰ Ngày-Giờ: {{ .DateTime }}\r\n"
"hostname" = "💻 Tên máy chủ: {{ .Hostname }}\r\n"
"version" = "🚀 Phiên bản X-UI: {{ .Version }}\r\n"
"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n"
"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n"
"ip" = "🌐 IP: {{ .IP }}\r\n"
"ips" = "🔢 Các IP: \r\n{{ .IPs }}\r\n"
"serverUpTime" = "⏳ Thời gian hoạt động của máy chủ: {{ .UpTime }} {{ .Unit }}\r\n"
"serverLoad" = "📈 Tải máy chủ: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
"serverMemory" = "📋 Bộ nhớ máy chủ: {{ .Current }}/{{ .Total }}\r\n"
"tcpCount" = "🔹 Số lượng kết nối TCP: {{ .Count }}\r\n"
"udpCount" = "🔸 Số lượng kết nối UDP: {{ .Count }}\r\n"
"traffic" = "🚦 Lưu lượng: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
"xrayStatus" = " Trạng thái Xray: {{ .State }}\r\n"
"username" = "👤 Tên người dùng: {{ .Username }}\r\n"
"time" = "⏰ Thời gian: {{ .Time }}\r\n"
"inbound" = "📍 Inbound: {{ .Remark }}\r\n"
"port" = "🔌 Cổng: {{ .Port }}\r\n"
"expire" = "📅 Ngày hết hạn: {{ .Time }}\r\n"
"expireIn" = "📅 Hết hạn sau: {{ .Time }}\r\n"
"active" = "💡 Hoạt động: ✅ Có\r\n"
"inactive" = "💡 Hoạt động: ❌ Không\r\n"
"email" = "📧 Email: {{ .Email }}\r\n"
"upload" = "🔼 Tải lên: ↑{{ .Upload }}\r\n"
"download" = "🔽 Tải xuống: ↓{{ .Download }}\r\n"
"total" = "📊 Tổng cộng: ↑↓{{ .UpDown }} / {{ .Total }}\r\n"
"TGUser" = "👤 Người dùng Telegram: {{ .TelegramID }}\r\n"
"exhaustedMsg" = "🚨 Sự cạn kiệt {{ .Type }}:\r\n"
"exhaustedCount" = "🚨 Số lần cạn kiệt {{ .Type }}:\r\n"
"disabled" = "🛑 Vô hiệu hóa: {{ .Disabled }}\r\n"
"depleteSoon" = "🔜 Sắp cạn kiệt: {{ .Deplete }}\r\n \r\n"
"backupTime" = "🗄 Thời gian sao lưu: {{ .Time }}\r\n"
"refreshedOn" = "\r\n📋🔄 Đã cập nhật lần cuối vào: {{ .Time }}\r\n \r\n"
[tgbot.buttons]
"closeKeyboard" = "❌ Đóng Bàn Phím"
"cancel" = "❌ Hủy"
"cancelReset" = "❌ Hủy Đặt Lại"
"cancelIpLimit" = "❌ Hủy Giới Hạn IP"
"confirmResetTraffic" = "✅ Xác Nhận Đặt Lại Lưu Lượng?"
"confirmClearIps" = "✅ Xác Nhận Xóa Các IP?"
"confirmRemoveTGUser" = "✅ Xác Nhận Xóa Người Dùng Telegram?"
"dbBackup" = "Tải Backup DB"
"serverUsage" = "Sử Dụng Máy Chủ"
"getInbounds" = "Lấy Inbounds"
"depleteSoon" = "Sắp Cạn Kiệt"
"clientUsage" = "Lấy Sử Dụng"
"commands" = "Lệnh"
"refresh" = "🔄 Cập Nhật"
"clearIPs" = "❌ Xóa IP"
"removeTGUser" = "❌ Xóa Người Dùng Telegram"
"selectTGUser" = "👤 Chọn Người Dùng Telegram"
"selectOneTGUser" = "👤 Chọn một người dùng telegram:"
"resetTraffic" = "📈 Đặt Lại Lưu Lượng"
"resetExpire" = "📅 Đặt Lại Ngày Hết Hạn"
"ipLog" = "🔢 Log IP"
"ipLimit" = "🔢 Giới Hạn IP"
"setTGUser" = "👤 Đặt Người Dùng Telegram"
"toggle" = "🔘 Bật / Tắt"
[tgbot.answers]
"errorOperation" = "❗ Lỗi Trong Quá Trình Thực Hiện."
"getInboundsFailed" = "❌ Không Thể Lấy Được Inbounds"
"canceled" = "❌ {{ .Email }} : Thao Tác Đã Bị Hủy."
"clientRefreshSuccess" = "✅ {{ .Email }} : Cập Nhật Thành Công Cho Khách Hàng."
"IpRefreshSuccess" = "✅ {{ .Email }} : Cập Nhật Thành Công Cho IPs."
"TGIdRefreshSuccess" = "✅ {{ .Email }} : Cập Nhật Thành Công Cho Người Dùng Telegram."
"resetTrafficSuccess" = "✅ {{ .Email }} : Đặt Lại Lưu Lượng Thành Công."
"expireResetSuccess" = "✅ {{ .Email }} : Đặt Lại Ngày Hết Hạn Thành Công."
"resetIpSuccess" = "✅ {{ .Email }} : Giới Hạn IP {{ .Count }} Đã Được Lưu Thành Công."
"clearIpSuccess" = "✅ {{ .Email }} : IPs Đã Được Xóa Thành Công."
"getIpLog" = "✅ {{ .Email }} : Lấy Log IP Thành Công."
"getUserInfo" = "✅ {{ .Email }} : Lấy Thông Tin Người Dùng Telegram Thành Công."
"removedTGUserSuccess" = "✅ {{ .Email }} : Người Dùng Telegram Đã Được Xóa Thành Công."
"enableSuccess" = "✅ {{ .Email }} : Đã Bật Thành Công."
"disableSuccess" = "✅ {{ .Email }} : Đã Tắt Thành Công."
"askToAddUserId" = "Cấu hình của bạn không được tìm thấy!\r\nVui lòng yêu cầu Quản trị viên sử dụng ID người dùng telegram của bạn trong cấu hình của bạn.\r\n\r\nID người dùng của bạn: <b>{{ .TgUserID }}</b>"
"askToAddUserName" = "Cấu hình của bạn không được tìm thấy!\r\nVui lòng yêu cầu Quản trị viên sử dụng tên người dùng hoặc ID người dùng telegram của bạn trong cấu hình của bạn.\r\n\r\nTên người dùng của bạn: <b>@{{ .TgUserName }}</b>\r\n\r\nID người dùng của bạn: <b>{{ .TgUserID }}</b>"

View File

@@ -11,7 +11,7 @@
"enable" = "启用"
"protocol" = "协议"
"search" = "搜尋"
"filter" = "过滤器"
"loading" = "加载中"
"second" = "秒"
"minute" = "分钟"
@@ -39,7 +39,6 @@
"depleted" = "耗尽"
"depletingSoon" = "即将耗尽"
"domainName" = "域名"
"additional" = "额外"
"monitor" = "监听"
"certificate" = "证书"
"fail" = "失败"
@@ -49,6 +48,7 @@
"clients" = "客户端"
"usage" = "用法"
"secretToken" = "秘密令牌"
"remained" = "仍然存在"
[menu]
"dashboard" = "系统状态"
@@ -72,17 +72,18 @@
"title" = "系统状态"
"memory" = "内存"
"hard" = "硬盘"
"xrayStatus" = "xray 状态"
"xrayStatus" = "状态"
"stopXray" = "停止"
"restartXray" = "重启"
"xraySwitch" = "切换版本"
"xraySwitchClick" = "点击你想切换的版本"
"xraySwitchClickDesk" = "请谨慎选择,旧版本可能配置不兼容"
"operationHours" = "运行时间"
"operationHoursDesc" = "系统自启动以来的运行时间"
"operationHours" = "系统正常运行时间"
"systemLoad" = "系统负载"
"systemLoadDesc" = "过去 1、5 和 15 分钟的系统平均负载"
"connectionTcpCountDesc" = "所有网卡的总 TCP 连接数。"
"connectionUdpCountDesc" = "所有网卡的总 UDP 连接数。"
"connectionCount" = "连接数"
"connectionCountDesc" = "所有网卡的总连接数"
"upSpeed" = "所有网卡的总上传速度"
"downSpeed" = "所有网卡的总下载速度"
"totalSent" = "系统启动以来所有网卡的总上传流量"
@@ -126,7 +127,6 @@
"network" = "网络"
"destinationPort" = "目标端口"
"targetAddress" = "目标地址"
"disableInsecureEncryption" = "禁用不安全加密"
"monitorDesc" = "默认留空即可"
"meansNoLimit" = "表示不限制"
"totalFlow" = "总流量"
@@ -167,7 +167,7 @@
"setDefaultCert" = "从面板设置证书"
"xtlsDesc" = "Xray核心需要1.7.5"
"realityDesc" = "Xray核心需要1.8.0及以上版本"
"telegramDesc" = "使用不带@的电报 ID 或聊天 ID可以在此处获取 @userinfobot"
"telegramDesc" = "使用 Telegram ID不包含 @ 符号或聊天 ID可以在 @userinfobot 处获取,或在机器人中使用'/id'命令"
"subscriptionDesc" = "您可以在详细信息上找到您的子链接,也可以对多个配置使用相同的名称"
[pages.client]
@@ -209,6 +209,7 @@
[pages.settings]
"title" = "设置"
"save" = "保存配置"
"infoDesc" = "此处的所有更改都需要保存并重启面板才能生效"
"restartPanel" = "重启面板"
"restartPanelDesc" = "确定要重启面板吗?点击确定将于 3 秒后重启,若重启后无法访问面板,请前往服务器查看面板日志信息"
"actions" = "动作"
@@ -218,15 +219,17 @@
"xrayConfiguration" = "xray 相关设置"
"TGBotSettings" = "TG提醒相关设置"
"panelListeningIP" = "面板监听 IP"
"panelListeningIPDesc" = "默认留空监听所有 IP,重启面板生效"
"panelListeningIPDesc" = "默认留空监听所有 IP"
"panelListeningDomain" = "面板监听域名"
"panelListeningDomainDesc" = "默认情况下留空以监视所有域名和 IP 地址"
"panelPort" = "面板监听端口"
"panelPortDesc" = "重启面板生效"
"publicKeyPath" = "面板证书公钥文件路径"
"publicKeyPathDesc" = "填写一个 '/' 开头的绝对路径,重启面板生效"
"publicKeyPathDesc" = "填写一个 '/' 开头的绝对路径"
"privateKeyPath" = "面板证书密钥文件路径"
"privateKeyPathDesc" = "填写一个 '/' 开头的绝对路径,重启面板生效"
"privateKeyPathDesc" = "填写一个 '/' 开头的绝对路径"
"panelUrlPath" = "面板 url 根路径"
"panelUrlPathDesc" = "必须以 '/' 开头,以 '/' 结尾,重启面板生效"
"panelUrlPathDesc" = "必须以 '/' 开头,以 '/' 结尾"
"oldUsername" = "原用户名"
"currentPassword" = "原密码"
"newUsername" = "新用户名"
@@ -236,11 +239,13 @@
"telegramToken" = "电报机器人TOKEN"
"telegramTokenDesc" = "重启面板生效"
"telegramChatId" = "以逗号分隔的多个 chatID 重启面板生效"
"telegramChatIdDesc" = "多个聊天 ID 逗号分隔。使用@userinfobot 获取您的聊天 ID。重新启动面板以应用更改。"
"telegramChatIdDesc" = "多个聊天 ID 逗号分隔。使用 @userinfobot 或在机器人中使用'/id'命令获取您的聊天 ID。"
"telegramNotifyTime" = "电报机器人通知时间"
"telegramNotifyTimeDesc" = "采用Crontab定时格式,重启面板生效"
"tgNotifyBackup" = "数据库备份"
"tgNotifyBackupDesc" = "正在发送数据库备份文件和报告通知。重启面板生效"
"tgNotifyBackupDesc" = "正在发送数据库备份文件和报告通知"
"tgNotifyLogin" = "登录通知"
"tgNotifyLoginDesc" = "当有人试图登录您的面板时显示用户名、IP 地址和时间"
"sessionMaxAge" = "会话最大年龄"
"sessionMaxAgeDesc" = "您可以保持登录状态的时间(单位:分钟)"
"expireTimeDiff" = "耗尽时间阈值"
@@ -250,65 +255,114 @@
"tgNotifyCpu" = "CPU 百分比警报阈值"
"tgNotifyCpuDesc" = "如果 CPU 使用率超过此百分比(单位:%),此 talegram bot 将向您发送通知"
"timeZone" = "时区"
"timeZoneDesc" = "定时任务按照该时区的时间运行,重启面板生效"
"timeZoneDesc" = "定时任务按照该时区的时间运行"
"subSettings" = "订阅"
"subEnable" = "启用服务"
"subEnableDesc" = "具有单独配置的订阅功能"
"subListen" = "监听IP"
"subListenDesc" = "留空默认监听所有IP"
"subPort" = "订阅端口"
"subPortDesc" = "服务订阅服务的端口号必须在服务器中未使用"
"subCertPath" = "订阅证书公钥文件路径"
"subCertPathDesc" = "填写以'/'开头的绝对路径"
"subKeyPath" = "订阅证书私钥文件路径"
"subKeyPathDesc" = "填写以'/'开头的绝对路径"
"subPath" = "订阅 URL 根路径"
"subPathDesc" = "必须以'/'开始并以'/'结束"
"subDomain" = "监听域"
"subDomainDesc" = "留空默认监控所有域名和IP"
"subUpdates" = "订阅更新间隔"
"subUpdatesDesc" = "客户端应用程序更新之间的间隔时间"
"subEncrypt" = "加密配置"
"subEncryptDesc" = "在订阅中加密返回的配置"
"subShowInfo" = "显示使用信息"
"subShowInfoDesc" = "在配置名称后显示剩余流量和日期"
[pages.settings.templates]
"title" = "模板"
"basicTemplate" = "基本模板"
"advancedTemplate" = "高级模板部件"
"completeTemplate" = "Xray 配置的完整模板"
"generalConfigs" = "一般配置"
"generalConfigsDesc" = "选项将阻止用户连接到特定协议和网站。"
"countryConfigs" = "国家配置"
"countryConfigsDesc" = "选项将阻止用户连接到特定国家/地区的域。"
"generalConfigs" = "通用配置"
"generalConfigsDesc" = "这些选项将提供一般调整"
"blockConfigs" = "阻塞配置"
"blockConfigsDesc" = "这些选项将阻止用户连接到特定协议和网站"
"blockCountryConfigs" = "阻止国家配置"
"blockCountryConfigsDesc" = "这些选项将阻止用户连接到特定国家/地区的域。"
"directCountryConfigs" = "直接国家配置"
"directCountryConfigsDesc" = "这些选项会将用户直接连接到特定国家/地区的域。"
"ipv4Configs" = "IPv4 配置"
"ipv4ConfigsDesc" = "此选项将仅通过 IPv4 路由到目标域"
"ipv4ConfigsDesc" = "此选项将仅通过 IPv4 路由到目标域"
"warpConfigs" = "WARP 配置"
"warpConfigsDesc" = "警告:在使用选项之前,请按照面板 GitHub 上的步骤在您的服务器上以 socks5 代理模式安装 WARP。 WARP 将通过 Cloudflare 服务器将流量路由到网站。"
"xrayConfigTemplate" = "xray 配置模板"
"xrayConfigTemplateDesc" = "以该模型为基础生成最终的xray配置文件重新启动面板生成效率"
"warpConfigsDesc" = "注意:在使用这些选项之前,请按照面板 GitHub 上的步骤在您的服务器上以 socks5 代理模式安装 WARP。 WARP 将通过 Cloudflare 服务器将流量路由到网站。"
"xrayConfigTemplate" = "Xray 配置模板"
"xrayConfigTemplateDesc" = "以该模型为基础生成最终的Xray配置文件重新启动面板生成效率"
"xrayConfigFreedomStrategy" = "配置自由协议的策略"
"xrayConfigFreedomStrategyDesc" = "在自由协议中设置网络输出策略"
"xrayConfigRoutingStrategy" = "配置路由域策略"
"xrayConfigRoutingStrategyDesc" = "设置DNS解析的整体路由策略"
"xrayConfigTorrent" = "禁止使用 bittorrent"
"xrayConfigTorrentDesc" = "更改配置模板避免用户使用bittorrent,重启面板生效"
"xrayConfigTorrentDesc" = "更改配置模板避免用户使用bittorrent"
"xrayConfigPrivateIp" = "禁止私人 IP 范围连接"
"xrayConfigPrivateIpDesc" = "更改配置模板以避免连接私有 IP 范围,重启面板生效"
"xrayConfigPrivateIpDesc" = "更改配置模板以避免连接私有 IP 范围"
"xrayConfigAds" = "屏蔽广告"
"xrayConfigAdsDesc" = "修改配置模板屏蔽广告,重启面板生效"
"xrayConfigPorn" = "禁止色情网站连接"
"xrayConfigPornDesc" = "更改配置模板避免连接色情网站,重启面板生效"
"xrayConfigAdsDesc" = "修改配置模板屏蔽广告"
"xrayConfigFamily" = "启用家庭友好配置"
"xrayConfigFamilyDesc" = "避免为家人连接到不安全的网站"
"xrayConfigSpeedtest" = "阻止测速网站"
"xrayConfigSpeedtestDesc" = "更改配置模板以避免连接到速度测试网站。 重新启动面板以应用更改。"
"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" = "直接连接到伊朗域"
"xrayConfigDirectIRDomainDesc" = "更改直接连接到伊朗域的配置模板"
"xrayConfigDirectChinaIp" = "直连中国IP范围"
"xrayConfigDirectChinaIpDesc" = "更改直连中国 IP 范围的配置模板"
"xrayConfigDirectChinaDomain" = "直连中国域名"
"xrayConfigDirectChinaDomainDesc" = "修改中国域名直连配置模板"
"xrayConfigDirectRussiaIp" = "直接连接到俄罗斯 IP 范围"
"xrayConfigDirectRussiaIpDesc" = "更改直接连接到俄罗斯 IP 范围的配置模板"
"xrayConfigDirectRussiaDomain" = "直接连接到俄罗斯域"
"xrayConfigDirectRussiaDomainDesc" = "更改直接连接到俄罗斯域的配置模板"
"xrayConfigGoogleIPv4" = "为谷歌使用 IPv4"
"xrayConfigGoogleIPv4Desc" = "添加谷歌连接IPv4的路由,重启面板生效"
"xrayConfigGoogleIPv4Desc" = "添加谷歌连接IPv4的路由"
"xrayConfigNetflixIPv4" = "为 Netflix 使用 IPv4"
"xrayConfigNetflixIPv4Desc" = "添加Netflix连接IPv4的路由,重启面板生效"
"xrayConfigNetflixIPv4Desc" = "添加Netflix连接IPv4的路由"
"xrayConfigGoogleWARP" = "将谷歌路由到 WARP"
"xrayConfigGoogleWARPDesc" = "为谷歌添加路由到WARP,重启面板生效"
"xrayConfigGoogleWARPDesc" = "为谷歌添加路由到WARP"
"xrayConfigOpenAIWARP" = "将 OpenAI (ChatGPT) 路由到 WARP"
"xrayConfigOpenAIWARPDesc" = "将OpenAIChatGPT路由添加到WARP,重启面板生效"
"xrayConfigOpenAIWARPDesc" = "将OpenAIChatGPT路由添加到WARP"
"xrayConfigNetflixWARP" = "将 Netflix 路由到 WARP"
"xrayConfigNetflixWARPDesc" = "为Netflix添加路由到WARP,重启面板生效"
"xrayConfigNetflixWARPDesc" = "为Netflix添加路由到WARP"
"xrayConfigSpotifyWARP" = "将 Spotify 路由到 WARP"
"xrayConfigSpotifyWARPDesc" = "为Spotify添加路由到WARP,重启面板生效"
"xrayConfigSpotifyWARPDesc" = "为Spotify添加路由到WARP"
"xrayConfigIRWARP" = "将伊朗域名路由到 WARP"
"xrayConfigIRWARPDesc" = "将伊朗域的路由添加到 WARP。 重启面板生效"
"xrayConfigInbounds" = "入站配置"
"xrayConfigInboundsDesc" = "更改配置模板接受特殊客户端,重启面板生效"
"xrayConfigInboundsDesc" = "更改配置模板接受特殊客户端"
"xrayConfigOutbounds" = "出站配置"
"xrayConfigOutboundsDesc" = "更改配置模板定义此服务器的传出方式,重启面板生效"
"xrayConfigOutboundsDesc" = "更改配置模板定义此服务器的传出方式"
"xrayConfigRoutings" = "路由规则配置"
"xrayConfigRoutingsDesc" = "更改配置模板为该服务器定义路由规则,重启面板生效"
"xrayConfigRoutingsDesc" = "更改配置模板为该服务器定义路由规则"
"manualLists" = "手动列表"
"manualListsDesc" = "请使用 JSON 数组格式"
"manualBlockedIPs" = "被阻止的 IP 列表"
"manualBlockedDomains" = "被阻止的域列表"
"manualDirectIPs" = "直接 IP 列表"
"manualDirectDomains" = "直接域列表"
"manualIPv4Domains" = "IPv4 域名列表"
"manualWARPDomains" = "WARP域名列表"
[pages.settings.security]
"admin" = "行政"
@@ -324,3 +378,118 @@
"modifyUser" = "修改用户"
"originalUserPassIncorrect" = "原用户名或原密码错误"
"userPassMustBeNotEmpty" = "新用户名和新密码不能为空"
[tgbot]
"keyboardClosed" = "❌ 自定义键盘已关闭!"
"noResult" = "❗ 没有结果!"
"noQuery" = "❌ 未找到查询!请重新使用命令!"
"wentWrong" = "❌ 出了点问题!"
"noIpRecord" = "❗ 没有IP记录"
"noInbounds" = "❗ 没有找到入站连接!"
"unlimited" = "♾ 无限制"
"month" = "月"
"months" = "月"
"day" = "天"
"days" = "天"
"hours" = "小时"
"unknown" = "未知"
"inbounds" = "入站连接"
"clients" = "客户端"
[tgbot.commands]
"unknown" = "❗ 未知命令"
"pleaseChoose" = "👇 请选择:\r\n"
"help" = "🤖 欢迎使用本机器人!它旨在为您提供来自服务器的特定数据,并允许您进行必要的修改。\r\n\r\n"
"start" = "👋 你好,<i>{{ .Firstname }}</i>。\r\n"
"welcome" = "🤖 欢迎来到<b>{{ .Hostname }}</b>管理机器人。\r\n"
"status" = "✅ 机器人正常运行!"
"usage" = "❗ 请输入要搜索的文本!"
"getID" = "🆔 您的ID为<code>{{ .ID }}</code>"
"helpAdminCommands" = "搜索客户端邮箱:\r\n<code>/usage [Email]</code>\r\n \r\n搜索入站连接包含客户端统计信息\r\n<code>/inbound [Remark]</code>"
"helpClientCommands" = "要搜索统计信息,请使用以下命令:\r\n \r\n<code>/usage [UUID|Password]</code>\r\n \r\n对于vmess/vless请使用UUID对于Trojan请使用密码。"
[tgbot.messages]
"cpuThreshold" = "🔴 CPU 使用率为 {{ .Percent }}%,超过阈值 {{ .Threshold }}%"
"selectUserFailed" = "❌ 用户选择错误!"
"userSaved" = "✅ 电报用户已保存。"
"loginSuccess" = "✅ 成功登录到面板。\r\n"
"loginFailed" = "❗️ 面板登录失败。\r\n"
"report" = "🕰 定时报告:{{ .RunTime }}\r\n"
"datetime" = "⏰ 日期时间:{{ .DateTime }}\r\n"
"hostname" = "💻 主机名:{{ .Hostname }}\r\n"
"version" = "🚀 X-UI 版本:{{ .Version }}\r\n"
"ipv6" = "🌐 IPv6{{ .IPv6 }}\r\n"
"ipv4" = "🌐 IPv4{{ .IPv4 }}\r\n"
"ip" = "🌐 IP{{ .IP }}\r\n"
"ips" = "🔢 IP 地址:\r\n{{ .IPs }}\r\n"
"serverUpTime" = "⏳ 服务器运行时间:{{ .UpTime }} {{ .Unit }}\r\n"
"serverLoad" = "📈 服务器负载:{{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
"serverMemory" = "📋 服务器内存:{{ .Current }}/{{ .Total }}\r\n"
"tcpCount" = "🔹 TCP 连接数:{{ .Count }}\r\n"
"udpCount" = "🔸 UDP 连接数:{{ .Count }}\r\n"
"traffic" = "🚦 流量:{{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
"xrayStatus" = " Xray 状态:{{ .State }}\r\n"
"username" = "👤 用户名:{{ .Username }}\r\n"
"time" = "⏰ 时间:{{ .Time }}\r\n"
"inbound" = "📍 入站:{{ .Remark }}\r\n"
"port" = "🔌 端口:{{ .Port }}\r\n"
"expire" = "📅 过期日期:{{ .Time }}\r\n"
"expireIn" = "📅 剩余时间:{{ .Time }}\r\n"
"active" = "💡 激活:✅\r\n"
"inactive" = "💡 激活: ❌\r\n"
"email" = "📧 邮箱:{{ .Email }}\r\n"
"upload" = "🔼 上传↑:{{ .Upload }}\r\n"
"download" = "🔽 下载↓:{{ .Download }}\r\n"
"total" = "📊 总计:{{ .UpDown }} / {{ .Total }}\r\n"
"TGUser" = "👤 电报用户:{{ .TelegramID }}\r\n"
"exhaustedMsg" = "🚨 耗尽的{{ .Type }}\r\n"
"exhaustedCount" = "🚨 耗尽的{{ .Type }}数量:\r\n"
"disabled" = "🛑 禁用:{{ .Disabled }}\r\n"
"depleteSoon" = "🔜 即将耗尽:{{ .Deplete }}\r\n \r\n"
"backupTime" = "🗄 备份时间:{{ .Time }}\r\n"
"refreshedOn" = "\r\n📋🔄 刷新时间:{{ .Time }}\r\n \r\n"
[tgbot.buttons]
"closeKeyboard" = "❌ 关闭键盘"
"cancel" = "❌ 取消"
"cancelReset" = "❌ 取消重置"
"cancelIpLimit" = "❌ 取消 IP 限制"
"confirmResetTraffic" = "✅ 确认重置流量?"
"confirmClearIps" = "✅ 确认清除 IP"
"confirmRemoveTGUser" = "✅ 确认移除 Telegram 用户?"
"dbBackup" = "获取数据库备份"
"serverUsage" = "服务器使用情况"
"getInbounds" = "获取入站信息"
"depleteSoon" = "即将耗尽"
"clientUsage" = "获取使用情况"
"commands" = "命令"
"refresh" = "🔄 刷新"
"clearIPs" = "❌ 清除 IP"
"removeTGUser" = "❌ 移除 Telegram 用户"
"selectTGUser" = "👤 选择 Telegram 用户"
"selectOneTGUser" = "👤 选择一个 Telegram 用户:"
"resetTraffic" = "📈 重置流量"
"resetExpire" = "📅 重置过期天数"
"ipLog" = "🔢 IP 日志"
"ipLimit" = "🔢 IP 限制"
"setTGUser" = "👤 设置 Telegram 用户"
"toggle" = "🔘 启用/禁用"
[tgbot.answers]
"errorOperation" = "❗ 操作错误。"
"getInboundsFailed" = "❌ 获取入站信息失败。"
"canceled" = "❌ {{ .Email }}:操作已取消。"
"clientRefreshSuccess" = "✅ {{ .Email }}:客户端刷新成功。"
"IpRefreshSuccess" = "✅ {{ .Email }}IP 刷新成功。"
"TGIdRefreshSuccess" = "✅ {{ .Email }}:客户端的 Telegram 用户刷新成功。"
"resetTrafficSuccess" = "✅ {{ .Email }}:流量已重置成功。"
"expireResetSuccess" = "✅ {{ .Email }}:过期天数已重置成功。"
"resetIpSuccess" = "✅ {{ .Email }}:成功保存 IP 限制数量为 {{ .Count }}。"
"clearIpSuccess" = "✅ {{ .Email }}IP 已成功清除。"
"getIpLog" = "✅ {{ .Email }}:获取 IP 日志。"
"getUserInfo" = "✅ {{ .Email }}:获取 Telegram 用户信息。"
"removedTGUserSuccess" = "✅ {{ .Email }}Telegram 用户已成功移除。"
"enableSuccess" = "✅ {{ .Email }}:已成功启用。"
"disableSuccess" = "✅ {{ .Email }}:已成功禁用。"
"askToAddUserId" = "未找到您的配置!\r\n请向管理员询问在您的配置中使用您的 Telegram 用户ID。\r\n\r\n您的用户ID<b>{{ .TgUserID }}</b>"
"askToAddUserName" = "未找到您的配置!\r\n请向管理员询问在您的配置中使用您的 Telegram 用户名或用户ID。\r\n\r\n您的用户名<b>@{{ .TgUserName }}</b>\r\n\r\n您的用户ID<b>{{ .TgUserID }}</b>"

View File

@@ -18,16 +18,15 @@ import (
"x-ui/util/common"
"x-ui/web/controller"
"x-ui/web/job"
"x-ui/web/locale"
"x-ui/web/middleware"
"x-ui/web/network"
"x-ui/web/service"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
sessions "github.com/Calidity/gin-sessions"
"github.com/Calidity/gin-sessions/cookie"
"github.com/gin-gonic/gin"
"github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/pelletier/go-toml/v2"
"github.com/robfig/cron/v3"
"golang.org/x/text/language"
)
//go:embed assets/*
@@ -85,7 +84,6 @@ type Server struct {
server *controller.ServerController
panel *controller.XUIController
api *controller.APIController
sub *controller.SUBController
xrayService service.XrayService
settingService service.SettingService
@@ -158,6 +156,15 @@ func (s *Server) initRouter() (*gin.Engine, error) {
engine := gin.Default()
webDomain, err := s.settingService.GetWebDomain()
if err != nil {
return nil, err
}
if webDomain != "" {
engine.Use(middleware.DomainValidatorMiddleware(webDomain))
}
secret, err := s.settingService.GetSecret()
if err != nil {
return nil, err
@@ -180,13 +187,23 @@ func (s *Server) initRouter() (*gin.Engine, error) {
c.Header("Cache-Control", "max-age=31536000")
}
})
err = s.initI18n(engine)
// init i18n
err = locale.InitLocalizer(i18nFS, &s.settingService)
if err != nil {
return nil, err
}
// Apply locale middleware for i18n
i18nWebFunc := func(key string, params ...string) string {
return locale.I18n(locale.Web, key, params...)
}
engine.FuncMap["i18n"] = i18nWebFunc
engine.Use(locale.LocalizerMiddleware())
// set static files and template
if config.IsDebug() {
// for develop
// for development
files, err := s.getHtmlFiles()
if err != nil {
return nil, err
@@ -194,107 +211,28 @@ func (s *Server) initRouter() (*gin.Engine, error) {
engine.LoadHTMLFiles(files...)
engine.StaticFS(basePath+"assets", http.FS(os.DirFS("web/assets")))
} else {
// for prod
t, err := s.getHtmlTemplate(engine.FuncMap)
// for production
template, err := s.getHtmlTemplate(engine.FuncMap)
if err != nil {
return nil, err
}
engine.SetHTMLTemplate(t)
engine.SetHTMLTemplate(template)
engine.StaticFS(basePath+"assets", http.FS(&wrapAssetsFS{FS: assetsFS}))
}
// Apply the redirect middleware (`/xui` to `/panel`)
engine.Use(middleware.RedirectMiddleware(basePath))
g := engine.Group(basePath)
s.index = controller.NewIndexController(g)
s.server = controller.NewServerController(g)
s.panel = controller.NewXUIController(g)
s.api = controller.NewAPIController(g)
s.sub = controller.NewSUBController(g)
return engine, nil
}
func (s *Server) initI18n(engine *gin.Engine) error {
bundle := i18n.NewBundle(language.SimplifiedChinese)
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
err := fs.WalkDir(i18nFS, "translation", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
data, err := i18nFS.ReadFile(path)
if err != nil {
return err
}
_, err = bundle.ParseMessageFileBytes(data, path)
return err
})
if err != nil {
return err
}
findI18nParamNames := func(key string) []string {
names := make([]string, 0)
keyLen := len(key)
for i := 0; i < keyLen-1; i++ {
if key[i:i+2] == "{{" { // 判断开头 "{{"
j := i + 2
isFind := false
for ; j < keyLen-1; j++ {
if key[j:j+2] == "}}" { // 结尾 "}}"
isFind = true
break
}
}
if isFind {
names = append(names, key[i+3:j])
}
}
}
return names
}
var localizer *i18n.Localizer
I18n := func(key string, params ...string) (string, error) {
names := findI18nParamNames(key)
if len(names) != len(params) {
return "", common.NewError("find names:", names, "---------- params:", params, "---------- num not equal")
}
templateData := map[string]interface{}{}
for i := range names {
templateData[names[i]] = params[i]
}
return localizer.Localize(&i18n.LocalizeConfig{
MessageID: key,
TemplateData: templateData,
})
}
engine.FuncMap["i18n"] = I18n
engine.Use(func(c *gin.Context) {
//accept := c.GetHeader("Accept-Language")
var lang string
if cookie, err := c.Request.Cookie("lang"); err == nil {
lang = cookie.Value
} else {
lang = c.GetHeader("Accept-Language")
}
localizer = i18n.NewLocalizer(bundle, lang)
c.Set("localizer", localizer)
c.Set("I18n", I18n)
c.Next()
})
return nil
}
func (s *Server) startTask() {
err := s.xrayService.RestartXray(true)
if err != nil {
@@ -309,19 +247,19 @@ 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())
// check client ips from log file every 10 sec
s.cron.AddJob("@every 10s", job.NewCheckClientIpJob())
// check client ips from log file every day
s.cron.AddJob("@daily", job.NewClearLogsJob())
// Make a traffic condition every day, 8:30
var entry cron.EntryID
isTgbotenabled, err := s.settingService.GetTgbotenabled()
if (err == nil) && (isTgbotenabled) {
runtime, err := s.settingService.GetTgbotRuntime()
if err != nil || runtime == "" {
logger.Errorf("Add NewStatsNotifyJob error[%s],Runtime[%s] invalid,wil run default", err, runtime)
logger.Errorf("Add NewStatsNotifyJob error[%s], Runtime[%s] invalid, will run default", err, runtime)
runtime = "@daily"
}
logger.Infof("Tg notify enabled,run at %s", runtime)
@@ -331,12 +269,14 @@ func (s *Server) startTask() {
return
}
// check for Telegram bot callback query hash storage reset
s.cron.AddJob("@every 2m", job.NewCheckHashStorageJob())
// Check CPU load and alarm to TgBot if threshold passes
cpuThreshold, err := s.settingService.GetTgCpu()
if (err == nil) && (cpuThreshold > 0) {
s.cron.AddJob("@every 10s", job.NewCheckCpuJob())
}
} else {
s.cron.Remove(entry)
}
@@ -416,7 +356,7 @@ func (s *Server) Start() (err error) {
isTgbotenabled, err := s.settingService.GetTgbotenabled()
if (err == nil) && (isTgbotenabled) {
tgBot := s.tgbotService.NewTgbot()
tgBot.Start()
tgBot.Start(i18nFS)
}
return nil
@@ -428,7 +368,7 @@ func (s *Server) Stop() error {
if s.cron != nil {
s.cron.Stop()
}
if s.tgbotService.IsRunnging() {
if s.tgbotService.IsRunning() {
s.tgbotService.Stop()
}
var err1 error

521
x-ui.sh
View File

@@ -54,8 +54,17 @@ elif [[ "${release}" == "debian" ]]; then
if [[ ${os_version} -lt 10 ]]; then
echo -e "${red} Please use Debian 10 or higher ${plain}\n" && exit 1
fi
elif [[ "${release}" == "arch" ]]; then
echo "OS is ArchLinux"
fi
# Declare Variables
log_folder="${XUI_LOG_FOLDER:=/var/log}"
iplimit_log_path="${log_folder}/3xipl.log"
iplimit_banned_log_path="${log_folder}/3xipl-banned.log"
confirm() {
if [[ $# > 1 ]]; then
echo && read -p "$1 [Default $2]: " temp
@@ -296,25 +305,28 @@ enable_bbr() {
fi
# Check the OS and install necessary packages
if [[ "$(cat /etc/os-release | grep -E '^ID=' | awk -F '=' '{print $2}')" == "ubuntu" ]]; then
sudo apt-get update && sudo apt-get install -yqq --no-install-recommends ca-certificates
elif [[ "$(cat /etc/os-release | grep -E '^ID=' | awk -F '=' '{print $2}')" == "debian" ]]; then
sudo apt-get update && sudo apt-get install -yqq --no-install-recommends ca-certificates
elif [[ "$(cat /etc/os-release | grep -E '^ID=' | awk -F '=' '{print $2}')" == "fedora" ]]; then
sudo dnf -y update && sudo dnf -y install ca-certificates
elif [[ "$(cat /etc/os-release | grep -E '^ID=' | awk -F '=' '{print $2}')" == "centos" ]]; then
sudo yum -y update && sudo yum -y install ca-certificates
else
echo "Unsupported operating system. Please check the script and install the necessary packages manually."
exit 1
fi
case "${release}" in
ubuntu|debian)
apt-get update && apt-get install -yqq --no-install-recommends ca-certificates
;;
centos)
yum -y update && yum -y install ca-certificates
;;
fedora)
dnf -y update && dnf -y install ca-certificates
;;
*)
echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n"
exit 1
;;
esac
# Enable BBR
echo "net.core.default_qdisc=fq" | sudo tee -a /etc/sysctl.conf
echo "net.ipv4.tcp_congestion_control=bbr" | sudo tee -a /etc/sysctl.conf
echo "net.core.default_qdisc=fq" | tee -a /etc/sysctl.conf
echo "net.ipv4.tcp_congestion_control=bbr" | tee -a /etc/sysctl.conf
# Apply changes
sudo sysctl -p
sysctl -p
# Verify that BBR is enabled
if [[ $(sysctl net.ipv4.tcp_congestion_control | awk '{print $3}') == "bbr" ]]; then
@@ -434,24 +446,24 @@ show_xray_status() {
open_ports() {
if ! command -v ufw &>/dev/null; then
echo "ufw firewall is not installed. Installing now..."
sudo apt-get update
sudo apt-get install -y ufw
apt-get update
apt-get install -y ufw
else
echo "ufw firewall is already installed"
fi
# Check if the firewall is inactive
if sudo ufw status | grep -q "Status: active"; then
if ufw status | grep -q "Status: active"; then
echo "firewall is already active"
else
# Open the necessary ports
sudo ufw allow ssh
sudo ufw allow http
sudo ufw allow https
sudo ufw allow 2053/tcp
ufw allow ssh
ufw allow http
ufw allow https
ufw allow 2053/tcp
# Enable the firewall
sudo ufw --force enable
ufw --force enable
fi
# Prompt the user to enter a list of ports
@@ -472,15 +484,15 @@ open_ports() {
end_port=$(echo $port | cut -d'-' -f2)
# Loop through the range and open each port
for ((i = start_port; i <= end_port; i++)); do
sudo ufw allow $i
ufw allow $i
done
else
sudo ufw allow "$port"
ufw allow "$port"
fi
done
# Confirm that the ports are open
sudo ufw status | grep $ports
ufw status | grep $ports
}
update_geo() {
@@ -498,7 +510,7 @@ update_geo() {
rm -f geoip.dat geosite.dat iran.dat
wget -N https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
wget -N https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
wget -N https://github.com/bootmortis/iran-hosted-domains/releases/latest/download/iran.dat
wget -N https://github.com/MasterKia/iran-hosted-domains/releases/latest/download/iran.dat
systemctl start x-ui
echo -e "${green}Geosite.dat + Geoip.dat + Iran.dat have been updated successfully in bin folder '${binfolder}'!${plain}"
before_show_menu
@@ -517,9 +529,29 @@ install_acme() {
return 0
}
#method for standalone mode
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
# 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
@@ -528,35 +560,42 @@ ssl_cert_issue() {
exit 1
fi
fi
#install socat second
if [[ "${release}" == "centos" ]] || [[ "${release}" == "fedora" ]]; then
yum install socat -y
else
apt install socat -y
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"
LOGE "install socat failed, please check logs"
exit 1
else
LOGI "install socat succeed..."
fi
#get the domain here,and we need verify it
# 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
# here we need to judge whether there exists cert already
local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}')
if [ ${currentCert} == ${domain} ]; then
local certInfo=$(~/.acme.sh/acme.sh --list)
LOGE "system already have certs here,can not issue again,current certs details:"
LOGE "system already has certs here,can not issue again,current certs details:"
LOGI "$certInfo"
exit 1
else
LOGI "your domain is ready for issuing cert now..."
fi
#create a directory for install cert
# create a directory for install cert
certPath="/root/cert/${domain}"
if [ ! -d "$certPath" ]; then
mkdir -p "$certPath"
@@ -565,15 +604,15 @@ ssl_cert_issue() {
mkdir -p "$certPath"
fi
#get needed port here
# 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
# 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
@@ -583,7 +622,7 @@ ssl_cert_issue() {
else
LOGE "issue certs succeed,installing certs..."
fi
#install cert
# install cert
~/.acme.sh/acme.sh --installcert -d ${domain} \
--key-file /root/cert/${domain}/privkey.pem \
--fullchain-file /root/cert/${domain}/fullchain.pem
@@ -607,38 +646,135 @@ ssl_cert_issue() {
ls -lah cert/*
chmod 755 $certPath/*
fi
}
warp_fixchatgpt() {
curl -fsSL https://gist.githubusercontent.com/hamid-gh98/dc5dd9b0cc5b0412af927b1ccdb294c7/raw/install_warp_proxy.sh | bash
echo ""
before_show_menu
ssl_cert_issue_CF() {
echo -E ""
LOGD "******Instructions for use******"
LOGI "This Acme script requires the following data:"
LOGI "1.Cloudflare Registered e-mail"
LOGI "2.Cloudflare Global API Key"
LOGI "3.The domain name that has been resolved dns to the current server by Cloudflare"
LOGI "4.The script applies for a certificate. The default installation path is /root/cert "
confirm "Confirmed?[y/n]" "y"
if [ $? -eq 0 ]; then
# 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=""
CF_AccountEmail=""
certPath=/root/cert
if [ ! -d "$certPath" ]; then
mkdir $certPath
else
rm -rf $certPath
mkdir $certPath
fi
LOGD "Please set a domain name:"
read -p "Input your domain here:" CF_Domain
LOGD "Your domain name is set to:${CF_Domain}"
LOGD "Please set the API key:"
read -p "Input your key here:" CF_GlobalKey
LOGD "Your API key is:${CF_GlobalKey}"
LOGD "Please set up registered email:"
read -p "Input your email here:" CF_AccountEmail
LOGD "Your registered email address is:${CF_AccountEmail}"
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt
if [ $? -ne 0 ]; then
LOGE "Default CA, Lets'Encrypt fail, script exiting..."
exit 1
fi
export CF_Key="${CF_GlobalKey}"
export CF_Email=${CF_AccountEmail}
~/.acme.sh/acme.sh --issue --dns dns_cf -d ${CF_Domain} -d *.${CF_Domain} --log
if [ $? -ne 0 ]; then
LOGE "Certificate issuance failed, script exiting..."
exit 1
else
LOGI "Certificate issued Successfully, Installing..."
fi
~/.acme.sh/acme.sh --installcert -d ${CF_Domain} -d *.${CF_Domain} --ca-file /root/cert/ca.cer \
--cert-file /root/cert/${CF_Domain}.cer --key-file /root/cert/${CF_Domain}.key \
--fullchain-file /root/cert/fullchain.cer
if [ $? -ne 0 ]; then
LOGE "Certificate installation failed, script exiting..."
exit 1
else
LOGI "Certificate installed Successfully,Turning on automatic updates..."
fi
~/.acme.sh/acme.sh --upgrade --auto-upgrade
if [ $? -ne 0 ]; then
LOGE "Auto update setup Failed, script exiting..."
ls -lah cert
chmod 755 $certPath
exit 1
else
LOGI "The certificate is installed and auto-renewal is turned on, Specific information is as follows"
ls -lah cert
chmod 755 $certPath
fi
else
show_menu
fi
}
warp_cloudflare() {
echo -e "${green}\t1.${plain} Install WARP socks5 proxy"
echo -e "${green}\t2.${plain} Account Type (free, plus, team)"
echo -e "${green}\t3.${plain} Turn on/off WireProxy"
echo -e "${green}\t4.${plain} Uninstall WARP"
read -p "Choose an option: " choice
case "$choice" in
1)
bash <(curl -sSL https://raw.githubusercontent.com/hamid-gh98/x-ui-scripts/main/install_warp_proxy.sh)
;;
2)
warp a
;;
3)
warp y
;;
4)
warp u
;;
*) echo "Invalid choice" ;;
esac
}
run_speedtest() {
# Check if Speedtest is already installed
if ! command -v speedtest &>/dev/null; then
if ! command -v speedtest &> /dev/null; then
# If not installed, install it
if command -v dnf &>/dev/null; then
sudo dnf install -y curl
curl -s https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh | sudo bash
sudo dnf install -y speedtest
elif command -v yum &>/dev/null; then
sudo yum install -y curl
curl -s https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh | sudo bash
sudo yum install -y speedtest
elif command -v apt-get &>/dev/null; then
sudo apt-get update && sudo apt-get install -y curl
curl -s https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh | sudo bash
sudo apt-get install -y speedtest
elif command -v apt &>/dev/null; then
sudo apt update && sudo apt install -y curl
curl -s https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh | sudo bash
sudo apt install -y speedtest
else
local pkg_manager=""
local speedtest_install_script=""
if command -v dnf &> /dev/null; then
pkg_manager="dnf"
speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh"
elif command -v yum &> /dev/null; then
pkg_manager="yum"
speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh"
elif command -v apt-get &> /dev/null; then
pkg_manager="apt-get"
speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh"
elif command -v apt &> /dev/null; then
pkg_manager="apt"
speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh"
fi
if [[ -z $pkg_manager ]]; then
echo "Error: Package manager not found. You may need to install Speedtest manually."
return 1
else
curl -s $speedtest_install_script | bash
$pkg_manager install -y speedtest
fi
fi
@@ -646,6 +782,218 @@ run_speedtest() {
speedtest
}
create_iplimit_jails() {
# Use default bantime if not passed => 5 minutes
local bantime="${1:-5}"
cat << EOF > /etc/fail2ban/jail.d/3x-ipl.conf
[3x-ipl]
enabled=true
filter=3x-ipl
action=3x-ipl
logpath=${iplimit_log_path}
maxretry=4
findtime=60
bantime=${bantime}m
EOF
cat << EOF > /etc/fail2ban/filter.d/3x-ipl.conf
[Definition]
datepattern = ^%%Y/%%m/%%d %%H:%%M:%%S
failregex = \[LIMIT_IP\]\s*Email\s*=\s*<F-USER>.+</F-USER>\s*\|\|\s*SRC\s*=\s*<ADDR>
ignoreregex =
EOF
cat << EOF > /etc/fail2ban/action.d/3x-ipl.conf
[INCLUDES]
before = iptables-common.conf
[Definition]
actionstart = <iptables> -N f2b-<name>
<iptables> -A f2b-<name> -j <returntype>
<iptables> -I <chain> -p <protocol> -j f2b-<name>
actionstop = <iptables> -D <chain> -p <protocol> -j f2b-<name>
<actionflush>
<iptables> -X f2b-<name>
actioncheck = <iptables> -n -L <chain> | grep -q 'f2b-<name>[ \t]'
actionban = <iptables> -I f2b-<name> 1 -s <ip> -j <blocktype>
echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") BAN [Email] = <F-USER> [IP] = <ip> banned for <bantime> seconds." >> ${iplimit_banned_log_path}
actionunban = <iptables> -D f2b-<name> -s <ip> -j <blocktype>
echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") UNBAN [Email] = <F-USER> [IP] = <ip> unbanned." >> ${iplimit_banned_log_path}
[Init]
EOF
echo -e "${green}Created Ip Limit jail files with a bantime of ${bantime} minutes.${plain}"
}
iplimit_remove_conflicts() {
local jail_files=(
/etc/fail2ban/jail.conf
/etc/fail2ban/jail.local
)
for file in "${jail_files[@]}"; do
# Check for [3x-ipl] config in jail file then remove it
if test -f "${file}" && grep -qw '3x-ipl' ${file}; then
sed -i "/\[3x-ipl\]/,/^$/d" ${file}
echo -e "${yellow}Removing conflicts of [3x-ipl] in jail (${file})!${plain}\n"
fi
done
}
iplimit_main() {
echo -e "\n${green}\t1.${plain} Install Fail2ban and configure IP Limit"
echo -e "${green}\t2.${plain} Change Ban Duration"
echo -e "${green}\t3.${plain} Unban Everyone"
echo -e "${green}\t4.${plain} Check Logs"
echo -e "${green}\t5.${plain} fail2ban status"
echo -e "${green}\t6.${plain} Uninstall IP Limit"
echo -e "${green}\t0.${plain} Back to Main Menu"
read -p "Choose an option: " choice
case "$choice" in
0)
show_menu ;;
1)
confirm "Proceed with installation of Fail2ban & IP Limit?" "y"
if [[ $? == 0 ]]; then
install_iplimit
else
iplimit_main
fi ;;
2)
read -rp "Please enter new Ban Duration in Minutes [default 5]: " NUM
if [[ $NUM =~ ^[0-9]+$ ]]; then
create_iplimit_jails ${NUM}
systemctl restart fail2ban
else
echo -e "${red}${NUM} is not a number! Please, try again.${plain}"
fi
iplimit_main ;;
3)
confirm "Proceed with Unbanning everyone from IP Limit jail?" "y"
if [[ $? == 0 ]]; then
fail2ban-client reload --restart --unban 3x-ipl
echo -e "${green}All users Unbanned successfully.${plain}"
iplimit_main
else
echo -e "${yellow}Cancelled.${plain}"
fi
iplimit_main ;;
4)
if test -f "${iplimit_banned_log_path}"; then
if [[ -s "${iplimit_banned_log_path}" ]]; then
cat ${iplimit_banned_log_path}
else
echo -e "${red}Log file is empty.${plain}\n"
fi
else
echo -e "${red}Log file not found. Please Install Fail2ban and IP Limit first.${plain}\n"
iplimit_main
fi ;;
5)
service fail2ban status
;;
6)
remove_iplimit ;;
*) echo "Invalid choice" ;;
esac
}
install_iplimit() {
if ! command -v fail2ban-client &>/dev/null; then
echo -e "${green}Fail2ban is not installed. Installing now...!${plain}\n"
# Check the OS and install necessary packages
case "${release}" in
ubuntu|debian)
apt update && apt install fail2ban -y ;;
centos)
yum -y update && yum -y install fail2ban ;;
fedora)
dnf -y update && dnf -y install fail2ban ;;
*)
echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n"
exit 1 ;;
esac
echo -e "${green}Fail2ban installed successfully!${plain}\n"
else
echo -e "${yellow}Fail2ban is already installed.${plain}\n"
fi
echo -e "${green}Configuring IP Limit...${plain}\n"
# make sure there's no conflict for jail files
iplimit_remove_conflicts
# Check if log file exists
if ! test -f "${iplimit_banned_log_path}"; then
touch ${iplimit_banned_log_path}
fi
# Check if service log file exists so fail2ban won't return error
if ! test -f "${iplimit_log_path}"; then
touch ${iplimit_log_path}
fi
# Create the iplimit jail files
# we didn't pass the bantime here to use the default value
create_iplimit_jails
# Launching fail2ban
if ! systemctl is-active --quiet fail2ban; then
systemctl start fail2ban
else
systemctl restart fail2ban
fi
systemctl enable fail2ban
echo -e "${green}IP Limit installed and configured successfully!${plain}\n"
before_show_menu
}
remove_iplimit(){
echo -e "${green}\t1.${plain} Only remove IP Limit configurations"
echo -e "${green}\t2.${plain} Uninstall Fail2ban and IP Limit"
echo -e "${green}\t0.${plain} Abort"
read -p "Choose an option: " num
case "$num" in
1)
rm -f /etc/fail2ban/filter.d/3x-ipl.conf
rm -f /etc/fail2ban/action.d/3x-ipl.conf
rm -f /etc/fail2ban/jail.d/3x-ipl.conf
systemctl restart fail2ban
echo -e "${green}IP Limit removed successfully!${plain}\n"
before_show_menu ;;
2)
rm -rf /etc/fail2ban
systemctl stop fail2ban
case "${release}" in
ubuntu|debian)
apt-get purge fail2ban -y;;
centos)
yum remove fail2ban -y;;
fedora)
dnf remove fail2ban -y;;
*)
echo -e "${red}Unsupported operating system. Please uninstall Fail2ban manually.${plain}\n"
exit 1 ;;
esac
echo -e "${green}Fail2ban and IP Limit removed successfully!${plain}\n"
before_show_menu ;;
0)
echo -e "${yellow}Cancelled.${plain}\n"
iplimit_main ;;
*)
echo -e "${red}Invalid option. Please select a valid number.${plain}\n"
remove_iplimit ;;
esac
}
show_usage() {
echo "x-ui control menu usages: "
echo "------------------------------------------"
@@ -686,15 +1034,18 @@ show_menu() {
${green}13.${plain} Enable x-ui On System Startup
${green}14.${plain} Disable x-ui On System Startup
————————————————
${green}15.${plain} Enable BBR
${green}16.${plain} Apply for an SSL Certificate
${green}17.${plain} Update Geo Files
${green}18.${plain} Active Firewall and open ports
${green}19.${plain} Install WARP
${green}20.${plain} Speedtest by Ookla
"
${green}15.${plain} SSL Certificate Management
${green}16.${plain} Cloudflare SSL Certificate
${green}17.${plain} IP Limit Management
${green}18.${plain} WARP Management
————————————————
${green}19.${plain} Enable BBR
${green}20.${plain} Update Geo Files
${green}21.${plain} Active Firewall and open ports
${green}22.${plain} Speedtest by Ookla
"
show_status
echo && read -p "Please enter your selection [0-20]: " num
echo && read -p "Please enter your selection [0-22]: " num
case "${num}" in
0)
@@ -743,25 +1094,31 @@ show_menu() {
check_install && disable
;;
15)
enable_bbr
ssl_cert_issue_main
;;
16)
ssl_cert_issue
ssl_cert_issue_CF
;;
17)
update_geo
iplimit_main
;;
18)
open_ports
warp_cloudflare
;;
19)
warp_fixchatgpt
enable_bbr
;;
20)
run_speedtest
update_geo
;;
21)
open_ports
;;
22)
run_speedtest
;;
*)
LOGE "Please enter the correct number [0-20]"
LOGE "Please enter the correct number [0-22]"
;;
esac
}

236
xray/api.go Normal file
View File

@@ -0,0 +1,236 @@
package xray
import (
"context"
"encoding/json"
"fmt"
"regexp"
"time"
"x-ui/logger"
"x-ui/util/common"
"github.com/xtls/xray-core/app/proxyman/command"
statsService "github.com/xtls/xray-core/app/stats/command"
"github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/infra/conf"
"github.com/xtls/xray-core/proxy/shadowsocks"
"github.com/xtls/xray-core/proxy/shadowsocks_2022"
"github.com/xtls/xray-core/proxy/trojan"
"github.com/xtls/xray-core/proxy/vless"
"github.com/xtls/xray-core/proxy/vmess"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
type XrayAPI struct {
HandlerServiceClient *command.HandlerServiceClient
StatsServiceClient *statsService.StatsServiceClient
grpcClient *grpc.ClientConn
isConnected bool
}
func (x *XrayAPI) Init(apiPort int) (err error) {
if apiPort == 0 {
return common.NewError("xray api port wrong:", apiPort)
}
x.grpcClient, err = grpc.Dial(fmt.Sprintf("127.0.0.1:%v", apiPort), grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return err
}
x.isConnected = true
hsClient := command.NewHandlerServiceClient(x.grpcClient)
ssClient := statsService.NewStatsServiceClient(x.grpcClient)
x.HandlerServiceClient = &hsClient
x.StatsServiceClient = &ssClient
return
}
func (x *XrayAPI) Close() {
x.grpcClient.Close()
x.HandlerServiceClient = nil
x.StatsServiceClient = nil
x.isConnected = false
}
func (x *XrayAPI) AddInbound(inbound []byte) error {
client := *x.HandlerServiceClient
conf := new(conf.InboundDetourConfig)
err := json.Unmarshal(inbound, conf)
if err != nil {
logger.Debug("Failed to unmarshal inbound:", err)
return err
}
config, err := conf.Build()
if err != nil {
logger.Debug("Failed to build inbound Detur:", err)
return err
}
inboundConfig := command.AddInboundRequest{Inbound: config}
_, err = client.AddInbound(context.Background(), &inboundConfig)
return err
}
func (x *XrayAPI) DelInbound(tag string) error {
client := *x.HandlerServiceClient
_, err := client.RemoveInbound(context.Background(), &command.RemoveInboundRequest{
Tag: tag,
})
return err
}
func (x *XrayAPI) AddUser(Protocol string, inboundTag string, user map[string]interface{}) error {
var account *serial.TypedMessage
switch Protocol {
case "vmess":
account = serial.ToTypedMessage(&vmess.Account{
Id: user["id"].(string),
})
case "vless":
account = serial.ToTypedMessage(&vless.Account{
Id: user["id"].(string),
Flow: user["flow"].(string),
})
case "trojan":
account = serial.ToTypedMessage(&trojan.Account{
Password: user["password"].(string),
})
case "shadowsocks":
var ssCipherType shadowsocks.CipherType
switch user["cipher"].(string) {
case "aes-128-gcm":
ssCipherType = shadowsocks.CipherType_AES_128_GCM
case "aes-256-gcm":
ssCipherType = shadowsocks.CipherType_AES_256_GCM
case "chacha20-poly1305":
ssCipherType = shadowsocks.CipherType_CHACHA20_POLY1305
case "xchacha20-poly1305":
ssCipherType = shadowsocks.CipherType_XCHACHA20_POLY1305
default:
ssCipherType = shadowsocks.CipherType_NONE
}
if ssCipherType != shadowsocks.CipherType_NONE {
account = serial.ToTypedMessage(&shadowsocks.Account{
Password: user["password"].(string),
CipherType: ssCipherType,
})
} else {
account = serial.ToTypedMessage(&shadowsocks_2022.User{
Key: user["password"].(string),
Email: user["email"].(string),
})
}
default:
return nil
}
client := *x.HandlerServiceClient
_, err := client.AlterInbound(context.Background(), &command.AlterInboundRequest{
Tag: inboundTag,
Operation: serial.ToTypedMessage(&command.AddUserOperation{
User: &protocol.User{
Email: user["email"].(string),
Account: account,
},
}),
})
return err
}
func (x *XrayAPI) RemoveUser(inboundTag string, email string) error {
client := *x.HandlerServiceClient
_, err := client.AlterInbound(context.Background(), &command.AlterInboundRequest{
Tag: inboundTag,
Operation: serial.ToTypedMessage(&command.RemoveUserOperation{
Email: email,
}),
})
return err
}
func (x *XrayAPI) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) {
if x.grpcClient == nil {
return nil, nil, common.NewError("xray api is not initialized")
}
var trafficRegex = regexp.MustCompile("(inbound|outbound)>>>([^>]+)>>>traffic>>>(downlink|uplink)")
var ClientTrafficRegex = regexp.MustCompile("(user)>>>([^>]+)>>>traffic>>>(downlink|uplink)")
client := *x.StatsServiceClient
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
request := &statsService.QueryStatsRequest{
Reset_: reset,
}
resp, err := client.QueryStats(ctx, request)
if err != nil {
return nil, nil, err
}
tagTrafficMap := map[string]*Traffic{}
emailTrafficMap := map[string]*ClientTraffic{}
clientTraffics := make([]*ClientTraffic, 0)
traffics := make([]*Traffic, 0)
for _, stat := range resp.GetStat() {
matchs := trafficRegex.FindStringSubmatch(stat.Name)
if len(matchs) < 3 {
matchs := ClientTrafficRegex.FindStringSubmatch(stat.Name)
if len(matchs) < 3 {
continue
} else {
isUser := matchs[1] == "user"
email := matchs[2]
isDown := matchs[3] == "downlink"
if !isUser {
continue
}
traffic, ok := emailTrafficMap[email]
if !ok {
traffic = &ClientTraffic{
Email: email,
}
emailTrafficMap[email] = traffic
clientTraffics = append(clientTraffics, traffic)
}
if isDown {
traffic.Down = stat.Value
} else {
traffic.Up = stat.Value
}
}
continue
}
isInbound := matchs[1] == "inbound"
tag := matchs[2]
isDown := matchs[3] == "downlink"
if tag == "api" {
continue
}
traffic, ok := tagTrafficMap[tag]
if !ok {
traffic = &Traffic{
IsInbound: isInbound,
Tag: tag,
}
tagTrafficMap[tag] = traffic
traffics = append(traffics, traffic)
}
if isDown {
traffic.Down = stat.Value
} else {
traffic.Up = stat.Value
}
}
return traffics, clientTraffics, nil
}

Some files were not shown because too many files have changed in this diff Show More