Compare commits

...

108 Commits

Author SHA1 Message Date
MHSanaei
329889ec00 v1.7.5 2023-08-02 17:19:50 +03:30
MHSanaei
95268dbc61 update dependencies 2023-08-02 17:02:32 +03:30
dependabot[bot]
9a3200c9b5 Bump github.com/mymmrac/telego from 0.25.1 to 0.26.0 (#852)
Bumps [github.com/mymmrac/telego](https://github.com/mymmrac/telego) from 0.25.1 to 0.26.0.
- [Release notes](https://github.com/mymmrac/telego/releases)
- [Commits](https://github.com/mymmrac/telego/compare/v0.25.1...v0.26.0)

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

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

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

change bootmortis project to MasterKia fork

* Update DockerInit.sh

replace bootmortis with MasterKia

* Update release.yml

Replace bootmortis with MasterKia
2023-08-02 00:27:12 +03:30
dependabot[bot]
07b9474212 Bump github.com/shirou/gopsutil/v3 from 3.23.6 to 3.23.7 (#845)
Bumps [github.com/shirou/gopsutil/v3](https://github.com/shirou/gopsutil) from 3.23.6 to 3.23.7.
- [Release notes](https://github.com/shirou/gopsutil/releases)
- [Commits](https://github.com/shirou/gopsutil/compare/v3.23.6...v3.23.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-01 13:18:29 +03:30
MHSanaei
bf971911d5 log level & syslog
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-07-31 20:11:47 +03:30
MHSanaei
c46ced0c4c fix logs in api
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-07-31 19:52:28 +03:30
MHSanaei
dd3bbbc4af [SS] fix bulk creation
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-07-31 19:49:27 +03:30
dependabot[bot]
a97f90d225 Bump svenstaro/upload-release-action from 2.6.1 to 2.7.0 (#834)
Bumps [svenstaro/upload-release-action](https://github.com/svenstaro/upload-release-action) from 2.6.1 to 2.7.0.
- [Release notes](https://github.com/svenstaro/upload-release-action/releases)
- [Changelog](https://github.com/svenstaro/upload-release-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/svenstaro/upload-release-action/compare/2.6.1...2.7.0)

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

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

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

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

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-01 15:57:07 +03:30
dependabot[bot]
467c5f807e Bump docker/setup-buildx-action from 2.7.0 to 2.8.0 (#674)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2.7.0 to 2.8.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2.7.0...v2.8.0)

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-22 12:03:01 +03:30
dependabot[bot]
e92aba0179 Bump github.com/mymmrac/telego from 0.25.0 to 0.25.1 (#635)
Bumps [github.com/mymmrac/telego](https://github.com/mymmrac/telego) from 0.25.0 to 0.25.1.
- [Release notes](https://github.com/mymmrac/telego/releases)
- [Commits](https://github.com/mymmrac/telego/compare/v0.25.0...v0.25.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-22 12:02:50 +03:30
MHSanaei
85fb2fda5e simplify with show remaining flow
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-06-21 14:31:09 +03:30
MHSanaei
f57e693023 better clients info tables
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-06-21 14:02:13 +03:30
dependabot[bot]
83f6f13b50 Bump github.com/xtls/xray-core from 1.8.1 to 1.8.3 (#621)
Bumps [github.com/xtls/xray-core](https://github.com/xtls/xray-core) from 1.8.1 to 1.8.3.
- [Release notes](https://github.com/xtls/xray-core/releases)
- [Commits](https://github.com/xtls/xray-core/compare/v1.8.1...v1.8.3)

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

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

* Logging for fail2ban service
Removed limitDevice and other unnecessary functions

* fixed shouldCleanLog

* last fix

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

* fixed check delay

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-14 15:38:57 +03:30
dependabot[bot]
526e2578b0 Bump docker/metadata-action from 4.5.0 to 4.6.0 (#603)
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 4.5.0 to 4.6.0.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](https://github.com/docker/metadata-action/compare/v4.5.0...v4.6.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-14 15:38:47 +03:30
MHSanaei
ad9134bc1a removed - alterId
https://github.com/XTLS/Xray-core/pull/2199
9112cfd39c
2023-06-13 17:26:50 +03:30
dependabot[bot]
b5657ab87d Bump golang.org/x/text from 0.9.0 to 0.10.0 (#595)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.9.0 to 0.10.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.9.0...v0.10.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-13 15:36:33 +03:30
dependabot[bot]
2c7ec3dc8a Bump docker/setup-buildx-action from 2.6.0 to 2.7.0 (#594)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2.6.0 to 2.7.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2.6.0...v2.7.0)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-01 13:45:02 +03:30
dependabot[bot]
d73a995257 Bump github.com/shirou/gopsutil/v3 from 3.23.4 to 3.23.5 (#554)
Bumps [github.com/shirou/gopsutil/v3](https://github.com/shirou/gopsutil) from 3.23.4 to 3.23.5.
- [Release notes](https://github.com/shirou/gopsutil/releases)
- [Commits](https://github.com/shirou/gopsutil/compare/v3.23.4...v3.23.5)

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

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

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

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

* Update settings.html

* Update custom.css

* Update custom.css
2023-05-27 14:14:35 +03:30
74 changed files with 3092 additions and 1632 deletions

View File

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

View File

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

7
DockerEntrypoint.sh Normal file
View File

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

View File

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

View File

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

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

View File

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

View File

@@ -1 +1 @@
1.6.1 1.7.5

View File

@@ -6,6 +6,7 @@ import (
"io/fs" "io/fs"
"os" "os"
"path" "path"
"x-ui/config" "x-ui/config"
"x-ui/database/model" "x-ui/database/model"
"x-ui/xray" "x-ui/xray"
@@ -26,7 +27,6 @@ var initializers = []func() error{
} }
func initUser() error { func initUser() error {
err := db.AutoMigrate(&model.User{}) err := db.AutoMigrate(&model.User{})
if err != nil { if err != nil {
return err return err
@@ -54,9 +54,11 @@ func initInbound() error {
func initSetting() error { func initSetting() error {
return db.AutoMigrate(&model.Setting{}) return db.AutoMigrate(&model.Setting{})
} }
func initInboundClientIps() error { func initInboundClientIps() error {
return db.AutoMigrate(&model.InboundClientIps{}) return db.AutoMigrate(&model.InboundClientIps{})
} }
func initClientTraffic() error { func initClientTraffic() error {
return db.AutoMigrate(&xray.ClientTraffic{}) return db.AutoMigrate(&xray.ClientTraffic{})
} }

View File

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

81
go.mod
View File

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

374
go.sum
View File

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

View File

@@ -8,7 +8,7 @@ plain='\033[0m'
cur_dir=$(pwd) cur_dir=$(pwd)
# check root # check root
[[ $EUID -ne 0 ]] && echo -e "${red}Fatal error${plain} Please run this script with root privilege \n " && exit 1 [[ $EUID -ne 0 ]] && echo -e "${red}Fatal error: ${plain} Please run this script with root privilege \n " && exit 1
# Check OS and set release variable # Check OS and set release variable
if [[ -f /etc/os-release ]]; then if [[ -f /etc/os-release ]]; then
@@ -41,12 +41,12 @@ if [[ "${release}" == "centos" ]]; then
fi fi
elif [[ "${release}" == "ubuntu" ]]; then elif [[ "${release}" == "ubuntu" ]]; then
if [[ ${os_version} -lt 20 ]]; then if [[ ${os_version} -lt 20 ]]; then
echo -e "${red}please use Ubuntu 20 or higher version${plain}\n" && exit 1 echo -e "${red}please use Ubuntu 20 or higher version!${plain}\n" && exit 1
fi fi
elif [[ "${release}" == "fedora" ]]; then elif [[ "${release}" == "fedora" ]]; then
if [[ ${os_version} -lt 36 ]]; then if [[ ${os_version} -lt 36 ]]; then
echo -e "${red}please use Fedora 36 or higher version${plain}\n" && exit 1 echo -e "${red}please use Fedora 36 or higher version!${plain}\n" && exit 1
fi fi
elif [[ "${release}" == "debian" ]]; then elif [[ "${release}" == "debian" ]]; then
@@ -68,7 +68,7 @@ install_base() {
esac esac
} }
#This function will be called when user installed x-ui out of sercurity # This function will be called when user installed x-ui out of sercurity
config_after_install() { config_after_install() {
echo -e "${yellow}Install/update finished! For security it's recommended to modify panel settings ${plain}" echo -e "${yellow}Install/update finished! For security it's recommended to modify panel settings ${plain}"
read -p "Do you want to continue with the modification [y/n]? ": config_confirm read -p "Do you want to continue with the modification [y/n]? ": config_confirm

View File

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

22
main.go
View File

@@ -12,7 +12,6 @@ import (
"x-ui/database" "x-ui/database"
"x-ui/logger" "x-ui/logger"
"x-ui/sub" "x-ui/sub"
"x-ui/v2ui"
"x-ui/web" "x-ui/web"
"x-ui/web/global" "x-ui/web/global"
"x-ui/web/service" "x-ui/web/service"
@@ -28,6 +27,8 @@ func runWebServer() {
logger.InitLogger(logging.DEBUG) logger.InitLogger(logging.DEBUG)
case config.Info: case config.Info:
logger.InitLogger(logging.INFO) logger.InitLogger(logging.INFO)
case config.Notice:
logger.InitLogger(logging.NOTICE)
case config.Warn: case config.Warn:
logger.InitLogger(logging.WARNING) logger.InitLogger(logging.WARNING)
case config.Error: case config.Error:
@@ -270,10 +271,6 @@ func main() {
runCmd := flag.NewFlagSet("run", flag.ExitOnError) 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) settingCmd := flag.NewFlagSet("setting", flag.ExitOnError)
var port int var port int
var username string var username string
@@ -301,7 +298,6 @@ func main() {
fmt.Println() fmt.Println()
fmt.Println("Commands:") fmt.Println("Commands:")
fmt.Println(" run run web panel") 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(" migrate migrate form other/old x-ui")
fmt.Println(" setting set settings") fmt.Println(" setting set settings")
} }
@@ -322,16 +318,6 @@ func main() {
runWebServer() runWebServer()
case "migrate": case "migrate":
migrateDb() 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": case "setting":
err := settingCmd.Parse(os.Args[2:]) err := settingCmd.Parse(os.Args[2:])
if err != nil { if err != nil {
@@ -356,12 +342,10 @@ func main() {
updateTgbotEnableSts(enabletgbot) updateTgbotEnableSts(enabletgbot)
} }
default: default:
fmt.Println("except 'run' or 'v2-ui' or 'setting' subcommands") fmt.Println("except 'run' or 'setting' subcommands")
fmt.Println() fmt.Println()
runCmd.Usage() runCmd.Usage()
fmt.Println() fmt.Println()
v2uiCmd.Usage()
fmt.Println()
settingCmd.Usage() settingCmd.Usage()
} }
} }

View File

@@ -7,10 +7,10 @@ import (
"net" "net"
"net/http" "net/http"
"strconv" "strconv"
"strings"
"x-ui/config" "x-ui/config"
"x-ui/logger" "x-ui/logger"
"x-ui/util/common" "x-ui/util/common"
"x-ui/web/middleware"
"x-ui/web/network" "x-ui/web/network"
"x-ui/web/service" "x-ui/web/service"
@@ -58,18 +58,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
} }
if subDomain != "" { if subDomain != "" {
validateDomain := func(c *gin.Context) { engine.Use(middleware.DomainValidatorMiddleware(subDomain))
host := strings.Split(c.Request.Host, ":")[0]
if host != subDomain {
c.AbortWithStatus(http.StatusForbidden)
return
}
c.Next()
}
engine.Use(validateDomain)
} }
g := engine.Group(subPath) g := engine.Group(subPath)
@@ -116,11 +105,13 @@ func (s *Server) Start() (err error) {
if err != nil { if err != nil {
return err return err
} }
listenAddr := net.JoinHostPort(listen, strconv.Itoa(port)) listenAddr := net.JoinHostPort(listen, strconv.Itoa(port))
listener, err := net.Listen("tcp", listenAddr) listener, err := net.Listen("tcp", listenAddr)
if err != nil { if err != nil {
return err return err
} }
if certFile != "" || keyFile != "" { if certFile != "" || keyFile != "" {
cert, err := tls.LoadX509KeyPair(certFile, keyFile) cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil { if err != nil {

View File

@@ -5,9 +5,11 @@ import (
"fmt" "fmt"
"net/url" "net/url"
"strings" "strings"
"time"
"x-ui/database" "x-ui/database"
"x-ui/database/model" "x-ui/database/model"
"x-ui/logger" "x-ui/logger"
"x-ui/util/common"
"x-ui/web/service" "x-ui/web/service"
"x-ui/xray" "x-ui/xray"
@@ -55,7 +57,7 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, []string, err
} }
for _, client := range clients { for _, client := range clients {
if client.Enable && client.SubID == subId { if client.Enable && client.SubID == subId {
link := s.getLink(inbound, client.Email) link := s.getLink(inbound, client.Email, client.ExpiryTime)
result = append(result, link) result = append(result, link)
clientTraffics = append(clientTraffics, s.getClientTraffics(inbound.ClientStats, client.Email)) clientTraffics = append(clientTraffics, s.getClientTraffics(inbound.ClientStats, client.Email))
} }
@@ -121,25 +123,36 @@ func (s *SubService) getFallbackMaster(dest string) (*model.Inbound, error) {
return inbound, nil return inbound, nil
} }
func (s *SubService) getLink(inbound *model.Inbound, email string) string { func (s *SubService) getLink(inbound *model.Inbound, email string, expiryTime int64) string {
switch inbound.Protocol { switch inbound.Protocol {
case "vmess": case "vmess":
return s.genVmessLink(inbound, email) return s.genVmessLink(inbound, email, expiryTime)
case "vless": case "vless":
return s.genVlessLink(inbound, email) return s.genVlessLink(inbound, email, expiryTime)
case "trojan": case "trojan":
return s.genTrojanLink(inbound, email) return s.genTrojanLink(inbound, email, expiryTime)
case "shadowsocks": case "shadowsocks":
return s.genShadowsocksLink(inbound, email) return s.genShadowsocksLink(inbound, email, expiryTime)
} }
return "" return ""
} }
func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string { func (s *SubService) genVmessLink(inbound *model.Inbound, email string, expiryTime int64) string {
if inbound.Protocol != model.VMess { if inbound.Protocol != model.VMess {
return "" return ""
} }
remark := fmt.Sprintf("%s-%s", inbound.Remark, email)
remainedTraffic := s.getRemainedTraffic(email)
expiryTimeString := getExpiryTime(expiryTime)
remark := ""
isTerminated := strings.Contains(expiryTimeString, "Terminated") || strings.Contains(remainedTraffic, "Terminated")
if isTerminated {
remark = fmt.Sprintf("%s: %s⛔", email, "Terminated")
} else {
remark = fmt.Sprintf("%s: %s - %s", email, remainedTraffic, expiryTimeString)
}
obj := map[string]interface{}{ obj := map[string]interface{}{
"v": "2", "v": "2",
"ps": remark, "ps": remark,
@@ -236,7 +249,6 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
} }
} }
obj["id"] = clients[clientIndex].ID obj["id"] = clients[clientIndex].ID
obj["aid"] = clients[clientIndex].AlterIds
if len(domains) > 0 { if len(domains) > 0 {
links := "" links := ""
@@ -257,7 +269,7 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr) return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
} }
func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string { func (s *SubService) genVlessLink(inbound *model.Inbound, email string, expiryTime int64) string {
address := s.address address := s.address
if inbound.Protocol != model.VLESS { if inbound.Protocol != model.VLESS {
return "" return ""
@@ -435,6 +447,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) link := fmt.Sprintf("vless://%s@%s:%d", uuid, address, port)
url, _ := url.Parse(link) url, _ := url.Parse(link)
q := url.Query() q := url.Query()
@@ -446,7 +462,16 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
// Set the new query values on the URL // Set the new query values on the URL
url.RawQuery = q.Encode() url.RawQuery = q.Encode()
remark := fmt.Sprintf("%s-%s", inbound.Remark, email) remainedTraffic := s.getRemainedTraffic(email)
expiryTimeString := getExpiryTime(expiryTime)
remark := ""
isTerminated := strings.Contains(expiryTimeString, "Terminated") || strings.Contains(remainedTraffic, "Terminated")
if isTerminated {
remark = fmt.Sprintf("%s: %s⛔", email, "Terminated")
} else {
remark = fmt.Sprintf("%s: %s - %s", email, remainedTraffic, expiryTimeString)
}
if len(domains) > 0 { if len(domains) > 0 {
links := "" links := ""
@@ -465,7 +490,7 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
return url.String() return url.String()
} }
func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string { func (s *SubService) genTrojanLink(inbound *model.Inbound, email string, expiryTime int64) string {
address := s.address address := s.address
if inbound.Protocol != model.Trojan { if inbound.Protocol != model.Trojan {
return "" return ""
@@ -576,7 +601,7 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
if pbkValue, ok := searchKey(realitySettings, "publicKey"); ok { if pbkValue, ok := searchKey(realitySettings, "publicKey"); ok {
params["pbk"], _ = pbkValue.(string) params["pbk"], _ = pbkValue.(string)
} }
if sidValue, ok := searchKey(realitySettings, "shortIds"); ok { if sidValue, ok := searchKey(realitySetting, "shortIds"); ok {
shortIds, _ := sidValue.([]interface{}) shortIds, _ := sidValue.([]interface{})
params["sid"], _ = shortIds[0].(string) params["sid"], _ = shortIds[0].(string)
} }
@@ -639,6 +664,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) link := fmt.Sprintf("trojan://%s@%s:%d", password, address, port)
url, _ := url.Parse(link) url, _ := url.Parse(link)
@@ -651,7 +680,16 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
// Set the new query values on the URL // Set the new query values on the URL
url.RawQuery = q.Encode() url.RawQuery = q.Encode()
remark := fmt.Sprintf("%s-%s", inbound.Remark, email) remainedTraffic := s.getRemainedTraffic(email)
expiryTimeString := getExpiryTime(expiryTime)
remark := ""
isTerminated := strings.Contains(expiryTimeString, "Terminated") || strings.Contains(remainedTraffic, "Terminated")
if isTerminated {
remark = fmt.Sprintf("%s: %s⛔", email, "Terminated")
} else {
remark = fmt.Sprintf("%s: %s - %s", email, remainedTraffic, expiryTimeString)
}
if len(domains) > 0 { if len(domains) > 0 {
links := "" links := ""
@@ -671,11 +709,13 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
return url.String() return url.String()
} }
func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) string { func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string, expiryTime int64) string {
address := s.address address := s.address
if inbound.Protocol != model.Shadowsocks { if inbound.Protocol != model.Shadowsocks {
return "" return ""
} }
var stream map[string]interface{}
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
clients, _ := s.inboundService.GetClients(inbound) clients, _ := s.inboundService.GetClients(inbound)
var settings map[string]interface{} var settings map[string]interface{}
@@ -689,9 +729,79 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
break break
} }
} }
encPart := fmt.Sprintf("%s:%s:%s", method, inboundPassword, clients[clientIndex].Password) streamNetwork := stream["network"].(string)
remark := fmt.Sprintf("%s-%s", inbound.Remark, clients[clientIndex].Email) params := make(map[string]string)
return fmt.Sprintf("ss://%s@%s:%d#%s", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port, remark) params["type"] = streamNetwork
switch streamNetwork {
case "tcp":
tcp, _ := stream["tcpSettings"].(map[string]interface{})
header, _ := tcp["header"].(map[string]interface{})
typeStr, _ := header["type"].(string)
if typeStr == "http" {
request := header["request"].(map[string]interface{})
requestPath, _ := request["path"].([]interface{})
params["path"] = requestPath[0].(string)
headers, _ := request["headers"].(map[string]interface{})
params["host"] = searchHost(headers)
params["headerType"] = "http"
}
case "kcp":
kcp, _ := stream["kcpSettings"].(map[string]interface{})
header, _ := kcp["header"].(map[string]interface{})
params["headerType"] = header["type"].(string)
params["seed"] = kcp["seed"].(string)
case "ws":
ws, _ := stream["wsSettings"].(map[string]interface{})
params["path"] = ws["path"].(string)
headers, _ := ws["headers"].(map[string]interface{})
params["host"] = searchHost(headers)
case "http":
http, _ := stream["httpSettings"].(map[string]interface{})
params["path"] = http["path"].(string)
params["host"] = searchHost(http)
case "quic":
quic, _ := stream["quicSettings"].(map[string]interface{})
params["quicSecurity"] = quic["security"].(string)
params["key"] = quic["key"].(string)
header := quic["header"].(map[string]interface{})
params["headerType"] = header["type"].(string)
case "grpc":
grpc, _ := stream["grpcSettings"].(map[string]interface{})
params["serviceName"] = grpc["serviceName"].(string)
if grpc["multiMode"].(bool) {
params["mode"] = "multi"
}
}
encPart := fmt.Sprintf("%s:%s", method, clients[clientIndex].Password)
if method[0] == '2' {
encPart = fmt.Sprintf("%s:%s:%s", method, inboundPassword, clients[clientIndex].Password)
}
link := fmt.Sprintf("ss://%s@%s:%d", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port)
url, _ := url.Parse(link)
q := url.Query()
for k, v := range params {
q.Add(k, v)
}
// Set the new query values on the URL
url.RawQuery = q.Encode()
remainedTraffic := s.getRemainedTraffic(email)
expiryTimeString := getExpiryTime(expiryTime)
remark := ""
isTerminated := strings.Contains(expiryTimeString, "Terminated") || strings.Contains(remainedTraffic, "Terminated")
if isTerminated {
remark = fmt.Sprintf("%s: %s⛔", clients[clientIndex].Email, "Terminated")
} else {
remark = fmt.Sprintf("%s: %s - %s", clients[clientIndex].Email, remainedTraffic, expiryTimeString)
}
url.Fragment = remark
return url.String()
} }
func searchKey(data interface{}, key string) (interface{}, bool) { func searchKey(data interface{}, key string) (interface{}, bool) {
@@ -735,3 +845,45 @@ func searchHost(headers interface{}) string {
return "" return ""
} }
func getExpiryTime(expiryTime int64) string {
now := time.Now().Unix()
expiryString := ""
timeDifference := expiryTime/1000 - now
isTerminated := timeDifference/3600 <= 0
if expiryTime == 0 {
expiryString = "♾ ⏳"
} else if timeDifference > 172800 {
expiryString = fmt.Sprintf("%d %s⏳", timeDifference/86400, "Days")
} else if expiryTime < 0 {
expiryString = fmt.Sprintf("%d %s⏳", expiryTime/-86400000, "Days")
} else if isTerminated {
expiryString = fmt.Sprintf("%s⛔", "Terminated")
} else {
expiryString = fmt.Sprintf("%d %s⏳", timeDifference/3600, "Hours")
}
return expiryString
}
func (s *SubService) getRemainedTraffic(email string) string {
traffic, err := s.inboundService.GetClientTrafficByEmail(email)
if err != nil {
logger.Warning(err)
}
remainedTraffic := ""
isTerminated := traffic.Total-(traffic.Up+traffic.Down) < 0
if traffic.Total == 0 {
remainedTraffic = "♾ 📊"
} else if isTerminated {
remainedTraffic = fmt.Sprintf("%s⛔", "Terminated")
} else {
remainedTraffic = fmt.Sprintf("%s%s", common.FormatTraffic(traffic.Total-(traffic.Up+traffic.Down)), "📊")
}
return remainedTraffic
}

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

@@ -202,9 +202,25 @@ body {
} }
.ant-card-dark:hover { .ant-card-dark:hover {
border-color: #e8e8e8; /*border-color: #e8e8e8;
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 70%); 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 { .ant-setting-textarea {
margin-top: 1.5rem; margin-top: 1.5rem;

View File

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

View File

@@ -168,6 +168,7 @@ class AllSetting {
constructor(data) { constructor(data) {
this.webListen = ""; this.webListen = "";
this.webDomain = "";
this.webPort = 2053; this.webPort = 2053;
this.webCertFile = ""; this.webCertFile = "";
this.webKeyFile = ""; this.webKeyFile = "";
@@ -180,6 +181,7 @@ class AllSetting {
this.tgBotChatId = ""; this.tgBotChatId = "";
this.tgRunTime = "@daily"; this.tgRunTime = "@daily";
this.tgBotBackup = false; this.tgBotBackup = false;
this.tgBotLoginNotify = false;
this.tgCpu = ""; this.tgCpu = "";
this.tgLang = "en-US"; this.tgLang = "en-US";
this.xrayTemplateConfig = ""; this.xrayTemplateConfig = "";
@@ -187,7 +189,7 @@ class AllSetting {
this.subEnable = false; this.subEnable = false;
this.subListen = ""; this.subListen = "";
this.subPort = "2096"; this.subPort = "2096";
this.subPath = "sub/"; this.subPath = "/sub/";
this.subDomain = ""; this.subDomain = "";
this.subCertFile = ""; this.subCertFile = "";
this.subKeyFile = ""; this.subKeyFile = "";

View File

@@ -4,7 +4,6 @@ const Protocols = {
TROJAN: 'trojan', TROJAN: 'trojan',
SHADOWSOCKS: 'shadowsocks', SHADOWSOCKS: 'shadowsocks',
DOKODEMO: 'dokodemo-door', DOKODEMO: 'dokodemo-door',
MTPROTO: 'mtproto',
SOCKS: 'socks', SOCKS: 'socks',
HTTP: 'http', HTTP: 'http',
}; };
@@ -17,8 +16,13 @@ const VmessMethods = {
}; };
const SSMethods = { const SSMethods = {
BLAKE3_AES_128_GCM: '2022-blake3-aes-128-gcm', AES_256_GCM: 'aes-256-gcm',
BLAKE3_AES_256_GCM: '2022-blake3-aes-256-gcm', AES_128_GCM: 'aes-128-gcm',
CHACHA20_POLY1305: 'chacha20-poly1305',
XCHACHA20_POLY1305: 'xchacha20-poly1305',
BLAKE3_AES_128_GCM: '2022-blake3-aes-128-gcm',
BLAKE3_AES_256_GCM: '2022-blake3-aes-256-gcm',
BLAKE3_CHACHA20_POLY1305: '2022-blake3-chacha20-poly1305',
}; };
const XTLS_FLOW_CONTROL = { const XTLS_FLOW_CONTROL = {
@@ -81,6 +85,7 @@ const SNIFFING_OPTION = {
HTTP: "http", HTTP: "http",
TLS: "tls", TLS: "tls",
QUIC: "quic", QUIC: "quic",
FAKEDNS: "fakedns"
}; };
Object.freeze(Protocols); Object.freeze(Protocols);
@@ -379,7 +384,10 @@ class WsStreamSettings extends XrayCommonClass {
} }
class HttpStreamSettings extends XrayCommonClass { class HttpStreamSettings extends XrayCommonClass {
constructor(path='/', host=['']) { constructor(
path='/',
host=[''],
) {
super(); super();
this.path = path; this.path = path;
this.host = host.length === 0 ? [''] : host; this.host = host.length === 0 ? [''] : host;
@@ -411,9 +419,10 @@ class HttpStreamSettings extends XrayCommonClass {
} }
} }
class QuicStreamSettings extends XrayCommonClass { class QuicStreamSettings extends XrayCommonClass {
constructor(security=VmessMethods.NONE, constructor(security=VmessMethods.NONE,
key='', type='none') { key=RandomUtil.randomSeq(10), type='none') {
super(); super();
this.security = security; this.security = security;
this.key = key; this.key = key;
@@ -442,7 +451,7 @@ class QuicStreamSettings extends XrayCommonClass {
class GrpcStreamSettings extends XrayCommonClass { class GrpcStreamSettings extends XrayCommonClass {
constructor( constructor(
serviceName="", serviceName="",
multiMode=false multiMode=false,
) { ) {
super(); super();
this.serviceName = serviceName; this.serviceName = serviceName;
@@ -459,16 +468,18 @@ class GrpcStreamSettings extends XrayCommonClass {
toJson() { toJson() {
return { return {
serviceName: this.serviceName, serviceName: this.serviceName,
multiMode: this.multiMode multiMode: this.multiMode,
} }
} }
} }
class TlsStreamSettings extends XrayCommonClass { class TlsStreamSettings extends XrayCommonClass {
constructor(serverName='', constructor(serverName='',
minVersion = TLS_VERSION_OPTION.TLS12, minVersion = TLS_VERSION_OPTION.TLS12,
maxVersion = TLS_VERSION_OPTION.TLS13, maxVersion = TLS_VERSION_OPTION.TLS13,
cipherSuites = '', cipherSuites = '',
rejectUnknownSni = false,
certificates=[new TlsStreamSettings.Cert()], certificates=[new TlsStreamSettings.Cert()],
alpn=[ALPN_OPTION.H2,ALPN_OPTION.HTTP1], alpn=[ALPN_OPTION.H2,ALPN_OPTION.HTTP1],
settings=new TlsStreamSettings.Settings()) { settings=new TlsStreamSettings.Settings()) {
@@ -477,6 +488,7 @@ class TlsStreamSettings extends XrayCommonClass {
this.minVersion = minVersion; this.minVersion = minVersion;
this.maxVersion = maxVersion; this.maxVersion = maxVersion;
this.cipherSuites = cipherSuites; this.cipherSuites = cipherSuites;
this.rejectUnknownSni = rejectUnknownSni;
this.certs = certificates; this.certs = certificates;
this.alpn = alpn; this.alpn = alpn;
this.settings = settings; this.settings = settings;
@@ -498,12 +510,14 @@ class TlsStreamSettings extends XrayCommonClass {
} }
if (!ObjectUtil.isEmpty(json.settings)) { if (!ObjectUtil.isEmpty(json.settings)) {
settings = new TlsStreamSettings.Settings(json.settings.allowInsecure , json.settings.fingerprint, json.settings.serverName, json.settings.domains); } settings = new TlsStreamSettings.Settings(json.settings.allowInsecure , json.settings.fingerprint, json.settings.serverName, json.settings.domains);
}
return new TlsStreamSettings( return new TlsStreamSettings(
json.serverName, json.serverName,
json.minVersion, json.minVersion,
json.maxVersion, json.maxVersion,
json.cipherSuites, json.cipherSuites,
json.rejectUnknownSni,
certs, certs,
json.alpn, json.alpn,
settings, settings,
@@ -516,6 +530,7 @@ class TlsStreamSettings extends XrayCommonClass {
minVersion: this.minVersion, minVersion: this.minVersion,
maxVersion: this.maxVersion, maxVersion: this.maxVersion,
cipherSuites: this.cipherSuites, cipherSuites: this.cipherSuites,
rejectUnknownSni: this.rejectUnknownSni,
certificates: TlsStreamSettings.toJsonArray(this.certs), certificates: TlsStreamSettings.toJsonArray(this.certs),
alpn: this.alpn, alpn: this.alpn,
settings: this.settings, settings: this.settings,
@@ -785,6 +800,27 @@ RealityStreamSettings.Settings = class extends XrayCommonClass {
} }
}; };
class SockoptStreamSettings extends XrayCommonClass {
constructor(
acceptProxyProtocol = false,
) {
super();
this.acceptProxyProtocol = acceptProxyProtocol;
}
static fromJson(json = {}) {
return new SockoptStreamSettings(
json.acceptProxyProtocol,
);
}
toJson() {
return {
acceptProxyProtocol: this.acceptProxyProtocol,
};
}
}
class StreamSettings extends XrayCommonClass { class StreamSettings extends XrayCommonClass {
constructor(network='tcp', constructor(network='tcp',
security='none', security='none',
@@ -797,6 +833,7 @@ class StreamSettings extends XrayCommonClass {
httpSettings=new HttpStreamSettings(), httpSettings=new HttpStreamSettings(),
quicSettings=new QuicStreamSettings(), quicSettings=new QuicStreamSettings(),
grpcSettings=new GrpcStreamSettings(), grpcSettings=new GrpcStreamSettings(),
sockopt = new SockoptStreamSettings(),
) { ) {
super(); super();
this.network = network; this.network = network;
@@ -810,6 +847,7 @@ class StreamSettings extends XrayCommonClass {
this.http = httpSettings; this.http = httpSettings;
this.quic = quicSettings; this.quic = quicSettings;
this.grpc = grpcSettings; this.grpc = grpcSettings;
this.sockopt = sockopt;
} }
get isTls() { get isTls() {
@@ -849,6 +887,16 @@ class StreamSettings extends XrayCommonClass {
} }
} }
get isSockopt() {
return ['http', 'grpc'].indexOf(this.network) !== -1;
}
set isSockopt(isSockopt) {
if (isSockopt) {
return ['http', 'grpc'].indexOf(this.network) !== -1;
}
}
static fromJson(json={}) { static fromJson(json={}) {
return new StreamSettings( return new StreamSettings(
@@ -863,6 +911,7 @@ class StreamSettings extends XrayCommonClass {
HttpStreamSettings.fromJson(json.httpSettings), HttpStreamSettings.fromJson(json.httpSettings),
QuicStreamSettings.fromJson(json.quicSettings), QuicStreamSettings.fromJson(json.quicSettings),
GrpcStreamSettings.fromJson(json.grpcSettings), GrpcStreamSettings.fromJson(json.grpcSettings),
SockoptStreamSettings.fromJson(json.sockopt),
); );
} }
@@ -880,12 +929,13 @@ class StreamSettings extends XrayCommonClass {
httpSettings: network === 'http' ? this.http.toJson() : undefined, httpSettings: network === 'http' ? this.http.toJson() : undefined,
quicSettings: network === 'quic' ? this.quic.toJson() : undefined, quicSettings: network === 'quic' ? this.quic.toJson() : undefined,
grpcSettings: network === 'grpc' ? this.grpc.toJson() : undefined, grpcSettings: network === 'grpc' ? this.grpc.toJson() : undefined,
sockopt: this.isSockopt ? this.sockopt.toJson() : undefined,
}; };
} }
} }
class Sniffing extends XrayCommonClass { class Sniffing extends XrayCommonClass {
constructor(enabled=true, destOverride=['http', 'tls', 'quic']) { constructor(enabled=true, destOverride=['http', 'tls', 'quic', 'fakedns']) {
super(); super();
this.enabled = enabled; this.enabled = enabled;
this.destOverride = destOverride; this.destOverride = destOverride;
@@ -895,7 +945,7 @@ class Sniffing extends XrayCommonClass {
let destOverride = ObjectUtil.clone(json.destOverride); let destOverride = ObjectUtil.clone(json.destOverride);
if (!ObjectUtil.isEmpty(destOverride) && !ObjectUtil.isArrEmpty(destOverride)) { if (!ObjectUtil.isEmpty(destOverride) && !ObjectUtil.isArrEmpty(destOverride)) {
if (ObjectUtil.isEmpty(destOverride[0])) { if (ObjectUtil.isEmpty(destOverride[0])) {
destOverride = ['http', 'tls', 'quic']; destOverride = ['http', 'tls', 'quic', 'fakedns'];
} }
} }
return new Sniffing( return new Sniffing(
@@ -965,7 +1015,6 @@ class Inbound extends XrayCommonClass {
} }
} }
//for Reality
get reality() { get reality() {
return this.stream.security === 'reality'; return this.stream.security === 'reality';
} }
@@ -1019,6 +1068,12 @@ class Inbound extends XrayCommonClass {
return ""; return "";
} }
} }
get isSSMultiUser() {
return this.method != SSMethods.BLAKE3_CHACHA20_POLY1305;
}
get isSS2022(){
return this.method.substring(0,4) === "2022";
}
get serverName() { get serverName() {
if (this.stream.isTls || this.stream.isXtls || this.stream.isReality) { if (this.stream.isTls || this.stream.isXtls || this.stream.isReality) {
@@ -1088,7 +1143,7 @@ class Inbound extends XrayCommonClass {
return this.settings.trojans[index].expiryTime < new Date().getTime(); return this.settings.trojans[index].expiryTime < new Date().getTime();
return false return false
case Protocols.SHADOWSOCKS: case Protocols.SHADOWSOCKS:
if(this.settings.shadowsockses[index].expiryTime > 0) if(this.settings.shadowsockses.length > 0 && this.settings.shadowsockses[index].expiryTime > 0)
return this.settings.shadowsockses[index].expiryTime < new Date().getTime(); return this.settings.shadowsockses[index].expiryTime < new Date().getTime();
return false return false
default: default:
@@ -1169,6 +1224,7 @@ class Inbound extends XrayCommonClass {
case Protocols.VMESS: case Protocols.VMESS:
case Protocols.VLESS: case Protocols.VLESS:
case Protocols.TROJAN: case Protocols.TROJAN:
case Protocols.SHADOWSOCKS:
return true; return true;
default: default:
return false; return false;
@@ -1207,7 +1263,6 @@ class Inbound extends XrayCommonClass {
add: address, add: address,
port: this.port, port: this.port,
id: this.settings.vmesses[clientIndex].id, id: this.settings.vmesses[clientIndex].id,
aid: this.settings.vmesses[clientIndex].alterId,
net: this.stream.network, net: this.stream.network,
type: 'none', type: 'none',
tls: this.stream.security, tls: this.stream.security,
@@ -1344,7 +1399,7 @@ class Inbound extends XrayCommonClass {
} }
} }
if (this.xtls) { else if (this.xtls) {
params.set("security", "xtls"); params.set("security", "xtls");
params.set("alpn", this.stream.xtls.alpn); params.set("alpn", this.stream.xtls.alpn);
if(this.stream.xtls.settings.allowInsecure){ if(this.stream.xtls.settings.allowInsecure){
@@ -1359,7 +1414,7 @@ class Inbound extends XrayCommonClass {
params.set("flow", this.settings.vlesses[clientIndex].flow); params.set("flow", this.settings.vlesses[clientIndex].flow);
} }
if (this.reality) { else if (this.reality) {
params.set("security", "reality"); params.set("security", "reality");
params.set("fp", this.stream.reality.settings.fingerprint); params.set("fp", this.stream.reality.settings.fingerprint);
params.set("pbk", this.stream.reality.settings.publicKey); params.set("pbk", this.stream.reality.settings.publicKey);
@@ -1380,6 +1435,10 @@ class Inbound extends XrayCommonClass {
} }
} }
else {
params.set("security", "none");
}
const link = `vless://${uuid}@${address}:${port}`; const link = `vless://${uuid}@${address}:${port}`;
const url = new URL(link); const url = new URL(link);
for (const [key, value] of params) { for (const [key, value] of params) {
@@ -1392,8 +1451,68 @@ class Inbound extends XrayCommonClass {
genSSLink(address='', remark='', clientIndex = 0) { genSSLink(address='', remark='', clientIndex = 0) {
let settings = this.settings; let settings = this.settings;
const port = this.port; const port = this.port;
const type = this.stream.network;
const params = new Map();
params.set("type", this.stream.network);
switch (type) {
case "tcp":
const tcp = this.stream.tcp;
if (tcp.type === 'http') {
const request = tcp.request;
params.set("path", request.path.join(','));
const index = request.headers.findIndex(header => header.name.toLowerCase() === 'host');
if (index >= 0) {
const host = request.headers[index].value;
params.set("host", host);
}
params.set("headerType", 'http');
}
break;
case "kcp":
const kcp = this.stream.kcp;
params.set("headerType", kcp.type);
params.set("seed", kcp.seed);
break;
case "ws":
const ws = this.stream.ws;
params.set("path", ws.path);
const index = ws.headers.findIndex(header => header.name.toLowerCase() === 'host');
if (index >= 0) {
const host = ws.headers[index].value;
params.set("host", host);
}
break;
case "http":
const http = this.stream.http;
params.set("path", http.path);
params.set("host", http.host);
break;
case "quic":
const quic = this.stream.quic;
params.set("quicSecurity", quic.security);
params.set("key", quic.key);
params.set("headerType", quic.type);
break;
case "grpc":
const grpc = this.stream.grpc;
params.set("serviceName", grpc.serviceName);
if(grpc.multiMode){
params.set("mode", "multi");
}
break;
}
return 'ss://' + safeBase64(settings.method + ':' + settings.password + ':' +settings.shadowsockses[clientIndex].password) + '@' + address + ':' + this.port + '#' + encodeURIComponent(remark); let password = new Array();
if (this.isSSMultiUser) password.push(settings.shadowsockses[clientIndex].password);
if (this.isSS2022) password.push(settings.password);
let link = `ss://${safeBase64(settings.method + ':' + password.join(':'))}@${address}:${this.port}`;
const url = new URL(link);
for (const [key, value] of params) {
url.searchParams.set(key, value)
}
url.hash = encodeURIComponent(remark);
return url.toString();
} }
genTrojanLink(address = '', remark = '', clientIndex = 0) { genTrojanLink(address = '', remark = '', clientIndex = 0) {
@@ -1465,7 +1584,7 @@ class Inbound extends XrayCommonClass {
} }
} }
if (this.reality) { else if (this.reality) {
params.set("security", "reality"); params.set("security", "reality");
params.set("fp", this.stream.reality.settings.fingerprint); params.set("fp", this.stream.reality.settings.fingerprint);
params.set("pbk", this.stream.reality.settings.publicKey); params.set("pbk", this.stream.reality.settings.publicKey);
@@ -1483,7 +1602,7 @@ class Inbound extends XrayCommonClass {
} }
} }
if (this.xtls) { else if (this.xtls) {
params.set("security", "xtls"); params.set("security", "xtls");
params.set("alpn", this.stream.xtls.alpn); params.set("alpn", this.stream.xtls.alpn);
if(this.stream.xtls.settings.allowInsecure){ if(this.stream.xtls.settings.allowInsecure){
@@ -1498,6 +1617,10 @@ class Inbound extends XrayCommonClass {
params.set("flow", this.settings.trojans[clientIndex].flow); params.set("flow", this.settings.trojans[clientIndex].flow);
} }
else {
params.set("security", "none");
}
const link = `trojan://${settings.trojans[clientIndex].password}@${address}:${this.port}`; const link = `trojan://${settings.trojans[clientIndex].password}@${address}:${this.port}`;
const url = new URL(link); const url = new URL(link);
for (const [key, value] of params) { for (const [key, value] of params) {
@@ -1586,7 +1709,6 @@ Inbound.Settings = class extends XrayCommonClass {
case Protocols.TROJAN: return new Inbound.TrojanSettings(protocol); case Protocols.TROJAN: return new Inbound.TrojanSettings(protocol);
case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings(protocol); case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings(protocol);
case Protocols.DOKODEMO: return new Inbound.DokodemoSettings(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.SOCKS: return new Inbound.SocksSettings(protocol);
case Protocols.HTTP: return new Inbound.HttpSettings(protocol); case Protocols.HTTP: return new Inbound.HttpSettings(protocol);
default: return null; default: return null;
@@ -1600,7 +1722,6 @@ Inbound.Settings = class extends XrayCommonClass {
case Protocols.TROJAN: return Inbound.TrojanSettings.fromJson(json); case Protocols.TROJAN: return Inbound.TrojanSettings.fromJson(json);
case Protocols.SHADOWSOCKS: return Inbound.ShadowsocksSettings.fromJson(json); case Protocols.SHADOWSOCKS: return Inbound.ShadowsocksSettings.fromJson(json);
case Protocols.DOKODEMO: return Inbound.DokodemoSettings.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.SOCKS: return Inbound.SocksSettings.fromJson(json);
case Protocols.HTTP: return Inbound.HttpSettings.fromJson(json); case Protocols.HTTP: return Inbound.HttpSettings.fromJson(json);
default: return null; default: return null;
@@ -1655,10 +1776,9 @@ Inbound.VmessSettings = class extends Inbound.Settings {
} }
}; };
Inbound.VmessSettings.Vmess = class extends XrayCommonClass { 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(); super();
this.id = id; this.id = id;
this.alterId = alterId;
this.email = email; this.email = email;
this.limitIp = limitIp; this.limitIp = limitIp;
this.totalGB = totalGB; this.totalGB = totalGB;
@@ -1671,7 +1791,6 @@ Inbound.VmessSettings.Vmess = class extends XrayCommonClass {
static fromJson(json={}) { static fromJson(json={}) {
return new Inbound.VmessSettings.Vmess( return new Inbound.VmessSettings.Vmess(
json.id, json.id,
json.alterId,
json.email, json.email,
json.limitIp, json.limitIp,
json.totalGB, json.totalGB,
@@ -1747,7 +1866,7 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
}; };
Inbound.VLESSSettings.VLESS = class extends XrayCommonClass { 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(); super();
this.id = id; this.id = id;
this.flow = flow; this.flow = flow;
@@ -1870,7 +1989,7 @@ Inbound.TrojanSettings = class extends Inbound.Settings {
} }
}; };
Inbound.TrojanSettings.Trojan = class extends XrayCommonClass { 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(); super();
this.password = password; this.password = password;
this.flow = flow; this.flow = flow;
@@ -2012,8 +2131,9 @@ Inbound.ShadowsocksSettings = class extends Inbound.Settings {
}; };
Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass { Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
constructor(password=RandomUtil.randomShadowsocksPassword(), email=RandomUtil.randomText(),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId='') { constructor(method='', password=RandomUtil.randomShadowsocksPassword(), email=RandomUtil.randomLowerAndNum(8),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16)) {
super(); super();
this.method = method;
this.password = password; this.password = password;
this.email = email; this.email = email;
this.limitIp = limitIp; this.limitIp = limitIp;
@@ -2026,6 +2146,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
toJson() { toJson() {
return { return {
method: this.method,
password: this.password, password: this.password,
email: this.email, email: this.email,
limitIp: this.limitIp, limitIp: this.limitIp,
@@ -2039,6 +2160,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
static fromJson(json = {}) { static fromJson(json = {}) {
return new Inbound.ShadowsocksSettings.Shadowsocks( return new Inbound.ShadowsocksSettings.Shadowsocks(
json.method,
json.password, json.password,
json.email, json.email,
json.limitIp, json.limitIp,
@@ -2106,36 +2228,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 { Inbound.SocksSettings = class extends Inbound.Settings {
constructor(protocol, auth='password', accounts=[new Inbound.SocksSettings.SocksAccount()], udp=false, ip='127.0.0.1') { constructor(protocol, auth='password', accounts=[new Inbound.SocksSettings.SocksAccount()], udp=false, ip='127.0.0.1') {
super(protocol); super(protocol);

View File

@@ -135,3 +135,21 @@ function doAllItemsExist(array1, array2) {
} }
return true; 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,9 +75,7 @@ class PromiseUtil {
} }
} }
const seq = 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''); const seq = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
const shortIdSeq = 'abcdef0123456789'.split('');
class RandomUtil { class RandomUtil {
static randomIntRange(min, max) { static randomIntRange(min, max) {
@@ -96,57 +94,33 @@ class RandomUtil {
return str; return str;
} }
static randomShortIdSeq(count) { static randomShortId() {
let str = ''; let str = '';
for (let i = 0; i < count; ++i) { for (let i = 0; i < 8; ++i) {
str += shortIdSeq[this.randomInt(16)]; str += seq[this.randomInt(16)];
} }
return str; return str;
} }
static randomShortId() { static randomLowerAndNum(len) {
return this.randomShortIdSeq(8);
}
static randomLowerAndNum(count) {
let str = ''; let str = '';
for (let i = 0; i < count; ++i) { for (let i = 0; i < len; ++i) {
str += seq[this.randomInt(36)]; str += seq[this.randomInt(36)];
} }
return str; 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() { static randomUUID() {
let d = new Date().getTime(); const template = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx';
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { return template.replace(/[xy]/g, function (c) {
let r = (d + Math.random() * 16) % 16 | 0; const randomValues = new Uint8Array(1);
d = Math.floor(d / 16); crypto.getRandomValues(randomValues);
return (c === 'x' ? r : (r & 0x7 | 0x8)).toString(16); 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 randomShadowsocksPassword() { static randomShadowsocksPassword() {
let array = new Uint8Array(32); let array = new Uint8Array(32);
window.crypto.getRandomValues(array); window.crypto.getRandomValues(array);

View File

@@ -40,7 +40,6 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
g.POST("/resetAllTraffics", a.resetAllTraffics) g.POST("/resetAllTraffics", a.resetAllTraffics)
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics) g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
g.POST("/delDepletedClients/:id", a.delDepletedClients) g.POST("/delDepletedClients/:id", a.delDepletedClients)
} }
func (a *InboundController) startTask() { func (a *InboundController) startTask() {
@@ -65,6 +64,7 @@ func (a *InboundController) getInbounds(c *gin.Context) {
} }
jsonObj(c, inbounds, nil) jsonObj(c, inbounds, nil)
} }
func (a *InboundController) getInbound(c *gin.Context) { func (a *InboundController) getInbound(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err != nil { if err != nil {
@@ -98,11 +98,12 @@ func (a *InboundController) addInbound(c *gin.Context) {
} }
user := session.GetLoginUser(c) user := session.GetLoginUser(c)
inbound.UserId = user.Id inbound.UserId = user.Id
inbound.Enable = true
inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port) inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
inbound, err = a.inboundService.AddInbound(inbound)
needRestart := false
inbound, needRestart, err = a.inboundService.AddInbound(inbound)
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.create"), inbound, err) jsonMsgObj(c, I18nWeb(c, "pages.inbounds.create"), inbound, err)
if err == nil { if err == nil && needRestart {
a.xrayService.SetToNeedRestart() a.xrayService.SetToNeedRestart()
} }
} }
@@ -113,9 +114,10 @@ func (a *InboundController) delInbound(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "delete"), err) jsonMsg(c, I18nWeb(c, "delete"), err)
return return
} }
err = a.inboundService.DelInbound(id) needRestart := true
needRestart, err = a.inboundService.DelInbound(id)
jsonMsgObj(c, I18nWeb(c, "delete"), id, err) jsonMsgObj(c, I18nWeb(c, "delete"), id, err)
if err == nil { if err == nil && needRestart {
a.xrayService.SetToNeedRestart() a.xrayService.SetToNeedRestart()
} }
} }
@@ -134,9 +136,10 @@ func (a *InboundController) updateInbound(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err) jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
return return
} }
inbound, err = a.inboundService.UpdateInbound(inbound) needRestart := true
inbound, needRestart, err = a.inboundService.UpdateInbound(inbound)
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.update"), inbound, err) jsonMsgObj(c, I18nWeb(c, "pages.inbounds.update"), inbound, err)
if err == nil { if err == nil && needRestart {
a.xrayService.SetToNeedRestart() a.xrayService.SetToNeedRestart()
} }
} }
@@ -145,12 +148,7 @@ func (a *InboundController) getClientIps(c *gin.Context) {
email := c.Param("email") email := c.Param("email")
ips, err := a.inboundService.GetInboundClientIps(email) ips, err := a.inboundService.GetInboundClientIps(email)
if err != nil { if err != nil || ips == "" {
jsonObj(c, "Failed to get client IPs", nil)
return
}
if ips == "" {
jsonObj(c, "No IP Record", nil) jsonObj(c, "No IP Record", nil)
return return
} }
@@ -168,6 +166,7 @@ func (a *InboundController) clearClientIps(c *gin.Context) {
} }
jsonMsg(c, "Log Cleared", nil) jsonMsg(c, "Log Cleared", nil)
} }
func (a *InboundController) addInboundClient(c *gin.Context) { func (a *InboundController) addInboundClient(c *gin.Context) {
data := &model.Inbound{} data := &model.Inbound{}
err := c.ShouldBind(data) err := c.ShouldBind(data)
@@ -176,13 +175,15 @@ func (a *InboundController) addInboundClient(c *gin.Context) {
return return
} }
err = a.inboundService.AddInboundClient(data) needRestart := true
needRestart, err = a.inboundService.AddInboundClient(data)
if err != nil { if err != nil {
jsonMsg(c, "Something went wrong!", err) jsonMsg(c, "Something went wrong!", err)
return return
} }
jsonMsg(c, "Client(s) added", nil) jsonMsg(c, "Client(s) added", nil)
if err == nil { if err == nil && needRestart {
a.xrayService.SetToNeedRestart() a.xrayService.SetToNeedRestart()
} }
} }
@@ -195,13 +196,15 @@ func (a *InboundController) delInboundClient(c *gin.Context) {
} }
clientId := c.Param("clientId") clientId := c.Param("clientId")
err = a.inboundService.DelInboundClient(id, clientId) needRestart := true
needRestart, err = a.inboundService.DelInboundClient(id, clientId)
if err != nil { if err != nil {
jsonMsg(c, "Something went wrong!", err) jsonMsg(c, "Something went wrong!", err)
return return
} }
jsonMsg(c, "Client deleted", nil) jsonMsg(c, "Client deleted", nil)
if err == nil { if err == nil && needRestart {
a.xrayService.SetToNeedRestart() a.xrayService.SetToNeedRestart()
} }
} }
@@ -216,13 +219,15 @@ func (a *InboundController) updateInboundClient(c *gin.Context) {
return return
} }
err = a.inboundService.UpdateInboundClient(inbound, clientId) needRestart := true
needRestart, err = a.inboundService.UpdateInboundClient(inbound, clientId)
if err != nil { if err != nil {
jsonMsg(c, "Something went wrong!", err) jsonMsg(c, "Something went wrong!", err)
return return
} }
jsonMsg(c, "Client updated", nil) jsonMsg(c, "Client updated", nil)
if err == nil { if err == nil && needRestart {
a.xrayService.SetToNeedRestart() a.xrayService.SetToNeedRestart()
} }
} }
@@ -235,13 +240,15 @@ func (a *InboundController) resetClientTraffic(c *gin.Context) {
} }
email := c.Param("email") email := c.Param("email")
err = a.inboundService.ResetClientTraffic(id, email) needRestart := true
needRestart, err = a.inboundService.ResetClientTraffic(id, email)
if err != nil { if err != nil {
jsonMsg(c, "Something went wrong!", err) jsonMsg(c, "Something went wrong!", err)
return return
} }
jsonMsg(c, "traffic reseted", nil) jsonMsg(c, "traffic reseted", nil)
if err == nil { if err == nil && needRestart {
a.xrayService.SetToNeedRestart() a.xrayService.SetToNeedRestart()
} }
} }
@@ -251,6 +258,8 @@ func (a *InboundController) resetAllTraffics(c *gin.Context) {
if err != nil { if err != nil {
jsonMsg(c, "Something went wrong!", err) jsonMsg(c, "Something went wrong!", err)
return return
} else {
a.xrayService.SetToNeedRestart()
} }
jsonMsg(c, "All traffics reseted", nil) jsonMsg(c, "All traffics reseted", nil)
} }
@@ -266,6 +275,8 @@ func (a *InboundController) resetAllClientTraffics(c *gin.Context) {
if err != nil { if err != nil {
jsonMsg(c, "Something went wrong!", err) jsonMsg(c, "Something went wrong!", err)
return return
} else {
a.xrayService.SetToNeedRestart()
} }
jsonMsg(c, "All traffics of client reseted", nil) jsonMsg(c, "All traffics of client reseted", nil)
} }

View File

@@ -64,7 +64,7 @@ func (a *IndexController) login(c *gin.Context) {
user := a.userService.CheckUser(form.Username, form.Password, form.LoginSecret) user := a.userService.CheckUser(form.Username, form.Password, form.LoginSecret)
timeStr := time.Now().Format("2006-01-02 15:04:05") timeStr := time.Now().Format("2006-01-02 15:04:05")
if user == nil { if user == nil {
logger.Infof("wrong username or password: \"%s\" \"%s\"", form.Username, form.Password) logger.Warningf("wrong username or password: \"%s\" \"%s\"", form.Username, form.Password)
a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0) a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0)
pureJsonMsg(c, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword")) pureJsonMsg(c, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword"))
return return
@@ -75,13 +75,13 @@ func (a *IndexController) login(c *gin.Context) {
sessionMaxAge, err := a.settingService.GetSessionMaxAge() sessionMaxAge, err := a.settingService.GetSessionMaxAge()
if err != nil { 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 { if sessionMaxAge > 0 {
err = session.SetMaxAge(c, sessionMaxAge*60) err = session.SetMaxAge(c, sessionMaxAge*60)
if err != nil { if err != nil {
logger.Infof("Unable to set session's max age") logger.Warningf("Unable to set session's max age")
} }
} }

View File

@@ -118,11 +118,9 @@ func (a *ServerController) restartXrayService(c *gin.Context) {
func (a *ServerController) getLogs(c *gin.Context) { func (a *ServerController) getLogs(c *gin.Context) {
count := c.Param("count") count := c.Param("count")
logs, err := a.serverService.GetLogs(count) level := c.PostForm("level")
if err != nil { syslog := c.PostForm("syslog")
jsonMsg(c, "getLogs", err) logs := a.serverService.GetLogs(count, level, syslog)
return
}
jsonObj(c, logs, nil) jsonObj(c, logs, nil)
} }

View File

@@ -65,77 +65,42 @@ func (a *SettingController) getDefaultJsonConfig(c *gin.Context) {
} }
func (a *SettingController) getDefaultSettings(c *gin.Context) { func (a *SettingController) getDefaultSettings(c *gin.Context) {
expireDiff, err := a.settingService.GetExpireDiff() type settingFunc func() (interface{}, error)
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err) settings := map[string]settingFunc{
return "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 { result := make(map[string]interface{})
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return for key, fn := range settings {
} value, err := fn()
defaultCert, err := a.settingService.GetCertFile() if err != nil {
if err != nil { jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err) return
return }
} result[key] = value
defaultKey, err := a.settingService.GetKeyFile()
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return
}
tgBotEnable, err := a.settingService.GetTgbotenabled()
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return
}
subEnable, err := a.settingService.GetSubEnable()
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return
}
subPort, err := a.settingService.GetSubPort()
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return
}
subPath, err := a.settingService.GetSubPath()
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return
}
subDomain, err := a.settingService.GetSubDomain()
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return
}
subKeyFile, err := a.settingService.GetSubKeyFile()
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return
}
subCertFile, err := a.settingService.GetSubCertFile()
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return
} }
subTLS := false subTLS := false
if subKeyFile != "" || subCertFile != "" { if result["subKeyFile"] != "" || result["subCertFile"] != "" {
subTLS = true subTLS = true
} }
result := map[string]interface{}{ result["subTLS"] = subTLS
"expireDiff": expireDiff,
"trafficDiff": trafficDiff, delete(result, "subKeyFile")
"defaultCert": defaultCert, delete(result, "subCertFile")
"defaultKey": defaultKey,
"tgBotEnable": tgBotEnable,
"subEnable": subEnable,
"subPort": subPort,
"subPath": subPath,
"subDomain": subDomain,
"subTLS": subTLS,
}
jsonObj(c, result, nil) jsonObj(c, result, nil)
} }

View File

@@ -28,6 +28,7 @@ type Pager struct {
type AllSetting struct { type AllSetting struct {
WebListen string `json:"webListen" form:"webListen"` WebListen string `json:"webListen" form:"webListen"`
WebDomain string `json:"webDomain" form:"webDomain"`
WebPort int `json:"webPort" form:"webPort"` WebPort int `json:"webPort" form:"webPort"`
WebCertFile string `json:"webCertFile" form:"webCertFile"` WebCertFile string `json:"webCertFile" form:"webCertFile"`
WebKeyFile string `json:"webKeyFile" form:"webKeyFile"` WebKeyFile string `json:"webKeyFile" form:"webKeyFile"`
@@ -40,6 +41,7 @@ type AllSetting struct {
TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"` TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"`
TgRunTime string `json:"tgRunTime" form:"tgRunTime"` TgRunTime string `json:"tgRunTime" form:"tgRunTime"`
TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"` TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"`
TgBotLoginNotify bool `json:"tgBotLoginNotify" form:"tgBotLoginNotify"`
TgCpu int `json:"tgCpu" form:"tgCpu"` TgCpu int `json:"tgCpu" form:"tgCpu"`
TgLang string `json:"tgLang" form:"tgLang"` TgLang string `json:"tgLang" form:"tgLang"`
XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"` XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"`

View File

@@ -18,7 +18,6 @@ type HashStorage struct {
sync.RWMutex sync.RWMutex
Data map[string]HashEntry Data map[string]HashEntry
Expiration time.Duration Expiration time.Duration
} }
func NewHashStorage(expiration time.Duration) *HashStorage { func NewHashStorage(expiration time.Duration) *HashStorage {
@@ -46,7 +45,6 @@ func (h *HashStorage) SaveHash(query string) string {
return md5HashString return md5HashString
} }
func (h *HashStorage) GetValue(hash string) (string, bool) { func (h *HashStorage) GetValue(hash string) (string, bool) {
h.RLock() h.RLock()
defer h.RUnlock() defer h.RUnlock()

View File

@@ -37,7 +37,7 @@
this.inbound = dbInbound.toInbound(); this.inbound = dbInbound.toInbound();
settings = JSON.parse(this.inbound.settings); settings = JSON.parse(this.inbound.settings);
this.client = settings.clients[clientIndex]; this.client = settings.clients[clientIndex];
remark = this.dbInbound.remark + "-" + this.client.email; remark = this.dbInbound.remark + ( this.client ? "-" + this.client.email : '');
address = this.dbInbound.address; address = this.dbInbound.address;
this.subId = ''; this.subId = '';
this.qrcodes = []; this.qrcodes = [];
@@ -68,8 +68,8 @@
qrModal: qrModal, qrModal: qrModal,
}, },
methods: { methods: {
copyToClipboard(elmentId,content) { copyToClipboard(elmentId, content) {
this.qrModal.clipboard = new ClipboardJS('#'+elmentId, { this.qrModal.clipboard = new ClipboardJS('#' + elmentId, {
text: () => content, text: () => content,
}); });
this.qrModal.clipboard.on('success', () => { this.qrModal.clipboard.on('success', () => {
@@ -77,29 +77,25 @@
this.qrModal.clipboard.destroy(); this.qrModal.clipboard.destroy();
}); });
}, },
setQrCode(elmentId,content) { setQrCode(elmentId, content) {
new QRious({ new QRious({
element: document.querySelector('#'+elmentId), element: document.querySelector('#' + elmentId),
size: 260, size: 260,
value: content, value: content,
}); });
}, },
genSubLink(subID) { genSubLink(subID) {
protocol = app.subSettings.tls ? "https://" : "http://"; const { domain: host, port, tls: isTLS, path: base } = app.subSettings;
hostName = app.subSettings.domain === "" ? window.location.hostname : app.subSettings.domain; return buildURL({ host, port, isTLS, base, path: subID });
subPort = app.subSettings.port;
port = (subPort === 443 && app.subSettings.tls) || (subPort === 80 && !app.subSettings.tls) ? "" : ":" + String(subPort);
subPath = app.subSettings.path;
return protocol + hostName + port + subPath + subID;
} }
}, },
updated() { updated() {
if (qrModal.client.subId){ if (qrModal.client && qrModal.client.subId) {
qrModal.subId = qrModal.client.subId; qrModal.subId = qrModal.client.subId;
this.setQrCode("qrCode-sub",this.genSubLink(qrModal.subId)); this.setQrCode("qrCode-sub", this.genSubLink(qrModal.subId));
} }
qrModal.qrcodes.forEach((element,index) => { qrModal.qrcodes.forEach((element, index) => {
this.setQrCode("qrCode-"+index, element.link); this.setQrCode("qrCode-" + index, element.link);
}); });
} }
}); });

View File

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

View File

@@ -57,6 +57,7 @@
</span> </span>
<a-input v-model.trim="clientsBulkModal.tgId"></a-input> <a-input v-model.trim="clientsBulkModal.tgId"></a-input>
</a-form-item> </a-form-item>
<br>
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
<span>{{ i18n "pages.inbounds.IPLimit" }}</span> <span>{{ i18n "pages.inbounds.IPLimit" }}</span>
@@ -199,21 +200,12 @@
this.inbound = dbInbound.toInbound(); this.inbound = dbInbound.toInbound();
this.delayedStart = false; this.delayedStart = false;
}, },
getClients(protocol, clientSettings) {
switch (protocol) {
case Protocols.VMESS: return clientSettings.vmesses;
case Protocols.VLESS: return clientSettings.vlesses;
case Protocols.TROJAN: return clientSettings.trojans;
case Protocols.SHADOWSOCKS: return clientSettings.shadowsockses;
default: return null;
}
},
newClient(protocol) { newClient(protocol) {
switch (protocol) { switch (protocol) {
case Protocols.VMESS: return new Inbound.VmessSettings.Vmess(); case Protocols.VMESS: return new Inbound.VmessSettings.Vmess();
case Protocols.VLESS: return new Inbound.VLESSSettings.VLESS(); case Protocols.VLESS: return new Inbound.VLESSSettings.VLESS();
case Protocols.TROJAN: return new Inbound.TrojanSettings.Trojan(); case Protocols.TROJAN: return new Inbound.TrojanSettings.Trojan();
case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings.Shadowsocks(); case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings.Shadowsocks(clientsBulkModal.inbound.settings.shadowsockses[0].method);
default: return null; default: return null;
} }
}, },

View File

@@ -70,7 +70,7 @@
case Protocols.VMESS: return clients.push(new Inbound.VmessSettings.Vmess()); case Protocols.VMESS: return clients.push(new Inbound.VmessSettings.Vmess());
case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS()); case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS());
case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan()); case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan());
case Protocols.SHADOWSOCKS: return clients.push(new Inbound.ShadowsocksSettings.Shadowsocks()); case Protocols.SHADOWSOCKS: return clients.push(new Inbound.ShadowsocksSettings.Shadowsocks(clients[0].method));
default: return null; default: return null;
} }
}, },
@@ -120,26 +120,32 @@
}, },
}, },
methods: { methods: {
async getDBClientIps(email, event) { async getDBClientIps(email) {
const msg = await HttpUtil.post('/panel/inbound/clientIps/' + email); const msg = await HttpUtil.post(`/panel/inbound/clientIps/${email}`);
if (!msg.success) { if (!msg.success) {
document.getElementById("clientIPs").value = msg.obj;
return; return;
} }
try { let ips = msg.obj;
ips = JSON.parse(msg.obj) if (typeof ips === 'string' && ips.startsWith('[') && ips.endsWith(']')) {
ips = ips.join(",") try {
event.target.value = ips ips = JSON.parse(ips);
} catch (error) { ips = Array.isArray(ips) ? ips.join("\n") : ips;
// text } catch (e) {
event.target.value = msg.obj console.error('Error parsing JSON:', e);
}
} }
document.getElementById("clientIPs").value = ips;
}, },
async clearDBClientIps(email) { async clearDBClientIps(email) {
const msg = await HttpUtil.post('/panel/inbound/clearClientIps/' + email); try {
if (!msg.success) { const msg = await HttpUtil.post(`/panel/inbound/clearClientIps/${email}`);
return; if (!msg.success) {
return;
}
document.getElementById("clientIPs").value = "";
} catch (error) {
} }
document.getElementById("clientIPs").value = ""
}, },
resetClientTraffic(email, dbInboundId, iconElement) { resetClientTraffic(email, dbInboundId, iconElement) {
this.$confirm({ this.$confirm({

View File

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

View File

@@ -1,12 +1,13 @@
{{define "form/inbound"}} {{define "form/inbound"}}
<!-- base --> <!-- base -->
<a-form layout="inline"> <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-form-item label='{{ i18n "enable" }}'>
<a-switch v-model="dbInbound.enable"></a-switch> <a-switch v-model="dbInbound.enable"></a-switch>
</a-form-item> </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-form-item label='{{ i18n "protocol" }}'>
<a-select v-model="inbound.protocol" style="width: 160px;" :disabled="isEdit" :dropdown-class-name="themeSwitcher.darkCardClass"> <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> <a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p ]]</a-select-option>

View File

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

View File

@@ -11,7 +11,7 @@
</template> </template>
</a-tooltip> </a-tooltip>
</span> </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-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="Password"> <a-form-item label="Password">
@@ -28,7 +28,7 @@
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span> </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-input v-model.trim="client.subId" style="width: 150px;"></a-input>
</a-form-item> </a-form-item>
<a-form-item v-if="client.email && app.tgBotEnable"> <a-form-item v-if="client.email && app.tgBotEnable">
@@ -102,10 +102,12 @@
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.trojans.length"> <a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.trojans.length">
<table width="100%"> <table width="100%">
<tr class="client-table-header"> <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>
<tr v-for="(client, index) in inbound.settings.trojans" :class="index % 2 == 1 ? 'client-table-odd-row' : ''"> <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> </tr>
</table> </table>
</a-collapse-panel> </a-collapse-panel>

View File

@@ -11,7 +11,7 @@
</template> </template>
</a-tooltip> </a-tooltip>
</span> </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-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="ID"> <a-form-item label="ID">
@@ -28,7 +28,7 @@
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span> </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-input v-model.trim="client.subId" style="width: 150px;"></a-input>
</a-form-item> </a-form-item>
<a-form-item v-if="client.email && app.tgBotEnable"> <a-form-item v-if="client.email && app.tgBotEnable">
@@ -108,10 +108,14 @@
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.vlesses.length"> <a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.vlesses.length">
<table width="100%"> <table width="100%">
<tr class="client-table-header"> <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>
<tr v-for="(client, index) in inbound.settings.vlesses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''"> <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> </tr>
</table> </table>
</a-collapse-panel> </a-collapse-panel>

View File

@@ -11,14 +11,10 @@
</template> </template>
</a-tooltip> </a-tooltip>
</span> </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-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item> </a-form-item>
<br> <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-form-item label="ID">
<a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"> </a-icon> <a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"> </a-icon>
<a-input v-model.trim="client.id" style="width: 300px;"></a-input> <a-input v-model.trim="client.id" style="width: 300px;"></a-input>
@@ -33,7 +29,7 @@
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span> </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-input v-model.trim="client.subId" style="width: 150px;"></a-input>
</a-form-item> </a-form-item>
<a-form-item v-if="client.email && app.tgBotEnable"> <a-form-item v-if="client.email && app.tgBotEnable">
@@ -101,10 +97,12 @@
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount" }}: ' + inbound.settings.vmesses.length"> <a-collapse-panel :header="'{{ i18n "pages.client.clientCount" }}: ' + inbound.settings.vmesses.length">
<table width="100%"> <table width="100%">
<tr class="client-table-header"> <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>
<tr v-for="(client, index) in inbound.settings.vmesses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''"> <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> </tr>
</table> </table>
</a-collapse-panel> </a-collapse-panel>

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,8 +3,7 @@
<a-form-item label="AcceptProxyProtocol"> <a-form-item label="AcceptProxyProtocol">
<a-switch v-model="inbound.stream.ws.acceptProxyProtocol"></a-switch> <a-switch v-model="inbound.stream.ws.acceptProxyProtocol"></a-switch>
</a-form-item> </a-form-item>
</a-form> <br>
<a-form layout="inline">
<a-form-item label='{{ i18n "path" }}'> <a-form-item label='{{ i18n "path" }}'>
<a-input v-model.trim="inbound.stream.ws.path"></a-input> <a-input v-model.trim="inbound.stream.ws.path"></a-input>
</a-form-item> </a-form-item>

View File

@@ -35,7 +35,6 @@
<a-form v-if="inbound.tls" layout="inline"> <a-form v-if="inbound.tls" layout="inline">
<a-form-item label='Multi Domain'> <a-form-item label='Multi Domain'>
<a-switch v-model="multiDomain"></a-switch> <a-switch v-model="multiDomain"></a-switch>
</a-form-item> </a-form-item>
<a-form-item v-if="multiDomain"> <a-form-item v-if="multiDomain">
<a-row> <a-row>
@@ -85,9 +84,14 @@
<a-checkbox v-for="key,value in ALPN_OPTION" :value="key">[[ value ]]</a-checkbox> <a-checkbox v-for="key,value in ALPN_OPTION" :value="key">[[ value ]]</a-checkbox>
</a-checkbox-group> </a-checkbox-group>
</a-form-item> </a-form-item>
<br>
<a-form-item label="Allow insecure"> <a-form-item label="Allow insecure">
<a-switch v-model="inbound.stream.tls.settings.allowInsecure"></a-switch> <a-switch v-model="inbound.stream.tls.settings.allowInsecure"></a-switch>
</a-form-item> </a-form-item>
<br>
<a-form-item label="Reject Unknown SNI">
<a-switch v-model="inbound.stream.tls.rejectUnknownSni"></a-switch>
</a-form-item>
<template v-for="cert,index in inbound.stream.tls.certs"> <template v-for="cert,index in inbound.stream.tls.certs">
<a-form-item label='{{ i18n "certificate" }}'> <a-form-item label='{{ i18n "certificate" }}'>
<a-radio-group v-model="cert.useFile" button-style="solid"> <a-radio-group v-model="cert.useFile" button-style="solid">

View File

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

View File

@@ -8,8 +8,7 @@
width="600px" width="600px"
> >
<table style="margin-bottom: 10px; width: 100%;"> <table style="margin-bottom: 10px; width: 100%;">
<tr> <tr><td>
<td>
<table> <table>
<tr><td>{{ i18n "protocol" }}</td><td><a-tag color="green">[[ dbInbound.protocol ]]</a-tag></td></tr> <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> <tr><td>{{ i18n "pages.inbounds.address" }}</td><td><a-tag color="blue">[[ dbInbound.address ]]</a-tag></td></tr>
@@ -21,7 +20,6 @@
<tr> <tr>
<td>{{ i18n "transmission" }}</td><td><a-tag color="green">[[ inbound.network ]]</a-tag></td> <td>{{ i18n "transmission" }}</td><td><a-tag color="green">[[ inbound.network ]]</a-tag></td>
</tr> </tr>
<template v-if="inbound.isTcp || inbound.isWs || inbound.isH2"> <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-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> <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> <tr><td>grpc multiMode</td><td><a-tag color="green">[[ inbound.stream.grpc.multiMode ]]</a-tag></td></tr>
</template> </template>
</table> </table>
</td> </td></tr>
</tr>
<tr colspan="2" v-if="dbInbound.hasLink()"> <tr colspan="2" v-if="dbInbound.hasLink()">
<td v-if="inbound.tls"> <td v-if="inbound.tls">
tls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br /> tls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
@@ -66,36 +63,56 @@
</tr> </tr>
</table> </table>
<template v-if="infoModal.clientSettings"> <template v-if="infoModal.clientSettings">
<a-divider>{{ i18n "pages.inbounds.client" }}</a-divider> <a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
<table style="margin-bottom: 10px;"> <table style="margin-bottom: 10px;">
<tr v-for="col,index in Object.keys(infoModal.clientSettings).slice(0, 3)">
<td>[[ col ]]</td>
<td><a-tag color="green">[[ infoModal.clientSettings[col] ]]</a-tag></td>
</tr>
<tr>
<td>{{ i18n "status" }}</td>
<td>
<a-tag v-if="isEnable" color="blue">{{ i18n "enabled" }}</a-tag>
<a-tag v-else color="red">{{ i18n "disabled" }}</a-tag>
<a-tag v-if="!isActive" color="red">{{ i18n "depleted" }}</a-tag>
</td>
</tr>
</table>
<table style="margin-bottom: 10px; width: 100%;">
<tr> <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.totalFlow" }}</th>
<th>{{ i18n "pages.inbounds.expireDate" }}</th> <th>{{ i18n "pages.inbounds.expireDate" }}</th>
</tr>
<tr> <tr>
<td> <td>
<a-tag v-if="infoModal.clientStats" color="green"> <a-tag v-if="infoModal.clientStats && infoModal.clientSettings.totalGB > 0" :color="statsColor(infoModal.clientStats)">
[[ sizeFormat(infoModal.clientStats['up']) ]] / [[ sizeFormat(infoModal.clientSettings.totalGB - infoModal.clientStats.up - infoModal.clientStats.down) ]]
[[ sizeFormat(infoModal.clientStats['down']) ]]
([[ sizeFormat(infoModal.clientStats['up'] + infoModal.clientStats['down']) ]])
</a-tag> </a-tag>
</td> </td>
<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> <a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
</td> </td>
<td> <td>
@@ -111,15 +128,44 @@
</table> </table>
<template v-if="app.subSettings.enable && infoModal.clientSettings.subId"> <template v-if="app.subSettings.enable && infoModal.clientSettings.subId">
<a-divider>Subscription link</a-divider> <a-divider>Subscription link</a-divider>
<a :href="[[ infoModal.subLink ]]" target="_blank">[[ infoModal.subLink ]]</a> <a-row>
<a-icon id="copy-sub-link" type="snippets" @click="copyToClipboard('copy-sub-link', infoModal.subLink)"></a-icon> <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>
<template v-if="app.tgBotEnable && infoModal.clientSettings.tgId"> <template v-if="app.tgBotEnable && infoModal.clientSettings.tgId">
<a-divider>Telegram Username</a-divider> <a-divider>Telegram Username</a-divider>
<a :href="[[ infoModal.tgLink ]]" target="_blank">@[[ infoModal.clientSettings.tgId ]]</a> <a-row>
<a-icon id="copy-tg-link" type="snippets" @click="copyToClipboard('copy-tg-link', '@' + infoModal.clientSettings.tgId)"></a-icon> <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>
<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>
<template v-else> <template v-else>
<a-divider></a-divider> <a-divider></a-divider>
<table v-if="inbound.protocol == Protocols.SHADOWSOCKS" style="margin-bottom: 10px; width: 100%;"> <table v-if="inbound.protocol == Protocols.SHADOWSOCKS" style="margin-bottom: 10px; width: 100%;">
@@ -127,21 +173,32 @@
<th>{{ i18n "encryption" }}</th> <th>{{ i18n "encryption" }}</th>
<th>{{ i18n "password" }}</th> <th>{{ i18n "password" }}</th>
<th>{{ i18n "pages.inbounds.network" }}</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="green">[[ inbound.settings.method ]]</a-tag></td>
<td><a-tag color="blue">[[ inbound.settings.password ]]</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> <td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td>
</tr> </tr>
</table> </table>
<template v-if="inbound.protocol == Protocols.SHADOWSOCKS && !inbound.isSSMultiUser">
<a-divider>URL</a-divider>
<a-row v-for="(link,index) in infoModal.links">
<a-col :span="22"><a-tag color="cyan">[[ link.remark ]]</a-tag><br />[[ link.link ]]</a-col>
<a-col :span="2" style="text-align: right;">
<a-tooltip title='{{ i18n "copy" }}'>
<button class="ant-btn ant-btn-primary" :id="'copy-url-link-'+index" @click="copyToClipboard('copy-url-link-'+index, link.link)">
<a-icon type="snippets"></a-icon>
</button>
</a-tooltip>
</a-col>
</a-row>
</template>
<table v-if="inbound.protocol == Protocols.DOKODEMO" style="margin-bottom: 10px; width: 100%;"> <table v-if="inbound.protocol == Protocols.DOKODEMO" style="margin-bottom: 10px; width: 100%;">
<tr> <tr>
<th>{{ i18n "pages.inbounds.targetAddress" }}</th> <th>{{ i18n "pages.inbounds.targetAddress" }}</th>
<th>{{ i18n "pages.inbounds.destinationPort" }}</th> <th>{{ i18n "pages.inbounds.destinationPort" }}</th>
<th>{{ i18n "pages.inbounds.network" }}</th> <th>{{ i18n "pages.inbounds.network" }}</th>
<th>FollowRedirect</th> <th>FollowRedirect</th>
</tr> </tr><tr>
<tr>
<td><a-tag color="green">[[ inbound.settings.address ]]</a-tag></td> <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="blue">[[ inbound.settings.port ]]</a-tag></td>
<td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td> <td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td>
@@ -154,18 +211,15 @@
<th>{{ i18n "password" }} Auth</th> <th>{{ i18n "password" }} Auth</th>
<th>{{ i18n "pages.inbounds.enable" }} udp</th> <th>{{ i18n "pages.inbounds.enable" }} udp</th>
<th>IP</th> <th>IP</th>
</tr> </tr><tr>
<tr>
<td><a-tag color="green">[[ inbound.settings.auth ]]</a-tag></td> <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="blue">[[ inbound.settings.udp]]</a-tag></td>
<td><a-tag color="green">[[ inbound.settings.ip ]]</a-tag></td> <td><a-tag color="green">[[ inbound.settings.ip ]]</a-tag></td>
</tr> </tr><tr v-if="inbound.settings.auth == 'password'">
<tr v-if="inbound.settings.auth == 'password'">
<td> </td> <td> </td>
<td>{{ i18n "username" }}</td> <td>{{ i18n "username" }}</td>
<td>{{ i18n "password" }}</td> <td>{{ i18n "password" }}</td>
</tr> </tr><tr v-for="account,index in inbound.settings.accounts">
<tr v-for="account,index in inbound.settings.accounts">
<td><a-tag color="green">[[ index ]]</a-tag></td> <td><a-tag color="green">[[ index ]]</a-tag></td>
<td><a-tag color="blue">[[ account.user ]]</a-tag></td> <td><a-tag color="blue">[[ account.user ]]</a-tag></td>
<td><a-tag color="green">[[ account.pass ]]</a-tag></td> <td><a-tag color="green">[[ account.pass ]]</a-tag></td>
@@ -177,8 +231,7 @@
<th> </th> <th> </th>
<th>{{ i18n "username" }}</th> <th>{{ i18n "username" }}</th>
<th>{{ i18n "password" }}</th> <th>{{ i18n "password" }}</th>
</tr> </tr><tr v-for="account,index in inbound.settings.accounts">
<tr v-for="account,index in inbound.settings.accounts">
<td><a-tag color="green">[[ index ]]</a-tag></td> <td><a-tag color="green">[[ index ]]</a-tag></td>
<td><a-tag color="blue">[[ account.user ]]</a-tag></td> <td><a-tag color="blue">[[ account.user ]]</a-tag></td>
<td><a-tag color="green">[[ account.pass ]]</a-tag></td> <td><a-tag color="green">[[ account.pass ]]</a-tag></td>
@@ -186,20 +239,8 @@
</table> </table>
</table> </table>
</template> </template>
<div v-if="dbInbound.hasLink()">
<a-divider>URL</a-divider>
<a-row v-for="(link,index) in infoModal.links">
<a-col :span="21"><a-tag color="cyan">[[ link.remark ]]</a-tag><br />[[ link.link ]]</a-col>
<a-col :span="3" style="text-align: right;">
<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>{{ i18n "copy" }}
</button>
</a-col>
</a-row>
</div>
</a-modal> </a-modal>
<script> <script>
const infoModal = { const infoModal = {
visible: false, visible: false,
inbound: new Inbound(), inbound: new Inbound(),
@@ -223,7 +264,7 @@
this.clientSettings = this.settings.clients ? Object.values(this.settings.clients)[index] : null; this.clientSettings = this.settings.clients ? Object.values(this.settings.clients)[index] : null;
this.isExpired = this.inbound.isExpiry(index); this.isExpired = this.inbound.isExpiry(index);
this.clientStats = this.settings.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : []; this.clientStats = this.settings.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : [];
remark = this.dbInbound.remark + "-" + this.clientSettings.email; remark = this.dbInbound.remark + ( this.clientSettings ? "-" + this.clientSettings.email : '');
address = this.dbInbound.address; address = this.dbInbound.address;
this.links = []; this.links = [];
if (this.inbound.tls && !ObjectUtil.isArrEmpty(this.inbound.stream.tls.settings.domains)) { if (this.inbound.tls && !ObjectUtil.isArrEmpty(this.inbound.stream.tls.settings.domains)) {
@@ -253,12 +294,8 @@
infoModal.visible = false; infoModal.visible = false;
}, },
genSubLink(subID) { genSubLink(subID) {
protocol = app.subSettings.tls ? "https://" : "http://"; const { domain: host, port, tls: isTLS, path: base } = app.subSettings;
hostName = app.subSettings.domain === "" ? window.location.hostname : app.subSettings.domain; return buildURL({ host, port, isTLS, base, path: subID });
subPort = app.subSettings.port;
port = (subPort === 443 && app.subSettings.tls) || (subPort === 80 && !app.subSettings.tls) ? "" : ":" + String(subPort);
subPath = app.subSettings.path;
return protocol + hostName + port + subPath + subID;
} }
}; };

View File

@@ -54,23 +54,11 @@
}, },
}; };
const protocols = {
VMESS: Protocols.VMESS,
VLESS: Protocols.VLESS,
TROJAN: Protocols.TROJAN,
SHADOWSOCKS: Protocols.SHADOWSOCKS,
DOKODEMO: Protocols.DOKODEMO,
SOCKS: Protocols.SOCKS,
HTTP: Protocols.HTTP,
};
new Vue({ new Vue({
delimiters: ['[[', ']]'], delimiters: ['[[', ']]'],
el: '#inbound-modal', el: '#inbound-modal',
data: { data: {
inModal: inModal, inModal: inModal,
Protocols: protocols,
SSMethods: SSMethods,
delayedStart: false, delayedStart: false,
get inbound() { get inbound() {
return inModal.inbound; return inModal.inbound;
@@ -96,7 +84,7 @@
set multiDomain(value) { set multiDomain(value) {
if (value) { if (value) {
inModal.inbound.stream.tls.server = ""; inModal.inbound.stream.tls.server = "";
inModal.inbound.stream.tls.settings.domains = [{remark: "", domain: window.location.host.split(":")[0]}]; inModal.inbound.stream.tls.settings.domains = [{ remark: "", domain: window.location.hostname }];
} else { } else {
inModal.inbound.stream.tls.server = ""; inModal.inbound.stream.tls.server = "";
inModal.inbound.stream.tls.settings.domains = []; inModal.inbound.stream.tls.settings.domains = [];
@@ -111,6 +99,31 @@
if (!inModal.inbound.canEnableReality()) { if (!inModal.inbound.canEnableReality()) {
this.inModal.inbound.reality = false; this.inModal.inbound.reality = false;
} }
if (this.inModal.inbound.protocol == Protocols.VLESS && !inModal.inbound.canEnableTlsFlow()) {
this.inModal.inbound.settings.vlesses.forEach(client => {
client.flow = "";
});
}
},
SSMethodChange() {
if (this.inModal.inbound.isSSMultiUser) {
if (this.inModal.inbound.settings.shadowsockses.length ==0){
this.inModal.inbound.settings.shadowsockses = [new Inbound.ShadowsocksSettings.Shadowsocks()];
}
if (["aes-128-gcm", "aes-256-gcm", "chacha20-poly1305", "xchacha20-poly1305"].includes(this.inModal.inbound.settings.method)) {
this.inModal.inbound.settings.shadowsockses.forEach(client => {
client.method = this.inModal.inbound.settings.method;
})
} else {
this.inModal.inbound.settings.shadowsockses.forEach(client => {
client.method = "";
})
}
} else {
if (this.inModal.inbound.settings.shadowsockses.length > 0){
this.inModal.inbound.settings.shadowsockses = [];
}
}
}, },
setDefaultCertData(index) { setDefaultCertData(index) {
inModal.inbound.stream.tls.certs[index].certFile = app.defaultCert; inModal.inbound.stream.tls.certs[index].certFile = app.defaultCert;

View File

@@ -131,7 +131,11 @@
<a-icon type="edit"></a-icon> <a-icon type="edit"></a-icon>
{{ i18n "edit" }} {{ i18n "edit" }}
</a-menu-item> </a-menu-item>
<template v-if="dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess || dbInbound.isSS"> <a-menu-item key="qrcode" v-if="dbInbound.isSS && !dbInbound.toInbound().isSSMultiUser">
<a-icon type="qrcode"></a-icon>
{{ i18n "qrCode" }}
</a-menu-item>
<template v-if="dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess || dbInbound.toInbound().isSSMultiUser">
<a-menu-item key="addClient"> <a-menu-item key="addClient">
<a-icon type="user-add"></a-icon> <a-icon type="user-add"></a-icon>
{{ i18n "pages.client.add"}} {{ i18n "pages.client.add"}}
@@ -206,12 +210,29 @@
</template> </template>
</template> </template>
<template slot="traffic" slot-scope="text, dbInbound"> <template slot="traffic" slot-scope="text, dbInbound">
<a-tag color="blue">[[ sizeFormat(dbInbound.up) ]] / [[ sizeFormat(dbInbound.down) ]]</a-tag> <a-popover :overlay-class-name="themeSwitcher.darkClass">
<template v-if="dbInbound.total > 0"> <template slot="content">
<a-tag v-if="dbInbound.up + dbInbound.down < dbInbound.total" color="cyan">[[ sizeFormat(dbInbound.total) ]]</a-tag> <table cellpadding="2" width="100%">
<a-tag v-else color="red">[[ sizeFormat(dbInbound.total) ]]</a-tag> <tr>
</template> <td>↑[[ sizeFormat(dbInbound.up) ]]</td>
<a-tag v-else color="green">{{ i18n "unlimited" }}</a-tag> <td>↓[[ sizeFormat(dbInbound.down) ]]</td>
</tr>
<tr v-if="dbInbound.total > 0 && dbInbound.up + dbInbound.down < dbInbound.total">
<td>{{ i18n "remained" }}</td>
<td>[[ sizeFormat(dbInbound.total - dbInbound.up - dbInbound.down) ]]</td>
</tr>
</table>
</template>
<a-tag :color="dbInbound.total == 0 ? 'green' : dbInbound.up + dbInbound.down < dbInbound.total ? 'cyan' : 'red'">
[[ sizeFormat(dbInbound.up + dbInbound.down) ]] /
<template v-if="dbInbound.total > 0">
[[ sizeFormat(dbInbound.total) ]]
</template>
<template v-else>
<svg style="fill: currentColor; height: 10px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z"/></svg>
</template>
</a-tag>
</a-popover>
</template> </template>
<template slot="enable" slot-scope="text, dbInbound"> <template slot="enable" slot-scope="text, dbInbound">
<a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound.id)"></a-switch> <a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound.id)"></a-switch>
@@ -238,7 +259,7 @@
{{template "client_table"}} {{template "client_table"}}
</a-table> </a-table>
<a-table <a-table
v-else-if="record.protocol === Protocols.TROJAN || record.protocol === Protocols.SHADOWSOCKS" v-else-if="record.protocol === Protocols.TROJAN || record.toInbound().isSSMultiUser"
:row-key="client => client.id" :row-key="client => client.id"
:columns="innerTrojanColumns" :columns="innerTrojanColumns"
:data-source="getInboundClients(record)" :data-source="getInboundClients(record)"
@@ -257,7 +278,6 @@
{{template "js" .}} {{template "js" .}}
{{template "component/themeSwitcher" .}} {{template "component/themeSwitcher" .}}
<script> <script>
const columns = [{ const columns = [{
title: '{{ i18n "pages.inbounds.operate" }}', title: '{{ i18n "pages.inbounds.operate" }}',
align: 'center', align: 'center',
@@ -291,12 +311,12 @@
}, { }, {
title: '{{ i18n "clients" }}', title: '{{ i18n "clients" }}',
align: 'left', align: 'left',
width: 50, width: 40,
scopedSlots: { customRender: 'clients' }, scopedSlots: { customRender: 'clients' },
}, { }, {
title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', title: '{{ i18n "pages.inbounds.traffic" }}',
align: 'center', align: 'center',
width: 120, width: 60,
scopedSlots: { customRender: 'traffic' }, scopedSlots: { customRender: 'traffic' },
}, { }, {
title: '{{ i18n "pages.inbounds.expireDate" }}', title: '{{ i18n "pages.inbounds.expireDate" }}',
@@ -309,17 +329,17 @@
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } }, { title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } },
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 40, scopedSlots: { customRender: 'enable' } }, { title: '{{ i18n "pages.inbounds.enable" }}', width: 40, scopedSlots: { customRender: 'enable' } },
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } }, { title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 120, scopedSlots: { customRender: 'traffic' } }, { title: '{{ i18n "pages.inbounds.traffic" }}', width: 50, scopedSlots: { customRender: 'traffic' } },
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } }, { title: '{{ i18n "pages.inbounds.expireDate" }}', width: 50, scopedSlots: { customRender: 'expiryTime' } },
{ title: 'UID', width: 120, dataIndex: "id" }, { title: 'UUID', width: 120, dataIndex: "id" },
]; ];
const innerTrojanColumns = [ const innerTrojanColumns = [
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } }, { title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } },
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 40, scopedSlots: { customRender: 'enable' } }, { title: '{{ i18n "pages.inbounds.enable" }}', width: 40, scopedSlots: { customRender: 'enable' } },
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } }, { title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 120, scopedSlots: { customRender: 'traffic' } }, { title: '{{ i18n "pages.inbounds.traffic" }}', width: 50, scopedSlots: { customRender: 'traffic' } },
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } }, { title: '{{ i18n "pages.inbounds.expireDate" }}', width: 50, scopedSlots: { customRender: 'expiryTime' } },
{ title: 'Password', width: 170, dataIndex: "password" }, { title: 'Password', width: 170, dataIndex: "password" },
]; ];
@@ -340,7 +360,7 @@
trafficDiff: 0, trafficDiff: 0,
defaultCert: '', defaultCert: '',
defaultKey: '', defaultKey: '',
clientCount: {}, clientCount: [],
isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false, isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false,
refreshing: false, refreshing: false,
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000, refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
@@ -392,12 +412,16 @@
setInbounds(dbInbounds) { setInbounds(dbInbounds) {
this.inbounds.splice(0); this.inbounds.splice(0);
this.dbInbounds.splice(0); this.dbInbounds.splice(0);
this.clientCount.splice(0);
for (const inbound of dbInbounds) { for (const inbound of dbInbounds) {
const dbInbound = new DBInbound(inbound); const dbInbound = new DBInbound(inbound);
to_inbound = dbInbound.toInbound() to_inbound = dbInbound.toInbound()
this.inbounds.push(to_inbound); this.inbounds.push(to_inbound);
this.dbInbounds.push(dbInbound); this.dbInbounds.push(dbInbound);
if ([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(inbound.protocol)) { if ([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(inbound.protocol)) {
if (inbound.protocol === Protocols.SHADOWSOCKS && (!to_inbound.isSSMultiUser)) {
continue;
}
this.clientCount[inbound.id] = this.getClientCounts(inbound, to_inbound); this.clientCount[inbound.id] = this.getClientCounts(inbound, to_inbound);
} }
} }

View File

@@ -78,15 +78,15 @@
<a-row> <a-row>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass"> <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> 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-card>
</a-col> </a-col>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass"> <a-card hoverable :class="themeSwitcher.darkCardClass">
{{ i18n "menu.link" }}: {{ 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="openLogs()">{{ i18n "pages.index.logs" }}</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="openConfig">{{ i18n "pages.index.config" }}</a-tag> <a-tag color="blue" style="cursor: pointer;" @click="openConfig">{{ i18n "pages.index.config" }}</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="openBackup">{{ i18n "pages.index.backup" }}</a-tag> <a-tag color="blue" style="cursor: pointer;" @click="openBackup">{{ i18n "pages.index.backup" }}</a-tag>
</a-card> </a-card>
@@ -253,7 +253,7 @@
<a-form-item label="Count"> <a-form-item label="Count">
<a-select v-model="logModal.rows" <a-select v-model="logModal.rows"
style="width: 80px" style="width: 80px"
@change="openLogs(logModal.rows)" @change="openLogs()"
:dropdown-class-name="themeSwitcher.darkCardClass"> :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="10">10</a-select-option> <a-select-option value="10">10</a-select-option>
<a-select-option value="20">20</a-select-option> <a-select-option value="20">20</a-select-option>
@@ -261,8 +261,23 @@
<a-select-option value="100">100</a-select-option> <a-select-option value="100">100</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="Log Level">
<a-select v-model="logModal.level"
style="width: 120px"
@change="openLogs()"
:dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="debug">Debug</a-select-option>
<a-select-option value="info">Info</a-select-option>
<a-select-option value="notice">Notice</a-select-option>
<a-select-option value="warning">Warning</a-select-option>
<a-select-option value="err">Error</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="SysLog">
<a-checkbox v-model="logModal.syslog" @change="openLogs()"></a-checkbox>
</a-form-item>
<a-form-item> <a-form-item>
<button class="ant-btn ant-btn-primary" @click="openLogs(logModal.rows)"><a-icon type="sync"></a-icon> Reload</button> <button class="ant-btn ant-btn-primary" @click="openLogs()"><a-icon type="sync"></a-icon> Reload</button>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<a-button type="primary" style="margin-bottom: 10px;" <a-button type="primary" style="margin-bottom: 10px;"
@@ -397,10 +412,11 @@
visible: false, visible: false,
logs: '', logs: '',
rows: 20, rows: 20,
show(logs, rows) { level: 'info',
syslog: false,
show(logs) {
this.visible = true; this.visible = true;
this.rows = rows; this.logs = logs? logs.join("\n"): "No Record...";
this.logs = logs.join("\n");
}, },
hide() { hide() {
this.visible = false; this.visible = false;
@@ -449,9 +465,13 @@
this.loadingTip = tip; this.loadingTip = tip;
}, },
async getStatus() { async getStatus() {
const msg = await HttpUtil.post('/server/status'); try {
if (msg.success) { const msg = await HttpUtil.post('/server/status');
this.setStatus(msg.obj); if (msg.success) {
this.setStatus(msg.obj);
}
} catch (e) {
console.error("Failed to get status:", e);
} }
}, },
setStatus(data) { setStatus(data) {
@@ -497,14 +517,14 @@
return; return;
} }
}, },
async openLogs(rows) { async openLogs(){
this.loading(true); this.loading(true);
const msg = await HttpUtil.post('server/logs/' + rows); const msg = await HttpUtil.post('server/logs/'+logModal.rows,{level: logModal.level, syslog: logModal.syslog});
this.loading(false); this.loading(false);
if (!msg.success) { if (!msg.success) {
return; return;
} }
logModal.show(msg.obj, rows); logModal.show(msg.obj);
}, },
async openConfig() { async openConfig() {
this.loading(true); this.loading(true);
@@ -560,11 +580,14 @@
}, },
}, },
async mounted() { async mounted() {
while (true) { let retries = 0;
while (retries < 5) {
try { try {
await this.getStatus(); await this.getStatus();
retries = 0;
} catch (e) { } catch (e) {
console.error(e); console.error("Error occurred while fetching status:", e);
retries++;
} }
await PromiseUtil.sleep(2000); await PromiseUtil.sleep(2000);
} }

View File

@@ -91,6 +91,7 @@
</a-row> </a-row>
<a-list item-layout="horizontal" :style="themeSwitcher.textStyle"> <a-list item-layout="horizontal" :style="themeSwitcher.textStyle">
<setting-list-item type="text" title='{{ i18n "pages.settings.panelListeningIP"}}' desc='{{ i18n "pages.settings.panelListeningIPDesc"}}' v-model="allSetting.webListen"></setting-list-item> <setting-list-item type="text" title='{{ i18n "pages.settings.panelListeningIP"}}' desc='{{ i18n "pages.settings.panelListeningIPDesc"}}' v-model="allSetting.webListen"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.panelListeningDomain"}}' desc='{{ i18n "pages.settings.panelListeningDomainDesc"}}' v-model="allSetting.webDomain"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.settings.panelPort"}}' desc='{{ i18n "pages.settings.panelPortDesc"}}' v-model="allSetting.webPort" :min="0"></setting-list-item> <setting-list-item type="number" title='{{ i18n "pages.settings.panelPort"}}' desc='{{ i18n "pages.settings.panelPortDesc"}}' v-model="allSetting.webPort" :min="0"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.publicKeyPath"}}' desc='{{ i18n "pages.settings.publicKeyPathDesc"}}' v-model="allSetting.webCertFile"></setting-list-item> <setting-list-item type="text" title='{{ i18n "pages.settings.publicKeyPath"}}' desc='{{ i18n "pages.settings.publicKeyPathDesc"}}' v-model="allSetting.webCertFile"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.privateKeyPath"}}' desc='{{ i18n "pages.settings.privateKeyPathDesc"}}' v-model="allSetting.webKeyFile"></setting-list-item> <setting-list-item type="text" title='{{ i18n "pages.settings.privateKeyPath"}}' desc='{{ i18n "pages.settings.privateKeyPathDesc"}}' v-model="allSetting.webKeyFile"></setting-list-item>
@@ -115,7 +116,7 @@
style="width: 100%" style="width: 100%"
> >
<a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs"> <a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs">
<span role="img" aria-label="l.name" v-text="l.icon"></span> <span role="img" :aria-label="l.name" v-text="l.icon"></span>
&nbsp;&nbsp;<span v-text="l.name"></span> &nbsp;&nbsp;<span v-text="l.name"></span>
</a-select-option> </a-select-option>
</a-select> </a-select>
@@ -306,23 +307,37 @@
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigNetflixWARP"}}' desc='{{ i18n "pages.settings.templates.xrayConfigNetflixWARPDesc"}}' v-model="NetflixWARPSettings"></setting-list-item> <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigNetflixWARP"}}' desc='{{ i18n "pages.settings.templates.xrayConfigNetflixWARPDesc"}}' v-model="NetflixWARPSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigSpotifyWARP"}}' desc='{{ i18n "pages.settings.templates.xrayConfigSpotifyWARPDesc"}}' v-model="SpotifyWARPSettings"></setting-list-item> <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigSpotifyWARP"}}' desc='{{ i18n "pages.settings.templates.xrayConfigSpotifyWARPDesc"}}' v-model="SpotifyWARPSettings"></setting-list-item>
</a-collapse-panel> </a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.manualLists"}}'> </a-collapse>
<a-row :xs="24" :sm="24" :lg="12"> </a-tab-pane>
<h2 class="collapse-title"> <a-tab-pane key="tpl-2" tab='{{ i18n "pages.settings.templates.manualLists"}}' style="padding-top: 20px;">
<a-icon type="warning"></a-icon> <a-row :xs="24" :sm="24" :lg="12">
{{ i18n "pages.settings.templates.manualListsDesc" }} <h2 class="collapse-title">
</h2> <a-icon type="warning"></a-icon>
</a-row> {{ i18n "pages.settings.templates.manualListsDesc" }}
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualBlockedIPs"}}' v-model="manualBlockedIPs"></setting-list-item> </h2>
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualBlockedDomains"}}' v-model="manualBlockedDomains"></setting-list-item> </a-row>
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualDirectIPs"}}' v-model="manualDirectIPs"></setting-list-item> <a-collapse>
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualDirectDomains"}}' v-model="manualDirectDomains"></setting-list-item> <a-collapse-panel header='{{ i18n "pages.settings.templates.manualBlockedIPs"}}'>
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualIPv4Domains"}}' v-model="manualIPv4Domains"></setting-list-item> <setting-list-item type="textarea" v-model="manualBlockedIPs"></setting-list-item>
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualWARPDomains"}}' v-model="manualWARPDomains"></setting-list-item> </a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.manualBlockedDomains"}}'>
<setting-list-item type="textarea" v-model="manualBlockedDomains"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.manualDirectIPs"}}'>
<setting-list-item type="textarea" v-model="manualDirectIPs"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.manualDirectDomains"}}'>
<setting-list-item type="textarea" v-model="manualDirectDomains"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.manualIPv4Domains"}}'>
<setting-list-item type="textarea" v-model="manualIPv4Domains"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.manualWARPDomains"}}'>
<setting-list-item type="textarea" v-model="manualWARPDomains"></setting-list-item>
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="tpl-2" tab='{{ i18n "pages.settings.templates.advancedTemplate"}}' style="padding-top: 20px;"> <a-tab-pane key="tpl-3" tab='{{ i18n "pages.settings.templates.advancedTemplate"}}' style="padding-top: 20px;">
<a-collapse> <a-collapse>
<a-collapse-panel header='{{ i18n "pages.settings.templates.xrayConfigInbounds"}}'> <a-collapse-panel header='{{ i18n "pages.settings.templates.xrayConfigInbounds"}}'>
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigInbounds"}}' desc='{{ i18n "pages.settings.templates.xrayConfigInboundsDesc"}}' v-model="inboundSettings"></setting-list-item> <setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigInbounds"}}' desc='{{ i18n "pages.settings.templates.xrayConfigInboundsDesc"}}' v-model="inboundSettings"></setting-list-item>
@@ -335,7 +350,7 @@
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="tpl-3" tab='{{ i18n "pages.settings.templates.completeTemplate"}}' style="padding-top: 20px;"> <a-tab-pane key="tpl-4" tab='{{ i18n "pages.settings.templates.completeTemplate"}}' style="padding-top: 20px;">
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigTemplate"}}' desc='{{ i18n "pages.settings.templates.xrayConfigTemplateDesc"}}' v-model="allSetting.xrayTemplateConfig"></setting-list-item> <setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigTemplate"}}' desc='{{ i18n "pages.settings.templates.xrayConfigTemplateDesc"}}' v-model="allSetting.xrayTemplateConfig"></setting-list-item>
</a-tab-pane> </a-tab-pane>
</a-tabs> </a-tabs>
@@ -355,6 +370,7 @@
<setting-list-item type="text" title='{{ i18n "pages.settings.telegramChatId"}}' desc='{{ i18n "pages.settings.telegramChatIdDesc"}}' v-model="allSetting.tgBotChatId"></setting-list-item> <setting-list-item type="text" title='{{ i18n "pages.settings.telegramChatId"}}' desc='{{ i18n "pages.settings.telegramChatIdDesc"}}' v-model="allSetting.tgBotChatId"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.telegramNotifyTime"}}' desc='{{ i18n "pages.settings.telegramNotifyTimeDesc"}}' v-model="allSetting.tgRunTime"></setting-list-item> <setting-list-item type="text" title='{{ i18n "pages.settings.telegramNotifyTime"}}' desc='{{ i18n "pages.settings.telegramNotifyTimeDesc"}}' v-model="allSetting.tgRunTime"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.tgNotifyBackup" }}' desc='{{ i18n "pages.settings.tgNotifyBackupDesc" }}' v-model="allSetting.tgBotBackup"></setting-list-item> <setting-list-item type="switch" title='{{ i18n "pages.settings.tgNotifyBackup" }}' desc='{{ i18n "pages.settings.tgNotifyBackupDesc" }}' v-model="allSetting.tgBotBackup"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.tgNotifyLogin" }}' desc='{{ i18n "pages.settings.tgNotifyLoginDesc" }}' v-model="allSetting.tgBotLoginNotify"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.settings.tgNotifyCpu" }}' desc='{{ i18n "pages.settings.tgNotifyCpuDesc" }}' v-model="allSetting.tgCpu" :min="0" :max="100"></setting-list-item> <setting-list-item type="number" title='{{ i18n "pages.settings.tgNotifyCpu" }}' desc='{{ i18n "pages.settings.tgNotifyCpuDesc" }}' v-model="allSetting.tgCpu" :min="0" :max="100"></setting-list-item>
<a-list-item> <a-list-item>
<a-row style="padding: 20px"> <a-row style="padding: 20px">
@@ -371,7 +387,7 @@
style="width: 100%" style="width: 100%"
> >
<a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs"> <a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs">
<span role="img" aria-label="l.name" v-text="l.icon"></span> <span role="img" :aria-label="l.name" v-text="l.icon"></span>
&nbsp;&nbsp;<span v-text="l.name"></span> &nbsp;&nbsp;<span v-text="l.name"></span>
</a-select-option> </a-select-option>
</a-select> </a-select>
@@ -391,9 +407,9 @@
<a-list item-layout="horizontal" :style="themeSwitcher.textStyle"> <a-list item-layout="horizontal" :style="themeSwitcher.textStyle">
<setting-list-item type="switch" title='{{ i18n "pages.settings.subEnable"}}' desc='{{ i18n "pages.settings.subEnableDesc"}}' v-model="allSetting.subEnable"></setting-list-item> <setting-list-item type="switch" title='{{ i18n "pages.settings.subEnable"}}' desc='{{ i18n "pages.settings.subEnableDesc"}}' v-model="allSetting.subEnable"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.subListen"}}' desc='{{ i18n "pages.settings.subListenDesc"}}' v-model="allSetting.subListen"></setting-list-item> <setting-list-item type="text" title='{{ i18n "pages.settings.subListen"}}' desc='{{ i18n "pages.settings.subListenDesc"}}' v-model="allSetting.subListen"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.subDomain"}}' desc='{{ i18n "pages.settings.subDomainDesc"}}' v-model="allSetting.subDomain"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.settings.subPort"}}' desc='{{ i18n "pages.settings.subPortDesc"}}' v-model.number="allSetting.subPort"></setting-list-item> <setting-list-item type="number" title='{{ i18n "pages.settings.subPort"}}' desc='{{ i18n "pages.settings.subPortDesc"}}' v-model.number="allSetting.subPort"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.subPath"}}' desc='{{ i18n "pages.settings.subPathDesc"}}' v-model="allSetting.subPath"></setting-list-item> <setting-list-item type="text" title='{{ i18n "pages.settings.subPath"}}' desc='{{ i18n "pages.settings.subPathDesc"}}' v-model="allSetting.subPath"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.subDomain"}}' desc='{{ i18n "pages.settings.subDomainDesc"}}' v-model="allSetting.subDomain"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.subCertPath"}}' desc='{{ i18n "pages.settings.subCertPathDesc"}}' v-model="allSetting.subCertFile"></setting-list-item> <setting-list-item type="text" title='{{ i18n "pages.settings.subCertPath"}}' desc='{{ i18n "pages.settings.subCertPathDesc"}}' v-model="allSetting.subCertFile"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.subKeyPath"}}' desc='{{ i18n "pages.settings.subKeyPathDesc"}}' v-model="allSetting.subKeyFile"></setting-list-item> <setting-list-item type="text" title='{{ i18n "pages.settings.subKeyPath"}}' desc='{{ i18n "pages.settings.subKeyPathDesc"}}' v-model="allSetting.subKeyFile"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.settings.subUpdates"}}' desc='{{ i18n "pages.settings.subUpdatesDesc"}}' v-model="allSetting.subUpdates"></setting-list-item> <setting-list-item type="number" title='{{ i18n "pages.settings.subUpdates"}}' desc='{{ i18n "pages.settings.subUpdatesDesc"}}' v-model="allSetting.subUpdates"></setting-list-item>
@@ -522,7 +538,7 @@
this.loading(false); this.loading(false);
if (msg.success) { if (msg.success) {
this.user = {}; this.user = {};
window.location.replace(basePath + "logout") window.location.replace(basePath + "logout");
} }
}, },
async restartPanel() { async restartPanel() {
@@ -541,12 +557,10 @@
if (msg.success) { if (msg.success) {
this.loading(true); this.loading(true);
await PromiseUtil.sleep(5000); await PromiseUtil.sleep(5000);
let protocol = "http://"; const { webCertFile, webKeyFile, webDomain: host, webPort: port, webBasePath: base } = this.allSetting;
if (this.allSetting.webCertFile !== "") { const isTLS = webCertFile !== "" || webKeyFile !== "";
protocol = "https://"; const url = buildURL({ host, port, isTLS, base, path: "panel/settings" });
} window.location.replace(url);
const { host } = window.location;
window.location.replace(protocol + host + this.allSetting.webBasePath + "panel/settings");
} }
}, },
async fetchUserSecret() { async fetchUserSecret() {

View File

@@ -2,58 +2,81 @@ package job
import ( import (
"encoding/json" "encoding/json"
"log"
"os" "os"
"regexp" "regexp"
"sync"
"x-ui/database"
"x-ui/database/model"
"x-ui/logger"
"x-ui/web/service"
"x-ui/xray"
"net"
"sort" "sort"
"strings" "strings"
"time" "time"
"github.com/go-cmd/cmd" "x-ui/database"
"x-ui/database/model"
"x-ui/logger"
"x-ui/xray"
) )
type CheckClientIpJob struct { type CheckClientIpJob struct {}
xrayService service.XrayService
AllowedIps []string
mutex sync.Mutex
}
var job *CheckClientIpJob var job *CheckClientIpJob
var AllowedIps []string var disAllowedIps []string
var ipRegx *regexp.Regexp = regexp.MustCompile(`[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+`) var ipFiles = []string{
var emailRegx *regexp.Regexp = regexp.MustCompile(`email:.+`) xray.GetIPLimitLogPath(),
xray.GetIPLimitBannedLogPath(),
xray.GetAccessPersistentLogPath(),
}
func NewCheckClientIpJob() *CheckClientIpJob { func NewCheckClientIpJob() *CheckClientIpJob {
job := &CheckClientIpJob{} job = new(CheckClientIpJob)
return job return job
} }
func (j *CheckClientIpJob) Run() { func (j *CheckClientIpJob) Run() {
logger.Debug("Check Client IP Job...") logger.Debug("Check Client IP Job...")
j.processLogFile()
// AllowedIps = []string{"192.168.1.183","192.168.1.197"} // create files required for iplimit if not exists
allowedIps := []byte(strings.Join(j.getAllowedIps(), ",")) for i := 0; i < len(ipFiles); i++ {
file, err := os.OpenFile(ipFiles[i], os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
// check if file exists, if not create one j.checkError(err)
_, err := os.Stat(xray.GetAllowedIPsPath()) defer file.Close()
if os.IsNotExist(err) {
_, err = os.OpenFile(xray.GetAllowedIPsPath(), os.O_RDWR|os.O_CREATE, 0755)
checkError(err)
} }
err = os.WriteFile(xray.GetAllowedIPsPath(), allowedIps, 0755)
checkError(err) // check for limit ip
if j.hasLimitIp() {
j.processLogFile()
}
}
func (j *CheckClientIpJob) hasLimitIp() bool {
db := database.GetDB()
var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Find(&inbounds).Error
if err != nil {
return false
}
for _, inbound := range inbounds {
if inbound.Settings == "" {
continue
}
settings := map[string][]model.Client{}
json.Unmarshal([]byte(inbound.Settings), &settings)
clients := settings["clients"]
for _, client := range clients {
limitIp := client.LimitIP
if limitIp > 0 {
return true
}
}
}
return false
} }
func (j *CheckClientIpJob) processLogFile() { func (j *CheckClientIpJob) processLogFile() {
accessLogPath := GetAccessLogPath() accessLogPath := xray.GetAccessLogPath()
if accessLogPath == "" { if accessLogPath == "" {
logger.Warning("access.log doesn't exist in your config.json") logger.Warning("access.log doesn't exist in your config.json")
return return
@@ -61,15 +84,12 @@ func (j *CheckClientIpJob) processLogFile() {
data, err := os.ReadFile(accessLogPath) data, err := os.ReadFile(accessLogPath)
InboundClientIps := make(map[string][]string) InboundClientIps := make(map[string][]string)
checkError(err) j.checkError(err)
// clean log
if err := os.Truncate(GetAccessLogPath(), 0); err != nil {
checkError(err)
}
lines := strings.Split(string(data), "\n") lines := strings.Split(string(data), "\n")
for _, line := range lines { for _, line := range lines {
ipRegx, _ := regexp.Compile(`[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+`)
emailRegx, _ := regexp.Compile(`email:.+`)
matchesIp := ipRegx.FindString(line) matchesIp := ipRegx.FindString(line)
if len(matchesIp) > 0 { if len(matchesIp) > 0 {
@@ -84,72 +104,70 @@ func (j *CheckClientIpJob) processLogFile() {
} }
matchesEmail = strings.TrimSpace(strings.Split(matchesEmail, "email: ")[1]) matchesEmail = strings.TrimSpace(strings.Split(matchesEmail, "email: ")[1])
if !contains(InboundClientIps[matchesEmail], ip) { if InboundClientIps[matchesEmail] != nil {
if j.contains(InboundClientIps[matchesEmail], ip) {
continue
}
InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail], ip)
} else {
InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail], ip) InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail], ip)
} }
} }
} }
j.setAllowedIps([]string{})
disAllowedIps = []string{}
shouldCleanLog := false
for clientEmail, ips := range InboundClientIps { for clientEmail, ips := range InboundClientIps {
inboundClientIps, err := GetInboundClientIps(clientEmail) inboundClientIps, err := j.getInboundClientIps(clientEmail)
sort.Strings(ips) sort.Strings(ips)
if err != nil { if err != nil {
addInboundClientIps(clientEmail, ips) j.addInboundClientIps(clientEmail, ips)
} else { } else {
j.updateInboundClientIps(inboundClientIps, clientEmail, ips) shouldCleanLog = j.updateInboundClientIps(inboundClientIps, clientEmail, ips)
} }
} }
// check if inbound connection is more than limited ip and drop connection // added delay before cleaning logs to reduce chance of logging IP that already has been banned
LimitDevice := func() { LimitDevice() } time.Sleep(time.Second * 2)
stop := schedule(LimitDevice, 1000*time.Millisecond) if shouldCleanLog {
time.Sleep(10 * time.Second) // copy access log to persistent file
stop <- true logAccessP, err := os.OpenFile(xray.GetAccessPersistentLogPath(), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
j.checkError(err)
input, err := os.ReadFile(accessLogPath)
j.checkError(err)
if _, err := logAccessP.Write(input); err != nil {
j.checkError(err)
}
defer logAccessP.Close()
} // clean access log
if err := os.Truncate(xray.GetAccessLogPath(), 0); err != nil {
func GetAccessLogPath() string { j.checkError(err)
config, err := os.ReadFile(xray.GetConfigPath())
checkError(err)
jsonConfig := map[string]interface{}{}
err = json.Unmarshal([]byte(config), &jsonConfig)
checkError(err)
if jsonConfig["log"] != nil {
jsonLog := jsonConfig["log"].(map[string]interface{})
if jsonLog["access"] != nil {
accessLogPath := jsonLog["access"].(string)
return accessLogPath
} }
} }
return ""
} }
func checkError(e error) { func (j *CheckClientIpJob) checkError(e error) {
if e != nil { if e != nil {
logger.Warning("client ip job err:", e) logger.Warning("client ip job err:", e)
} }
} }
func contains(s []string, str string) bool { func (j *CheckClientIpJob) contains(s []string, str string) bool {
for _, v := range s { for _, v := range s {
if v == str { if v == str {
return true return true
} }
} }
return false return false
} }
func GetInboundClientIps(clientEmail string) (*model.InboundClientIps, error) { func (j *CheckClientIpJob) getInboundClientIps(clientEmail string) (*model.InboundClientIps, error) {
db := database.GetDB() db := database.GetDB()
InboundClientIps := &model.InboundClientIps{} InboundClientIps := &model.InboundClientIps{}
err := db.Model(model.InboundClientIps{}).Where("client_email = ?", clientEmail).First(InboundClientIps).Error err := db.Model(model.InboundClientIps{}).Where("client_email = ?", clientEmail).First(InboundClientIps).Error
@@ -159,10 +177,10 @@ func GetInboundClientIps(clientEmail string) (*model.InboundClientIps, error) {
return InboundClientIps, nil return InboundClientIps, nil
} }
func addInboundClientIps(clientEmail string, ips []string) error { func (j *CheckClientIpJob) addInboundClientIps(clientEmail string, ips []string) error {
inboundClientIps := &model.InboundClientIps{} inboundClientIps := &model.InboundClientIps{}
jsonIps, err := json.Marshal(ips) jsonIps, err := json.Marshal(ips)
checkError(err) j.checkError(err)
inboundClientIps.ClientEmail = clientEmail inboundClientIps.ClientEmail = clientEmail
inboundClientIps.Ips = string(jsonIps) inboundClientIps.Ips = string(jsonIps)
@@ -171,10 +189,10 @@ func addInboundClientIps(clientEmail string, ips []string) error {
tx := db.Begin() tx := db.Begin()
defer func() { defer func() {
if r := recover(); r != nil { if err == nil {
tx.Rollback()
} else {
tx.Commit() tx.Commit()
} else {
tx.Rollback()
} }
}() }()
@@ -184,187 +202,72 @@ func addInboundClientIps(clientEmail string, ips []string) error {
} }
return nil return nil
} }
func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmail string, ips []string) error {
func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmail string, ips []string) bool {
jsonIps, err := json.Marshal(ips)
j.checkError(err)
inboundClientIps.ClientEmail = clientEmail
inboundClientIps.Ips = string(jsonIps)
// check inbound limitation // check inbound limitation
inbound, err := GetInboundByEmail(clientEmail) inbound, err := j.getInboundByEmail(clientEmail)
checkError(err) j.checkError(err)
if inbound.Settings == "" { if inbound.Settings == "" {
logger.Debug("wrong data ", inbound) logger.Debug("wrong data ", inbound)
return nil return false
} }
settings := map[string][]model.Client{} settings := map[string][]model.Client{}
json.Unmarshal([]byte(inbound.Settings), &settings) json.Unmarshal([]byte(inbound.Settings), &settings)
clients := settings["clients"] clients := settings["clients"]
shouldCleanLog := false
// create iplimit log file channel
logIpFile, err := os.OpenFile(xray.GetIPLimitLogPath(), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
if err != nil {
logger.Errorf("failed to create or open ip limit log file: %s", err)
}
defer logIpFile.Close()
log.SetOutput(logIpFile)
log.SetFlags(log.LstdFlags)
for _, client := range clients { for _, client := range clients {
if client.Email == clientEmail { if client.Email == clientEmail {
limitIp := client.LimitIP limitIp := client.LimitIP
if limitIp < len(ips) && limitIp != 0 && inbound.Enable {
for _, ip := range ips[:limitIp] { if limitIp != 0 {
j.addAllowedIp(ip) 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])
}
} }
} }
} }
} }
logger.Debug("disAllowedIps ", disAllowedIps)
jsonIps, err := json.Marshal(ips) // marshal the possibly truncated list of IPs sort.Strings(disAllowedIps)
checkError(err)
inboundClientIps.ClientEmail = clientEmail
inboundClientIps.Ips = string(jsonIps)
logger.Debug("Allowed IPs: ", ips)
db := database.GetDB() db := database.GetDB()
err = db.Save(inboundClientIps).Error err = db.Save(inboundClientIps).Error
if err != nil { if err != nil {
return err return shouldCleanLog
} }
return nil return shouldCleanLog
} }
func (j *CheckClientIpJob) setAllowedIps(ips []string) { func (j *CheckClientIpJob) getInboundByEmail(clientEmail string) (*model.Inbound, error) {
j.mutex.Lock()
defer j.mutex.Unlock()
j.AllowedIps = ips
}
func (j *CheckClientIpJob) addAllowedIp(ip string) {
j.mutex.Lock()
defer j.mutex.Unlock()
j.AllowedIps = append(j.AllowedIps, ip)
}
func (j *CheckClientIpJob) getAllowedIps() []string {
j.mutex.Lock()
defer j.mutex.Unlock()
return j.AllowedIps
}
func DisableInbound(id int) error {
db := database.GetDB()
result := db.Model(model.Inbound{}).
Where("id = ? and enable = ?", id, true).
Update("enable", false)
err := result.Error
logger.Warning("disable inbound with id:", id)
if err == nil {
job.xrayService.SetToNeedRestart()
}
return err
}
func GetInboundByEmail(clientEmail string) (*model.Inbound, error) {
db := database.GetDB() db := database.GetDB()
var inbounds *model.Inbound var inbounds *model.Inbound
err := db.Model(model.Inbound{}).Where("settings LIKE ?", "%"+clientEmail+"%").Find(&inbounds).Error err := db.Model(model.Inbound{}).Where("settings LIKE ?", "%"+clientEmail+"%").Find(&inbounds).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }
return inbounds, nil return inbounds, nil
} }
func LimitDevice() {
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, " ")
destIp, destPort, srcIp, srcPort := "", "", "", ""
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(AllowedIps, 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

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

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

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

View File

@@ -29,7 +29,7 @@ type SettingService interface {
func InitLocalizer(i18nFS embed.FS, settingService SettingService) error { func InitLocalizer(i18nFS embed.FS, settingService SettingService) error {
// set default bundle to english // set default bundle to english
i18nBundle = i18n.NewBundle(language.English) i18nBundle = i18n.NewBundle(language.MustParse("en-US"))
i18nBundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) i18nBundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
// parse files // parse files

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

@@ -15,6 +15,7 @@ import (
) )
type InboundService struct { type InboundService struct {
xrayApi xray.XrayAPI
} }
func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) { func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) {
@@ -133,37 +134,64 @@ func (s *InboundService) checkEmailExistForInbound(inbound *model.Inbound) (stri
return "", nil return "", nil
} }
func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, error) { func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, bool, error) {
exist, err := s.checkPortExist(inbound.Port, 0) exist, err := s.checkPortExist(inbound.Port, 0)
if err != nil { if err != nil {
return inbound, err return inbound, false, err
} }
if exist { if exist {
return inbound, common.NewError("Port already exists:", inbound.Port) return inbound, false, common.NewError("Port already exists:", inbound.Port)
} }
existEmail, err := s.checkEmailExistForInbound(inbound) existEmail, err := s.checkEmailExistForInbound(inbound)
if err != nil { if err != nil {
return inbound, err return inbound, false, err
} }
if existEmail != "" { if existEmail != "" {
return inbound, common.NewError("Duplicate email:", existEmail) return inbound, false, common.NewError("Duplicate email:", existEmail)
} }
clients, err := s.GetClients(inbound) clients, err := s.GetClients(inbound)
if err != nil { if err != nil {
return inbound, err return inbound, false, err
} }
db := database.GetDB() 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 { if err == nil {
for _, client := range clients { for _, client := range clients {
s.AddClientStat(inbound.Id, &client) s.AddClientStat(tx, inbound.Id, &client)
} }
} }
return inbound, err
needRestart := false
if inbound.Enable {
s.xrayApi.Init(p.GetAPIPort())
inboundJson, err1 := json.MarshalIndent(inbound.GenXrayInboundConfig(), "", " ")
if err1 != nil {
logger.Debug("Unable to marshal inbound config:", err1)
}
err1 = s.xrayApi.AddInbound(inboundJson)
if err1 == nil {
logger.Debug("New inbound added by api:", inbound.Tag)
} else {
logger.Debug("Unable to add inbound by api:", err1)
needRestart = true
}
s.xrayApi.Close()
}
return inbound, needRestart, err
} }
func (s *InboundService) AddInbounds(inbounds []*model.Inbound) error { func (s *InboundService) AddInbounds(inbounds []*model.Inbound) error {
@@ -198,27 +226,47 @@ func (s *InboundService) AddInbounds(inbounds []*model.Inbound) error {
return nil return nil
} }
func (s *InboundService) DelInbound(id int) error { func (s *InboundService) DelInbound(id int) (bool, error) {
db := database.GetDB() db := database.GetDB()
var tag string
needRestart := false
result := db.Model(model.Inbound{}).Select("tag").Where("id = ? and enable = ?", id, true).First(&tag)
if result.Error == nil {
s.xrayApi.Init(p.GetAPIPort())
err1 := s.xrayApi.DelInbound(tag)
if err1 == nil {
logger.Debug("Inbound deleted by api:", tag)
} else {
logger.Debug("Unable to delete inbound by api:", err1)
needRestart = true
}
s.xrayApi.Close()
} else {
logger.Debug("No enabled inbound founded to removing by api", tag)
}
// Delete client traffics of inbounds
err := db.Where("inbound_id = ?", id).Delete(xray.ClientTraffic{}).Error err := db.Where("inbound_id = ?", id).Delete(xray.ClientTraffic{}).Error
if err != nil { if err != nil {
return err return false, err
} }
inbound, err := s.GetInbound(id) inbound, err := s.GetInbound(id)
if err != nil { if err != nil {
return err return false, err
} }
clients, err := s.GetClients(inbound) clients, err := s.GetClients(inbound)
if err != nil { if err != nil {
return err return false, err
} }
for _, client := range clients { for _, client := range clients {
err := s.DelClientIPs(db, client.Email) err := s.DelClientIPs(db, client.Email)
if err != nil { if err != nil {
return err return false, err
} }
} }
return db.Delete(model.Inbound{}, id).Error
return needRestart, db.Delete(model.Inbound{}, id).Error
} }
func (s *InboundService) GetInbound(id int) (*model.Inbound, error) { func (s *InboundService) GetInbound(id int) (*model.Inbound, error) {
@@ -231,19 +279,27 @@ func (s *InboundService) GetInbound(id int) (*model.Inbound, error) {
return inbound, nil return inbound, nil
} }
func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound, error) { func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound, bool, error) {
exist, err := s.checkPortExist(inbound.Port, inbound.Id) exist, err := s.checkPortExist(inbound.Port, inbound.Id)
if err != nil { if err != nil {
return inbound, err return inbound, false, err
} }
if exist { if exist {
return inbound, common.NewError("Port already exists:", inbound.Port) return inbound, false, common.NewError("Port already exists:", inbound.Port)
} }
oldInbound, err := s.GetInbound(inbound.Id) oldInbound, err := s.GetInbound(inbound.Id)
if err != nil { if err != nil {
return inbound, err return inbound, false, err
} }
tag := oldInbound.Tag
err = s.updateClientTraffics(oldInbound, inbound)
if err != nil {
return inbound, false, err
}
oldInbound.Up = inbound.Up oldInbound.Up = inbound.Up
oldInbound.Down = inbound.Down oldInbound.Down = inbound.Down
oldInbound.Total = inbound.Total oldInbound.Total = inbound.Total
@@ -258,40 +314,118 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
oldInbound.Sniffing = inbound.Sniffing oldInbound.Sniffing = inbound.Sniffing
oldInbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port) oldInbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
needRestart := false
s.xrayApi.Init(p.GetAPIPort())
if s.xrayApi.DelInbound(tag) == nil {
logger.Debug("Old inbound deleted by api:", tag)
}
if inbound.Enable {
inboundJson, err2 := json.MarshalIndent(oldInbound.GenXrayInboundConfig(), "", " ")
if err2 != nil {
logger.Debug("Unable to marshal updated inbound config:", err2)
needRestart = true
} else {
err2 = s.xrayApi.AddInbound(inboundJson)
if err2 == nil {
logger.Debug("Updated inbound added by api:", oldInbound.Tag)
} else {
logger.Debug("Unable to update inbound by api:", err2)
needRestart = true
}
}
}
s.xrayApi.Close()
db := database.GetDB() db := database.GetDB()
return inbound, db.Save(oldInbound).Error return inbound, needRestart, db.Save(oldInbound).Error
} }
func (s *InboundService) AddInboundClient(data *model.Inbound) error { func (s *InboundService) updateClientTraffics(oldInbound *model.Inbound, newInbound *model.Inbound) error {
clients, err := s.GetClients(data) oldClients, err := s.GetClients(oldInbound)
if err != nil { if err != nil {
return err 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{} var settings map[string]interface{}
err = json.Unmarshal([]byte(data.Settings), &settings) err = json.Unmarshal([]byte(data.Settings), &settings)
if err != nil { if err != nil {
return err return false, err
} }
interfaceClients := settings["clients"].([]interface{}) interfaceClients := settings["clients"].([]interface{})
existEmail, err := s.checkEmailsExistForClients(clients) existEmail, err := s.checkEmailsExistForClients(clients)
if err != nil { if err != nil {
return err return false, err
} }
if existEmail != "" { if existEmail != "" {
return common.NewError("Duplicate email:", existEmail) return false, common.NewError("Duplicate email:", existEmail)
} }
oldInbound, err := s.GetInbound(data.Id) oldInbound, err := s.GetInbound(data.Id)
if err != nil { if err != nil {
return err return false, err
} }
var oldSettings map[string]interface{} var oldSettings map[string]interface{}
err = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings) err = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings)
if err != nil { if err != nil {
return err return false, err
} }
oldClients := oldSettings["clients"].([]interface{}) oldClients := oldSettings["clients"].([]interface{})
@@ -301,30 +435,65 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) error {
newSettings, err := json.MarshalIndent(oldSettings, "", " ") newSettings, err := json.MarshalIndent(oldSettings, "", " ")
if err != nil { if err != nil {
return err return false, err
} }
oldInbound.Settings = string(newSettings) 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 { for _, client := range clients {
if len(client.Email) > 0 { if len(client.Email) > 0 {
s.AddClientStat(data.Id, &client) s.AddClientStat(tx, data.Id, &client)
if client.Enable {
cipher := ""
if oldInbound.Protocol == "shadowsocks" {
cipher = oldSettings["method"].(string)
}
err1 := s.xrayApi.AddUser(string(oldInbound.Protocol), oldInbound.Tag, map[string]interface{}{
"email": client.Email,
"id": client.ID,
"flow": client.Flow,
"password": client.Password,
"cipher": cipher,
})
if err1 == nil {
logger.Debug("Client added by api:", client.Email)
} else {
logger.Debug("Error in adding client by api:", err1)
needRestart = true
}
}
} else {
needRestart = true
} }
} }
db := database.GetDB() s.xrayApi.Close()
return db.Save(oldInbound).Error
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) oldInbound, err := s.GetInbound(inboundId)
if err != nil { if err != nil {
logger.Error("Load Old Data Error") logger.Error("Load Old Data Error")
return err return false, err
} }
var settings map[string]interface{} var settings map[string]interface{}
err = json.Unmarshal([]byte(oldInbound.Settings), &settings) err = json.Unmarshal([]byte(oldInbound.Settings), &settings)
if err != nil { if err != nil {
return err return false, err
} }
email := "" email := ""
@@ -351,7 +520,7 @@ func (s *InboundService) DelInboundClient(inboundId int, clientId string) error
settings["clients"] = newClients settings["clients"] = newClients
newSettings, err := json.MarshalIndent(settings, "", " ") newSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil { if err != nil {
return err return false, err
} }
oldInbound.Settings = string(newSettings) oldInbound.Settings = string(newSettings)
@@ -360,39 +529,52 @@ func (s *InboundService) DelInboundClient(inboundId int, clientId string) error
err = s.DelClientStat(db, email) err = s.DelClientStat(db, email)
if err != nil { if err != nil {
logger.Error("Delete stats Data Error") logger.Error("Delete stats Data Error")
return err return false, err
} }
err = s.DelClientIPs(db, email) err = s.DelClientIPs(db, email)
if err != nil { if err != nil {
logger.Error("Error in delete client IPs") logger.Error("Error in delete client IPs")
return err return false, err
} }
return db.Save(oldInbound).Error needRestart := false
if len(email) > 0 {
s.xrayApi.Init(p.GetAPIPort())
err1 := s.xrayApi.RemoveUser(oldInbound.Tag, email)
if err1 == nil {
logger.Debug("Client deleted by api:", email)
needRestart = false
} else {
logger.Debug("Unable to del client by api:", err1)
needRestart = true
}
s.xrayApi.Close()
}
return needRestart, db.Save(oldInbound).Error
} }
func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId string) error { func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId string) (bool, error) {
clients, err := s.GetClients(data) clients, err := s.GetClients(data)
if err != nil { if err != nil {
return err return false, err
} }
var settings map[string]interface{} var settings map[string]interface{}
err = json.Unmarshal([]byte(data.Settings), &settings) err = json.Unmarshal([]byte(data.Settings), &settings)
if err != nil { if err != nil {
return err return false, err
} }
inerfaceClients := settings["clients"].([]interface{}) inerfaceClients := settings["clients"].([]interface{})
oldInbound, err := s.GetInbound(data.Id) oldInbound, err := s.GetInbound(data.Id)
if err != nil { if err != nil {
return err return false, err
} }
oldClients, err := s.GetClients(oldInbound) oldClients, err := s.GetClients(oldInbound)
if err != nil { if err != nil {
return err return false, err
} }
oldEmail := "" oldEmail := ""
@@ -416,17 +598,17 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
if len(clients[0].Email) > 0 && clients[0].Email != oldEmail { if len(clients[0].Email) > 0 && clients[0].Email != oldEmail {
existEmail, err := s.checkEmailsExistForClients(clients) existEmail, err := s.checkEmailsExistForClients(clients)
if err != nil { if err != nil {
return err return false, err
} }
if existEmail != "" { if existEmail != "" {
return common.NewError("Duplicate email:", existEmail) return false, common.NewError("Duplicate email:", existEmail)
} }
} }
var oldSettings map[string]interface{} var oldSettings map[string]interface{}
err = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings) err = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings)
if err != nil { if err != nil {
return err return false, err
} }
settingsClients := oldSettings["clients"].([]interface{}) settingsClients := oldSettings["clients"].([]interface{})
settingsClients[clientIndex] = inerfaceClients[0] settingsClients[clientIndex] = inerfaceClients[0]
@@ -434,36 +616,75 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
newSettings, err := json.MarshalIndent(oldSettings, "", " ") newSettings, err := json.MarshalIndent(oldSettings, "", " ")
if err != nil { if err != nil {
return err return false, err
} }
oldInbound.Settings = string(newSettings) oldInbound.Settings = string(newSettings)
db := database.GetDB() db := database.GetDB()
tx := db.Begin()
defer func() {
if err != nil {
tx.Rollback()
} else {
tx.Commit()
}
}()
if len(clients[0].Email) > 0 { if len(clients[0].Email) > 0 {
if len(oldEmail) > 0 { if len(oldEmail) > 0 {
err = s.UpdateClientStat(oldEmail, &clients[0]) err = s.UpdateClientStat(oldEmail, &clients[0])
if err != nil { 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 { if err != nil {
return err return false, err
} }
} else { } else {
s.AddClientStat(data.Id, &clients[0]) s.AddClientStat(tx, data.Id, &clients[0])
} }
} else { } else {
err = s.DelClientStat(db, oldEmail) err = s.DelClientStat(tx, oldEmail)
if err != nil { if err != nil {
return err return false, err
} }
err = s.DelClientIPs(db, oldEmail) err = s.DelClientIPs(tx, oldEmail)
if err != nil { if err != nil {
return err return false, err
} }
} }
return db.Save(oldInbound).Error needRestart := false
if len(oldEmail) > 0 {
s.xrayApi.Init(p.GetAPIPort())
if s.xrayApi.RemoveUser(oldInbound.Tag, oldEmail) == nil {
logger.Debug("Old client deleted by api:", clients[0].Email)
}
if clients[0].Enable {
cipher := ""
if oldInbound.Protocol == "shadowsocks" {
cipher = oldSettings["method"].(string)
}
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,
"cipher": cipher,
})
if err1 == nil {
logger.Debug("Client edited by api:", clients[0].Email)
} else {
logger.Debug("Error in adding client by api:", err1)
needRestart = true
}
}
s.xrayApi.Close()
} else {
logger.Debug("Client old email not found")
needRestart = true
}
return needRestart, tx.Save(oldInbound).Error
} }
func (s *InboundService) AddTraffic(traffics []*xray.Traffic) error { func (s *InboundService) AddTraffic(traffics []*xray.Traffic) error {
@@ -489,6 +710,7 @@ func (s *InboundService) AddTraffic(traffics []*xray.Traffic) error {
return err return err
} }
func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err error) { func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err error) {
if len(traffics) == 0 { if len(traffics) == 0 {
return nil return nil
@@ -515,6 +737,11 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
return err return err
} }
// Avoid empty slice error
if len(dbClientTraffics) == 0 {
return nil
}
dbClientTraffics, err = s.adjustTraffics(tx, dbClientTraffics) dbClientTraffics, err = s.adjustTraffics(tx, dbClientTraffics)
if err != nil { if err != nil {
return err return err
@@ -590,26 +817,78 @@ func (s *InboundService) adjustTraffics(tx *gorm.DB, dbClientTraffics []*xray.Cl
return dbClientTraffics, nil return dbClientTraffics, nil
} }
func (s *InboundService) DisableInvalidInbounds() (int64, error) { func (s *InboundService) DisableInvalidInbounds() (bool, int64, error) {
db := database.GetDB() db := database.GetDB()
now := time.Now().Unix() * 1000 now := time.Now().Unix() * 1000
needRestart := false
if p != nil {
var tags []string
err := db.Table("inbounds").
Select("inbounds.tag").
Where("((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ?", now, true).
Scan(&tags).Error
if err != nil {
return false, 0, err
}
s.xrayApi.Init(p.GetAPIPort())
for _, tag := range tags {
err1 := s.xrayApi.DelInbound(tag)
if err == nil {
logger.Debug("Inbound disabled by api:", tag)
} else {
logger.Debug("Error in disabling inbound by api:", err1)
needRestart = true
}
}
s.xrayApi.Close()
}
result := db.Model(model.Inbound{}). result := db.Model(model.Inbound{}).
Where("((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ?", now, true). Where("((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ?", now, true).
Update("enable", false) Update("enable", false)
err := result.Error err := result.Error
count := result.RowsAffected count := result.RowsAffected
return count, err return needRestart, count, err
} }
func (s *InboundService) DisableInvalidClients() (int64, error) { func (s *InboundService) DisableInvalidClients() (bool, int64, error) {
db := database.GetDB() db := database.GetDB()
now := time.Now().Unix() * 1000 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 {
err1 := s.xrayApi.RemoveUser(result.Tag, result.Email)
if err1 == nil {
logger.Debug("Client disabled by api:", result.Email)
} else {
logger.Debug("Error in disabling client by api:", err1)
needRestart = true
}
}
s.xrayApi.Close()
}
result := db.Model(xray.ClientTraffic{}). result := db.Model(xray.ClientTraffic{}).
Where("((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ?", now, true). Where("((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ?", now, true).
Update("enable", false) Update("enable", false)
err := result.Error err := result.Error
count := result.RowsAffected count := result.RowsAffected
return count, err return needRestart, count, err
} }
func (s *InboundService) MigrationRemoveOrphanedTraffics() { func (s *InboundService) MigrationRemoveOrphanedTraffics() {
@@ -624,9 +903,7 @@ func (s *InboundService) MigrationRemoveOrphanedTraffics() {
`) `)
} }
func (s *InboundService) AddClientStat(inboundId int, client *model.Client) error { func (s *InboundService) AddClientStat(tx *gorm.DB, inboundId int, client *model.Client) error {
db := database.GetDB()
clientTraffic := xray.ClientTraffic{} clientTraffic := xray.ClientTraffic{}
clientTraffic.InboundId = inboundId clientTraffic.InboundId = inboundId
clientTraffic.Email = client.Email clientTraffic.Email = client.Email
@@ -635,7 +912,7 @@ func (s *InboundService) AddClientStat(inboundId int, client *model.Client) erro
clientTraffic.Enable = true clientTraffic.Enable = true
clientTraffic.Up = 0 clientTraffic.Up = 0
clientTraffic.Down = 0 clientTraffic.Down = 0
result := db.Create(&clientTraffic) result := tx.Create(&clientTraffic)
err := result.Error err := result.Error
if err != nil { if err != nil {
return err return err
@@ -779,7 +1056,11 @@ func (s *InboundService) SetClientTelegramUserID(trafficId int, tgId string) err
return err return err
} }
inbound.Settings = string(modifiedSettings) inbound.Settings = string(modifiedSettings)
return s.UpdateInboundClient(inbound, clientId) _, err = s.UpdateInboundClient(inbound, clientId)
if err != nil {
return err
}
return nil
} }
func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, error) { func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, error) {
@@ -835,7 +1116,13 @@ func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, er
return false, err return false, err
} }
inbound.Settings = string(modifiedSettings) 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 { func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int) error {
@@ -889,7 +1176,11 @@ func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int
return err return err
} }
inbound.Settings = string(modifiedSettings) 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 { func (s *InboundService) ResetClientExpiryTimeByEmail(clientEmail string, expiry_time int64) error {
@@ -943,7 +1234,11 @@ func (s *InboundService) ResetClientExpiryTimeByEmail(clientEmail string, expiry
return err return err
} }
inbound.Settings = string(modifiedSettings) 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 { func (s *InboundService) ResetClientTrafficByEmail(clientEmail string) error {
@@ -954,26 +1249,71 @@ func (s *InboundService) ResetClientTrafficByEmail(clientEmail string) error {
Updates(map[string]interface{}{"enable": true, "up": 0, "down": 0}) Updates(map[string]interface{}{"enable": true, "up": 0, "down": 0})
err := result.Error err := result.Error
if err != nil { if err != nil {
return err return err
} }
return nil return nil
} }
func (s *InboundService) ResetClientTraffic(id int, clientEmail string) error { func (s *InboundService) ResetClientTraffic(id int, clientEmail string) (bool, error) {
db := database.GetDB() needRestart := false
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
traffic, err := s.GetClientTrafficByEmail(clientEmail)
if err != nil { 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())
cipher := ""
if string(inbound.Protocol) == "shadowsocks" {
var oldSettings map[string]interface{}
err = json.Unmarshal([]byte(inbound.Settings), &oldSettings)
if err != nil {
return false, err
}
cipher = oldSettings["method"].(string)
}
err1 := s.xrayApi.AddUser(string(inbound.Protocol), inbound.Tag, map[string]interface{}{
"email": client.Email,
"id": client.ID,
"flow": client.Flow,
"password": client.Password,
"cipher": cipher,
})
if err1 == nil {
logger.Debug("Client enabled due to reset traffic:", clientEmail)
} else {
logger.Debug("Error in enabling client by api:", err1)
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 { func (s *InboundService) ResetAllClientTraffics(id int) error {
@@ -1185,6 +1525,7 @@ func (s *InboundService) GetInboundClientIps(clientEmail string) (string, error)
} }
return InboundClientIps.Ips, nil return InboundClientIps.Ips, nil
} }
func (s *InboundService) ClearClientIps(clientEmail string) error { func (s *InboundService) ClearClientIps(clientEmail string) error {
db := database.GetDB() db := database.GetDB()
@@ -1192,7 +1533,6 @@ func (s *InboundService) ClearClientIps(clientEmail string) error {
Where("client_email = ?", clientEmail). Where("client_email = ?", clientEmail).
Update("ips", "") Update("ips", "")
err := result.Error err := result.Error
if err != nil { if err != nil {
return err return err
} }
@@ -1211,10 +1551,19 @@ func (s *InboundService) SearchInbounds(query string) ([]*model.Inbound, error)
func (s *InboundService) MigrationRequirements() { func (s *InboundService) MigrationRequirements() {
db := database.GetDB() db := database.GetDB()
tx := db.Begin()
var err error
defer func() {
if err == nil {
tx.Commit()
} else {
tx.Rollback()
}
}()
// Fix inbounds based problems // Fix inbounds based problems
var inbounds []*model.Inbound 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 { if err != nil && err != gorm.ErrRecordNotFound {
return return
} }
@@ -1249,6 +1598,7 @@ func (s *InboundService) MigrationRequirements() {
inbounds[inbound_index].Settings = string(modifiedSettings) inbounds[inbound_index].Settings = string(modifiedSettings)
} }
// Add client traffic row for all clients which has email // 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 { if err != nil {
@@ -1257,17 +1607,17 @@ func (s *InboundService) MigrationRequirements() {
for _, modelClient := range modelClients { for _, modelClient := range modelClients {
if len(modelClient.Email) > 0 { if len(modelClient.Email) > 0 {
var count int64 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 { 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 // Remove orphaned traffics
db.Where("inbound_id = 0").Delete(xray.ClientTraffic{}) tx.Where("inbound_id = 0").Delete(xray.ClientTraffic{})
} }
func (s *InboundService) MigrateDB() { func (s *InboundService) MigrateDB() {

View File

@@ -12,8 +12,10 @@ import (
"os" "os"
"os/exec" "os/exec"
"runtime" "runtime"
"strconv"
"strings" "strings"
"time" "time"
"x-ui/config" "x-ui/config"
"x-ui/database" "x-ui/database"
"x-ui/logger" "x-ui/logger"
@@ -86,31 +88,24 @@ type ServerService struct {
inboundService InboundService inboundService InboundService
} }
const DebugMode = false // Set to true during development
func getPublicIP(url string) string { func getPublicIP(url string) string {
resp, err := http.Get(url) resp, err := http.Get(url)
if err != nil { if err != nil {
if DebugMode {
logger.Warning("get public IP failed:", err)
}
return "N/A" return "N/A"
} }
defer resp.Body.Close() defer resp.Body.Close()
ip, err := io.ReadAll(resp.Body) ip, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
if DebugMode {
logger.Warning("read public IP failed:", err)
}
return "N/A" return "N/A"
} }
if string(ip) == "" { ipString := string(ip)
return "N/A" // default value if ipString == "" {
return "N/A"
} }
return string(ip) return ipString
} }
func (s *ServerService) GetStatus(lastStatus *Status) *Status { func (s *ServerService) GetStatus(lastStatus *Status) *Status {
@@ -230,7 +225,7 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
} }
func (s *ServerService) GetXrayVersions() ([]string, error) { func (s *ServerService) GetXrayVersions() ([]string, error) {
url := "https://api.github.com/repos/MHSanaei/Xray-core/releases" url := "https://api.github.com/repos/XTLS/Xray-core/releases"
resp, err := http.Get(url) resp, err := http.Get(url)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -249,9 +244,11 @@ func (s *ServerService) GetXrayVersions() ([]string, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
versions := make([]string, 0, len(releases)) var versions []string
for _, release := range releases { for _, release := range releases {
versions = append(versions, release.TagName) if release.TagName >= "v1.7.5" {
versions = append(versions, release.TagName)
}
} }
return versions, nil return versions, nil
} }
@@ -297,7 +294,7 @@ func (s *ServerService) downloadXRay(version string) (string, error) {
} }
fileName := fmt.Sprintf("Xray-%s-%s.zip", osName, arch) fileName := fmt.Sprintf("Xray-%s-%s.zip", osName, arch)
url := fmt.Sprintf("https://github.com/MHSanaei/Xray-core/releases/download/%s/%s", version, fileName) url := fmt.Sprintf("https://github.com/XTLS/Xray-core/releases/download/%s/%s", version, fileName)
resp, err := http.Get(url) resp, err := http.Get(url)
if err != nil { if err != nil {
return "", err return "", err
@@ -378,54 +375,44 @@ func (s *ServerService) UpdateXray(version string) error {
if err != nil { if err != nil {
return err return err
} }
err = copyZipFile("iran.dat", xray.GetIranPath())
if err != nil {
return err
}
return nil return nil
} }
func (s *ServerService) GetLogs(count string) ([]string, error) { func (s *ServerService) GetLogs(count string, level string, syslog string) []string {
// Define the journalctl command and its arguments c, _ := strconv.Atoi(count)
var cmdArgs []string var lines []string
if runtime.GOOS == "linux" {
cmdArgs = []string{"journalctl", "-u", "x-ui", "--no-pager", "-n", count} if syslog == "true" {
cmdArgs := []string{"journalctl", "-u", "x-ui", "--no-pager", "-n", count, "-p", level}
// Run the command
cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return []string{"Failed to run journalctl command!"}
}
lines = strings.Split(out.String(), "\n")
} else { } else {
return []string{"Unsupported operating system"}, nil lines = logger.GetLogs(c, level)
} }
// Run the command return lines
cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return nil, err
}
lines := strings.Split(out.String(), "\n")
return lines, nil
} }
func (s *ServerService) GetConfigJson() (interface{}, error) { func (s *ServerService) GetConfigJson() (interface{}, error) {
// Open the file for reading config, err := s.xrayService.GetXrayConfig()
file, err := os.Open(xray.GetConfigPath())
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer file.Close() contents, err := json.MarshalIndent(config, "", " ")
// Read the file contents
fileContents, err := io.ReadAll(file)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var jsonData interface{} var jsonData interface{}
err = json.Unmarshal(fileContents, &jsonData) err = json.Unmarshal(contents, &jsonData)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -24,6 +24,7 @@ var xrayTemplateConfig string
var defaultValueMap = map[string]string{ var defaultValueMap = map[string]string{
"xrayTemplateConfig": xrayTemplateConfig, "xrayTemplateConfig": xrayTemplateConfig,
"webListen": "", "webListen": "",
"webDomain": "",
"webPort": "2053", "webPort": "2053",
"webCertFile": "", "webCertFile": "",
"webKeyFile": "", "webKeyFile": "",
@@ -38,13 +39,14 @@ var defaultValueMap = map[string]string{
"tgBotChatId": "", "tgBotChatId": "",
"tgRunTime": "@daily", "tgRunTime": "@daily",
"tgBotBackup": "false", "tgBotBackup": "false",
"tgBotLoginNotify": "false",
"tgCpu": "0", "tgCpu": "0",
"tgLang": "en-US", "tgLang": "en-US",
"secretEnable": "false", "secretEnable": "false",
"subEnable": "false", "subEnable": "false",
"subListen": "", "subListen": "",
"subPort": "2096", "subPort": "2096",
"subPath": "sub/", "subPath": "/sub/",
"subDomain": "", "subDomain": "",
"subCertFile": "", "subCertFile": "",
"subKeyFile": "", "subKeyFile": "",
@@ -225,6 +227,10 @@ func (s *SettingService) GetListen() (string, error) {
return s.getString("webListen") return s.getString("webListen")
} }
func (s *SettingService) GetWebDomain() (string, error) {
return s.getString("webDomain")
}
func (s *SettingService) GetTgBotToken() (string, error) { func (s *SettingService) GetTgBotToken() (string, error) {
return s.getString("tgBotToken") return s.getString("tgBotToken")
} }
@@ -261,6 +267,10 @@ func (s *SettingService) GetTgBotBackup() (bool, error) {
return s.getBool("tgBotBackup") return s.getBool("tgBotBackup")
} }
func (s *SettingService) GetTgBotLoginNotify() (bool, error) {
return s.getBool("tgBotLoginNotify")
}
func (s *SettingService) GetTgCpu() (int, error) { func (s *SettingService) GetTgCpu() (int, error) {
return s.getInt("tgCpu") return s.getInt("tgCpu")
} }

View File

@@ -77,13 +77,15 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
return err return err
} }
for _, adminId := range strings.Split(tgBotid, ",") { if tgBotid != "" {
id, err := strconv.Atoi(adminId) for _, adminId := range strings.Split(tgBotid, ",") {
if err != nil { id, err := strconv.Atoi(adminId)
logger.Warning("Failed to get IDs from GetTgBotChatId:", err) if err != nil {
return err logger.Warning("Failed to get IDs from GetTgBotChatId:", err)
return err
}
adminIds = append(adminIds, int64(id))
} }
adminIds = append(adminIds, int64(id))
} }
bot, err = telego.NewBot(tgBottoken) bot, err = telego.NewBot(tgBottoken)
@@ -188,7 +190,7 @@ func (t *Tgbot) OnReceive() {
} }
func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin bool) { func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin bool) {
msg := "" msg, onlyMessage := "", false
command, commandArgs := tu.ParseCommand(message.Text) command, commandArgs := tu.ParseCommand(message.Text)
@@ -204,8 +206,13 @@ func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin boo
} }
msg += "\n\n" + t.I18nBot("tgbot.commands.pleaseChoose") msg += "\n\n" + t.I18nBot("tgbot.commands.pleaseChoose")
case "status": case "status":
onlyMessage = true
msg += t.I18nBot("tgbot.commands.status") msg += t.I18nBot("tgbot.commands.status")
case "id":
onlyMessage = true
msg += t.I18nBot("tgbot.commands.getID", "ID=="+strconv.FormatInt(message.From.ID, 10))
case "usage": case "usage":
onlyMessage = true
if len(commandArgs) > 0 { if len(commandArgs) > 0 {
if isAdmin { if isAdmin {
t.searchClient(chatId, commandArgs[0]) t.searchClient(chatId, commandArgs[0])
@@ -216,6 +223,7 @@ func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin boo
msg += t.I18nBot("tgbot.commands.usage") msg += t.I18nBot("tgbot.commands.usage")
} }
case "inbound": case "inbound":
onlyMessage = true
if isAdmin && len(commandArgs) > 0 { if isAdmin && len(commandArgs) > 0 {
t.searchInbound(chatId, commandArgs[0]) t.searchInbound(chatId, commandArgs[0])
} else { } else {
@@ -224,6 +232,11 @@ func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin boo
default: default:
msg += t.I18nBot("tgbot.commands.unknown") msg += t.I18nBot("tgbot.commands.unknown")
} }
if onlyMessage {
t.SendMsgToTgbot(chatId, msg)
return
}
t.SendAnswer(chatId, msg, isAdmin) t.SendAnswer(chatId, msg, isAdmin)
} }
@@ -498,6 +511,7 @@ func (t *Tgbot) SendMsgToTgbot(chatId int64, msg string, replyMarkup ...telego.R
if !isRunning { if !isRunning {
return return
} }
if msg == "" { if msg == "" {
logger.Info("[tgbot] message is empty!") logger.Info("[tgbot] message is empty!")
return return
@@ -629,6 +643,11 @@ func (t *Tgbot) UserLoginNotify(username string, ip string, time string, status
return return
} }
loginNotifyEnabled, err := t.settingService.GetTgBotLoginNotify()
if err != nil || !loginNotifyEnabled {
return
}
msg := "" msg := ""
if status == LoginSuccess { if status == LoginSuccess {
msg += t.I18nBot("tgbot.messages.loginSuccess") msg += t.I18nBot("tgbot.messages.loginSuccess")
@@ -659,9 +678,9 @@ func (t *Tgbot) getInboundUsages() string {
info += t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic((inbound.Up+inbound.Down)), "Upload=="+common.FormatTraffic(inbound.Up), "Download=="+common.FormatTraffic(inbound.Down)) info += t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic((inbound.Up+inbound.Down)), "Upload=="+common.FormatTraffic(inbound.Up), "Download=="+common.FormatTraffic(inbound.Down))
if inbound.ExpiryTime == 0 { if inbound.ExpiryTime == 0 {
info += t.I18nBot("tgbot.messages.expire", "DateTime=="+t.I18nBot("tgbot.unlimited")) info += t.I18nBot("tgbot.messages.expire", "Time=="+t.I18nBot("tgbot.unlimited"))
} else { } else {
info += t.I18nBot("tgbot.messages.expire", "DateTime=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05")) info += t.I18nBot("tgbot.messages.expire", "Time=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
} }
} }
} }
@@ -697,14 +716,21 @@ func (t *Tgbot) getClientUsage(chatId int64, tgUserName string, tgUserID string)
return return
} }
now := time.Now().Unix()
for _, traffic := range traffics { for _, traffic := range traffics {
expiryTime := "" expiryTime := ""
flag := false
diff := traffic.ExpiryTime/1000 - now
if traffic.ExpiryTime == 0 { if traffic.ExpiryTime == 0 {
expiryTime = t.I18nBot("tgbot.unlimited") expiryTime = t.I18nBot("tgbot.unlimited")
} else if diff > 172800 || !traffic.Enable {
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
} else if traffic.ExpiryTime < 0 { } else if traffic.ExpiryTime < 0 {
expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days")) expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
flag = true
} else { } else {
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05") expiryTime = fmt.Sprintf("%d %s", diff/3600, t.I18nBot("tgbot.hours"))
flag = true
} }
total := "" total := ""
@@ -715,13 +741,22 @@ func (t *Tgbot) getClientUsage(chatId int64, tgUserName string, tgUserID string)
} }
output := "" output := ""
output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
output += t.I18nBot("tgbot.messages.active", "Enable=="+strconv.FormatBool(traffic.Enable))
output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email) output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
if traffic.Enable {
output += t.I18nBot("tgbot.messages.active")
if flag {
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
} else {
output += t.I18nBot("tgbot.messages.expire", "Time=="+expiryTime)
}
} else {
output += t.I18nBot("tgbot.messages.inactive")
output += t.I18nBot("tgbot.messages.expire", "Time=="+expiryTime)
}
output += t.I18nBot("tgbot.messages.upload", "Upload=="+common.FormatTraffic(traffic.Up)) output += t.I18nBot("tgbot.messages.upload", "Upload=="+common.FormatTraffic(traffic.Up))
output += t.I18nBot("tgbot.messages.download", "Download=="+common.FormatTraffic(traffic.Down)) output += t.I18nBot("tgbot.messages.download", "Download=="+common.FormatTraffic(traffic.Down))
output += t.I18nBot("tgbot.messages.total", "UpDown=="+common.FormatTraffic((traffic.Up+traffic.Down)), "Total=="+total) output += t.I18nBot("tgbot.messages.total", "UpDown=="+common.FormatTraffic((traffic.Up+traffic.Down)), "Total=="+total)
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime) output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
t.SendMsgToTgbot(chatId, output) t.SendMsgToTgbot(chatId, output)
} }
@@ -775,6 +810,7 @@ func (t *Tgbot) clientTelegramUserInfo(chatId int64, email string, messageID ...
output := "" output := ""
output += t.I18nBot("tgbot.messages.email", "Email=="+email) output += t.I18nBot("tgbot.messages.email", "Email=="+email)
output += t.I18nBot("tgbot.messages.TGUser", "TelegramID=="+tgId) output += t.I18nBot("tgbot.messages.TGUser", "TelegramID=="+tgId)
output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
inlineKeyboard := tu.InlineKeyboard( inlineKeyboard := tu.InlineKeyboard(
tu.InlineKeyboardRow( tu.InlineKeyboardRow(
@@ -791,7 +827,7 @@ func (t *Tgbot) clientTelegramUserInfo(chatId int64, email string, messageID ...
t.SendMsgToTgbot(chatId, output, inlineKeyboard) t.SendMsgToTgbot(chatId, output, inlineKeyboard)
requestUser := telego.KeyboardButtonRequestUser{ requestUser := telego.KeyboardButtonRequestUser{
RequestID: int32(traffic.Id), RequestID: int32(traffic.Id),
UserIsBot: false, UserIsBot: new(bool),
} }
keyboard := tu.Keyboard( keyboard := tu.Keyboard(
tu.KeyboardRow( tu.KeyboardRow(
@@ -819,13 +855,20 @@ func (t *Tgbot) searchClient(chatId int64, email string, messageID ...int) {
return return
} }
now := time.Now().Unix()
expiryTime := "" expiryTime := ""
flag := false
diff := traffic.ExpiryTime/1000 - now
if traffic.ExpiryTime == 0 { if traffic.ExpiryTime == 0 {
expiryTime = t.I18nBot("tgbot.unlimited") expiryTime = t.I18nBot("tgbot.unlimited")
} else if diff > 172800 || !traffic.Enable {
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
} else if traffic.ExpiryTime < 0 { } else if traffic.ExpiryTime < 0 {
expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days")) expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
flag = true
} else { } else {
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05") expiryTime = fmt.Sprintf("%d %s", diff/3600, t.I18nBot("tgbot.hours"))
flag = true
} }
total := "" total := ""
@@ -836,13 +879,22 @@ func (t *Tgbot) searchClient(chatId int64, email string, messageID ...int) {
} }
output := "" output := ""
output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
output += t.I18nBot("tgbot.messages.active", "Enable=="+strconv.FormatBool(traffic.Enable))
output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email) output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
if traffic.Enable {
output += t.I18nBot("tgbot.messages.active")
if flag {
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
} else {
output += t.I18nBot("tgbot.messages.expire", "Time=="+expiryTime)
}
} else {
output += t.I18nBot("tgbot.messages.inactive")
output += t.I18nBot("tgbot.messages.expire", "Time=="+expiryTime)
}
output += t.I18nBot("tgbot.messages.upload", "Upload=="+common.FormatTraffic(traffic.Up)) output += t.I18nBot("tgbot.messages.upload", "Upload=="+common.FormatTraffic(traffic.Up))
output += t.I18nBot("tgbot.messages.download", "Download=="+common.FormatTraffic(traffic.Down)) output += t.I18nBot("tgbot.messages.download", "Download=="+common.FormatTraffic(traffic.Down))
output += t.I18nBot("tgbot.messages.total", "UpDown=="+common.FormatTraffic((traffic.Up+traffic.Down)), "Total=="+total) output += t.I18nBot("tgbot.messages.total", "UpDown=="+common.FormatTraffic((traffic.Up+traffic.Down)), "Total=="+total)
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime) output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
inlineKeyboard := tu.InlineKeyboard( inlineKeyboard := tu.InlineKeyboard(
tu.InlineKeyboardRow( tu.InlineKeyboardRow(
@@ -887,6 +939,7 @@ func (t *Tgbot) searchInbound(chatId int64, remark string) {
return return
} }
now := time.Now().Unix()
for _, inbound := range inbouds { for _, inbound := range inbouds {
info := "" info := ""
info += t.I18nBot("tgbot.messages.inbound", "Remark=="+inbound.Remark) info += t.I18nBot("tgbot.messages.inbound", "Remark=="+inbound.Remark)
@@ -894,20 +947,26 @@ func (t *Tgbot) searchInbound(chatId int64, remark string) {
info += t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic((inbound.Up+inbound.Down)), "Upload=="+common.FormatTraffic(inbound.Up), "Download=="+common.FormatTraffic(inbound.Down)) info += t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic((inbound.Up+inbound.Down)), "Upload=="+common.FormatTraffic(inbound.Up), "Download=="+common.FormatTraffic(inbound.Down))
if inbound.ExpiryTime == 0 { if inbound.ExpiryTime == 0 {
info += t.I18nBot("tgbot.messages.expire", "DateTime=="+t.I18nBot("tgbot.unlimited")) info += t.I18nBot("tgbot.messages.expire", "Time=="+t.I18nBot("tgbot.unlimited"))
} else { } else {
info += t.I18nBot("tgbot.messages.expire", "DateTime=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05")) info += t.I18nBot("tgbot.messages.expire", "Time=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
} }
t.SendMsgToTgbot(chatId, info) t.SendMsgToTgbot(chatId, info)
for _, traffic := range inbound.ClientStats { for _, traffic := range inbound.ClientStats {
expiryTime := "" expiryTime := ""
flag := false
diff := traffic.ExpiryTime/1000 - now
if traffic.ExpiryTime == 0 { if traffic.ExpiryTime == 0 {
expiryTime = t.I18nBot("tgbot.unlimited") expiryTime = t.I18nBot("tgbot.unlimited")
} else if diff > 172800 || !traffic.Enable {
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
} else if traffic.ExpiryTime < 0 { } else if traffic.ExpiryTime < 0 {
expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days")) expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
flag = true
} else { } else {
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05") expiryTime = fmt.Sprintf("%d %s", diff/3600, t.I18nBot("tgbot.hours"))
flag = true
} }
total := "" total := ""
@@ -918,13 +977,22 @@ func (t *Tgbot) searchInbound(chatId int64, remark string) {
} }
output := "" output := ""
output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
output += t.I18nBot("tgbot.messages.active", "Enable=="+strconv.FormatBool(traffic.Enable))
output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email) output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
if traffic.Enable {
output += t.I18nBot("tgbot.messages.active")
if flag {
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
} else {
output += t.I18nBot("tgbot.messages.expire", "Time=="+expiryTime)
}
} else {
output += t.I18nBot("tgbot.messages.inactive")
output += t.I18nBot("tgbot.messages.expire", "Time=="+expiryTime)
}
output += t.I18nBot("tgbot.messages.upload", "Upload=="+common.FormatTraffic(traffic.Up)) output += t.I18nBot("tgbot.messages.upload", "Upload=="+common.FormatTraffic(traffic.Up))
output += t.I18nBot("tgbot.messages.download", "Download=="+common.FormatTraffic(traffic.Down)) output += t.I18nBot("tgbot.messages.download", "Download=="+common.FormatTraffic(traffic.Down))
output += t.I18nBot("tgbot.messages.total", "UpDown=="+common.FormatTraffic((traffic.Up+traffic.Down)), "Total=="+total) output += t.I18nBot("tgbot.messages.total", "UpDown=="+common.FormatTraffic((traffic.Up+traffic.Down)), "Total=="+total)
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime) output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
t.SendMsgToTgbot(chatId, output) t.SendMsgToTgbot(chatId, output)
} }
@@ -945,13 +1013,20 @@ func (t *Tgbot) searchForClient(chatId int64, query string) {
return return
} }
now := time.Now().Unix()
expiryTime := "" expiryTime := ""
flag := false
diff := traffic.ExpiryTime/1000 - now
if traffic.ExpiryTime == 0 { if traffic.ExpiryTime == 0 {
expiryTime = t.I18nBot("tgbot.unlimited") expiryTime = t.I18nBot("tgbot.unlimited")
} else if diff > 172800 || !traffic.Enable {
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
} else if traffic.ExpiryTime < 0 { } else if traffic.ExpiryTime < 0 {
expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days")) expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
flag = true
} else { } else {
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05") expiryTime = fmt.Sprintf("%d %s", diff/3600, t.I18nBot("tgbot.hours"))
flag = true
} }
total := "" total := ""
@@ -962,13 +1037,22 @@ func (t *Tgbot) searchForClient(chatId int64, query string) {
} }
output := "" output := ""
output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
output += t.I18nBot("tgbot.messages.active", "Enable=="+strconv.FormatBool(traffic.Enable))
output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email) output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
if traffic.Enable {
output += t.I18nBot("tgbot.messages.active")
if flag {
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
} else {
output += t.I18nBot("tgbot.messages.expire", "Time=="+expiryTime)
}
} else {
output += t.I18nBot("tgbot.messages.inactive")
output += t.I18nBot("tgbot.messages.expire", "Time=="+expiryTime)
}
output += t.I18nBot("tgbot.messages.upload", "Upload=="+common.FormatTraffic(traffic.Up)) output += t.I18nBot("tgbot.messages.upload", "Upload=="+common.FormatTraffic(traffic.Up))
output += t.I18nBot("tgbot.messages.download", "Download=="+common.FormatTraffic(traffic.Down)) output += t.I18nBot("tgbot.messages.download", "Download=="+common.FormatTraffic(traffic.Down))
output += t.I18nBot("tgbot.messages.total", "UpDown=="+common.FormatTraffic((traffic.Up+traffic.Down)), "Total=="+total) output += t.I18nBot("tgbot.messages.total", "UpDown=="+common.FormatTraffic((traffic.Up+traffic.Down)), "Total=="+total)
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime) output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
t.SendMsgToTgbot(chatId, output) t.SendMsgToTgbot(chatId, output)
} }
@@ -1033,9 +1117,9 @@ func (t *Tgbot) getExhausted() string {
output += t.I18nBot("tgbot.messages.port", "Port=="+strconv.Itoa(inbound.Port)) output += t.I18nBot("tgbot.messages.port", "Port=="+strconv.Itoa(inbound.Port))
output += t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic((inbound.Up+inbound.Down)), "Upload=="+common.FormatTraffic(inbound.Up), "Download=="+common.FormatTraffic(inbound.Down)) output += t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic((inbound.Up+inbound.Down)), "Upload=="+common.FormatTraffic(inbound.Up), "Download=="+common.FormatTraffic(inbound.Down))
if inbound.ExpiryTime == 0 { if inbound.ExpiryTime == 0 {
output += t.I18nBot("tgbot.messages.expire", "DateTime=="+t.I18nBot("tgbot.unlimited")) output += t.I18nBot("tgbot.messages.expire", "Time=="+t.I18nBot("tgbot.unlimited"))
} else { } else {
output += t.I18nBot("tgbot.messages.expire", "DateTime=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05")) output += t.I18nBot("tgbot.messages.expire", "Time=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
} }
output += "\r\n \r\n" output += "\r\n \r\n"
} }
@@ -1052,12 +1136,18 @@ func (t *Tgbot) getExhausted() string {
for _, traffic := range exhaustedClients { for _, traffic := range exhaustedClients {
expiryTime := "" expiryTime := ""
flag := false
diff := (traffic.ExpiryTime - now) / 1000
if traffic.ExpiryTime == 0 { if traffic.ExpiryTime == 0 {
expiryTime = t.I18nBot("tgbot.unlimited") expiryTime = t.I18nBot("tgbot.unlimited")
} else if traffic.ExpiryTime < 0 { } else if diff > 172800 || !traffic.Enable {
expiryTime += fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
} else {
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05") expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
} else if traffic.ExpiryTime < 0 {
expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
flag = true
} else {
expiryTime = fmt.Sprintf("%d %s", diff/3600, t.I18nBot("tgbot.hours"))
flag = true
} }
total := "" total := ""
@@ -1067,13 +1157,22 @@ func (t *Tgbot) getExhausted() string {
total = common.FormatTraffic((traffic.Total)) total = common.FormatTraffic((traffic.Total))
} }
output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
output += t.I18nBot("tgbot.messages.active", "Enable=="+strconv.FormatBool(traffic.Enable))
output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email) output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
if traffic.Enable {
output += t.I18nBot("tgbot.messages.active")
if flag {
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
} else {
output += t.I18nBot("tgbot.messages.expire", "Time=="+expiryTime)
}
} else {
output += t.I18nBot("tgbot.messages.inactive")
output += t.I18nBot("tgbot.messages.expire", "Time=="+expiryTime)
}
output += t.I18nBot("tgbot.messages.upload", "Upload=="+common.FormatTraffic(traffic.Up)) output += t.I18nBot("tgbot.messages.upload", "Upload=="+common.FormatTraffic(traffic.Up))
output += t.I18nBot("tgbot.messages.download", "Download=="+common.FormatTraffic(traffic.Down)) output += t.I18nBot("tgbot.messages.download", "Download=="+common.FormatTraffic(traffic.Down))
output += t.I18nBot("tgbot.messages.total", "UpDown=="+common.FormatTraffic((traffic.Up+traffic.Down)), "Total=="+total) output += t.I18nBot("tgbot.messages.total", "UpDown=="+common.FormatTraffic((traffic.Up+traffic.Down)), "Total=="+total)
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime) output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
output += "\r\n \r\n" output += "\r\n \r\n"
} }
} }

View File

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

View File

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

View File

@@ -39,7 +39,6 @@
"depleted" = "Depleted" "depleted" = "Depleted"
"depletingSoon" = "Depleting soon" "depletingSoon" = "Depleting soon"
"domainName" = "Domain name" "domainName" = "Domain name"
"additional" = "Alter"
"monitor" = "Listening IP" "monitor" = "Listening IP"
"certificate" = "Certificate" "certificate" = "Certificate"
"fail" = "Fail" "fail" = "Fail"
@@ -49,6 +48,7 @@
"clients" = "Clients" "clients" = "Clients"
"usage" = "Usage" "usage" = "Usage"
"secretToken" = "Secret Token" "secretToken" = "Secret Token"
"remained" = "Remained"
[menu] [menu]
"dashboard" = "System Status" "dashboard" = "System Status"
@@ -72,7 +72,7 @@
"title" = "System Status" "title" = "System Status"
"memory" = "Memory" "memory" = "Memory"
"hard" = "Hard Disk" "hard" = "Hard Disk"
"xrayStatus" = "Xray Status" "xrayStatus" = "Status"
"stopXray" = "Stop" "stopXray" = "Stop"
"restartXray" = "Restart" "restartXray" = "Restart"
"xraySwitch" = "SwitchV" "xraySwitch" = "SwitchV"
@@ -168,7 +168,7 @@
"setDefaultCert" = "Set cert from panel" "setDefaultCert" = "Set cert from panel"
"xtlsDesc" = "Xray core needs to be 1.7.5" "xtlsDesc" = "Xray core needs to be 1.7.5"
"realityDesc" = "Xray core needs to be 1.8.0 or higher." "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" "subscriptionDesc" = "you can find your sub link on Details, also you can use the same name for several configurations"
[pages.client] [pages.client]
@@ -221,6 +221,8 @@
"TGBotSettings" = "Telegram Bot Settings" "TGBotSettings" = "Telegram Bot Settings"
"panelListeningIP" = "Panel Listening IP" "panelListeningIP" = "Panel Listening IP"
"panelListeningIPDesc" = "Leave blank by default to monitor all IPs." "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" "panelPort" = "Panel Port"
"panelPortDesc" = "The port used to display this panel" "panelPortDesc" = "The port used to display this panel"
"publicKeyPath" = "Panel Certificate Public Key File Path" "publicKeyPath" = "Panel Certificate Public Key File Path"
@@ -238,11 +240,13 @@
"telegramToken" = "Telegram Token" "telegramToken" = "Telegram Token"
"telegramTokenDesc" = "You must get the token from the manager of Telegram bots @botfather" "telegramTokenDesc" = "You must get the token from the manager of Telegram bots @botfather"
"telegramChatId" = "Telegram Admin Chat IDs" "telegramChatId" = "Telegram Admin Chat IDs"
"telegramChatIdDesc" = "Multiple Chat IDs separated by comma. use @userinfobot to get your Chat IDs." "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" "telegramNotifyTime" = "Telegram bot notification time"
"telegramNotifyTimeDesc" = "Use Crontab timing format." "telegramNotifyTimeDesc" = "Use Crontab timing format."
"tgNotifyBackup" = "Database Backup" "tgNotifyBackup" = "Database Backup"
"tgNotifyBackupDesc" = "Include database backup file with report notification." "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" "sessionMaxAge" = "Session maximum age"
"sessionMaxAgeDesc" = "The duration of a login session (unit: minute)" "sessionMaxAgeDesc" = "The duration of a login session (unit: minute)"
"expireTimeDiff" = "Expiration threshold for notification" "expireTimeDiff" = "Expiration threshold for notification"
@@ -384,6 +388,7 @@
"months" = "Months" "months" = "Months"
"day" = "Day" "day" = "Day"
"days" = "Days" "days" = "Days"
"hours" = "Hours"
"unknown" = "Unknown" "unknown" = "Unknown"
"inbounds" = "Inbounds" "inbounds" = "Inbounds"
"clients" = "Clients" "clients" = "Clients"
@@ -396,6 +401,7 @@
"welcome" = "🤖 Welcome to <b>{{ .Hostname }}</b> management bot.\r\n" "welcome" = "🤖 Welcome to <b>{{ .Hostname }}</b> management bot.\r\n"
"status" = "✅ Bot is ok!" "status" = "✅ Bot is ok!"
"usage" = "❗ Please provide a text to search!" "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>" "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." "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."
@@ -424,20 +430,21 @@
"time" = "⏰ Time: {{ .Time }}\r\n" "time" = "⏰ Time: {{ .Time }}\r\n"
"inbound" = "📍 Inbound: {{ .Remark }}\r\n" "inbound" = "📍 Inbound: {{ .Remark }}\r\n"
"port" = "🔌 Port: {{ .Port }}\r\n" "port" = "🔌 Port: {{ .Port }}\r\n"
"expire" = "📅 Expire Date: {{ .DateTime }}\r\n \r\n" "expire" = "📅 Expire Date: {{ .Time }}\r\n"
"expireIn" = "📅 Expire In: {{ .Time }}\r\n \r\n" "expireIn" = "📅 Expire In: {{ .Time }}\r\n"
"active" = "💡 Active: {{ .Enable }}\r\n" "active" = "💡 Active: ✅ Yes\r\n"
"inactive" = "💡 Active: ❌ No\r\n"
"email" = "📧 Email: {{ .Email }}\r\n" "email" = "📧 Email: {{ .Email }}\r\n"
"upload" = "🔼 Upload: {{ .Upload }}\r\n" "upload" = "🔼 Upload: {{ .Upload }}\r\n"
"download" = "🔽 Download: {{ .Download }}\r\n" "download" = "🔽 Download: {{ .Download }}\r\n"
"total" = "🔄 Total: {{ .UpDown }} / {{ .Total }}\r\n" "total" = "📊 Total: ↑↓{{ .UpDown }} / {{ .Total }}\r\n"
"TGUser" = "👤 Telegram User: {{ .TelegramID }}\r\n" "TGUser" = "👤 Telegram User: {{ .TelegramID }}\r\n"
"exhaustedMsg" = "🚨 Exhausted {{ .Type }}:\r\n" "exhaustedMsg" = "🚨 Exhausted {{ .Type }}:\r\n"
"exhaustedCount" = "🚨 Exhausted {{ .Type }} count:\r\n" "exhaustedCount" = "🚨 Exhausted {{ .Type }} count:\r\n"
"disabled" = "🛑 Disabled: {{ .Disabled }}\r\n" "disabled" = "🛑 Disabled: {{ .Disabled }}\r\n"
"depleteSoon" = "🔜 Deplete soon: {{ .Deplete }}\r\n \r\n" "depleteSoon" = "🔜 Deplete soon: {{ .Deplete }}\r\n \r\n"
"backupTime" = "🗄 Backup Time: {{ .Time }}\r\n" "backupTime" = "🗄 Backup Time: {{ .Time }}\r\n"
"refreshedOn" = "🔄🕒 Refreshed On: {{ .Time }}\r\n" "refreshedOn" = "\r\n📋🔄 Refreshed On: {{ .Time }}\r\n \r\n"
[tgbot.buttons] [tgbot.buttons]
"closeKeyboard" = "❌ Close Keyboard" "closeKeyboard" = "❌ Close Keyboard"

View File

@@ -39,7 +39,6 @@
"depleted" = "منقضی" "depleted" = "منقضی"
"depletingSoon" = "در حال انقضا" "depletingSoon" = "در حال انقضا"
"domainName" = "آدرس دامنه" "domainName" = "آدرس دامنه"
"additional" = "آی دی جایگزین"
"monitor" = "آی پی اتصال" "monitor" = "آی پی اتصال"
"certificate" = "گواهی دیجیتال" "certificate" = "گواهی دیجیتال"
"fail" = "خطا" "fail" = "خطا"
@@ -49,6 +48,7 @@
"clients" = "کاربران" "clients" = "کاربران"
"usage" = "استفاده" "usage" = "استفاده"
"secretToken" = "توکن امنیتی" "secretToken" = "توکن امنیتی"
"remained" = "باقیمانده"
[menu] [menu]
"dashboard" = "وضعیت سیستم" "dashboard" = "وضعیت سیستم"
@@ -72,7 +72,7 @@
"title" = "وضعیت سیستم" "title" = "وضعیت سیستم"
"memory" = "حافظه رم" "memory" = "حافظه رم"
"hard" = "حافظه دیسک" "hard" = "حافظه دیسک"
"xrayStatus" = "وضعیت Xray" "xrayStatus" = "وضعیت"
"stopXray" = "توقف" "stopXray" = "توقف"
"restartXray" = "شروع مجدد" "restartXray" = "شروع مجدد"
"xraySwitch" = "تغییر ورژن" "xraySwitch" = "تغییر ورژن"
@@ -168,7 +168,7 @@
"setDefaultCert" = "استفاده از گواهی پنل" "setDefaultCert" = "استفاده از گواهی پنل"
"xtlsDesc" = "هسته Xray باید 1.7.5 باشد" "xtlsDesc" = "هسته Xray باید 1.7.5 باشد"
"realityDesc" = "هسته Xray باید 1.8.0 و بالاتر باشد" "realityDesc" = "هسته Xray باید 1.8.0 و بالاتر باشد"
"telegramDesc" = "از آیدی تلگرام بدون @ یا آیدی چت استفاده کنید (می توانید آن را از اینجا دریافت کنید @userinfobot)" "telegramDesc" = "از آیدی تلگرام بدون @ یا آیدی چت استفاده کنید (می توانید آن را از اینجا دریافت کنید @userinfobot یا در ربات دستور '/id' را وارد کنید)"
"subscriptionDesc" = "می توانید ساب لینک خود را در جزئیات پیدا کنید، همچنین می توانید از همین نام برای چندین کانفیگ استفاده کنید" "subscriptionDesc" = "می توانید ساب لینک خود را در جزئیات پیدا کنید، همچنین می توانید از همین نام برای چندین کانفیگ استفاده کنید"
[pages.client] [pages.client]
@@ -221,6 +221,8 @@
"TGBotSettings" = "تنظیمات ربات تلگرام" "TGBotSettings" = "تنظیمات ربات تلگرام"
"panelListeningIP" = "محدودیت آی پی پنل" "panelListeningIP" = "محدودیت آی پی پنل"
"panelListeningIPDesc" = "برای استفاده از تمام آی‌پیها به طور پیش فرض خالی بگذارید" "panelListeningIPDesc" = "برای استفاده از تمام آی‌پیها به طور پیش فرض خالی بگذارید"
"panelListeningDomain" = "محدودیت دامین پنل"
"panelListeningDomainDesc" = "برای استفاده از تمام دامنه‌ها و آی‌پی‌ها به طور پیش فرض خالی بگذارید"
"panelPort" = "پورت پنل" "panelPort" = "پورت پنل"
"panelPortDesc" = "پورت مورد استفاده برای نمایش این پنل" "panelPortDesc" = "پورت مورد استفاده برای نمایش این پنل"
"publicKeyPath" = "مسیر فایل گواهی کلید عمومی پنل" "publicKeyPath" = "مسیر فایل گواهی کلید عمومی پنل"
@@ -238,11 +240,13 @@
"telegramToken" = "توکن تلگرام" "telegramToken" = "توکن تلگرام"
"telegramTokenDesc" = "توکن را باید از مدیر بات های تلگرام دریافت کنید @botfather" "telegramTokenDesc" = "توکن را باید از مدیر بات های تلگرام دریافت کنید @botfather"
"telegramChatId" = "آی دی تلگرام مدیریت" "telegramChatId" = "آی دی تلگرام مدیریت"
"telegramChatIdDesc" = "از @userinfobot برای دریافت شناسه های چت خود استفاده کنید. با استفاده از کاما میتونید چند آی دی را از هم جدا کنید. " "telegramChatIdDesc" = "از @userinfobot یا دستور '/id' در ربات برای دریافت شناسه های چت خود استفاده کنید. با استفاده از کاما میتونید چند آی دی را از هم جدا کنید. "
"telegramNotifyTime" = "مدت زمان نوتیفیکیشن ربات تلگرام" "telegramNotifyTime" = "مدت زمان نوتیفیکیشن ربات تلگرام"
"telegramNotifyTimeDesc" = "از فرمت زمان بندی لینوکس استفاده کنید " "telegramNotifyTimeDesc" = "از فرمت زمان بندی لینوکس استفاده کنید "
"tgNotifyBackup" = "پشتیبان گیری از پایگاه داده" "tgNotifyBackup" = "پشتیبان گیری از پایگاه داده"
"tgNotifyBackupDesc" = "ارسال کپی فایل پایگاه داده به همراه گزارش دوره ای" "tgNotifyBackupDesc" = "ارسال کپی فایل پایگاه داده به همراه گزارش دوره ای"
"tgNotifyLogin" = "اعلان ورود"
"tgNotifyLoginDesc" = "نام کاربری، آدرس ای پی، و زمان وقتی که فردی سعی می‌کند به پنل شما وارد شود نمایش میدهد"
"sessionMaxAge" = "بیشینه زمان جلسه وب" "sessionMaxAge" = "بیشینه زمان جلسه وب"
"sessionMaxAgeDesc" = "بیشینه زمانی که میتوانید لاگین بمانید (واحد: دقیقه)" "sessionMaxAgeDesc" = "بیشینه زمانی که میتوانید لاگین بمانید (واحد: دقیقه)"
"expireTimeDiff" = "آستانه زمان باقی مانده" "expireTimeDiff" = "آستانه زمان باقی مانده"
@@ -384,6 +388,7 @@
"months" = "ماه‌ها" "months" = "ماه‌ها"
"day" = "روز" "day" = "روز"
"days" = "روزها" "days" = "روزها"
"hours" = "ساعت ها"
"unknown" = "نامشخص" "unknown" = "نامشخص"
"inbounds" = "ورودی‌ها" "inbounds" = "ورودی‌ها"
"clients" = "کلاینت‌ها" "clients" = "کلاینت‌ها"
@@ -396,6 +401,7 @@
"welcome" = "🤖 به ربات مدیریت <b>{{ .Hostname }}</b> خوش آمدید.\r\n" "welcome" = "🤖 به ربات مدیریت <b>{{ .Hostname }}</b> خوش آمدید.\r\n"
"status" = "✅ ربات در حالت عادی است!" "status" = "✅ ربات در حالت عادی است!"
"usage" = "❗ لطفاً یک متن برای جستجو وارد کنید!" "usage" = "❗ لطفاً یک متن برای جستجو وارد کنید!"
"getID" = "🆔 شناسه شما: <code>{{ .ID }}</code>"
"helpAdminCommands" = "برای جستجوی ایمیل مشتری:\r\n<code>/usage [ایمیل]</code>\r\n \r\nبرای جستجوی ورودی‌ها (با آمار مشتری):\r\n<code>/inbound [توضیح]</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 استفاده کنید." "helpClientCommands" = "برای جستجوی آمار، فقط از دستور زیر استفاده کنید:\r\n \r\n<code>/usage [UUID|رمز عبور]</code>\r\n \r\nاز UUID برای vmess/vless و از رمز عبور برای Trojan استفاده کنید."
@@ -424,20 +430,21 @@
"time" = "⏰ زمان: {{ .Time }}\r\n" "time" = "⏰ زمان: {{ .Time }}\r\n"
"inbound" = "📍 ورودی: {{ .Remark }}\r\n" "inbound" = "📍 ورودی: {{ .Remark }}\r\n"
"port" = "🔌 پورت: {{ .Port }}\r\n" "port" = "🔌 پورت: {{ .Port }}\r\n"
"expire" = "📅 تاریخ انقضا: {{ .DateTime }}\r\n \r\n" "expire" = "📅 تاریخ انقضا: {{ .Time }}\r\n"
"expireIn" = "📅 باقیمانده از انقضا: {{ .Time }}\r\n \r\n" "expireIn" = "📅 باقیمانده از انقضا: {{ .Time }}\r\n"
"active" = "💡 فعال: {{ .Enable }}\r\n" "active" = "💡 فعال: \r\n"
"inactive" = "💡 فعال: ❌\r\n"
"email" = "📧 ایمیل: {{ .Email }}\r\n" "email" = "📧 ایمیل: {{ .Email }}\r\n"
"upload" = "🔼 آپلود↑: {{ .Upload }}\r\n" "upload" = "🔼 آپلود↑: {{ .Upload }}\r\n"
"download" = "🔽 دانلود↓: {{ .Download }}\r\n" "download" = "🔽 دانلود↓: {{ .Download }}\r\n"
"total" = "🔄 کل: {{ .UpDown }} / {{ .Total }}\r\n" "total" = "📊 کل: {{ .UpDown }} / {{ .Total }}\r\n"
"TGUser" = "👤 کاربر تلگرام: {{ .TelegramID }}\r\n" "TGUser" = "👤 کاربر تلگرام: {{ .TelegramID }}\r\n"
"exhaustedMsg" = "🚨 {{ .Type }} به اتمام رسیده است:\r\n" "exhaustedMsg" = "🚨 {{ .Type }} به اتمام رسیده است:\r\n"
"exhaustedCount" = "🚨 تعداد {{ .Type }} به اتمام رسیده:\r\n" "exhaustedCount" = "🚨 تعداد {{ .Type }} به اتمام رسیده:\r\n"
"disabled" = "🛑 غیرفعال: {{ .Disabled }}\r\n" "disabled" = "🛑 غیرفعال: {{ .Disabled }}\r\n"
"depleteSoon" = "🔜 به زودی به پایان خواهد رسید: {{ .Deplete }}\r\n \r\n" "depleteSoon" = "🔜 به زودی به پایان خواهد رسید: {{ .Deplete }}\r\n \r\n"
"backupTime" = "🗄 زمان پشتیبان‌گیری: {{ .Time }}\r\n" "backupTime" = "🗄 زمان پشتیبان‌گیری: {{ .Time }}\r\n"
"refreshedOn" = "🔄🕒 تازه‌سازی شده در: {{ .Time }}\r\n" "refreshedOn" = "\r\n📋🔄 تازه‌سازی شده در: {{ .Time }}\r\n \r\n"
[tgbot.buttons] [tgbot.buttons]
"closeKeyboard" = "❌ بستن کیبورد" "closeKeyboard" = "❌ بستن کیبورد"

View File

@@ -39,7 +39,6 @@
"depleted" = "Исчерпано" "depleted" = "Исчерпано"
"depletingSoon" = "Почти исчерпано" "depletingSoon" = "Почти исчерпано"
"domainName" = "Домен" "domainName" = "Домен"
"additional" = "Дополнительно"
"monitor" = "Порт IP" "monitor" = "Порт IP"
"certificate" = "Сертификат" "certificate" = "Сертификат"
"fail" = "Неудачно" "fail" = "Неудачно"
@@ -49,6 +48,7 @@
"clients" = "Клиенты" "clients" = "Клиенты"
"usage" = "Использование" "usage" = "Использование"
"secretToken" = "Секретный токен" "secretToken" = "Секретный токен"
"remained" = "остались"
[menu] [menu]
"dashboard" = "Статус системы" "dashboard" = "Статус системы"
@@ -72,7 +72,7 @@
"title" = "Статус системы" "title" = "Статус системы"
"memory" = "Память" "memory" = "Память"
"hard" = "Жесткий диск" "hard" = "Жесткий диск"
"xrayStatus" = "Статус Xray" "xrayStatus" = "Статус"
"stopXray" = "Остановить Xray" "stopXray" = "Остановить Xray"
"restartXray" = "Рестарт Xray" "restartXray" = "Рестарт Xray"
"xraySwitch" = "Переключить версию" "xraySwitch" = "Переключить версию"
@@ -168,7 +168,7 @@
"setDefaultCert" = "Установить сертификат с панели" "setDefaultCert" = "Установить сертификат с панели"
"xtlsDesc" = "Версия Xray должна быть не ниже 1.7.5" "xtlsDesc" = "Версия Xray должна быть не ниже 1.7.5"
"realityDesc" = "Версия Xray должна быть не ниже 1.8.0" "realityDesc" = "Версия Xray должна быть не ниже 1.8.0"
"telegramDesc" = "Используйте Telegram ID без @ или ID пользователя (вы можете получить его у @userinfobot)" "telegramDesc" = "Используйте идентификатор Telegram без символа @ или идентификатора чата (можно получить его здесь @userinfobot или использовать команду '/id' в боте)"
"subscriptionDesc" = "Вы можете найти свою ссылку подписки в разделе «Подробнее», также вы можете использовать одно и то же имя для нескольких конфигураций" "subscriptionDesc" = "Вы можете найти свою ссылку подписки в разделе «Подробнее», также вы можете использовать одно и то же имя для нескольких конфигураций"
[pages.client] [pages.client]
@@ -221,6 +221,8 @@
"TGBotSettings" = "Настройки Telegram бота" "TGBotSettings" = "Настройки Telegram бота"
"panelListeningIP" = "IP адрес панели" "panelListeningIP" = "IP адрес панели"
"panelListeningIPDesc" = "Оставьте пустым для подключения с любого IP" "panelListeningIPDesc" = "Оставьте пустым для подключения с любого IP"
"panelListeningDomain" = "Домен прослушивания панели"
"panelListeningDomainDesc" = "По умолчанию оставьте пустым, чтобы отслеживать все домены и IP-адреса"
"panelPort" = "Порт панели" "panelPort" = "Порт панели"
"panelPortDesc" = "Порт, используемый для отображения этой панели" "panelPortDesc" = "Порт, используемый для отображения этой панели"
"publicKeyPath" = "Путь к файлу публичного ключа сертификата панели" "publicKeyPath" = "Путь к файлу публичного ключа сертификата панели"
@@ -238,11 +240,13 @@
"telegramToken" = "Токен Telegram бота" "telegramToken" = "Токен Telegram бота"
"telegramTokenDesc" = "Необходимо получить токен у менеджера ботов Telegram @botfather" "telegramTokenDesc" = "Необходимо получить токен у менеджера ботов Telegram @botfather"
"telegramChatId" = "Telegram ID админа бота" "telegramChatId" = "Telegram ID админа бота"
"telegramChatIdDesc" = "Несколько Telegram ID, разделённых запятой. Используйте @userinfobot, чтобы получить Telegram ID" "telegramChatIdDesc" = "Множественные идентификаторы чата, разделенные запятыми. Чтобы получить свои идентификаторы чатов, используйте @userinfobot или команду '/id' в боте."
"telegramNotifyTime" = "Частота уведомлений бота Telegram" "telegramNotifyTime" = "Частота уведомлений бота Telegram"
"telegramNotifyTimeDesc" = "Используйте формат времени Crontab" "telegramNotifyTimeDesc" = "Используйте формат времени Crontab"
"tgNotifyBackup" = "Резервное копирование базы данных" "tgNotifyBackup" = "Резервное копирование базы данных"
"tgNotifyBackupDesc" = "Включать файл резервной копии базы данных с уведомлением об отчете" "tgNotifyBackupDesc" = "Включать файл резервной копии базы данных с уведомлением об отчете"
"tgNotifyLogin" = "Уведомление о входе"
"tgNotifyLoginDesc" = "Отображает имя пользователя, IP-адрес и время, когда кто-то пытается войти в вашу панель."
"sessionMaxAge" = "Продолжительность сессии" "sessionMaxAge" = "Продолжительность сессии"
"sessionMaxAgeDesc" = "Продолжительность сессии в системе (значение: минута)" "sessionMaxAgeDesc" = "Продолжительность сессии в системе (значение: минута)"
"expireTimeDiff" = "Порог истечения срока сессии для уведомления" "expireTimeDiff" = "Порог истечения срока сессии для уведомления"
@@ -384,6 +388,7 @@
"months" = "Месяцев" "months" = "Месяцев"
"day" = "День" "day" = "День"
"days" = "Дней" "days" = "Дней"
"hours" = "Часов"
"unknown" = "Неизвестно" "unknown" = "Неизвестно"
"inbounds" = "Входящие" "inbounds" = "Входящие"
"clients" = "Клиенты" "clients" = "Клиенты"
@@ -396,6 +401,7 @@
"welcome" = "🤖 Добро пожаловать в бота управления <b>{{ .Hostname }}</b>.\r\n" "welcome" = "🤖 Добро пожаловать в бота управления <b>{{ .Hostname }}</b>.\r\n"
"status" = "✅ Бот работает нормально!" "status" = "✅ Бот работает нормально!"
"usage" = "❗ Пожалуйста, укажите текст для поиска!" "usage" = "❗ Пожалуйста, укажите текст для поиска!"
"getID" = "🆔 Ваш ID: <code>{{ .ID }}</code>"
"helpAdminCommands" = "Поиск по электронной почте клиента:\r\n<code>/usage [Email]</code>\r\n \r\nПоиск входящих соединений (со статистикой клиента):\r\n<code>/inbound [Remark]</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." "helpClientCommands" = "Для получения статистики используйте следующую команду:\r\n \r\n<code>/usage [UUID|Password]</code>\r\n \r\nИспользуйте UUID для vmess/vless и пароль для Trojan."
@@ -424,20 +430,21 @@
"time" = "⏰ Время: {{ .Time }}\r\n" "time" = "⏰ Время: {{ .Time }}\r\n"
"inbound" = "📍 Входящий поток: {{ .Remark }}\r\n" "inbound" = "📍 Входящий поток: {{ .Remark }}\r\n"
"port" = "🔌 Порт: {{ .Port }}\r\n" "port" = "🔌 Порт: {{ .Port }}\r\n"
"expire" = "📅 Дата окончания: {{ .DateTime }}\r\n \r\n" "expire" = "📅 Дата окончания: {{ .Time }}\r\n"
"expireIn" = "📅 Окончание через: {{ .Time }}\r\n \r\n" "expireIn" = "📅 Окончание через: {{ .Time }}\r\n"
"active" = "💡 Активен: {{ .Enable }}\r\n" "active" = "💡 Активен: ✅ Да\r\n"
"inactive" = "💡 Активен: ❌ Нет\r\n"
"email" = "📧 Email: {{ .Email }}\r\n" "email" = "📧 Email: {{ .Email }}\r\n"
"upload" = "🔼 Загрузка↑: {{ .Upload }}\r\n" "upload" = "🔼 Исходящий трафик: {{ .Upload }}\r\n"
"download" = "🔽 Скачивание↓: {{ .Download }}\r\n" "download" = "🔽 Входящий трафик: {{ .Download }}\r\n"
"total" = "🔄 Всего: {{ .UpDown }} / {{ .Total }}\r\n" "total" = "📊 Всего: ↑↓{{ .UpDown }} из {{ .Total }}\r\n"
"TGUser" = "👤 Пользователь Telegram: {{ .TelegramID }}\r\n" "TGUser" = "👤 Пользователь Telegram: {{ .TelegramID }}\r\n"
"exhaustedMsg" = "🚨 Исчерпаны {{ .Type }}:\r\n" "exhaustedMsg" = "🚨 Исчерпаны {{ .Type }}:\r\n"
"exhaustedCount" = "🚨 Количество исчерпанных {{ .Type }}:\r\n" "exhaustedCount" = "🚨 Количество исчерпанных {{ .Type }}:\r\n"
"disabled" = "🛑 Отключено: {{ .Disabled }}\r\n" "disabled" = "🛑 Отключено: {{ .Disabled }}\r\n"
"depleteSoon" = "🔜 Скоро исчерпание: {{ .Deplete }}\r\n \r\n" "depleteSoon" = "🔜 Скоро исчерпание: {{ .Deplete }}\r\n \r\n"
"backupTime" = "🗄 Время резервного копирования: {{ .Time }}\r\n" "backupTime" = "🗄 Время резервного копирования: {{ .Time }}\r\n"
"refreshedOn" = "🔄🕒 Обновлено: {{ .Time }}\r\n" "refreshedOn" = "\r\n📋🔄 Обновлено: {{ .Time }}\r\n \r\n"
[tgbot.buttons] [tgbot.buttons]
"closeKeyboard" = "❌ Закрыть клавиатуру" "closeKeyboard" = "❌ Закрыть клавиатуру"

View File

@@ -39,7 +39,6 @@
"depleted" = "耗尽" "depleted" = "耗尽"
"depletingSoon" = "即将耗尽" "depletingSoon" = "即将耗尽"
"domainName" = "域名" "domainName" = "域名"
"additional" = "额外"
"monitor" = "监听" "monitor" = "监听"
"certificate" = "证书" "certificate" = "证书"
"fail" = "失败" "fail" = "失败"
@@ -49,6 +48,7 @@
"clients" = "客户端" "clients" = "客户端"
"usage" = "用法" "usage" = "用法"
"secretToken" = "秘密令牌" "secretToken" = "秘密令牌"
"remained" = "仍然存在"
[menu] [menu]
"dashboard" = "系统状态" "dashboard" = "系统状态"
@@ -72,7 +72,7 @@
"title" = "系统状态" "title" = "系统状态"
"memory" = "内存" "memory" = "内存"
"hard" = "硬盘" "hard" = "硬盘"
"xrayStatus" = "xray 状态" "xrayStatus" = "状态"
"stopXray" = "停止" "stopXray" = "停止"
"restartXray" = "重启" "restartXray" = "重启"
"xraySwitch" = "切换版本" "xraySwitch" = "切换版本"
@@ -168,7 +168,7 @@
"setDefaultCert" = "从面板设置证书" "setDefaultCert" = "从面板设置证书"
"xtlsDesc" = "Xray核心需要1.7.5" "xtlsDesc" = "Xray核心需要1.7.5"
"realityDesc" = "Xray核心需要1.8.0及以上版本" "realityDesc" = "Xray核心需要1.8.0及以上版本"
"telegramDesc" = "使用不带@的电报 ID 或聊天 ID可以在此处获取 @userinfobot" "telegramDesc" = "使用 Telegram ID不包含 @ 符号或聊天 ID可以在 @userinfobot 处获取,或在机器人中使用'/id'命令"
"subscriptionDesc" = "您可以在详细信息上找到您的子链接,也可以对多个配置使用相同的名称" "subscriptionDesc" = "您可以在详细信息上找到您的子链接,也可以对多个配置使用相同的名称"
[pages.client] [pages.client]
@@ -221,6 +221,8 @@
"TGBotSettings" = "TG提醒相关设置" "TGBotSettings" = "TG提醒相关设置"
"panelListeningIP" = "面板监听 IP" "panelListeningIP" = "面板监听 IP"
"panelListeningIPDesc" = "默认留空监听所有 IP" "panelListeningIPDesc" = "默认留空监听所有 IP"
"panelListeningDomain" = "面板监听域名"
"panelListeningDomainDesc" = "默认情况下留空以监视所有域名和 IP 地址"
"panelPort" = "面板监听端口" "panelPort" = "面板监听端口"
"panelPortDesc" = "重启面板生效" "panelPortDesc" = "重启面板生效"
"publicKeyPath" = "面板证书公钥文件路径" "publicKeyPath" = "面板证书公钥文件路径"
@@ -238,11 +240,13 @@
"telegramToken" = "电报机器人TOKEN" "telegramToken" = "电报机器人TOKEN"
"telegramTokenDesc" = "重启面板生效" "telegramTokenDesc" = "重启面板生效"
"telegramChatId" = "以逗号分隔的多个 chatID 重启面板生效" "telegramChatId" = "以逗号分隔的多个 chatID 重启面板生效"
"telegramChatIdDesc" = "多个聊天 ID 逗号分隔。使用@userinfobot 获取您的聊天 ID。重新启动面板以应用更改。" "telegramChatIdDesc" = "多个聊天 ID 逗号分隔。使用 @userinfobot 或在机器人中使用'/id'命令获取您的聊天 ID。"
"telegramNotifyTime" = "电报机器人通知时间" "telegramNotifyTime" = "电报机器人通知时间"
"telegramNotifyTimeDesc" = "采用Crontab定时格式,重启面板生效" "telegramNotifyTimeDesc" = "采用Crontab定时格式,重启面板生效"
"tgNotifyBackup" = "数据库备份" "tgNotifyBackup" = "数据库备份"
"tgNotifyBackupDesc" = "正在发送数据库备份文件和报告通知" "tgNotifyBackupDesc" = "正在发送数据库备份文件和报告通知"
"tgNotifyLogin" = "登录通知"
"tgNotifyLoginDesc" = "当有人试图登录您的面板时显示用户名、IP 地址和时间"
"sessionMaxAge" = "会话最大年龄" "sessionMaxAge" = "会话最大年龄"
"sessionMaxAgeDesc" = "您可以保持登录状态的时间(单位:分钟)" "sessionMaxAgeDesc" = "您可以保持登录状态的时间(单位:分钟)"
"expireTimeDiff" = "耗尽时间阈值" "expireTimeDiff" = "耗尽时间阈值"
@@ -384,6 +388,7 @@
"months" = "月" "months" = "月"
"day" = "天" "day" = "天"
"days" = "天" "days" = "天"
"hours" = "小时"
"unknown" = "未知" "unknown" = "未知"
"inbounds" = "入站连接" "inbounds" = "入站连接"
"clients" = "客户端" "clients" = "客户端"
@@ -396,6 +401,7 @@
"welcome" = "🤖 欢迎来到<b>{{ .Hostname }}</b>管理机器人。\r\n" "welcome" = "🤖 欢迎来到<b>{{ .Hostname }}</b>管理机器人。\r\n"
"status" = "✅ 机器人正常运行!" "status" = "✅ 机器人正常运行!"
"usage" = "❗ 请输入要搜索的文本!" "usage" = "❗ 请输入要搜索的文本!"
"getID" = "🆔 您的ID为<code>{{ .ID }}</code>"
"helpAdminCommands" = "搜索客户端邮箱:\r\n<code>/usage [Email]</code>\r\n \r\n搜索入站连接包含客户端统计信息\r\n<code>/inbound [Remark]</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请使用密码。" "helpClientCommands" = "要搜索统计信息,请使用以下命令:\r\n \r\n<code>/usage [UUID|Password]</code>\r\n \r\n对于vmess/vless请使用UUID对于Trojan请使用密码。"
@@ -424,20 +430,21 @@
"time" = "⏰ 时间:{{ .Time }}\r\n" "time" = "⏰ 时间:{{ .Time }}\r\n"
"inbound" = "📍 入站:{{ .Remark }}\r\n" "inbound" = "📍 入站:{{ .Remark }}\r\n"
"port" = "🔌 端口:{{ .Port }}\r\n" "port" = "🔌 端口:{{ .Port }}\r\n"
"expire" = "📅 过期日期:{{ .DateTime }}\r\n \r\n" "expire" = "📅 过期日期:{{ .Time }}\r\n"
"expireIn" = "📅 剩余时间:{{ .Time }}\r\n \r\n" "expireIn" = "📅 剩余时间:{{ .Time }}\r\n"
"active" = "💡 激活:{{ .Enable }}\r\n" "active" = "💡 激活:\r\n"
"inactive" = "💡 激活: ❌\r\n"
"email" = "📧 邮箱:{{ .Email }}\r\n" "email" = "📧 邮箱:{{ .Email }}\r\n"
"upload" = "🔼 上传↑:{{ .Upload }}\r\n" "upload" = "🔼 上传↑:{{ .Upload }}\r\n"
"download" = "🔽 下载↓:{{ .Download }}\r\n" "download" = "🔽 下载↓:{{ .Download }}\r\n"
"total" = "🔄 总计:{{ .UpDown }} / {{ .Total }}\r\n" "total" = "📊 总计:{{ .UpDown }} / {{ .Total }}\r\n"
"TGUser" = "👤 电报用户:{{ .TelegramID }}\r\n" "TGUser" = "👤 电报用户:{{ .TelegramID }}\r\n"
"exhaustedMsg" = "🚨 耗尽的{{ .Type }}\r\n" "exhaustedMsg" = "🚨 耗尽的{{ .Type }}\r\n"
"exhaustedCount" = "🚨 耗尽的{{ .Type }}数量:\r\n" "exhaustedCount" = "🚨 耗尽的{{ .Type }}数量:\r\n"
"disabled" = "🛑 禁用:{{ .Disabled }}\r\n" "disabled" = "🛑 禁用:{{ .Disabled }}\r\n"
"depleteSoon" = "🔜 即将耗尽:{{ .Deplete }}\r\n \r\n" "depleteSoon" = "🔜 即将耗尽:{{ .Deplete }}\r\n \r\n"
"backupTime" = "🗄 备份时间:{{ .Time }}\r\n" "backupTime" = "🗄 备份时间:{{ .Time }}\r\n"
"refreshedOn" = "🔄🕒 刷新时间:{{ .Time }}\r\n" "refreshedOn" = "\r\n📋🔄 刷新时间:{{ .Time }}\r\n \r\n"
[tgbot.buttons] [tgbot.buttons]
"closeKeyboard" = "❌ 关闭键盘" "closeKeyboard" = "❌ 关闭键盘"

View File

@@ -19,11 +19,12 @@ import (
"x-ui/web/controller" "x-ui/web/controller"
"x-ui/web/job" "x-ui/web/job"
"x-ui/web/locale" "x-ui/web/locale"
"x-ui/web/middleware"
"x-ui/web/network" "x-ui/web/network"
"x-ui/web/service" "x-ui/web/service"
"github.com/gin-contrib/sessions" sessions "github.com/Calidity/gin-sessions"
"github.com/gin-contrib/sessions/cookie" "github.com/Calidity/gin-sessions/cookie"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/robfig/cron/v3" "github.com/robfig/cron/v3"
) )
@@ -144,28 +145,6 @@ func (s *Server) getHtmlTemplate(funcMap template.FuncMap) (*template.Template,
return t, nil 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) { func (s *Server) initRouter() (*gin.Engine, error) {
if config.IsDebug() { if config.IsDebug() {
gin.SetMode(gin.DebugMode) gin.SetMode(gin.DebugMode)
@@ -177,6 +156,15 @@ func (s *Server) initRouter() (*gin.Engine, error) {
engine := gin.Default() 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() secret, err := s.settingService.GetSecret()
if err != nil { if err != nil {
return nil, err return nil, err
@@ -233,7 +221,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
} }
// Apply the redirect middleware (`/xui` to `/panel`) // Apply the redirect middleware (`/xui` to `/panel`)
engine.Use(redirectMiddleware(basePath)) engine.Use(middleware.RedirectMiddleware(basePath))
g := engine.Group(basePath) g := engine.Group(basePath)
@@ -265,6 +253,9 @@ func (s *Server) startTask() {
// check client ips from log file every 10 sec // check client ips from log file every 10 sec
s.cron.AddJob("@every 10s", job.NewCheckClientIpJob()) s.cron.AddJob("@every 10s", job.NewCheckClientIpJob())
// check client ips from log file every 3 day
s.cron.AddJob("@every 3d", job.NewClearLogsJob())
// Make a traffic condition every day, 8:30 // Make a traffic condition every day, 8:30
var entry cron.EntryID var entry cron.EntryID
isTgbotenabled, err := s.settingService.GetTgbotenabled() isTgbotenabled, err := s.settingService.GetTgbotenabled()

389
x-ui.sh
View File

@@ -56,6 +56,13 @@ elif [[ "${release}" == "debian" ]]; then
fi fi
fi fi
# Declare Variables
log_folder="${XUI_LOG_FOLDER:=/var/log}"
iplimit_log_path="${log_folder}/3xipl.log"
iplimit_banned_log_path="${log_folder}/3xipl-banned.log"
confirm() { confirm() {
if [[ $# > 1 ]]; then if [[ $# > 1 ]]; then
echo && read -p "$1 [Default $2]: " temp echo && read -p "$1 [Default $2]: " temp
@@ -296,25 +303,28 @@ enable_bbr() {
fi fi
# Check the OS and install necessary packages # Check the OS and install necessary packages
if [[ "$(cat /etc/os-release | grep -E '^ID=' | awk -F '=' '{print $2}')" == "ubuntu" ]]; then case "${release}" in
sudo apt-get update && sudo apt-get install -yqq --no-install-recommends ca-certificates ubuntu|debian)
elif [[ "$(cat /etc/os-release | grep -E '^ID=' | awk -F '=' '{print $2}')" == "debian" ]]; then apt-get update && apt-get install -yqq --no-install-recommends ca-certificates
sudo apt-get update && sudo apt-get install -yqq --no-install-recommends ca-certificates ;;
elif [[ "$(cat /etc/os-release | grep -E '^ID=' | awk -F '=' '{print $2}')" == "fedora" ]]; then centos)
sudo dnf -y update && sudo dnf -y install ca-certificates yum -y update && yum -y install ca-certificates
elif [[ "$(cat /etc/os-release | grep -E '^ID=' | awk -F '=' '{print $2}')" == "centos" ]]; then ;;
sudo yum -y update && sudo yum -y install ca-certificates fedora)
else dnf -y update && dnf -y install ca-certificates
echo "Unsupported operating system. Please check the script and install the necessary packages manually." ;;
exit 1 *)
fi echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n"
exit 1
;;
esac
# Enable BBR # Enable BBR
echo "net.core.default_qdisc=fq" | sudo tee -a /etc/sysctl.conf echo "net.core.default_qdisc=fq" | tee -a /etc/sysctl.conf
echo "net.ipv4.tcp_congestion_control=bbr" | sudo tee -a /etc/sysctl.conf echo "net.ipv4.tcp_congestion_control=bbr" | tee -a /etc/sysctl.conf
# Apply changes # Apply changes
sudo sysctl -p sysctl -p
# Verify that BBR is enabled # Verify that BBR is enabled
if [[ $(sysctl net.ipv4.tcp_congestion_control | awk '{print $3}') == "bbr" ]]; then if [[ $(sysctl net.ipv4.tcp_congestion_control | awk '{print $3}') == "bbr" ]]; then
@@ -434,24 +444,24 @@ show_xray_status() {
open_ports() { open_ports() {
if ! command -v ufw &>/dev/null; then if ! command -v ufw &>/dev/null; then
echo "ufw firewall is not installed. Installing now..." echo "ufw firewall is not installed. Installing now..."
sudo apt-get update apt-get update
sudo apt-get install -y ufw apt-get install -y ufw
else else
echo "ufw firewall is already installed" echo "ufw firewall is already installed"
fi fi
# Check if the firewall is inactive # Check if the firewall is inactive
if sudo ufw status | grep -q "Status: active"; then if ufw status | grep -q "Status: active"; then
echo "firewall is already active" echo "firewall is already active"
else else
# Open the necessary ports # Open the necessary ports
sudo ufw allow ssh ufw allow ssh
sudo ufw allow http ufw allow http
sudo ufw allow https ufw allow https
sudo ufw allow 2053/tcp ufw allow 2053/tcp
# Enable the firewall # Enable the firewall
sudo ufw --force enable ufw --force enable
fi fi
# Prompt the user to enter a list of ports # Prompt the user to enter a list of ports
@@ -472,15 +482,15 @@ open_ports() {
end_port=$(echo $port | cut -d'-' -f2) end_port=$(echo $port | cut -d'-' -f2)
# Loop through the range and open each port # Loop through the range and open each port
for ((i = start_port; i <= end_port; i++)); do for ((i = start_port; i <= end_port; i++)); do
sudo ufw allow $i ufw allow $i
done done
else else
sudo ufw allow "$port" ufw allow "$port"
fi fi
done done
# Confirm that the ports are open # Confirm that the ports are open
sudo ufw status | grep $ports ufw status | grep $ports
} }
update_geo() { update_geo() {
@@ -498,7 +508,7 @@ update_geo() {
rm -f geoip.dat geosite.dat iran.dat rm -f geoip.dat geosite.dat iran.dat
wget -N https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat wget -N https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
wget -N https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat wget -N https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
wget -N https://github.com/bootmortis/iran-hosted-domains/releases/latest/download/iran.dat wget -N https://github.com/MasterKia/iran-hosted-domains/releases/latest/download/iran.dat
systemctl start x-ui systemctl start x-ui
echo -e "${green}Geosite.dat + Geoip.dat + Iran.dat have been updated successfully in bin folder '${binfolder}'!${plain}" echo -e "${green}Geosite.dat + Geoip.dat + Iran.dat have been updated successfully in bin folder '${binfolder}'!${plain}"
before_show_menu before_show_menu
@@ -518,9 +528,9 @@ install_acme() {
} }
ssl_cert_issue_main() { ssl_cert_issue_main() {
echo "1) Get SSL" echo -e "${green}\t1.${plain} Get SSL"
echo "2) Revoke" echo -e "${green}\t2.${plain} Revoke"
echo "3) Force Renew" echo -e "${green}\t3.${plain} Force Renew"
read -p "Choose an option: " choice read -p "Choose an option: " choice
case "$choice" in case "$choice" in
1) ssl_cert_issue ;; 1) ssl_cert_issue ;;
@@ -539,7 +549,7 @@ ssl_cert_issue_main() {
} }
ssl_cert_issue() { ssl_cert_issue() {
#check for acme.sh first # check for acme.sh first
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
echo "acme.sh could not be found. we will install it" echo "acme.sh could not be found. we will install it"
install_acme install_acme
@@ -548,24 +558,30 @@ ssl_cert_issue() {
exit 1 exit 1
fi fi
fi fi
#install socat second # install socat second
if [[ "${release}" == "centos" ]] || [[ "${release}" == "fedora" ]]; then case "${release}" in
yum install socat -y ubuntu|debian)
else apt update && apt install socat -y ;;
apt install socat -y centos)
fi yum -y update && yum -y install socat ;;
fedora)
dnf -y update && dnf -y install socat ;;
*)
echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n"
exit 1 ;;
esac
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
LOGE "install socat failed,please check logs" LOGE "install socat failed, please check logs"
exit 1 exit 1
else else
LOGI "install socat succeed..." LOGI "install socat succeed..."
fi fi
#get the domain here,and we need verify it # get the domain here,and we need verify it
local domain="" local domain=""
read -p "Please enter your domain name:" domain read -p "Please enter your domain name:" domain
LOGD "your domain is:${domain},check it..." LOGD "your domain is:${domain},check it..."
#here we need to judge whether there exists cert already # here we need to judge whether there exists cert already
local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}') local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}')
if [ ${currentCert} == ${domain} ]; then if [ ${currentCert} == ${domain} ]; then
@@ -577,7 +593,7 @@ ssl_cert_issue() {
LOGI "your domain is ready for issuing cert now..." LOGI "your domain is ready for issuing cert now..."
fi fi
#create a directory for install cert # create a directory for install cert
certPath="/root/cert/${domain}" certPath="/root/cert/${domain}"
if [ ! -d "$certPath" ]; then if [ ! -d "$certPath" ]; then
mkdir -p "$certPath" mkdir -p "$certPath"
@@ -586,15 +602,15 @@ ssl_cert_issue() {
mkdir -p "$certPath" mkdir -p "$certPath"
fi fi
#get needed port here # get needed port here
local WebPort=80 local WebPort=80
read -p "please choose which port do you use,default will be 80 port:" WebPort read -p "please choose which port do you use,default will be 80 port:" WebPort
if [[ ${WebPort} -gt 65535 || ${WebPort} -lt 1 ]]; then if [[ ${WebPort} -gt 65535 || ${WebPort} -lt 1 ]]; then
LOGE "your input ${WebPort} is invalid,will use default port" LOGE "your input ${WebPort} is invalid,will use default port"
fi fi
LOGI "will use port:${WebPort} to issue certs,please make sure this port is open..." LOGI "will use port:${WebPort} to issue certs,please make sure this port is open..."
#NOTE:This should be handled by user # NOTE:This should be handled by user
#open the port and kill the occupied progress # open the port and kill the occupied progress
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt
~/.acme.sh/acme.sh --issue -d ${domain} --standalone --httpport ${WebPort} ~/.acme.sh/acme.sh --issue -d ${domain} --standalone --httpport ${WebPort}
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
@@ -604,7 +620,7 @@ ssl_cert_issue() {
else else
LOGE "issue certs succeed,installing certs..." LOGE "issue certs succeed,installing certs..."
fi fi
#install cert # install cert
~/.acme.sh/acme.sh --installcert -d ${domain} \ ~/.acme.sh/acme.sh --installcert -d ${domain} \
--key-file /root/cert/${domain}/privkey.pem \ --key-file /root/cert/${domain}/privkey.pem \
--fullchain-file /root/cert/${domain}/fullchain.pem --fullchain-file /root/cert/${domain}/fullchain.pem
@@ -628,13 +644,29 @@ ssl_cert_issue() {
ls -lah cert/* ls -lah cert/*
chmod 755 $certPath/* chmod 755 $certPath/*
fi fi
} }
warp_fixchatgpt() { warp_cloudflare() {
curl -fsSL https://gist.githubusercontent.com/hamid-gh98/dc5dd9b0cc5b0412af927b1ccdb294c7/raw/install_warp_proxy.sh | bash echo -e "${green}\t1.${plain} Install WARP socks5 proxy"
echo "" echo -e "${green}\t2.${plain} Account Type (free, plus, team)"
before_show_menu echo -e "${green}\t3.${plain} Turn on/off WireProxy"
echo -e "${green}\t4.${plain} Uninstall WARP"
read -p "Choose an option: " choice
case "$choice" in
1)
bash <(curl -sSL https://raw.githubusercontent.com/hamid-gh98/x-ui-scripts/main/install_warp_proxy.sh)
;;
2)
warp a
;;
3)
warp y
;;
4)
warp u
;;
*) echo "Invalid choice" ;;
esac
} }
run_speedtest() { run_speedtest() {
@@ -642,24 +674,19 @@ run_speedtest() {
if ! command -v speedtest &> /dev/null; then if ! command -v speedtest &> /dev/null; then
# If not installed, install it # If not installed, install it
local pkg_manager="" local pkg_manager=""
local curl_install_cmd=""
local speedtest_install_script="" local speedtest_install_script=""
if command -v dnf &> /dev/null; then if command -v dnf &> /dev/null; then
pkg_manager="dnf" pkg_manager="dnf"
curl_install_cmd="sudo dnf install -y curl"
speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh" speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh"
elif command -v yum &> /dev/null; then elif command -v yum &> /dev/null; then
pkg_manager="yum" pkg_manager="yum"
curl_install_cmd="sudo yum install -y curl"
speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh" speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh"
elif command -v apt-get &> /dev/null; then elif command -v apt-get &> /dev/null; then
pkg_manager="apt-get" pkg_manager="apt-get"
curl_install_cmd="sudo apt-get update && sudo apt-get install -y curl"
speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh" speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh"
elif command -v apt &> /dev/null; then elif command -v apt &> /dev/null; then
pkg_manager="apt" pkg_manager="apt"
curl_install_cmd="sudo apt update && sudo apt install -y curl"
speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh" speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh"
fi fi
@@ -667,9 +694,8 @@ run_speedtest() {
echo "Error: Package manager not found. You may need to install Speedtest manually." echo "Error: Package manager not found. You may need to install Speedtest manually."
return 1 return 1
else else
$curl_install_cmd curl -s $speedtest_install_script | bash
curl -s $speedtest_install_script | sudo bash $pkg_manager install -y speedtest
sudo $pkg_manager install -y speedtest
fi fi
fi fi
@@ -677,6 +703,218 @@ run_speedtest() {
speedtest speedtest
} }
create_iplimit_jails() {
# Use default bantime if not passed => 5 minutes
local bantime="${1:-5}"
cat << EOF > /etc/fail2ban/jail.d/3x-ipl.conf
[3x-ipl]
enabled=true
filter=3x-ipl
action=3x-ipl
logpath=${iplimit_log_path}
maxretry=4
findtime=60
bantime=${bantime}m
EOF
cat << EOF > /etc/fail2ban/filter.d/3x-ipl.conf
[Definition]
datepattern = ^%%Y/%%m/%%d %%H:%%M:%%S
failregex = \[LIMIT_IP\]\s*Email\s*=\s*<F-USER>.+</F-USER>\s*\|\|\s*SRC\s*=\s*<ADDR>
ignoreregex =
EOF
cat << EOF > /etc/fail2ban/action.d/3x-ipl.conf
[INCLUDES]
before = iptables-common.conf
[Definition]
actionstart = <iptables> -N f2b-<name>
<iptables> -A f2b-<name> -j <returntype>
<iptables> -I <chain> -p <protocol> -j f2b-<name>
actionstop = <iptables> -D <chain> -p <protocol> -j f2b-<name>
<actionflush>
<iptables> -X f2b-<name>
actioncheck = <iptables> -n -L <chain> | grep -q 'f2b-<name>[ \t]'
actionban = <iptables> -I f2b-<name> 1 -s <ip> -j <blocktype>
echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") BAN [Email] = <F-USER> [IP] = <ip> banned for <bantime> seconds." >> ${iplimit_banned_log_path}
actionunban = <iptables> -D f2b-<name> -s <ip> -j <blocktype>
echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") UNBAN [Email] = <F-USER> [IP] = <ip> unbanned." >> ${iplimit_banned_log_path}
[Init]
EOF
echo -e "${green}Created Ip Limit jail files with a bantime of ${bantime} minutes.${plain}"
}
iplimit_remove_conflicts() {
local jail_files=(
/etc/fail2ban/jail.conf
/etc/fail2ban/jail.local
)
for file in "${jail_files[@]}"; do
# Check for [3x-ipl] config in jail file then remove it
if test -f "${file}" && grep -qw '3x-ipl' ${file}; then
sed -i "/\[3x-ipl\]/,/^$/d" ${file}
echo -e "${yellow}Removing conflicts of [3x-ipl] in jail (${file})!${plain}\n"
fi
done
}
iplimit_main() {
echo -e "\n${green}\t1.${plain} Install Fail2ban and configure IP Limit"
echo -e "${green}\t2.${plain} Change Ban Duration"
echo -e "${green}\t3.${plain} Unban Everyone"
echo -e "${green}\t4.${plain} Check Logs"
echo -e "${green}\t5.${plain} fail2ban status"
echo -e "${green}\t6.${plain} Uninstall IP Limit"
echo -e "${green}\t0.${plain} Back to Main Menu"
read -p "Choose an option: " choice
case "$choice" in
0)
show_menu ;;
1)
confirm "Proceed with installation of Fail2ban & IP Limit?" "y"
if [[ $? == 0 ]]; then
install_iplimit
else
iplimit_main
fi ;;
2)
read -rp "Please enter new Ban Duration in Minutes [default 5]: " NUM
if [[ $NUM =~ ^[0-9]+$ ]]; then
create_iplimit_jails ${NUM}
systemctl restart fail2ban
else
echo -e "${red}${NUM} is not a number! Please, try again.${plain}"
fi
iplimit_main ;;
3)
confirm "Proceed with Unbanning everyone from IP Limit jail?" "y"
if [[ $? == 0 ]]; then
fail2ban-client reload --restart --unban 3x-ipl
echo -e "${green}All users Unbanned successfully.${plain}"
iplimit_main
else
echo -e "${yellow}Cancelled.${plain}"
fi
iplimit_main ;;
4)
if test -f "${iplimit_banned_log_path}"; then
if [[ -s "${iplimit_banned_log_path}" ]]; then
cat ${iplimit_banned_log_path}
else
echo -e "${red}Log file is empty.${plain}\n"
fi
else
echo -e "${red}Log file not found. Please Install Fail2ban and IP Limit first.${plain}\n"
iplimit_main
fi ;;
5)
service fail2ban status
;;
6)
remove_iplimit ;;
*) echo "Invalid choice" ;;
esac
}
install_iplimit() {
if ! command -v fail2ban-client &>/dev/null; then
echo -e "${green}Fail2ban is not installed. Installing now...!${plain}\n"
# Check the OS and install necessary packages
case "${release}" in
ubuntu|debian)
apt update && apt install fail2ban -y ;;
centos)
yum -y update && yum -y install fail2ban ;;
fedora)
dnf -y update && dnf -y install fail2ban ;;
*)
echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n"
exit 1 ;;
esac
echo -e "${green}Fail2ban installed successfully!${plain}\n"
else
echo -e "${yellow}Fail2ban is already installed.${plain}\n"
fi
echo -e "${green}Configuring IP Limit...${plain}\n"
# make sure there's no conflict for jail files
iplimit_remove_conflicts
# Check if log file exists
if ! test -f "${iplimit_banned_log_path}"; then
touch ${iplimit_banned_log_path}
fi
# Check if service log file exists so fail2ban won't return error
if ! test -f "${iplimit_log_path}"; then
touch ${iplimit_log_path}
fi
# Create the iplimit jail files
# we didn't pass the bantime here to use the default value
create_iplimit_jails
# Launching fail2ban
if ! systemctl is-active --quiet fail2ban; then
systemctl start fail2ban
else
systemctl restart fail2ban
fi
systemctl enable fail2ban
echo -e "${green}IP Limit installed and configured successfully!${plain}\n"
before_show_menu
}
remove_iplimit(){
echo -e "${green}\t1.${plain} Only remove IP Limit configurations"
echo -e "${green}\t2.${plain} Uninstall Fail2ban and IP Limit"
echo -e "${green}\t0.${plain} Abort"
read -p "Choose an option: " num
case "$num" in
1)
rm -f /etc/fail2ban/filter.d/3x-ipl.conf
rm -f /etc/fail2ban/action.d/3x-ipl.conf
rm -f /etc/fail2ban/jail.d/3x-ipl.conf
systemctl restart fail2ban
echo -e "${green}IP Limit removed successfully!${plain}\n"
before_show_menu ;;
2)
rm -rf /etc/fail2ban
systemctl stop fail2ban
case "${release}" in
ubuntu|debian)
apt-get purge fail2ban -y;;
centos)
yum remove fail2ban -y;;
fedora)
dnf remove fail2ban -y;;
*)
echo -e "${red}Unsupported operating system. Please uninstall Fail2ban manually.${plain}\n"
exit 1 ;;
esac
echo -e "${green}Fail2ban and IP Limit removed successfully!${plain}\n"
before_show_menu ;;
0)
echo -e "${yellow}Cancelled.${plain}\n"
iplimit_main ;;
*)
echo -e "${red}Invalid option. Please select a valid number.${plain}\n"
remove_iplimit ;;
esac
}
show_usage() { show_usage() {
echo "x-ui control menu usages: " echo "x-ui control menu usages: "
echo "------------------------------------------" echo "------------------------------------------"
@@ -717,15 +955,17 @@ show_menu() {
${green}13.${plain} Enable x-ui On System Startup ${green}13.${plain} Enable x-ui On System Startup
${green}14.${plain} Disable x-ui On System Startup ${green}14.${plain} Disable x-ui On System Startup
———————————————— ————————————————
${green}15.${plain} Enable BBR ${green}15.${plain} SSL Certificate Management
${green}16.${plain} SSL Certificate Management ${green}16.${plain} IP Limit Management
${green}17.${plain} Update Geo Files ${green}17.${plain} WARP Management
${green}18.${plain} Active Firewall and open ports ————————————————
${green}19.${plain} Install WARP ${green}18.${plain} Enable BBR
${green}20.${plain} Speedtest by Ookla ${green}19.${plain} Update Geo Files
" ${green}20.${plain} Active Firewall and open ports
${green}21.${plain} Speedtest by Ookla
"
show_status 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 case "${num}" in
0) 0)
@@ -774,25 +1014,28 @@ show_menu() {
check_install && disable check_install && disable
;; ;;
15) 15)
enable_bbr
;;
16)
ssl_cert_issue_main ssl_cert_issue_main
;; ;;
16)
iplimit_main
;;
17) 17)
update_geo warp_cloudflare
;; ;;
18) 18)
open_ports enable_bbr
;; ;;
19) 19)
warp_fixchatgpt update_geo
;; ;;
20) 20)
open_ports
;;
21)
run_speedtest run_speedtest
;; ;;
*) *)
LOGE "Please enter the correct number [0-20]" LOGE "Please enter the correct number [0-21]"
;; ;;
esac esac
} }

236
xray/api.go Normal file
View File

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

View File

@@ -3,30 +3,23 @@ package xray
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"context"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io/fs" "io/fs"
"os" "os"
"os/exec" "os/exec"
"regexp"
"runtime" "runtime"
"strings" "strings"
"sync" "sync"
"time" "syscall"
"x-ui/config" "x-ui/config"
"x-ui/logger"
"x-ui/util/common" "x-ui/util/common"
"github.com/Workiva/go-datastructures/queue" "github.com/Workiva/go-datastructures/queue"
statsservice "github.com/xtls/xray-core/app/stats/command"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
) )
var trafficRegex = regexp.MustCompile("(inbound|outbound)>>>([^>]+)>>>traffic>>>(downlink|uplink)")
var ClientTrafficRegex = regexp.MustCompile("(user)>>>([^>]+)>>>traffic>>>(downlink|uplink)")
func GetBinaryName() string { func GetBinaryName() string {
return fmt.Sprintf("xray-%s-%s", runtime.GOOS, runtime.GOARCH) return fmt.Sprintf("xray-%s-%s", runtime.GOOS, runtime.GOARCH)
} }
@@ -47,12 +40,40 @@ func GetGeoipPath() string {
return config.GetBinFolderPath() + "/geoip.dat" return config.GetBinFolderPath() + "/geoip.dat"
} }
func GetIranPath() string { func GetIPLimitLogPath() string {
return config.GetBinFolderPath() + "/iran.dat" return config.GetLogFolder() + "/3xipl.log"
} }
func GetAllowedIPsPath() string { func GetIPLimitBannedLogPath() string {
return config.GetBinFolderPath() + "/AllowedIPs" return config.GetLogFolder() + "/3xipl-banned.log"
}
func GetAccessPersistentLogPath() string {
return config.GetLogFolder() + "/3xipl-access-persistent.log"
}
func GetAccessLogPath() string {
config, err := os.ReadFile(GetConfigPath())
if err != nil {
logger.Warningf("Something went wrong: %s", err)
}
jsonConfig := map[string]interface{}{}
err = json.Unmarshal([]byte(config), &jsonConfig)
if err != nil {
logger.Warningf("Something went wrong: %s", err)
}
if jsonConfig["log"] != nil {
jsonLog := jsonConfig["log"].(map[string]interface{})
if jsonLog["access"] != nil {
accessLogPath := jsonLog["access"].(string)
return accessLogPath
}
}
return ""
} }
func stopProcess(p *Process) { func stopProcess(p *Process) {
@@ -173,7 +194,7 @@ func (p *process) Start() (err error) {
return common.NewErrorf("Failed to write configuration file: %v", err) return common.NewErrorf("Failed to write configuration file: %v", err)
} }
cmd := exec.Command(GetBinaryPath(), "-c", configPath, "-restrictedIPsPath", GetAllowedIPsPath()) cmd := exec.Command(GetBinaryPath(), "-c", configPath)
p.cmd = cmd p.cmd = cmd
stdReader, err := cmd.StdoutPipe() stdReader, err := cmd.StdoutPipe()
@@ -236,87 +257,5 @@ func (p *process) Stop() error {
if !p.IsRunning() { if !p.IsRunning() {
return errors.New("xray is not running") return errors.New("xray is not running")
} }
return p.cmd.Process.Kill() return p.cmd.Process.Signal(syscall.SIGTERM)
}
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.WithTransportCredentials(insecure.NewCredentials()))
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
} }