Compare commits

..

157 Commits

Author SHA1 Message Date
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
85 changed files with 5632 additions and 2575 deletions

View File

@@ -13,13 +13,13 @@ jobs:
- name: Check out the code
uses: actions/checkout@v3
- 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.7.0
- 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.5.2
- name: Setup Go
uses: actions/setup-go@v4.0.1
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: 'stable'
- 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/mhsanaei/Xray-core/releases/latest/download/Xray-linux-64.zip
unzip Xray-linux-64.zip
rm -f Xray-linux-64.zip
else
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
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
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.6.1
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

270
README.md
View File

@@ -9,7 +9,6 @@
[![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)**
**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.6`:
To install your desired version you can add the version to the end of install command. Example for ver `v1.6.1`:
```
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v1.4.6
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v1.6.1
```
# 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 `16. SSL Certificate Management`
# Features
@@ -147,7 +56,145 @@ 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://gist.githubusercontent.com/hamid-gh98/dc5dd9b0cc5b0412af927b1ccdb294c7/raw/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>
# 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,15 +223,20 @@ 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
<details>
<summary>Click for API routes details</summary>
- `/login` with `PUSH` user data: `{username: '', password: ''}` for login
- `/panel/api/inbounds` base for following actions:
@@ -194,6 +246,7 @@ Reference syntax:
| `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,6 +267,27 @@ 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"` |
Example:
```sh
XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
```
</details>
# A Special Thanks To

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 {

View File

@@ -1 +1 @@
1.4.6
1.7.0

View File

@@ -17,7 +17,16 @@ 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 {
return err
@@ -54,7 +63,7 @@ func initClientTraffic() error {
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 +84,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 +101,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"`

64
go.mod
View File

@@ -3,34 +3,38 @@ 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.25.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.8
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.5
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.10.0
google.golang.org/grpc v1.56.1
gorm.io/driver/sqlite v1.5.2
gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55
)
require (
github.com/BurntSushi/toml v1.2.1 // indirect
github.com/bytedance/sonic v1.8.8 // indirect
github.com/BurntSushi/toml v1.3.2 // indirect
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/bytedance/sonic v1.9.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect
github.com/fasthttp/router v1.4.19 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gaukas/godicttls v0.0.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.13.0 // indirect
github.com/go-playground/validator/v10 v10.14.1 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
@@ -38,26 +42,36 @@ require (
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.6 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // 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/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/refraction-networking/utls v1.3.2 // indirect
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
github.com/sagernet/sing v0.2.6 // 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.11 // indirect
github.com/tklauser/numcpus v0.6.0 // 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
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.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
golang.org/x/crypto v0.10.0 // indirect
golang.org/x/net v0.11.0 // indirect
golang.org/x/sys v0.9.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

189
go.sum
View File

@@ -1,74 +1,63 @@
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/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.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165/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/fasthttp/router v1.4.19 h1:RLE539IU/S4kfb4MP56zgP0TIBU9kEg0ID9GpWO0vqk=
github.com/fasthttp/router v1.4.19/go.mod h1:+Fh3YOd8x1+he6ZS+d2iUDBH9MGGZ1xQFUor0DE9rKE=
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
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.3 h1:YNDIf0d9adcxOijiLrEzpfZGAkNwLRzPaG6OjU7EITk=
github.com/gaukas/godicttls v0.0.3/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
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/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/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/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.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k=
github.com/go-playground/validator/v10 v10.14.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/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
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 v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20230406165453-00490a63f317 h1:hFhpt7CTmR3DX+b4R19ydQFtofxT0Sv3QsKNMVQYTMQ=
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 h1:hR7/MlvK23p6+lIw9SN1TigNLn9ZnF3W4SYRKq2gAHs=
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=
@@ -76,102 +65,105 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
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.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/klauspost/compress v1.16.6 h1:91SKEy4K37vkp255cJ8QesJhjyRO0hn9i9G0GoUwLsk=
github.com/klauspost/compress v1.16.6/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/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/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/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.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mymmrac/telego v0.25.1 h1:tMNmrRm0YGyLS56CBi0NDHwO1ZI6V7QMgX4KWSWuT1U=
github.com/mymmrac/telego v0.25.1/go.mod h1:nBO4SUqRV8j60JOS7trIr6bHPofwYCGJxYeqtQWgu2c=
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/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/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.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
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/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/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U=
github.com/quic-go/qtls-go1-20 v0.2.2 h1:WLOPx6OY/hxtTxKV1Zrq20FtXtDEkeY00CGQm8GEa3E=
github.com/quic-go/quic-go v0.33.0 h1:ItNoTDN/Fm/zBlq769lLJc8ECe9gYaW40veHCCco7y0=
github.com/quic-go/quic-go v0.35.1 h1:b0kzj6b/cQAf05cT0CkQubHM31wiA+xH3IBkxP62poo=
github.com/refraction-networking/utls v1.3.2 h1:o+AkWB57mkcoW36ET7uJ002CpBWHu0KPxi6vzxvPnv8=
github.com/refraction-networking/utls v1.3.2/go.mod h1:fmoaOww2bxzzEpIKOebIsnBvjQpqP7L2vcm/9KUfm/E=
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/sagernet/sing v0.2.6 h1:Fvqv7/Bwc72ERT6dE8yQLLY6SMc/syO3VMCtxVO4DNw=
github.com/sagernet/sing v0.2.6/go.mod h1:Ta8nHnDLAwqySzKhGoKk4ZIB+vJ3GTKj7UPrWYvM+4w=
github.com/sagernet/sing-shadowsocks v0.2.2 h1:ezSdVhrmIcwDXmCZF3bOJVMuVtTQWpda+1Op+Ie2TA4=
github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c h1:vK2wyt9aWYHHvNLWniwijBu/n4pySypiKRhN32u/JGo=
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/shirou/gopsutil/v3 v3.23.5 h1:5SgDCeQ0KW0S4N0znjeM/eFHXXOKyv2dVNgRq/c9P6Y=
github.com/shirou/gopsutil/v3 v3.23.5/go.mod h1:Ng3Maa27Q2KARVJ0SPZF5NdrQSC3XHKP8IIWrHgMeLY=
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/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.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.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
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/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/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.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.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
@@ -181,79 +173,76 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
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.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
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-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.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
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-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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
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-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.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.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/sys v0.9.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.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.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y=
golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg=
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/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc h1:XSJ8Vk1SWuNr8S18z1NZSziL0CPIXLCCMDOEFtHBOFc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
google.golang.org/grpc v1.56.1 h1:z0dNfjIl0VpaZ9iSVjA6daGatAYwPGstTjt5vkRMFkQ=
google.golang.org/grpc v1.56.1/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
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=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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.2 h1:TpQ+/dqCY4uCigCFyrfnrJnrW9zjpelWVoEVNy5qJkc=
gorm.io/driver/sqlite v1.5.2/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55 h1:sC1Xj4TYrLqg1n3AN10w871An7wJM0gzgcm8jkIkECQ=
gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gvisor.dev/gvisor v0.0.0-20220901235040-6ca97ef2ce1c h1:m5lcgWnL3OElQNVyp3qcncItJ2c0sQlSGjYK2+nJTA4=
lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0=
lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@@ -2,17 +2,28 @@ package logger
import (
"os"
"sync"
"github.com/op/go-logging"
)
var logger *logging.Logger
var (
logger *logging.Logger
mu sync.Mutex
)
func init() {
InitLogger(logging.INFO)
}
func InitLogger(level logging.Level) {
mu.Lock()
defer mu.Unlock()
if logger != nil {
return
}
format := logging.MustStringFormatter(
`%{time:2006/01/02 15:04:05} %{level} - %{message}`,
)
@@ -21,39 +32,67 @@ func InitLogger(level logging.Level) {
backendFormatter := logging.NewBackendFormatter(backend, format)
backendLeveled := logging.AddModuleLevel(backendFormatter)
backendLeveled.SetLevel(level, "")
newLogger.SetBackend(backendLeveled)
newLogger.SetBackend(logging.MultiLogger(backendLeveled))
logger = newLogger
}
func Debug(args ...interface{}) {
logger.Debug(args...)
if logger != nil {
logger.Debug(args...)
}
}
func Debugf(format string, args ...interface{}) {
logger.Debugf(format, args...)
if logger != nil {
logger.Debugf(format, args...)
}
}
func Info(args ...interface{}) {
logger.Info(args...)
if logger != nil {
logger.Info(args...)
}
}
func Infof(format string, args ...interface{}) {
logger.Infof(format, args...)
if logger != nil {
logger.Infof(format, args...)
}
}
func Warning(args ...interface{}) {
logger.Warning(args...)
if logger != nil {
logger.Warning(args...)
}
}
func Warningf(format string, args ...interface{}) {
logger.Warningf(format, args...)
if logger != nil {
logger.Warningf(format, args...)
}
}
func Error(args ...interface{}) {
logger.Error(args...)
if logger != nil {
logger.Error(args...)
}
}
func Errorf(format string, args ...interface{}) {
logger.Errorf(format, args...)
if logger != nil {
logger.Errorf(format, args...)
}
}
func Notice(args ...interface{}) {
if logger != nil {
logger.Notice(args...)
}
}
func Noticef(format string, args ...interface{}) {
if logger != nil {
logger.Noticef(format, args...)
}
}

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

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,17 +1,14 @@
package controller
package sub
import (
"encoding/base64"
"strings"
"x-ui/web/service"
"github.com/gin-gonic/gin"
)
type SUBController struct {
BaseController
subService service.SubService
subService SubService
}
func NewSUBController(g *gin.RouterGroup) *SUBController {
@@ -21,7 +18,7 @@ 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)
}
@@ -29,7 +26,7 @@ func (a *SUBController) initRouter(g *gin.RouterGroup) {
func (a *SUBController) subs(c *gin.Context) {
subId := c.Param("subid")
host := strings.Split(c.Request.Host, ":")[0]
subs, header, err := a.subService.GetSubs(subId, host)
subs, headers, err := a.subService.GetSubs(subId, host)
if err != nil || len(subs) == 0 {
c.String(400, "Error!")
} else {
@@ -38,8 +35,10 @@ 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)))
}

View File

@@ -1,4 +1,4 @@
package service
package sub
import (
"encoding/base64"
@@ -8,35 +8,51 @@ import (
"x-ui/database"
"x-ui/database/model"
"x-ui/logger"
"x-ui/web/service"
"x-ui/xray"
"github.com/goccy/go-json"
"gorm.io/gorm"
)
type SubService struct {
address string
inboundService InboundService
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) ([]string, []string, error) {
s.address = host
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 +82,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 +108,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":
@@ -162,6 +194,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 +217,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 +227,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 +236,21 @@ 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"] = remark + "-" + 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
}
jsonStr, _ := json.MarshalIndent(obj, "", " ")
return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
@@ -213,7 +263,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 +319,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 +344,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 +380,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 +434,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()
@@ -387,6 +450,20 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
url.RawQuery = q.Encode()
remark := fmt.Sprintf("%s-%s", inbound.Remark, email)
if len(domains) > 0 {
links := ""
for index, d := range domains {
domain := d.(map[string]interface{})
url.Fragment = remark + "-" + domain["remark"].(string)
url.Host = fmt.Sprintf("%s:%d", domain["domain"].(string), port)
if index > 0 {
links += "\n"
}
links += url.String()
}
return links
}
url.Fragment = remark
return url.String()
}
@@ -398,7 +475,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 +531,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 +556,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 +579,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 +588,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 +642,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)
@@ -569,6 +659,21 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
url.RawQuery = q.Encode()
remark := fmt.Sprintf("%s-%s", inbound.Remark, email)
if len(domains) > 0 {
links := ""
for index, d := range domains {
domain := d.(map[string]interface{})
url.Fragment = remark + "-" + domain["remark"].(string)
url.Host = fmt.Sprintf("%s:%d", domain["domain"].(string), port)
if index > 0 {
links += "\n"
}
links += url.String()
}
return links
}
url.Fragment = remark
return url.String()
}
@@ -578,7 +683,7 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
if inbound.Protocol != model.Shadowsocks {
return ""
}
clients, _ := s.inboundService.getClients(inbound)
clients, _ := s.inboundService.GetClients(inbound)
var settings map[string]interface{}
json.Unmarshal([]byte(inbound.Settings), &settings)
@@ -592,7 +697,8 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
}
}
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)
remark := fmt.Sprintf("%s-%s", inbound.Remark, clients[clientIndex].Email)
return fmt.Sprintf("ss://%s@%s:%d#%s", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port, 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;
@@ -203,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;
@@ -214,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;
}
@@ -273,12 +289,12 @@ 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-select-dropdown-menu-item-active {
.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);

View File

@@ -6,7 +6,7 @@ const supportLangs = [
},
{
name: 'فارسی',
value: 'fa_IR',
value: 'fa-IR',
icon: '🇮🇷',
},
{
@@ -16,7 +16,7 @@ const supportLangs = [
},
{
name: 'Русский',
value: 'ru_RU',
value: 'ru-RU',
icon: '🇷🇺',
},
];

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,19 @@ class AllSetting {
this.tgBotChatId = "";
this.tgRunTime = "@daily";
this.tgBotBackup = false;
this.tgBotLoginNotify = false;
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.timeLocation = "Asia/Tehran";

View File

@@ -4,7 +4,6 @@ const Protocols = {
TROJAN: 'trojan',
SHADOWSOCKS: 'shadowsocks',
DOKODEMO: 'dokodemo-door',
MTPROTO: 'mtproto',
SOCKS: 'socks',
HTTP: 'http',
};
@@ -379,10 +378,15 @@ class WsStreamSettings extends XrayCommonClass {
}
class HttpStreamSettings extends XrayCommonClass {
constructor(path='/', host=['']) {
constructor(
path='/',
host=[''],
sockopt={acceptProxyProtocol: false}
) {
super();
this.path = path;
this.host = host.length === 0 ? [''] : host;
this.sockopt = sockopt;
}
addHost(host) {
@@ -394,7 +398,7 @@ class HttpStreamSettings extends XrayCommonClass {
}
static fromJson(json={}) {
return new HttpStreamSettings(json.path, json.host);
return new HttpStreamSettings(json.path, json.host, json.sockopt);
}
toJson() {
@@ -407,10 +411,12 @@ class HttpStreamSettings extends XrayCommonClass {
return {
path: this.path,
host: host,
sockopt: this.sockopt,
}
}
}
class QuicStreamSettings extends XrayCommonClass {
constructor(security=VmessMethods.NONE,
key='', type='none') {
@@ -442,33 +448,39 @@ class QuicStreamSettings extends XrayCommonClass {
class GrpcStreamSettings extends XrayCommonClass {
constructor(
serviceName="",
multiMode=false
multiMode=false,
sockopt={acceptProxyProtocol: false}
) {
super();
this.serviceName = serviceName;
this.multiMode = multiMode;
this.sockopt = sockopt;
}
static fromJson(json={}) {
return new GrpcStreamSettings(
json.serviceName,
json.multiMode
json.multiMode,
json.sockopt
);
}
toJson() {
return {
serviceName: this.serviceName,
multiMode: this.multiMode
multiMode: this.multiMode,
sockopt: this.sockopt
}
}
}
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 +489,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 +511,13 @@ 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 +530,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,
@@ -566,17 +580,19 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
};
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 +600,7 @@ TlsStreamSettings.Settings = class extends XrayCommonClass {
allowInsecure: this.allowInsecure,
fingerprint: this.fingerprint,
serverName: this.serverName,
domains: this.domains,
};
}
};
@@ -599,8 +616,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 +723,7 @@ class RealityStreamSettings extends XrayCommonClass {
minClient = '',
maxClient = '',
maxTimediff = 0,
shortIds = RandomUtil.randowShortId(),
shortIds = RandomUtil.randomShortId(),
settings= new RealityStreamSettings.Settings()
){
super();
@@ -725,7 +742,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 +775,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,6 +795,7 @@ RealityStreamSettings.Settings = class extends XrayCommonClass {
publicKey: this.publicKey,
fingerprint: this.fingerprint,
serverName: this.serverName,
spiderX: this.spiderX,
};
}
};
@@ -1202,7 +1222,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 +1358,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 +1373,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 +1389,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}`;
@@ -1457,7 +1483,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 +1496,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 +1516,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 +1531,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 +1549,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, index) + '\r\n';
});
} else {
link += this.genLink(address, remark + '-' + client.email, index) + '\r\n';
}
});
return link;
case Protocols.SHADOWSOCKS:
return (this.genSSLink(address, remark) + '\r\n');
default: return '';
}
}
@@ -1582,7 +1608,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 +1621,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;
@@ -1651,10 +1675,9 @@ Inbound.VmessSettings = class extends Inbound.Settings {
}
};
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 +1690,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 +1765,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 +1888,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,7 +2030,7 @@ 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(password=RandomUtil.randomShadowsocksPassword(), email=RandomUtil.randomLowerAndNum(8),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16)) {
super();
this.password = password;
this.email = email;
@@ -2102,36 +2124,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 {
@@ -32,7 +37,7 @@ 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)
}
@@ -95,3 +100,7 @@ func (a *APIController) resetAllClientTraffics(c *gin.Context) {
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

@@ -60,25 +60,25 @@ 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)
}
func (a *InboundController) getClientTraffics(c *gin.Context) {
email := c.Param("email")
clientTraffics, err := a.inboundService.GetClientTrafficByEmail(email)
@@ -93,7 +93,7 @@ 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)
@@ -101,7 +101,7 @@ func (a *InboundController) addInbound(c *gin.Context) {
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)
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.create"), inbound, err)
if err == nil {
a.xrayService.SetToNeedRestart()
}
@@ -110,11 +110,11 @@ 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)
jsonMsgObj(c, I18nWeb(c, "delete"), id, err)
if err == nil {
a.xrayService.SetToNeedRestart()
}
@@ -123,7 +123,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,11 +131,11 @@ 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)
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.update"), inbound, err)
if err == nil {
a.xrayService.SetToNeedRestart()
}
@@ -145,12 +145,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 +163,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 := false
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 +188,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 := false
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 +212,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 := false
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 +232,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 := false
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 +255,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 +264,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 +272,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 +281,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,7 +118,8 @@ func (a *ServerController) restartXrayService(c *gin.Context) {
func (a *ServerController) getLogs(c *gin.Context) {
count := c.Param("count")
logs, err := a.serverService.GetLogs(count)
logLevel := c.PostForm("logLevel")
logs, err := a.serverService.GetLogs(count, logLevel)
if err != nil {
jsonMsg(c, "getLogs", err)
return
@@ -136,14 +142,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,49 @@ 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() },
}
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 +108,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 +137,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 +157,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,20 @@ 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"`
}
func (s *AllSetting) CheckValid() error {
@@ -54,10 +65,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 +91,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,54 @@
<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.email;
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) => {
this.qrcodes.push({
remark: remark + "-" + domain.remark,
link: this.inbound.genLink(domain.domain, remark + "-" + domain.remark, 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 +68,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 });
}
},
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;
@@ -204,6 +205,7 @@
case Protocols.VMESS: return clientSettings.vmesses;
case Protocols.VLESS: return clientSettings.vlesses;
case Protocols.TROJAN: return clientSettings.trojans;
case Protocols.SHADOWSOCKS: return clientSettings.shadowsockses;
default: return null;
}
},
@@ -212,6 +214,7 @@
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();
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) {
@@ -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

@@ -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="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 +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>
@@ -96,10 +96,12 @@
<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>
@@ -112,6 +114,7 @@
</a-select>
</a-form-item>
<a-form-item 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

@@ -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,10 +97,12 @@
<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>

View File

@@ -1,5 +1,9 @@
{{define "form/streamGRPC"}}
<a-form layout="inline">
<a-form-item label="AcceptProxyProtocol">
<a-switch v-model="inbound.stream.grpc.sockopt.acceptProxyProtocol"></a-switch>
</a-form-item>
<br>
<a-form-item label="ServiceName">
<a-input v-model.trim="inbound.stream.grpc.serviceName"></a-input>
</a-form-item>

View File

@@ -1,5 +1,9 @@
{{define "form/streamHTTP"}}
<a-form layout="inline">
<a-form-item label="AcceptProxyProtocol">
<a-switch v-model="inbound.stream.http.sockopt.acceptProxyProtocol"></a-switch>
</a-form-item>
<br>
<a-form-item label='{{ i18n "path" }}'>
<a-input v-model.trim="inbound.stream.http.path"></a-input>
</a-form-item>

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">
@@ -67,36 +84,45 @@
<a-checkbox v-for="key,value in ALPN_OPTION" :value="key">[[ value ]]</a-checkbox>
</a-checkbox-group>
</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>
</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-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>
<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>
</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 +137,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 +184,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,29 @@
<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></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,68 @@
</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>
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 v-else>tls: <a-tag color="red">{{ i18n "disabled" }}</a-tag>
</td>
</tr>
</table>
<template v-if="infoModal.clientSettings">
<a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
<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,18 +126,46 @@
</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%;">
@@ -129,8 +173,7 @@
<th>{{ i18n "encryption" }}</th>
<th>{{ i18n "password" }}</th>
<th>{{ i18n "pages.inbounds.network" }}</th>
</tr>
<tr>
</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>
@@ -142,8 +185,7 @@
<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>
@@ -156,18 +198,15 @@
<th>{{ i18n "password" }} Auth</th>
<th>{{ i18n "pages.inbounds.enable" }} udp</th>
<th>IP</th>
</tr>
<tr>
</tr><tr>
<td><a-tag color="green">[[ inbound.settings.auth ]]</a-tag></td>
<td><a-tag color="blue">[[ inbound.settings.udp]]</a-tag></td>
<td><a-tag color="green">[[ inbound.settings.ip ]]</a-tag></td>
</tr>
<tr v-if="inbound.settings.auth == 'password'">
</tr><tr v-if="inbound.settings.auth == 'password'">
<td> </td>
<td>{{ i18n "username" }}</td>
<td>{{ i18n "password" }}</td>
</tr>
<tr v-for="account,index in inbound.settings.accounts">
</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>
@@ -179,8 +218,7 @@
<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>
@@ -188,14 +226,8 @@
</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 +238,52 @@
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.email;
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) => {
this.links.push({
remark: remark + "-" + domain.remark,
link: this.inbound.genLink(domain.domain, remark + "-" + domain.remark, 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 });
}
};
const infoModalApp = new Vue({
@@ -248,12 +309,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 +321,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

@@ -90,6 +90,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() {
@@ -100,13 +112,13 @@
this.inModal.inbound.reality = false;
}
},
setDefaultCertData() {
inModal.inbound.stream.tls.certs[0].certFile = app.defaultCert;
inModal.inbound.stream.tls.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() {
inModal.inbound.stream.xtls.certs[0].certFile = app.defaultCert;
inModal.inbound.stream.xtls.certs[0].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

@@ -105,7 +105,17 @@
</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 }"
@@ -165,7 +175,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 +206,27 @@
</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></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>
@@ -281,12 +306,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,17 +324,17 @@
{ 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: '{{ i18n "pages.inbounds.traffic" }}', width: 50, scopedSlots: { customRender: 'traffic' } },
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 50, scopedSlots: { customRender: 'expiryTime' } },
{ title: 'Password', width: 170, dataIndex: "password" },
];
@@ -323,6 +348,8 @@
inbounds: [],
dbInbounds: [],
searchKey: '',
enableFilter: false,
filterBy: '',
searchedInbounds: [],
expireDiff: 0,
trafficDiff: 0,
@@ -332,6 +359,14 @@
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) {
@@ -341,6 +376,7 @@
this.refreshing = true;
const msg = await HttpUtil.post('/panel/inbound/list');
if (!msg.success) {
this.refreshing = false;
return;
}
this.setInbounds(msg.obj);
@@ -353,10 +389,20 @@
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);
@@ -366,11 +412,15 @@
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)) {
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 = [];
@@ -428,6 +478,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":
@@ -697,13 +779,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);
@@ -790,10 +891,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
@@ -804,7 +905,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(logModal.rows, logModal.logLevel)">{{ 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,26 +108,69 @@
</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>
<a-row>
<a-col :span="12">
{{ i18n "pages.index.systemLoad" }}: [[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
<a-tooltip>
<template slot="title">
{{ i18n "pages.index.systemLoadDesc" }}
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</a-col>
<a-col :span="12">
{{ i18n "pages.index.operationHours" }}:
<a-tag color="green">[[ formatSecond(status.uptime) ]]</a-tag>
</a-col>
</a-row>
</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-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>
<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" }}
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
<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">
@@ -138,7 +178,7 @@
<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 +188,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 +253,7 @@
<a-form-item label="Count">
<a-select v-model="logModal.rows"
style="width: 80px"
@change="openLogs(logModal.rows)"
@change="openLogs(logModal.rows, logModal.logLevel)"
: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 +261,20 @@
<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.logLevel"
style="width: 120px"
@change="openLogs(logModal.rows, logModal.logLevel)"
: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>
<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(logModal.rows, logModal.logLevel)"><a-icon type="sync"></a-icon> Reload</button>
</a-form-item>
<a-form-item>
<a-button type="primary" style="margin-bottom: 10px;"
@@ -294,11 +346,14 @@
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;
@@ -309,11 +364,14 @@
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;
@@ -351,6 +409,7 @@
visible: false,
logs: '',
rows: 20,
logLevel: 'info',
show(logs, rows) {
this.visible = true;
this.rows = rows;
@@ -403,9 +462,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) {
@@ -451,9 +514,9 @@
return;
}
},
async openLogs(rows) {
async openLogs(rows, logLevel) {
this.loading(true);
const msg = await HttpUtil.post('server/logs/' + rows);
const msg = await HttpUtil.post('server/logs/' + rows, { logLevel: `${logLevel}` });
this.loading(false);
if (!msg.success) {
return;
@@ -514,11 +577,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,6 +2,7 @@ package job
import (
"encoding/json"
"log"
"os"
"regexp"
"x-ui/database"
@@ -10,12 +11,9 @@ import (
"x-ui/web/service"
"x-ui/xray"
"net"
"sort"
"strings"
"time"
"github.com/go-cmd/cmd"
)
type CheckClientIpJob struct {
@@ -32,19 +30,66 @@ func NewCheckClientIpJob() *CheckClientIpJob {
func (j *CheckClientIpJob) Run() {
logger.Debug("Check Client IP Job...")
processLogFile()
// disAllowedIps = []string{"192.168.1.183","192.168.1.197"}
if hasLimitIp() {
//create log file for Fail2ban IP Limit
logIpFile, err := os.OpenFile("/var/log/3xipl.log", os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
checkError(err)
defer logIpFile.Close()
log.SetOutput(logIpFile)
log.SetFlags(log.LstdFlags)
//create file to collect access.log to another file accessp.log (p=persistent)
logAccessP, err := os.OpenFile("/usr/local/x-ui/accessp.log", os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
checkError(err)
defer logAccessP.Close()
processLogFile()
}
blockedIps := []byte(strings.Join(disAllowedIps, ","))
err := os.WriteFile(xray.GetBlockedIPsPath(), blockedIps, 0755)
checkError(err)
// check if file exists, if not create one
_, err := os.Stat(xray.GetBlockedIPsPath())
if os.IsNotExist(err) {
_, err = os.OpenFile(xray.GetBlockedIPsPath(), os.O_RDWR|os.O_CREATE, 0755)
checkError(err)
}
err = os.WriteFile(xray.GetBlockedIPsPath(), blockedIps, 0755)
checkError(err)
}
func 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 processLogFile() {
accessLogPath := GetAccessLogPath()
if accessLogPath == "" {
logger.Warning("xray log not init in config.json")
logger.Warning("access.log doesn't exist in your config.json")
return
}
@@ -52,11 +97,6 @@ func processLogFile() {
InboundClientIps := make(map[string][]string)
checkError(err)
// clean log
if err := os.Truncate(GetAccessLogPath(), 0); err != nil {
checkError(err)
}
lines := strings.Split(string(data), "\n")
for _, line := range lines {
ipRegx, _ := regexp.Compile(`[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+`)
@@ -73,7 +113,7 @@ 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) {
@@ -88,23 +128,37 @@ func processLogFile() {
}
disAllowedIps = []string{}
shouldCleanLog := false
for clientEmail, ips := range InboundClientIps {
inboundClientIps, err := GetInboundClientIps(clientEmail)
sort.Strings(ips)
if err != nil {
addInboundClientIps(clientEmail, ips)
} else {
updateInboundClientIps(inboundClientIps, clientEmail, ips)
shouldCleanLog = updateInboundClientIps(inboundClientIps, clientEmail, ips)
}
}
// check if inbound connection is more than limited ip and drop connection
LimitDevice := func() { LimitDevice() }
stop := schedule(LimitDevice, 1000*time.Millisecond)
time.Sleep(10 * time.Second)
stop <- true
time.Sleep(time.Second * 3)
//added 3 seconds delay before cleaning logs to reduce chance of logging IP that already has been banned
if shouldCleanLog {
//copy log
logAccessP, err := os.OpenFile("/usr/local/x-ui/accessp.log", os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
checkError(err)
input, err := os.ReadFile(accessLogPath)
checkError(err)
if _, err := logAccessP.Write(input); err != nil {
checkError(err)
}
defer logAccessP.Close()
// clean log
if err := os.Truncate(GetAccessLogPath(), 0); err != nil {
checkError(err)
}
}
}
func GetAccessLogPath() string {
@@ -155,9 +209,6 @@ func addInboundClientIps(clientEmail string, ips []string) error {
jsonIps, err := json.Marshal(ips)
checkError(err)
// Trim any leading/trailing whitespace from clientEmail
clientEmail = strings.TrimSpace(clientEmail)
inboundClientIps.ClientEmail = clientEmail
inboundClientIps.Ips = string(jsonIps)
@@ -178,7 +229,7 @@ func addInboundClientIps(clientEmail string, ips []string) error {
}
return nil
}
func updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmail string, ips []string) error {
func updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmail string, ips []string) bool {
jsonIps, err := json.Marshal(ips)
checkError(err)
@@ -192,23 +243,29 @@ func updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmai
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"]
var disAllowedIps []string // initialize the slice
shouldCleanLog := false
for _, client := range clients {
if client.Email == clientEmail {
limitIp := client.LimitIP
if limitIp < len(ips) && limitIp != 0 && inbound.Enable {
if limitIp != 0 {
disAllowedIps = append(disAllowedIps, ips[limitIp:]...)
shouldCleanLog = true
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,9 +275,9 @@ 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 {
@@ -247,103 +304,3 @@ func GetInboundByEmail(clientEmail string) (*model.Inbound, error) {
}
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

@@ -15,19 +15,21 @@ func NewCheckInboundJob() *CheckInboundJob {
}
func (j *CheckInboundJob) Run() {
count, err := j.inboundService.DisableInvalidClients()
needRestart, count, err := j.inboundService.DisableInvalidClients()
if err != nil {
logger.Warning("disable invalid Client err:", err)
logger.Warning("Error in disabling invalid clients:", err)
} else if count > 0 {
logger.Debugf("disabled %v Client", count)
j.xrayService.SetToNeedRestart()
logger.Debugf("%v clients disabled", count)
if needRestart {
j.xrayService.SetToNeedRestart()
}
}
count, err = j.inboundService.DisableInvalidInbounds()
if err != nil {
logger.Warning("disable invalid inbounds err:", err)
logger.Warning("Error in disabling invalid inbounds:", err)
} else if count > 0 {
logger.Debugf("disabled %v inbounds", count)
logger.Debugf("%v inbounds disabled", count)
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": {}
}
}

View File

@@ -15,6 +15,7 @@ import (
)
type InboundService struct {
xrayApi xray.XrayAPI
}
func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) {
@@ -51,7 +52,7 @@ func (s *InboundService) checkPortExist(port int, ignoreId int) (bool, error) {
return count > 0, nil
}
func (s *InboundService) getClients(inbound *model.Inbound) ([]model.Client, error) {
func (s *InboundService) GetClients(inbound *model.Inbound) ([]model.Client, error) {
settings := map[string][]model.Client{}
json.Unmarshal([]byte(inbound.Settings), &settings)
if settings == nil {
@@ -110,7 +111,7 @@ func (s *InboundService) checkEmailsExistForClients(clients []model.Client) (str
}
func (s *InboundService) checkEmailExistForInbound(inbound *model.Inbound) (string, error) {
clients, err := s.getClients(inbound)
clients, err := s.GetClients(inbound)
if err != nil {
return "", err
}
@@ -150,17 +151,25 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, err
return inbound, common.NewError("Duplicate email:", existEmail)
}
clients, err := s.getClients(inbound)
clients, err := s.GetClients(inbound)
if err != nil {
return inbound, err
}
db := database.GetDB()
tx := db.Begin()
defer func() {
if err == nil {
tx.Commit()
} else {
tx.Rollback()
}
}()
err = db.Save(inbound).Error
err = tx.Save(inbound).Error
if err == nil {
for _, client := range clients {
s.AddClientStat(inbound.Id, &client)
s.AddClientStat(tx, inbound.Id, &client)
}
}
return inbound, err
@@ -208,7 +217,7 @@ func (s *InboundService) DelInbound(id int) error {
if err != nil {
return err
}
clients, err := s.getClients(inbound)
clients, err := s.GetClients(inbound)
if err != nil {
return err
}
@@ -244,6 +253,12 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
if err != nil {
return inbound, err
}
err = s.updateClientTraffics(oldInbound, inbound)
if err != nil {
return inbound, err
}
oldInbound.Up = inbound.Up
oldInbound.Down = inbound.Down
oldInbound.Total = inbound.Total
@@ -262,36 +277,92 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
return inbound, db.Save(oldInbound).Error
}
func (s *InboundService) AddInboundClient(data *model.Inbound) error {
clients, err := s.getClients(data)
func (s *InboundService) updateClientTraffics(oldInbound *model.Inbound, newInbound *model.Inbound) error {
oldClients, err := s.GetClients(oldInbound)
if err != nil {
return err
}
newClients, err := s.GetClients(newInbound)
if err != nil {
return err
}
db := database.GetDB()
tx := db.Begin()
defer func() {
if err != nil {
tx.Rollback()
} else {
tx.Commit()
}
}()
var emailExists bool
for _, oldClient := range oldClients {
emailExists = false
for _, newClient := range newClients {
if oldClient.Email == newClient.Email {
emailExists = true
break
}
}
if !emailExists {
err = s.DelClientStat(tx, oldClient.Email)
if err != nil {
return err
}
}
}
for _, newClient := range newClients {
emailExists = false
for _, oldClient := range oldClients {
if newClient.Email == oldClient.Email {
emailExists = true
break
}
}
if !emailExists {
err = s.AddClientStat(tx, oldInbound.Id, &newClient)
if err != nil {
return err
}
}
}
return nil
}
func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {
clients, err := s.GetClients(data)
if err != nil {
return false, err
}
var settings map[string]interface{}
err = json.Unmarshal([]byte(data.Settings), &settings)
if err != nil {
return err
return false, err
}
interfaceClients := settings["clients"].([]interface{})
existEmail, err := s.checkEmailsExistForClients(clients)
if err != nil {
return err
return false, err
}
if existEmail != "" {
return common.NewError("Duplicate email:", existEmail)
return false, common.NewError("Duplicate email:", existEmail)
}
oldInbound, err := s.GetInbound(data.Id)
if err != nil {
return err
return false, err
}
var oldSettings map[string]interface{}
err = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings)
if err != nil {
return err
return false, err
}
oldClients := oldSettings["clients"].([]interface{})
@@ -301,30 +372,57 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) error {
newSettings, err := json.MarshalIndent(oldSettings, "", " ")
if err != nil {
return err
return false, err
}
oldInbound.Settings = string(newSettings)
db := database.GetDB()
tx := db.Begin()
defer func() {
if err != nil {
tx.Rollback()
} else {
tx.Commit()
}
}()
needRestart := false
s.xrayApi.Init(p.GetAPIPort())
for _, client := range clients {
if len(client.Email) > 0 {
s.AddClientStat(data.Id, &client)
s.AddClientStat(tx, data.Id, &client)
err1 := s.xrayApi.AddUser(string(oldInbound.Protocol), oldInbound.Tag, map[string]interface{}{
"email": client.Email,
"id": client.ID,
"flow": client.Flow,
"password": client.Password,
})
if err1 == nil {
logger.Debug("Client added by api:", client.Email)
} else {
needRestart = true
}
} else {
needRestart = true
}
}
db := database.GetDB()
return db.Save(oldInbound).Error
s.xrayApi.Close()
return needRestart, tx.Save(oldInbound).Error
}
func (s *InboundService) DelInboundClient(inboundId int, clientId string) error {
func (s *InboundService) DelInboundClient(inboundId int, clientId string) (bool, error) {
oldInbound, err := s.GetInbound(inboundId)
if err != nil {
logger.Error("Load Old Data Error")
return err
return false, err
}
var settings map[string]interface{}
err = json.Unmarshal([]byte(oldInbound.Settings), &settings)
if err != nil {
return err
return false, err
}
email := ""
@@ -351,7 +449,7 @@ func (s *InboundService) DelInboundClient(inboundId int, clientId string) error
settings["clients"] = newClients
newSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return err
return false, err
}
oldInbound.Settings = string(newSettings)
@@ -360,39 +458,49 @@ func (s *InboundService) DelInboundClient(inboundId int, clientId string) error
err = s.DelClientStat(db, email)
if err != nil {
logger.Error("Delete stats Data Error")
return err
return false, err
}
err = s.DelClientIPs(db, email)
if err != nil {
logger.Error("Error in delete client IPs")
return err
return false, err
}
return db.Save(oldInbound).Error
needRestart := true
s.xrayApi.Init(p.GetAPIPort())
if len(email) > 0 {
err = s.xrayApi.RemoveUser(oldInbound.Tag, email)
if err == nil {
logger.Debug("Client deleted by api:", email)
needRestart = false
}
}
s.xrayApi.Close()
return needRestart, db.Save(oldInbound).Error
}
func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId string) error {
clients, err := s.getClients(data)
func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId string) (bool, error) {
clients, err := s.GetClients(data)
if err != nil {
return err
return false, err
}
var settings map[string]interface{}
err = json.Unmarshal([]byte(data.Settings), &settings)
if err != nil {
return err
return false, err
}
inerfaceClients := settings["clients"].([]interface{})
oldInbound, err := s.GetInbound(data.Id)
if err != nil {
return err
return false, err
}
oldClients, err := s.getClients(oldInbound)
oldClients, err := s.GetClients(oldInbound)
if err != nil {
return err
return false, err
}
oldEmail := ""
@@ -416,17 +524,17 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
if len(clients[0].Email) > 0 && clients[0].Email != oldEmail {
existEmail, err := s.checkEmailsExistForClients(clients)
if err != nil {
return err
return false, err
}
if existEmail != "" {
return common.NewError("Duplicate email:", existEmail)
return false, common.NewError("Duplicate email:", existEmail)
}
}
var oldSettings map[string]interface{}
err = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings)
if err != nil {
return err
return false, err
}
settingsClients := oldSettings["clients"].([]interface{})
settingsClients[clientIndex] = inerfaceClients[0]
@@ -434,36 +542,66 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
newSettings, err := json.MarshalIndent(oldSettings, "", " ")
if err != nil {
return err
return false, err
}
oldInbound.Settings = string(newSettings)
db := database.GetDB()
tx := db.Begin()
defer func() {
if err != nil {
tx.Rollback()
} else {
tx.Commit()
}
}()
if len(clients[0].Email) > 0 {
if len(oldEmail) > 0 {
err = s.UpdateClientStat(oldEmail, &clients[0])
if err != nil {
return err
return false, err
}
err = s.UpdateClientIPs(db, oldEmail, clients[0].Email)
err = s.UpdateClientIPs(tx, oldEmail, clients[0].Email)
if err != nil {
return err
return false, err
}
} else {
s.AddClientStat(data.Id, &clients[0])
s.AddClientStat(tx, data.Id, &clients[0])
}
} else {
err = s.DelClientStat(db, oldEmail)
err = s.DelClientStat(tx, oldEmail)
if err != nil {
return err
return false, err
}
err = s.DelClientIPs(db, oldEmail)
err = s.DelClientIPs(tx, oldEmail)
if err != nil {
return err
return false, err
}
}
return db.Save(oldInbound).Error
needRestart := true
s.xrayApi.Init(p.GetAPIPort())
if len(oldEmail) > 0 {
s.xrayApi.RemoveUser(oldInbound.Tag, oldEmail)
if clients[0].Enable {
err1 := s.xrayApi.AddUser(string(oldInbound.Protocol), oldInbound.Tag, map[string]interface{}{
"email": clients[0].Email,
"id": clients[0].ID,
"flow": clients[0].Flow,
"password": clients[0].Password,
})
if err1 == nil {
logger.Debug("Client edited by api:", clients[0].Email)
needRestart = false
}
} else {
logger.Debug("Client disabled by api:", clients[0].Email)
needRestart = false
}
}
s.xrayApi.Close()
return needRestart, tx.Save(oldInbound).Error
}
func (s *InboundService) AddTraffic(traffics []*xray.Traffic) error {
@@ -489,6 +627,7 @@ func (s *InboundService) AddTraffic(traffics []*xray.Traffic) error {
return err
}
func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err error) {
if len(traffics) == 0 {
return nil
@@ -601,15 +740,42 @@ func (s *InboundService) DisableInvalidInbounds() (int64, error) {
return count, err
}
func (s *InboundService) DisableInvalidClients() (int64, error) {
func (s *InboundService) DisableInvalidClients() (bool, int64, error) {
db := database.GetDB()
now := time.Now().Unix() * 1000
needRestart := false
if p != nil {
var results []struct {
Tag string
Email string
}
err := db.Table("inbounds").
Select("inbounds.tag, client_traffics.email").
Joins("JOIN client_traffics ON inbounds.id = client_traffics.inbound_id").
Where("((client_traffics.total > 0 AND client_traffics.up + client_traffics.down >= client_traffics.total) OR (client_traffics.expiry_time > 0 AND client_traffics.expiry_time <= ?)) AND client_traffics.enable = ?", now, true).
Scan(&results).Error
if err != nil {
return false, 0, err
}
s.xrayApi.Init(p.GetAPIPort())
for _, result := range results {
err = s.xrayApi.RemoveUser(result.Tag, result.Email)
if err == nil {
logger.Debug("Client deleted by api:", result.Email)
} else {
needRestart = true
}
}
s.xrayApi.Close()
}
result := db.Model(xray.ClientTraffic{}).
Where("((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ?", now, true).
Update("enable", false)
err := result.Error
count := result.RowsAffected
return count, err
return needRestart, count, err
}
func (s *InboundService) MigrationRemoveOrphanedTraffics() {
@@ -624,9 +790,7 @@ func (s *InboundService) MigrationRemoveOrphanedTraffics() {
`)
}
func (s *InboundService) AddClientStat(inboundId int, client *model.Client) error {
db := database.GetDB()
func (s *InboundService) AddClientStat(tx *gorm.DB, inboundId int, client *model.Client) error {
clientTraffic := xray.ClientTraffic{}
clientTraffic.InboundId = inboundId
clientTraffic.Email = client.Email
@@ -635,7 +799,7 @@ func (s *InboundService) AddClientStat(inboundId int, client *model.Client) erro
clientTraffic.Enable = true
clientTraffic.Up = 0
clientTraffic.Down = 0
result := db.Create(&clientTraffic)
result := tx.Create(&clientTraffic)
err := result.Error
if err != nil {
return err
@@ -673,6 +837,21 @@ func (s *InboundService) DelClientIPs(tx *gorm.DB, email string) error {
return tx.Where("client_email = ?", email).Delete(model.InboundClientIps{}).Error
}
func (s *InboundService) GetClientInboundByTrafficID(trafficId int) (traffic *xray.ClientTraffic, inbound *model.Inbound, err error) {
db := database.GetDB()
var traffics []*xray.ClientTraffic
err = db.Model(xray.ClientTraffic{}).Where("id = ?", trafficId).Find(&traffics).Error
if err != nil {
logger.Warning(err)
return nil, nil, err
}
if len(traffics) > 0 {
inbound, err = s.GetInbound(traffics[0].InboundId)
return traffics[0], inbound, err
}
return nil, nil, nil
}
func (s *InboundService) GetClientInboundByEmail(email string) (traffic *xray.ClientTraffic, inbound *model.Inbound, err error) {
db := database.GetDB()
var traffics []*xray.ClientTraffic
@@ -688,6 +867,89 @@ func (s *InboundService) GetClientInboundByEmail(email string) (traffic *xray.Cl
return nil, nil, nil
}
func (s *InboundService) GetClientByEmail(clientEmail string) (*xray.ClientTraffic, *model.Client, error) {
traffic, inbound, err := s.GetClientInboundByEmail(clientEmail)
if err != nil {
return nil, nil, err
}
if inbound == nil {
return nil, nil, common.NewError("Inbound Not Found For Email:", clientEmail)
}
clients, err := s.GetClients(inbound)
if err != nil {
return nil, nil, err
}
for _, client := range clients {
if client.Email == clientEmail {
return traffic, &client, nil
}
}
return nil, nil, common.NewError("Client Not Found In Inbound For Email:", clientEmail)
}
func (s *InboundService) SetClientTelegramUserID(trafficId int, tgId string) error {
traffic, inbound, err := s.GetClientInboundByTrafficID(trafficId)
if err != nil {
return err
}
if inbound == nil {
return common.NewError("Inbound Not Found For Traffic ID:", trafficId)
}
clientEmail := traffic.Email
oldClients, err := s.GetClients(inbound)
if err != nil {
return err
}
clientId := ""
for _, oldClient := range oldClients {
if oldClient.Email == clientEmail {
if inbound.Protocol == "trojan" {
clientId = oldClient.Password
} else {
clientId = oldClient.ID
}
break
}
}
if len(clientId) == 0 {
return common.NewError("Client Not Found For Email:", clientEmail)
}
var settings map[string]interface{}
err = json.Unmarshal([]byte(inbound.Settings), &settings)
if err != nil {
return err
}
clients := settings["clients"].([]interface{})
var newClients []interface{}
for client_index := range clients {
c := clients[client_index].(map[string]interface{})
if c["email"] == clientEmail {
c["tgId"] = tgId
newClients = append(newClients, interface{}(c))
}
}
settings["clients"] = newClients
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return err
}
inbound.Settings = string(modifiedSettings)
_, err = s.UpdateInboundClient(inbound, clientId)
if err != nil {
return err
}
return nil
}
func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, error) {
_, inbound, err := s.GetClientInboundByEmail(clientEmail)
if err != nil {
@@ -697,7 +959,7 @@ func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, er
return false, common.NewError("Inbound Not Found For Email:", clientEmail)
}
oldClients, err := s.getClients(inbound)
oldClients, err := s.GetClients(inbound)
if err != nil {
return false, err
}
@@ -741,7 +1003,13 @@ func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, er
return false, err
}
inbound.Settings = string(modifiedSettings)
return !clientOldEnabled, s.UpdateInboundClient(inbound, clientId)
_, err = s.UpdateInboundClient(inbound, clientId)
if err != nil {
return false, err
}
return !clientOldEnabled, nil
}
func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int) error {
@@ -753,7 +1021,7 @@ func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int
return common.NewError("Inbound Not Found For Email:", clientEmail)
}
oldClients, err := s.getClients(inbound)
oldClients, err := s.GetClients(inbound)
if err != nil {
return err
}
@@ -795,9 +1063,13 @@ func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int
return err
}
inbound.Settings = string(modifiedSettings)
return s.UpdateInboundClient(inbound, clientId)
}
_, err = s.UpdateInboundClient(inbound, clientId)
if err != nil {
return err
}
return nil
}
func (s *InboundService) ResetClientExpiryTimeByEmail(clientEmail string, expiry_time int64) error {
_, inbound, err := s.GetClientInboundByEmail(clientEmail)
if err != nil {
@@ -807,7 +1079,7 @@ func (s *InboundService) ResetClientExpiryTimeByEmail(clientEmail string, expiry
return common.NewError("Inbound Not Found For Email:", clientEmail)
}
oldClients, err := s.getClients(inbound)
oldClients, err := s.GetClients(inbound)
if err != nil {
return err
}
@@ -849,7 +1121,12 @@ func (s *InboundService) ResetClientExpiryTimeByEmail(clientEmail string, expiry
return err
}
inbound.Settings = string(modifiedSettings)
return s.UpdateInboundClient(inbound, clientId)
_, err = s.UpdateInboundClient(inbound, clientId)
if err != nil {
return err
}
return nil
}
func (s *InboundService) ResetClientTrafficByEmail(clientEmail string) error {
@@ -867,19 +1144,54 @@ func (s *InboundService) ResetClientTrafficByEmail(clientEmail string) error {
return nil
}
func (s *InboundService) ResetClientTraffic(id int, clientEmail string) error {
db := database.GetDB()
result := db.Model(xray.ClientTraffic{}).
Where("inbound_id = ? and email = ?", id, clientEmail).
Updates(map[string]interface{}{"enable": true, "up": 0, "down": 0})
err := result.Error
func (s *InboundService) ResetClientTraffic(id int, clientEmail string) (bool, error) {
needRestart := false
traffic, err := s.GetClientTrafficByEmail(clientEmail)
if err != nil {
return err
return false, err
}
return nil
if !traffic.Enable {
inbound, err := s.GetInbound(id)
if err != nil {
return false, err
}
clients, err := s.GetClients(inbound)
if err != nil {
return false, err
}
for _, client := range clients {
if client.Email == clientEmail {
s.xrayApi.Init(p.GetAPIPort())
err1 := s.xrayApi.AddUser(string(inbound.Protocol), inbound.Tag, map[string]interface{}{
"email": client.Email,
"id": client.ID,
"flow": client.Flow,
"password": client.Password,
})
if err1 == nil {
logger.Debug("Client enabled due to reset traffic:", clientEmail)
} else {
needRestart = true
}
s.xrayApi.Close()
break
}
}
}
traffic.Up = 0
traffic.Down = 0
traffic.Enable = true
db := database.GetDB()
err = db.Save(traffic).Error
if err != nil {
return false, err
}
return needRestart, nil
}
func (s *InboundService) ResetAllClientTraffics(id int) error {
@@ -1006,7 +1318,7 @@ func (s *InboundService) GetClientTrafficTgBot(tguname string) ([]*xray.ClientTr
}
var emails []string
for _, inbound := range inbounds {
clients, err := s.getClients(inbound)
clients, err := s.GetClients(inbound)
if err != nil {
logger.Error("Unable to get clients from inbound")
}
@@ -1091,6 +1403,7 @@ func (s *InboundService) GetInboundClientIps(clientEmail string) (string, error)
}
return InboundClientIps.Ips, nil
}
func (s *InboundService) ClearClientIps(clientEmail string) error {
db := database.GetDB()
@@ -1117,10 +1430,19 @@ func (s *InboundService) SearchInbounds(query string) ([]*model.Inbound, error)
func (s *InboundService) MigrationRequirements() {
db := database.GetDB()
tx := db.Begin()
var err error
defer func() {
if err == nil {
tx.Commit()
} else {
tx.Rollback()
}
}()
// Fix inbounds based problems
var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Where("protocol IN (?)", []string{"vmess", "vless", "trojan"}).Find(&inbounds).Error
err = tx.Model(model.Inbound{}).Where("protocol IN (?)", []string{"vmess", "vless", "trojan"}).Find(&inbounds).Error
if err != nil && err != gorm.ErrRecordNotFound {
return
}
@@ -1155,25 +1477,26 @@ func (s *InboundService) MigrationRequirements() {
inbounds[inbound_index].Settings = string(modifiedSettings)
}
// Add client traffic row for all clients which has email
modelClients, err := s.getClients(inbounds[inbound_index])
modelClients, err := s.GetClients(inbounds[inbound_index])
if err != nil {
return
}
for _, modelClient := range modelClients {
if len(modelClient.Email) > 0 {
var count int64
db.Model(xray.ClientTraffic{}).Where("email = ?", modelClient.Email).Count(&count)
tx.Model(xray.ClientTraffic{}).Where("email = ?", modelClient.Email).Count(&count)
if count == 0 {
s.AddClientStat(inbounds[inbound_index].Id, &modelClient)
s.AddClientStat(tx, inbounds[inbound_index].Id, &modelClient)
}
}
}
}
db.Save(inbounds)
tx.Save(inbounds)
// Remove orphaned traffics
db.Where("inbound_id = 0").Delete(xray.ClientTraffic{})
tx.Where("inbound_id = 0").Delete(xray.ClientTraffic{})
}
func (s *InboundService) MigrateDB() {

View File

@@ -38,9 +38,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 +71,10 @@ 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"`
}
type Release struct {
@@ -80,6 +86,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 +119,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 +202,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 = ""
@@ -336,16 +380,17 @@ func (s *ServerService) UpdateXray(version string) error {
}
func (s *ServerService) GetLogs(count string) ([]string, error) {
// Define the journalctl command and its arguments
func (s *ServerService) GetLogs(count string, logLevel string) ([]string, error) {
var cmdArgs []string
if runtime.GOOS == "linux" {
cmdArgs = []string{"journalctl", "-u", "x-ui", "--no-pager", "-n", count}
if logLevel != "" {
cmdArgs = append(cmdArgs, "-p", logLevel)
}
} else {
return []string{"Unsupported operating system"}, nil
}
// Run the command
cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
var out bytes.Buffer
cmd.Stdout = &out

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,18 @@ var defaultValueMap = map[string]string{
"tgBotChatId": "",
"tgRunTime": "@daily",
"tgBotBackup": "false",
"tgBotLoginNotify": "false",
"tgCpu": "0",
"tgLang": "en-US",
"secretEnable": "false",
"subEnable": "false",
"subListen": "",
"subPort": "2096",
"subPath": "/sub/",
"subDomain": "",
"subCertFile": "",
"subKeyFile": "",
"subUpdates": "12",
}
type SettingService struct {
@@ -216,6 +227,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 +267,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 +354,48 @@ 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) 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 {
@@ -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" {
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."
@@ -148,8 +149,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 +168,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 +210,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 +220,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 +256,24 @@
"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"
[pages.settings.templates]
"title" = "Templates"
@@ -260,57 +281,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 +375,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" = "آپلود دیتابیس"
@@ -167,7 +168,7 @@
"setDefaultCert" = "استفاده از گواهی پنل"
"xtlsDesc" = "هسته Xray باید 1.7.5 باشد"
"realityDesc" = "هسته Xray باید 1.8.0 و بالاتر باشد"
"telegramDesc" = "از آیدی تلگرام بدون @ یا آیدی چت استفاده کنید (می توانید آن را از اینجا دریافت کنید @userinfobot)"
"telegramDesc" = "از آیدی تلگرام بدون @ یا آیدی چت استفاده کنید (می توانید آن را از اینجا دریافت کنید @userinfobot یا در ربات دستور '/id' را وارد کنید)"
"subscriptionDesc" = "می توانید ساب لینک خود را در جزئیات پیدا کنید، همچنین می توانید از همین نام برای چندین کانفیگ استفاده کنید"
[pages.client]
@@ -209,6 +210,7 @@
[pages.settings]
"title" = "تنظیمات"
"save" = "ذخیره"
"infoDesc" = "برای اعمال تغییرات در این بخش باید پس از ذخیره کردن، پنل را ریستارت کنید"
"restartPanel" = "ریستارت پنل"
"restartPanelDesc" = "آیا مطمئن هستید که می خواهید پنل را دوباره راه اندازی کنید؟ برای راه اندازی مجدد روی OK کلیک کنید. اگر بعد از 3 ثانیه نمی توانید به پنل دسترسی پیدا کنید، لطفاً برای مشاهده اطلاعات گزارش پانل به سرور برگردید"
"actions" = "عملیات ها"
@@ -218,29 +220,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 +257,23 @@
"tgNotifyCpuDesc" = "این ربات تلگرام در صورت استفاده پردازنده بیشتر از این درصد برای شما پیام ارسال می کند.(واحد: درصد)"
"timeZone" = "منظقه زمانی"
"timeZoneDesc" = "وظایف برنامه ریزی شده بر اساس این منطقه زمانی اجرا می شوند. پنل را مجدداً راه اندازی می کند تا اعمال شود"
"subSettings" = "سابسکریپشن"
"subEnable" = "فعال کردن سرویس"
"subEnableDesc" = "ویژگی سابسکریپشن با پیکربندی جداگانه"
"subListen" = "محدودیت آی‌پی"
"subListenDesc" = "برای استفاده از همه آی‌پی ها به طور پیش فرض خالی بگذارید"
"subPort" = "پورت سرویس"
"subPortDesc" = "شماره پورت برای ارائه خدمات سابسکریپشن باید خالی باشد"
"subCertPath" = "مسیر فایل کلید عمومی گواهی سابسکریپشن"
"subCertPathDesc" = "یک مسیر مطلق که با '/' شروع می شود را پر کنید."
"subKeyPath" = "مسیر فایل کلید خصوصی گواهی سابسکریپشن"
"subKeyPathDesc" = "یک مسیر مطلق که با '/' شروع می شود را پر کنید."
"subPath" = "مسیر ریشه سابسکریپشن"
"subPathDesc" = "باید با '/' شروع شود و با '/' ختم شود."
"subDomain" = "دامنه مخصوص سابسکریپشن"
"subDomainDesc" = "برای نظارت بر همه دامنه ها و آی‌پی ها به طور پیش فرض خالی بگذارید"
"subUpdates" = "فاصله به روز رسانی های سابسکریپشن"
"subUpdatesDesc" = "ساعت های فاصله بین به روز رسانی در برنامه کاربر"
[pages.settings.templates]
"title" = "الگوها"
@@ -258,57 +281,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 +373,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,126 +1,127 @@
"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" = "Сеть"
@@ -130,7 +131,7 @@
"monitorDesc" = "Оставьте пустым по умолчанию"
"meansNoLimit" = "Значит без ограничений"
"totalFlow" = "Общий расход"
"leaveBlankToNeverExpire" = "Оставьте пустым = бессрочно"
"leaveBlankToNeverExpire" = "Оставьте пустым, чтобы не истекало"
"noRecommendKeepDefault" = "Нет требований для сохранения настроек по умолчанию"
"certificatePath" = "Путь файла сертификата"
"certificateContent" = "Содержимое файла сертификата"
@@ -138,39 +139,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 +183,7 @@
"last" = "Последний"
"prefix" = "Префикс"
"postfix" = "Постфикс"
"delayedStart" = "Начать со времени первого подключения"
"delayedStart" = "Начать со момента первого подключения"
"expireDays" = "Срок действия"
"days" = "дней"
@@ -192,18 +191,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 +210,156 @@
[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" = "Часовой интервал между обновлениями в клиентском приложении"
[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 +367,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

@@ -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" = "系统启动以来所有网卡的总上传流量"
@@ -167,7 +168,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 +210,7 @@
[pages.settings]
"title" = "设置"
"save" = "保存配置"
"infoDesc" = "此处的所有更改都需要保存并重启面板才能生效"
"restartPanel" = "重启面板"
"restartPanelDesc" = "确定要重启面板吗?点击确定将于 3 秒后重启,若重启后无法访问面板,请前往服务器查看面板日志信息"
"actions" = "动作"
@@ -218,15 +220,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 +240,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 +256,110 @@
"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" = "客户端应用程序更新之间的间隔时间"
[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 +375,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
@@ -147,28 +145,6 @@ func (s *Server) getHtmlTemplate(funcMap template.FuncMap) (*template.Template,
return t, nil
}
func redirectMiddleware(basePath string) gin.HandlerFunc {
return func(c *gin.Context) {
// Redirect from old '/xui' path to '/panel'
path := c.Request.URL.Path
redirects := map[string]string{
"panel/API": "panel/api",
"xui/API": "panel/api",
"xui": "panel",
}
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()
}
}
func (s *Server) initRouter() (*gin.Engine, error) {
if config.IsDebug() {
gin.SetMode(gin.DebugMode)
@@ -180,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
@@ -202,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
@@ -216,17 +211,17 @@ 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(redirectMiddleware(basePath))
engine.Use(middleware.RedirectMiddleware(basePath))
g := engine.Group(basePath)
@@ -234,92 +229,10 @@ func (s *Server) initRouter() (*gin.Engine, error) {
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 {
@@ -337,8 +250,8 @@ func (s *Server) startTask() {
// 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 20 sec
s.cron.AddJob("@every 20s", job.NewCheckClientIpJob())
// Make a traffic condition every day, 8:30
var entry cron.EntryID
@@ -346,7 +259,7 @@ func (s *Server) startTask() {
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)
@@ -356,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)
}
@@ -441,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
@@ -453,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

304
x-ui.sh
View File

@@ -517,7 +517,27 @@ 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
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
@@ -547,9 +567,10 @@ ssl_cert_issue() {
LOGD "your domain is:${domain},check it..."
#here we need to judge whether there exists cert already
local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}')
if [ ${currentCert} == ${domain} ]; then
local certInfo=$(~/.acme.sh/acme.sh --list)
LOGE "system already 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
@@ -610,35 +631,56 @@ ssl_cert_issue() {
}
warp_fixchatgpt() {
curl -fsSL https://gist.githubusercontent.com/hamid-gh98/dc5dd9b0cc5b0412af927b1ccdb294c7/raw/install_warp_proxy.sh | bash
echo ""
before_show_menu
warp_cloudflare() {
echo -e "${green}\t1.${plain} install WARP"
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://gist.githubusercontent.com/hamid-gh98/dc5dd9b0cc5b0412af927b1ccdb294c7/raw/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 | sudo bash
sudo $pkg_manager install -y speedtest
fi
fi
@@ -646,6 +688,187 @@ run_speedtest() {
speedtest
}
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} 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
echo -e "\n[3x-ipl]\nenabled=true\nfilter=3x-ipl\naction=3x-ipl\nlogpath=/var/log/3xipl.log\nmaxretry=3\nfindtime=100\nbantime=${NUM}m" > /etc/fail2ban/jail.d/3x-ipl.conf
sudo systemctl restart fail2ban
echo -e "${green}Bantime set to ${NUM} minutes successfully.${plain}"
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 "/var/log/3xipl-banned.log"; then
if [[ -s "/var/log/3xipl-banned.log" ]]; then
cat /var/log/3xipl-banned.log
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)
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)
sudo apt-get update && sudo apt-get install fail2ban -y ;;
centos)
sudo yum -y update && sudo yum -y install fail2ban ;;
fedora)
sudo dnf -y update && sudo 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"
#Check if [3x-ipl] exists in jail.local (just making sure there's no double config for jail)
if grep -qw '3x-ipl' /etc/fail2ban/jail.local || grep -qw '3x-ipl' /etc/fail2ban/jail.conf; then
echo -e "${red}Found conflicts in /etc/fail2ban/jail.conf or jail.local file!\nPlease manually remove anything related 3x-ipl in that files and try again.\nInstallation of IP Limit failed.${plain}\n"
exit 1
fi
#Check if log file exists
if ! test -f "/var/log/3xipl-banned.log"; then
touch /var/log/3xipl-banned.log
fi
#Check if service log file exists so fail2ban won't return error
if ! test -f "/var/log/3xipl.log"; then
touch /var/log/3xipl.log
fi
echo -e "\n[3x-ipl]\nenabled=true\nfilter=3x-ipl\naction=3x-ipl\nlogpath=/var/log/3xipl.log\nmaxretry=3\nfindtime=100\nbantime=5m" > /etc/fail2ban/jail.d/3x-ipl.conf
sudo cat > /etc/fail2ban/filter.d/3x-ipl.conf << EOF
[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
sudo cat > /etc/fail2ban/action.d/3x-ipl.conf << 'EOF'
[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." >> /var/log/3xipl-banned.log
actionunban = <iptables> -D f2b-<name> -s <ip> -j <blocktype>
echo "$(date +"%%Y/%%m/%%d %%H:%%M:%%S") UNBAN [Email] = <F-USER> [IP] = <ip> unbanned." >> /var/log/3xipl-banned.log
[Init]
EOF
#Launching fail2ban
if ! sudo systemctl is-active --quiet fail2ban; then
sudo systemctl start fail2ban
else
systemctl restart fail2ban
fi
sudo 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
sudo systemctl restart fail2ban
echo -e "${green}IP Limit removed successfully!${plain}\n"
before_show_menu ;;
2)
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
sudo systemctl stop fail2ban
sudo systemctl disable fail2ban
case "${release}" in
ubuntu|debian)
sudo apt-get remove fail2ban -y ;;
centos)
sudo yum -y remove fail2ban ;;
fedora)
sudo dnf -y remove fail2ban ;;
*)
echo -e "${red}Unsupported operating system. Please uninstall Fail2ban manually.${plain}\n"
exit 1 ;;
esac
rm -rf /etc/fail2ban/*
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 +909,17 @@ 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} IP Limit Management
${green}17.${plain} WARP Management
————————————————
${green}18.${plain} Enable BBR
${green}19.${plain} Update Geo Files
${green}20.${plain} Active Firewall and open ports
${green}21.${plain} Speedtest by Ookla
"
show_status
echo && read -p "Please enter your selection [0-20]: " num
echo && read -p "Please enter your selection [0-21]: " num
case "${num}" in
0)
@@ -743,25 +968,28 @@ show_menu() {
check_install && disable
;;
15)
enable_bbr
ssl_cert_issue_main
;;
16)
ssl_cert_issue
iplimit_main
;;
17)
update_geo
warp_cloudflare
;;
18)
open_ports
enable_bbr
;;
19)
warp_fixchatgpt
update_geo
;;
20)
run_speedtest
open_ports
;;
21)
run_speedtest
;;
*)
LOGE "Please enter the correct number [0-20]"
LOGE "Please enter the correct number [0-21]"
;;
esac
}

181
xray/api.go Normal file
View File

@@ -0,0 +1,181 @@
package xray
import (
"context"
"fmt"
"regexp"
"time"
"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/proxy/shadowsocks"
"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) 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":
account = serial.ToTypedMessage(&shadowsocks.Account{
Password: user["password"].(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
}

View File

@@ -3,28 +3,22 @@ package xray
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io/fs"
"os"
"os/exec"
"regexp"
"runtime"
"strings"
"time"
"sync"
"syscall"
"x-ui/config"
"x-ui/util/common"
"github.com/Workiva/go-datastructures/queue"
statsservice "github.com/xtls/xray-core/app/stats/command"
"google.golang.org/grpc"
)
var trafficRegex = regexp.MustCompile("(inbound|outbound)>>>([^>]+)>>>traffic>>>(downlink|uplink)")
var ClientTrafficRegex = regexp.MustCompile("(user)>>>([^>]+)>>>traffic>>>(downlink|uplink)")
func GetBinaryName() string {
return fmt.Sprintf("xray-%s-%s", runtime.GOOS, runtime.GOARCH)
}
@@ -50,7 +44,7 @@ func GetIranPath() string {
}
func GetBlockedIPsPath() string {
return config.GetBinFolderPath() + "/blockedIPs"
return config.GetBinFolderPath() + "/BlockedIps"
}
func stopProcess(p *Process) {
@@ -183,11 +177,11 @@ func (p *process) Start() (err error) {
return err
}
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer func() {
common.Recover("")
stdReader.Close()
}()
defer wg.Done()
reader := bufio.NewReaderSize(stdReader, 8192)
for {
line, _, err := reader.ReadLine()
@@ -202,10 +196,7 @@ func (p *process) Start() (err error) {
}()
go func() {
defer func() {
common.Recover("")
errReader.Close()
}()
defer wg.Done()
reader := bufio.NewReaderSize(errReader, 8192)
for {
line, _, err := reader.ReadLine()
@@ -224,6 +215,7 @@ func (p *process) Start() (err error) {
if err != nil {
p.exitErr = err
}
wg.Wait()
}()
p.refreshVersion()
@@ -236,87 +228,5 @@ func (p *process) Stop() error {
if !p.IsRunning() {
return errors.New("xray is not running")
}
return p.cmd.Process.Kill()
}
func (p *process) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) {
if p.apiPort == 0 {
return nil, nil, common.NewError("xray api port wrong:", p.apiPort)
}
conn, err := grpc.Dial(fmt.Sprintf("127.0.0.1:%v", p.apiPort), grpc.WithInsecure())
if err != nil {
return nil, nil, err
}
defer conn.Close()
client := statsservice.NewStatsServiceClient(conn)
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
return p.cmd.Process.Signal(syscall.SIGTERM)
}