Compare commits

...

161 Commits
1.4.0 ... 1.5.4

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

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

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

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

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

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-04 21:12:42 +00:00
Alireza Ahmadi
f55478422b [install] stop service after download #464 2023-08-03 21:11:24 +02:00
Alireza Ahmadi
619e0c69cd Merge pull request #463 from alireza0/dependabot/go_modules/github.com/shirou/gopsutil/v3-3.23.7
Bump github.com/shirou/gopsutil/v3 from 3.23.6 to 3.23.7
2023-08-02 15:19:05 +02:00
dependabot[bot]
da5dc3a04f Bump github.com/shirou/gopsutil/v3 from 3.23.6 to 3.23.7
Bumps [github.com/shirou/gopsutil/v3](https://github.com/shirou/gopsutil) from 3.23.6 to 3.23.7.
- [Release notes](https://github.com/shirou/gopsutil/releases)
- [Commits](https://github.com/shirou/gopsutil/compare/v3.23.6...v3.23.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-01 21:24:53 +00:00
Alireza Ahmadi
1f920bb08c v1.5.2 2023-08-01 17:41:19 +02:00
Alireza Ahmadi
3528135297 security alert without tls 2023-08-01 01:17:09 +02:00
Alireza Ahmadi
bca0f63239 Merge pull request #454 from alireza0/dependabot/go_modules/google.golang.org/grpc-1.57.0
Bump google.golang.org/grpc from 1.56.2 to 1.57.0
2023-07-30 16:30:52 +02:00
Alireza Ahmadi
607fdc9f47 Merge pull request #460 from alireza0/dependabot/github_actions/svenstaro/upload-release-action-2.7.0
Bump svenstaro/upload-release-action from 2.6.1 to 2.7.0
2023-07-30 16:30:38 +02:00
Alireza Ahmadi
8775eb70e2 [docker] use xray 1.8.3 by default 2023-07-30 16:30:08 +02:00
Alireza Ahmadi
59204cdb0c in mem logs & syslog 2023-07-30 16:29:49 +02:00
Alireza Ahmadi
49eedb7057 fix logs in api 2023-07-30 16:28:44 +02:00
Alireza Ahmadi
312c551cfb [SS] fix bulk creation 2023-07-30 16:26:20 +02:00
dependabot[bot]
c5389d86a3 Bump svenstaro/upload-release-action from 2.6.1 to 2.7.0
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>
2023-07-28 21:45:21 +00:00
dependabot[bot]
adf73cd87a Bump google.golang.org/grpc from 1.56.2 to 1.57.0
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>
2023-07-26 21:53:19 +00:00
Alireza Ahmadi
a3f4e6f35c [api] fix actions for shadowsocks #443 2023-07-23 11:56:22 +02:00
Alireza Ahmadi
b8eee6e373 full multiuser shadowsocks 2023-07-23 11:54:39 +02:00
Alireza Ahmadi
b3d3f76e84 remove duplicate manual list 2023-07-23 11:52:13 +02:00
Alireza Ahmadi
3be40f8595 fix logs after api changes #443 2023-07-23 11:51:27 +02:00
Alireza Ahmadi
d2e50d0493 v1.5.1
Plus some fixes
2023-07-16 18:43:38 +02:00
Alireza Ahmadi
8051a70ef2 Merge pull request #428 from alireza0/dependabot/go_modules/github.com/pelletier/go-toml/v2-2.0.9
Bump github.com/pelletier/go-toml/v2 from 2.0.8 to 2.0.9
2023-07-16 17:07:47 +02:00
Alireza Ahmadi
2327a3cac6 Merge pull request #420 from sudospaes/bug-fix-1
Fix bug: flow remains on transmission update
2023-07-16 17:07:36 +02:00
dependabot[bot]
f915712ffc Bump github.com/pelletier/go-toml/v2 from 2.0.8 to 2.0.9
Bumps [github.com/pelletier/go-toml/v2](https://github.com/pelletier/go-toml) from 2.0.8 to 2.0.9.
- [Release notes](https://github.com/pelletier/go-toml/releases)
- [Changelog](https://github.com/pelletier/go-toml/blob/v2/.goreleaser.yaml)
- [Commits](https://github.com/pelletier/go-toml/compare/v2.0.8...v2.0.9)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-16 15:07:24 +00:00
Alireza Ahmadi
1459ae8115 xray-core 1.8.3 by default 2023-07-16 17:06:22 +02:00
Alireza Ahmadi
5fbb234b06 [feature] using xray api for inbound #351 2023-07-16 17:05:15 +02:00
Alireza Ahmadi
f23aa3e51d get updated config.json 2023-07-16 17:00:52 +02:00
SudoSpace
4b2169d9f6 revert changes 2023-07-16 14:33:07 +03:30
Alireza Ahmadi
bd8028fae5 support old shadowsocks methods 2023-07-13 20:41:04 +02:00
Alireza Ahmadi
678ec4fdfc remove unnecessary codes 2023-07-13 20:40:19 +02:00
SudoSpace
95bdb064bb Clone Bug Fix
Bug fix, set duplicate publicKey, privateKey, shortIds in cloned inbound from reality inbounds
2023-07-09 14:42:52 +03:30
SudoSpace
65a69e967d Better bug fix
bug fix, can't connect after change transmission type from tcp to other types.
2023-07-09 14:36:42 +03:30
SudoSpace
9798bf5552 revert changes 2023-07-09 14:34:12 +03:30
SudoSpace
a50f8dfa4b Merge branch 'alireza0:main' into bug-fix-1 2023-07-09 14:32:44 +03:30
Alireza Ahmadi
5ee7bfa5a3 Merge pull request #418 from alireza0/dependabot/go_modules/google.golang.org/grpc-1.56.2
Bump google.golang.org/grpc from 1.56.1 to 1.56.2
2023-07-09 00:37:04 +02:00
SudoSpace
0c9816b7ba Bug Fix
Bug fix, can't connect after change transmission type from tcp to other types

If flow is not equal to "" in transmit except tcp, connection will not be made. This problem occurs when we change the transmission from tcp to another type. Also, if tcp itself is alone and without tls and reality, flow must be empty in it.
2023-07-08 19:52:35 +03:30
dependabot[bot]
3cb2e346be Bump google.golang.org/grpc from 1.56.1 to 1.56.2
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.56.1 to 1.56.2.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.56.1...v1.56.2)

---
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>
2023-07-06 21:34:39 +00:00
Alireza Ahmadi
09eb52483c Merge pull request #416 from alireza0/dependabot/go_modules/github.com/shirou/gopsutil/v3-3.23.6
Bump github.com/shirou/gopsutil/v3 from 3.23.5 to 3.23.6
2023-07-05 17:05:44 +02:00
Alireza Ahmadi
48f0ed7d12 Merge pull request #417 from alireza0/dependabot/go_modules/golang.org/x/text-0.11.0
Bump golang.org/x/text from 0.10.0 to 0.11.0
2023-07-05 17:05:34 +02:00
dependabot[bot]
6a04aa6d56 Bump golang.org/x/text from 0.10.0 to 0.11.0
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.10.0 to 0.11.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.10.0...v0.11.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-04 21:46:02 +00:00
dependabot[bot]
828c2b1a54 Bump github.com/shirou/gopsutil/v3 from 3.23.5 to 3.23.6
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>
2023-07-03 21:48:22 +00:00
Alireza Ahmadi
a957a3b238 infinity icon 2023-06-30 20:25:30 +02:00
Alireza Ahmadi
96395639d1 [tg] optional login notification
Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com>
2023-06-30 11:18:06 +02:00
Alireza Ahmadi
edf8e3d586 Merge pull request #414 from alireza0/dependabot/go_modules/gorm.io/gorm-1.25.2
Bump gorm.io/gorm from 1.25.2-0.20230530020048-26663ab9bf55 to 1.25.2
2023-06-30 11:16:09 +02:00
dependabot[bot]
f831849b68 Bump gorm.io/gorm from 1.25.2-0.20230530020048-26663ab9bf55 to 1.25.2
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>
2023-06-29 21:50:16 +00:00
Alireza Ahmadi
bb41377dfa Merge pull request #407 from alireza0/dependabot/go_modules/google.golang.org/grpc-1.56.1
Bump google.golang.org/grpc from 1.56.0 to 1.56.1
2023-06-29 20:08:43 +02:00
dependabot[bot]
8a366edf4d Bump google.golang.org/grpc from 1.56.0 to 1.56.1
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>
2023-06-21 21:18:40 +00:00
Alireza Ahmadi
3d2c6bf444 simplify with show remaining flow #397 2023-06-20 22:00:33 +02:00
Alireza Ahmadi
5f2ccf3d65 proxy protocol limitation 2023-06-20 17:41:42 +02:00
Alireza Ahmadi
3f8227214a better clients info tables 2023-06-20 13:10:05 +02:00
Alireza Ahmadi
ac58a65750 remove trojan flow 2023-06-20 12:42:31 +02:00
Alireza Ahmadi
fe29c48d89 remove vmess alterId 2023-06-20 12:27:01 +02:00
Alireza Ahmadi
76346299d6 Merge pull request #406 from alireza0/dependabot/go_modules/github.com/xtls/xray-core-1.8.3
Bump github.com/xtls/xray-core from 1.8.1 to 1.8.3
2023-06-20 12:06:41 +02:00
Alireza Ahmadi
0c08f23519 Merge pull request #393 from alireza0/dependabot/go_modules/google.golang.org/grpc-1.56.0
Bump google.golang.org/grpc from 1.55.0 to 1.56.0
2023-06-20 12:06:29 +02:00
Alireza Ahmadi
fef958f606 Merge pull request #390 from X-Oracle/new_branch
Adding 'fakedns' to sniffing object in xray.js
2023-06-20 12:06:17 +02:00
dependabot[bot]
45af57e44b Bump github.com/xtls/xray-core from 1.8.1 to 1.8.3
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>
2023-06-19 22:08:32 +00:00
Alireza Ahmadi
86508876f9 fix default language in initLocalizer 2023-06-16 11:20:00 +02:00
dependabot[bot]
ec1fcfd9d0 Bump google.golang.org/grpc from 1.55.0 to 1.56.0
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.55.0 to 1.56.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.55.0...v1.56.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-15 22:04:33 +00:00
Alireza Ahmadi
e1e5899a90 gracefully shutdown xray-core 2023-06-14 22:29:39 +02:00
Alireza Ahmadi
06ae50c402 Merge pull request #386 from alireza0/dependabot/go_modules/golang.org/x/text-0.10.0
Bump golang.org/x/text from 0.9.0 to 0.10.0
2023-06-14 20:54:03 +02:00
X-Oracle
5b9c3f83ea Adding 'fakedns' to sniffing object in xray.js 2023-06-14 17:39:50 +03:30
dependabot[bot]
f79fadbb1e Bump golang.org/x/text from 0.9.0 to 0.10.0
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>
2023-06-12 22:07:57 +00:00
Alireza Ahmadi
0742d776b1 Merge pull request #384 from sudospaes/Encrypt-Sub-Option
Add encrypt on/off switch to subscription
2023-06-11 20:37:13 +02:00
Sudo Space
0650362222 Add encrypt subscription ON/OFF switch 2023-06-11 20:34:27 +03:30
Alireza Ahmadi
002b8177ec Merge pull request #381 from alireza0/dependabot/go_modules/gorm.io/driver/sqlite-1.5.2
Bump gorm.io/driver/sqlite from 1.5.1 to 1.5.2
2023-06-10 08:40:30 +02:00
dependabot[bot]
206f37e0a1 Bump gorm.io/driver/sqlite from 1.5.1 to 1.5.2
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>
2023-06-09 22:07:01 +00:00
Alireza Ahmadi
d74feb3495 remove mtproto 2023-06-09 15:32:10 +02:00
Alireza Ahmadi
74d54f4c8b Merge pull request #378 from sudospaes/Add-Reject-Unknown-SNI
Add rejectUnknownSni to TLS Object
2023-06-09 10:46:44 +02:00
Alireza Ahmadi
5d2d1283a8 Merge pull request #377 from sudospaes/Fix-Ignore-shortid-in-Trojan-link-generator-in-subService
Fix ignore shortIds in Trojan reality link generator in subService.go
2023-06-09 10:44:40 +02:00
Sudo Space
9e3361cfdb Add TLS rejectUnknownSni 2023-06-08 16:49:19 +03:30
Sudo Space
c897153140 Fix ignore shortIds in Trojan sub link 2023-06-08 16:42:47 +03:30
Alireza Ahmadi
37e90ad587 v1.5.0 2023-06-04 18:30:12 +02:00
Alireza Ahmadi
6f15de752c [reality] random shortId #361 2023-06-04 17:52:49 +02:00
Alireza Ahmadi
81a21b8ae0 [api] backward compatibility: add client by update 2023-06-04 16:43:08 +02:00
Alireza Ahmadi
04aaf94016 Improve DB performance 2023-06-04 16:40:59 +02:00
Alireza Ahmadi
fc84ddc6c1 Fix conflict 2023-06-04 11:01:59 +02:00
Alireza Ahmadi
9f7e2f21f3 [feature] using xray api #351 2023-06-04 10:53:43 +02:00
Alireza Ahmadi
29078f38e9 Merge pull request #356 from alireza0/dependabot/go_modules/github.com/gin-gonic/gin-1.9.1
Bump github.com/gin-gonic/gin from 1.9.0 to 1.9.1
2023-06-02 08:30:42 +02:00
dependabot[bot]
8a8e6cf741 Bump github.com/gin-gonic/gin from 1.9.0 to 1.9.1
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>
2023-06-02 06:30:07 +00:00
Alireza Ahmadi
eac1f4d3b3 Merge pull request #357 from alireza0/dependabot/go_modules/github.com/shirou/gopsutil/v3-3.23.5
Bump github.com/shirou/gopsutil/v3 from 3.23.4 to 3.23.5
2023-06-02 08:29:16 +02:00
dependabot[bot]
1146c146ae Bump github.com/shirou/gopsutil/v3 from 3.23.4 to 3.23.5
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>
2023-06-01 22:15:14 +00:00
Alireza Ahmadi
3e5a71d65f Merge pull request #350 from hamid-gh98/main
🔀 New Feature + Fix URLs + Some Improvements 🛠️🌐
2023-06-01 12:51:04 +02:00
Alireza Ahmadi
438a15cebc Merge pull request #353 from alireza0/dependabot/github_actions/svenstaro/upload-release-action-2.6.1
Bump svenstaro/upload-release-action from 2.6.0 to 2.6.1
2023-06-01 12:32:53 +02:00
dependabot[bot]
025eee20b5 Bump svenstaro/upload-release-action from 2.6.0 to 2.6.1
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>
2023-05-31 22:03:28 +00:00
Hamidreza
4356afebc6 FIX tgbot adminIds 2023-05-31 05:02:40 +03:30
Hamidreza Ghavami
7a6f5eef28 Update README.md 2023-05-31 04:27:25 +04:30
Hamidreza Ghavami
dbbdbee737 add /id command to tgbot to get user id 2023-05-31 03:55:30 +04:30
Hamidreza Ghavami
bf63d00e3a move manual list to new tab 2023-05-31 03:44:09 +04:30
Hamidreza Ghavami
8f4fb47e27 fix urls + use the new buildURL func 2023-05-31 03:39:41 +04:30
Hamidreza Ghavami
8bf6604ed6 add buildURL func 2023-05-31 03:32:19 +04:30
Hamidreza Ghavami
64c1461a3f fix qrModal.client is null 2023-05-31 03:31:36 +04:30
Hamidreza Ghavami
29d1c616fa some improvements 2023-05-31 03:30:45 +04:30
Hamidreza Ghavami
09a719e179 Update translations 2023-05-31 03:27:50 +04:30
Hamidreza Ghavami
8077307d85 use the middlewares 2023-05-31 03:26:19 +04:30
Hamidreza Ghavami
06629939be add an option for webDomain 2023-05-31 03:24:31 +04:30
Hamidreza Ghavami
754ebbdabb create and move middlewares to seperate folder 2023-05-31 03:20:18 +04:30
Alireza Ahmadi
3c4fa7b422 Merge pull request #336 from alireza0/dependabot/go_modules/github.com/pelletier/go-toml/v2-2.0.8
Bump github.com/pelletier/go-toml/v2 from 2.0.7 to 2.0.8
2023-05-25 20:18:51 +02:00
dependabot[bot]
ebda91b036 Bump github.com/pelletier/go-toml/v2 from 2.0.7 to 2.0.8
Bumps [github.com/pelletier/go-toml/v2](https://github.com/pelletier/go-toml) from 2.0.7 to 2.0.8.
- [Release notes](https://github.com/pelletier/go-toml/releases)
- [Changelog](https://github.com/pelletier/go-toml/blob/v2/.goreleaser.yaml)
- [Commits](https://github.com/pelletier/go-toml/compare/v2.0.7...v2.0.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-24 22:04:40 +00:00
Alireza Ahmadi
bc9ab420e6 Merge pull request #333 from alireza0/dependabot/github_actions/svenstaro/upload-release-action-2.6.0
Bump svenstaro/upload-release-action from 2.5.0 to 2.6.0
2023-05-24 12:27:56 +02:00
dependabot[bot]
d0d6748d3a Bump svenstaro/upload-release-action from 2.5.0 to 2.6.0
Bumps [svenstaro/upload-release-action](https://github.com/svenstaro/upload-release-action) from 2.5.0 to 2.6.0.
- [Release notes](https://github.com/svenstaro/upload-release-action/releases)
- [Changelog](https://github.com/svenstaro/upload-release-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/svenstaro/upload-release-action/compare/2.5.0...2.6.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-23 22:03:04 +00:00
Alireza Ahmadi
d2d34e3c7f v1.4.1 2023-05-23 01:41:20 +02:00
Alireza Ahmadi
9ddac54320 [sub] auto random subID 2023-05-22 23:45:17 +02:00
Alireza Ahmadi
ecee4e4482 [feature] fallback link calculation #323 2023-05-22 23:44:22 +02:00
Alireza Ahmadi
6f4d5655b2 Merge branch 'main' of https://github.com/alireza0/x-ui 2023-05-22 23:28:10 +02:00
Alireza Ahmadi
7305953cf1 Correction: small changes 2023-05-22 23:28:01 +02:00
Alireza Ahmadi
b8eb123d49 Merge pull request #329 from hamid-gh98/main
FIX restart redirect :)
2023-05-22 22:32:12 +02:00
Hamidreza Ghavami
a05977440c FIX restart redirect :) 2023-05-22 22:35:19 +04:30
Alireza Ahmadi
0bf6987fbc Merge pull request #324 from hamid-gh98/main
[tgbot] Multi language + More...
2023-05-22 11:45:36 +02:00
Alireza Ahmadi
3cdafd600e [sub] fix port in vless multidomain #323 2023-05-22 10:14:16 +02:00
Hamidreza Ghavami
441efa65b5 Update translations 2023-05-21 07:54:10 +04:30
Hamidreza Ghavami
a241a97ef6 Update tgbot locale + add I18nBot 2023-05-21 07:43:20 +04:30
Hamidreza Ghavami
e4300badc5 Update README.md 2023-05-21 06:52:07 +04:30
Hamidreza Ghavami
4b3abe9cdf add createBackup api 2023-05-21 06:51:54 +04:30
Hamidreza Ghavami
61d11ac66a init i18n in tgbot 2023-05-21 06:45:39 +04:30
Hamidreza Ghavami
a34496910d add tgBot localizer 2023-05-21 06:43:37 +04:30
Hamidreza Ghavami
b5f16a476b update controllers to use I18nWeb func 2023-05-21 06:42:05 +04:30
Hamidreza Ghavami
43dc1e5522 update I18n function for controller 2023-05-21 06:40:40 +04:30
Hamidreza Ghavami
dd363d2ead create LocalizerMiddleware func 2023-05-21 06:38:42 +04:30
Hamidreza Ghavami
068decfb5e add localizer middleware to web.go 2023-05-21 06:38:02 +04:30
Hamidreza Ghavami
f77cec681a replace new localizer to web.go 2023-05-21 06:37:01 +04:30
Hamidreza Ghavami
c0e670a5b6 Refactor i18n localizer 2023-05-21 06:32:52 +04:30
Hamidreza Ghavami
afeab09753 some fix for tgbot 2023-05-21 06:29:35 +04:30
Hamidreza Ghavami
2990f71fe2 Add tgLang option to settings 2023-05-21 06:18:21 +04:30
Hamidreza Ghavami
60eb61bd60 Add tgLang option 2023-05-21 06:17:25 +04:30
Hamidreza Ghavami
f2394844ff Update translations 2023-05-21 06:15:09 +04:30
Hamidreza Ghavami
d31feba2c8 Update .gitignore 2023-05-21 06:13:21 +04:30
Hamidreza Ghavami
0c189e50ec Add manual list for ipv4 and fixed it 2023-05-21 06:12:57 +04:30
Hamidreza Ghavami
15fdb58433 FIX redirect after restart panel 2023-05-21 06:08:03 +04:30
Hamidreza Ghavami
9c170fda26 update settings ui 2023-05-21 06:07:02 +04:30
73 changed files with 3440 additions and 1657 deletions

View File

@@ -14,7 +14,7 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v4 uses: actions/setup-go@v4
with: with:
go-version: 'stable' go-version: '1.20'
- name: build linux amd64 version - name: build linux amd64 version
run: | run: |
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o xui-release -v main.go CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o xui-release -v main.go
@@ -26,19 +26,19 @@ jobs:
mv xui-release x-ui mv xui-release x-ui
mkdir bin mkdir bin
cd bin cd bin
wget https://github.com/XTLS/Xray-core/releases/download/v1.8.1/Xray-linux-64.zip wget https://github.com/XTLS/Xray-core/releases/download/v1.8.3/Xray-linux-64.zip
unzip Xray-linux-64.zip unzip Xray-linux-64.zip
rm -f Xray-linux-64.zip geoip.dat geosite.dat iran.dat 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/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-amd64 mv xray xray-linux-amd64
cd .. cd ..
cd .. cd ..
- name: package - name: package
run: tar -zcvf x-ui-linux-amd64.tar.gz x-ui run: tar -zcvf x-ui-linux-amd64.tar.gz x-ui
- name: upload - name: upload
uses: svenstaro/upload-release-action@2.5.0 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 }}
@@ -67,19 +67,19 @@ jobs:
mv xui-release x-ui mv xui-release x-ui
mkdir bin mkdir bin
cd bin cd bin
wget https://github.com/xtls/xray-core/releases/download/v1.8.1/Xray-linux-arm64-v8a.zip wget https://github.com/xtls/xray-core/releases/download/v1.8.3/Xray-linux-arm64-v8a.zip
unzip Xray-linux-arm64-v8a.zip unzip Xray-linux-arm64-v8a.zip
rm -f Xray-linux-64.zip geoip.dat geosite.dat iran.dat 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/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-arm64
cd .. cd ..
cd .. cd ..
- name: package - name: package
run: tar -zcvf x-ui-linux-arm64.tar.gz x-ui run: tar -zcvf x-ui-linux-arm64.tar.gz x-ui
- name: upload - name: upload
uses: svenstaro/upload-release-action@2.5.0 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 }}
@@ -108,19 +108,19 @@ jobs:
mv xui-release x-ui mv xui-release x-ui
mkdir bin mkdir bin
cd bin cd bin
wget https://github.com/xtls/xray-core/releases/download/v1.8.1/Xray-linux-s390x.zip wget https://github.com/xtls/xray-core/releases/download/v1.8.3/Xray-linux-s390x.zip
unzip Xray-linux-s390x.zip unzip Xray-linux-s390x.zip
rm -f Xray-linux-64.zip geoip.dat geosite.dat iran.dat 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/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-s390x mv xray xray-linux-s390x
cd .. cd ..
cd .. cd ..
- name: package - name: package
run: tar -zcvf x-ui-linux-s390x.tar.gz x-ui run: tar -zcvf x-ui-linux-s390x.tar.gz x-ui
- name: upload - name: upload
uses: svenstaro/upload-release-action@2.5.0 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 }}

16
.gitignore vendored
View File

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

View File

@@ -11,11 +11,11 @@ else
fi fi
mkdir -p build/bin mkdir -p build/bin
cd build/bin cd build/bin
wget "https://github.com/XTLS/Xray-core/releases/download/v1.8.1/Xray-linux-${ARCH}.zip" wget "https://github.com/XTLS/Xray-core/releases/download/v1.8.3/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 ../../ cd ../../

View File

@@ -25,9 +25,14 @@ xray panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese,Russia
**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 :**
- Tron USDT (TRC20): `TYTq73Gj6dJ67qe58JVPD9zpjW2cc9XgVz`
- Tezos (XTZ): tz2Wnh2SsY1eezXrcLChu6idWpgdHzUFQcts
# Install & Upgrade to latest version # Install & Upgrade to latest version
``` ```sh
bash <(curl -Ls https://raw.githubusercontent.com/alireza0/x-ui/master/install.sh) bash <(curl -Ls https://raw.githubusercontent.com/alireza0/x-ui/master/install.sh)
``` ```
@@ -35,21 +40,23 @@ bash <(curl -Ls https://raw.githubusercontent.com/alireza0/x-ui/master/install.s
To install your desired version you can add the version to the end of install command. Example for ver `0.5.2`: To install your desired version you can add the version to the end of install command. Example for ver `0.5.2`:
``` ```sh
bash <(curl -Ls https://raw.githubusercontent.com/alireza0/x-ui/master/install.sh) 0.5.2 bash <(curl -Ls https://raw.githubusercontent.com/alireza0/x-ui/master/install.sh) 0.5.2
``` ```
## Manual install & upgrade ## Manual install & upgrade
1. First download the latest compressed package from https://github.com/alireza0/x-ui/releases , generally choose Architecture `amd64` 1. First download the latest compressed package from https://github.com/alireza0/x-ui/releases, generally choose Architecture `amd64`
2. Then upload the compressed package to the server's `/root/` directory and `root` rootlog in to the server with user 2. Then upload the compressed package to the server's `/root/` directory and `root` rootlog in to the server with user
> If your server cpu architecture is not `amd64` replace another architecture > If your server cpu architecture is not `amd64` replace another architecture
``` ```sh
ARCH=$(uname -m)
[[ "${ARCH}" == "s390x" ]] && XUI_ARCH="s390x" || [[ "${ARCH}" == "aarch64" || "${ARCH}" == "arm64" ]] && XUI_ARCH="arm64" || XUI_ARCH="amd64"
cd /root/ cd /root/
rm x-ui/ /usr/local/x-ui/ /usr/bin/x-ui -rf rm x-ui/ /usr/local/x-ui/ /usr/bin/x-ui -rf
tar zxvf x-ui-linux-amd64.tar.gz 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 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 x-ui/x-ui.sh /usr/bin/x-ui
cp -f x-ui/x-ui.service /etc/systemd/system/ cp -f x-ui/x-ui.service /etc/systemd/system/
@@ -120,6 +127,7 @@ docker build -t x-ui .
| :----: | ------------------------------- | ----------------------------------------- | | :----: | ------------------------------- | ----------------------------------------- |
| `GET` | `"/"` | Get all inbounds | | `GET` | `"/"` | Get all inbounds |
| `GET` | `"/get/:id"` | Get inbound with inbound.id | | `GET` | `"/get/:id"` | Get inbound with inbound.id |
| `GET` | `"/createbackup"` | Telegram bot sends backup to admins |
| `POST` | `"/add"` | Add inbound | | `POST` | `"/add"` | Add inbound |
| `POST` | `"/del/:id"` | Delete Inbound | | `POST` | `"/del/:id"` | Delete Inbound |
| `POST` | `"/update/:id"` | Update Inbound | | `POST` | `"/update/:id"` | Update Inbound |
@@ -199,29 +207,16 @@ 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> </details>
# Common problem
<details>
<summary>Click for details</summary>
## Migrating from v2-ui
First install the latest version of x-ui on the server where v2-ui is installed, and then use the following command to migrate, which will migrate the native v2-ui `All inbound account data` to x-ui`Panel settings and username passwords are not migrated`
> Please `Close v2-ui` and `restart x-ui`, otherwise the inbound of v2-ui will cause a `port conflict with the inbound of x-ui`
```
x-ui v2-ui
```
# T-Shoots: # T-Shoots:
**If you upgrade from an old version or other forks, for enable traffic for users you should do :** **If you upgrade from an old version or other forks, for enable traffic for users you should do :**
@@ -272,6 +267,11 @@ restart panel
- [HexaSoftwareTech](https://github.com/HexaSoftwareTech/) - [HexaSoftwareTech](https://github.com/HexaSoftwareTech/)
- [MHSanaei](https://github.com/MHSanaei) - [MHSanaei](https://github.com/MHSanaei)
# Acknowledgment
- [Iran Hosted Domains](https://github.com/bootmortis/iran-hosted-domains) (License: **MIT**): _A comprehensive list of Iranian domains and services that are hosted within the country._
- [PersianBlocker](https://github.com/MasterKia/PersianBlocker) (License: **AGPLv3**): _An optimal and extensive list to block ads and trackers on Persian websites._
## Stargazers over time ## Stargazers over time
[![Stargazers over time](https://starchart.cc/alireza0/x-ui.svg)](https://starchart.cc/alireza0/x-ui) [![Stargazers over time](https://starchart.cc/alireza0/x-ui.svg)](https://starchart.cc/alireza0/x-ui)

View File

@@ -1 +1 @@
1.4.0 1.5.4

View File

@@ -70,7 +70,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"`
TotalGB int64 `json:"totalGB" form:"totalGB"` TotalGB int64 `json:"totalGB" form:"totalGB"`
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`

69
go.mod
View File

@@ -5,58 +5,87 @@ go 1.20
require ( require (
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-contrib/sessions v0.0.4
github.com/gin-gonic/gin v1.9.0 github.com/gin-gonic/gin v1.9.1
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
github.com/goccy/go-json v0.10.2 github.com/goccy/go-json v0.10.2
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.7 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.12.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.3
gorm.io/gorm v1.25.1 gorm.io/gorm v1.25.4
) )
require ( require (
github.com/BurntSushi/toml v1.2.1 // indirect github.com/BurntSushi/toml v1.2.1 // indirect
github.com/bytedance/sonic v1.8.7 // indirect github.com/andybalholm/brotli v1.0.5 // indirect
github.com/bytedance/sonic v1.9.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect
github.com/francoispqt/gojay v1.2.13 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gaukas/godicttls v0.0.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/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.12.0 // indirect github.com/go-playground/validator/v10 v10.14.0 // 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/cpuid/v2 v2.2.4 // indirect github.com/klauspost/compress v1.16.6 // indirect
github.com/leodido/go-urn v1.2.3 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect
github.com/mattn/go-isatty v0.0.18 // 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/shoenig/go-m1cpu v0.1.5 // 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.2 // indirect
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
github.com/sagernet/sing v0.2.5 // indirect
github.com/sagernet/sing-shadowsocks v0.2.2 // indirect
github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c // indirect
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/tklauser/go-sysconf v0.3.11 // indirect github.com/tklauser/go-sysconf v0.3.11 // indirect
github.com/tklauser/numcpus v0.6.0 // indirect github.com/tklauser/numcpus v0.6.0 // 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/yusufpapurcu/wmi v1.2.2 // indirect github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e // indirect
github.com/xtls/reality v0.0.0-20230613075828-e07c3b04b983 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
golang.org/x/arch v0.3.0 // indirect golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.8.0 // indirect golang.org/x/crypto v0.10.0 // indirect
golang.org/x/net v0.9.0 // indirect golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
golang.org/x/sys v0.7.0 // indirect golang.org/x/mod v0.11.0 // indirect
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect golang.org/x/net v0.11.0 // indirect
golang.org/x/sys v0.10.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.10.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect
google.golang.org/protobuf v1.30.0 // indirect google.golang.org/protobuf v1.30.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
) )

314
go.sum
View File

@@ -1,34 +1,64 @@
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.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
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/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/antonlindstrom/pgstore v0.0.0-20200229204646-b08ebf1105e0/go.mod h1:2Ti6VUHVxpC0VSmTZzEvpzysnaGAfGBOoMIz5ykPyyw= github.com/antonlindstrom/pgstore v0.0.0-20200229204646-b08ebf1105e0/go.mod h1:2Ti6VUHVxpC0VSmTZzEvpzysnaGAfGBOoMIz5ykPyyw=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw= github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI= github.com/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.7 h1:d3sry5vGgVq/OpgozRUNP6xBsSo0mtNdwliApw+SAMQ= github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.8.7/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/chenzhuoyu/base64x v0.0.0-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 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/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/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/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/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gaukas/godicttls v0.0.3 h1:YNDIf0d9adcxOijiLrEzpfZGAkNwLRzPaG6OjU7EITk= github.com/gaukas/godicttls v0.0.3 h1:YNDIf0d9adcxOijiLrEzpfZGAkNwLRzPaG6OjU7EITk=
github.com/gaukas/godicttls v0.0.3/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
github.com/ghodss/yaml v1.0.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 h1:gq4fNa1Zmp564iHP5G6EBuktilEos8VKhe2sza1KMgo=
github.com/gin-contrib/sessions v0.0.4/go.mod h1:pQ3sIyviBBGcxgyR8mkeJuXbeV3h3NYmhJADQTq5+Vo= 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.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= 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-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
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.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
@@ -40,26 +70,46 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+
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.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-playground/validator/v10 v10.12.0 h1:E4gtWgxWxp8YSxExrQFv5BpCahla0PVF2oTTEYaWQGI= github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.12.0/go.mod h1:hCAPuzYvKdP33pxWa+2+6AIKXEKqjIUyqsNCtbsSJrA= 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-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
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/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.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.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/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=
@@ -69,84 +119,152 @@ github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/z
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.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 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/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw= github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw=
github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.16.6 h1:91SKEy4K37vkp255cJ8QesJhjyRO0hn9i9G0GoUwLsk=
github.com/klauspost/compress v1.16.6/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.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/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.2.3 h1:6BE2vPT0lqoz3fmOesHZiaiFh7889ssCo2GMvLCfiuA= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.3/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/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/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.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.18/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/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc= github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc=
github.com/miekg/dns v1.1.53 h1:ZBkuHr5dxHtB1caEOlZTLPo7D3L3TWckgUUs/RHfDxw= github.com/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 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/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.7 h1:muncTPStnKRos5dpVKULv2FVd4bMOhNePj9CjgDb8Us= github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0=
github.com/pelletier/go-toml/v2 v2.0.7/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= 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.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 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/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/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg= github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
github.com/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U= github.com/quic-go/qtls-go1-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/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.2 h1:o+AkWB57mkcoW36ET7uJ002CpBWHu0KPxi6vzxvPnv8= github.com/refraction-networking/utls v1.3.2 h1:o+AkWB57mkcoW36ET7uJ002CpBWHu0KPxi6vzxvPnv8=
github.com/refraction-networking/utls v1.3.2/go.mod h1:fmoaOww2bxzzEpIKOebIsnBvjQpqP7L2vcm/9KUfm/E=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 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/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/sagernet/sing-shadowsocks v0.2.1 h1:FvdLQOqpvxHBJUcUe4fvgiYP2XLLwH5i1DtXQviVEPw= github.com/sagernet/sing v0.2.5 h1:N8sUluR8GZvR9DqUiH3FA3vBb4m/EDdOVTYUrDzJvmY=
github.com/sagernet/sing v0.2.5/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/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 h1:LF57Z/Fpb/WdGLjt2HZilNnmZOxg/q2bSKTQhgbrLrQ= github.com/shirou/gopsutil/v3 v3.23.7 h1:C+fHO8hfIppoJ1WdsVm1RoI0RwXoNdfTK7yWXV0wVj4=
github.com/shoenig/go-m1cpu v0.1.5/go.mod h1:Wwvst4LR89UxjeFtLRMrpgRiyY4xPsejnVZym39dbAQ= github.com/shirou/gopsutil/v3 v3.23.7/go.mod h1:c4gnmoRC0hQuaLqvxnx1//VXQ0Ms/X9UnJF8pddY5z4=
github.com/shoenig/test v0.6.3 h1:GVXWJFk9PiOjN0KoJ7VrJGH6uLPnqxR7/fe3HUPfE0c= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/test v0.6.3/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.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.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 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
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.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/tinylib/msgp v1.1.5/go.mod h1:eQsjooMTnV42mHu917E26IogZ2930nFyBQdofk10Udg= github.com/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=
@@ -160,95 +278,173 @@ github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLY
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/xtls/reality v0.0.0-20230331223127-176a94313eda h1:psRJD2RrZbnI0OWyHvXfgYCPqlRM5q5SPDcjDoDBWhE= github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU=
github.com/xtls/xray-core v1.8.1 h1:iSTTqXj82ZdwC1ah+eV331X4JTcnrDz+WuKuB/EB3P4= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
github.com/xtls/xray-core v1.8.1/go.mod h1:AXxSso0MZwUE4NhRocCfHCg73BtJ+T2dSpQVo1Cg9VM= github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
github.com/xtls/reality v0.0.0-20230613075828-e07c3b04b983 h1:AMyzgjkh54WocjQSlCnT1LhDc/BKiUqtNOv40AkpURs=
github.com/xtls/reality v0.0.0-20230613075828-e07c3b04b983/go.mod h1:rkuAY1S9F8eI8gDiPDYvACE8e2uwkyg8qoOTuwWov7Y=
github.com/xtls/xray-core v1.8.3 h1:lxaVklPjLKqUU4ua4qH8SBaRcAaNHlH+LmXOx0U/Ejg=
github.com/xtls/xray-core v1.8.3/go.mod h1:i7t4JFnq828P2+XK0XjGQ8W9x78iu+EJ7jI4l3sonIw=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.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 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
github.com/yusufpapurcu/wmi v1.2.2/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=
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.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.3.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.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
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.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
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-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 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.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.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0 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-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/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-20230530153820-e85fd2cbaebc h1:XSJ8Vk1SWuNr8S18z1NZSziL0CPIXLCCMDOEFtHBOFc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
google.golang.org/grpc v1.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.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/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.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.3 h1:7/0dUgX28KAcopdfbRWWl68Rflh6osa4rDh+m51KL2g=
gorm.io/driver/sqlite v1.5.1/go.mod h1:7MZZ2Z8bqyfSQA1gYEV6MagQWj3cpUkJj9Z+d1HEMEQ= gorm.io/driver/sqlite v1.5.3/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
gorm.io/gorm v1.25.1 h1:nsSALe5Pr+cM3V1qwwQ7rOkw+6UeLrX5O4v3llhHa64= gorm.io/gorm v1.25.4 h1:iyNd8fNAe8W9dvtlgeRI5zSVZPsq3OpcTu37cYcpCmw=
gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= gorm.io/gorm v1.25.4/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
gvisor.dev/gvisor v0.0.0-20220901235040-6ca97ef2ce1c h1:m5lcgWnL3OElQNVyp3qcncItJ2c0sQlSGjYK2+nJTA4= gvisor.dev/gvisor v0.0.0-20220901235040-6ca97ef2ce1c 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=
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

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

View File

@@ -1,25 +1,47 @@
package logger package logger
import ( import (
"github.com/op/go-logging" "fmt"
"os" "os"
"time"
"github.com/op/go-logging"
) )
var logger *logging.Logger var logger *logging.Logger
var logBuffer []struct {
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) {
format := logging.MustStringFormatter(
`%{time:2006/01/02 15:04:05} %{level} - %{message}`,
)
newLogger := logging.MustGetLogger("x-ui") newLogger := logging.MustGetLogger("x-ui")
backend := logging.NewLogBackend(os.Stderr, "", 0) var err error
var backend logging.Backend
var format logging.Formatter
ppid := os.Getppid()
if ppid == 1 {
backend, err = logging.NewSyslogBackend("")
format = logging.MustStringFormatter(
`%{level} - %{message}`,
)
}
if err != nil || ppid != 1 {
backend = logging.NewLogBackend(os.Stderr, "", 0)
format = logging.MustStringFormatter(
`%{time:2006/01/02 15:04:05} %{level} - %{message}`,
)
}
backendFormatter := logging.NewBackendFormatter(backend, format) backendFormatter := logging.NewBackendFormatter(backend, format)
backendLeveled := logging.AddModuleLevel(backendFormatter) backendLeveled := logging.AddModuleLevel(backendFormatter)
backendLeveled.SetLevel(level, "") backendLeveled.SetLevel(level, "x-ui")
newLogger.SetBackend(backendLeveled) newLogger.SetBackend(backendLeveled)
logger = newLogger logger = newLogger
@@ -27,32 +49,70 @@ func InitLogger(level logging.Level) {
func Debug(args ...interface{}) { func Debug(args ...interface{}) {
logger.Debug(args...) logger.Debug(args...)
addToBuffer("DEBUG", fmt.Sprint(args...))
} }
func Debugf(format string, args ...interface{}) { func Debugf(format string, args ...interface{}) {
logger.Debugf(format, args...) logger.Debugf(format, args...)
addToBuffer("DEBUG", fmt.Sprintf(format, args...))
} }
func Info(args ...interface{}) { func Info(args ...interface{}) {
logger.Info(args...) logger.Info(args...)
addToBuffer("INFO", fmt.Sprint(args...))
} }
func Infof(format string, args ...interface{}) { func Infof(format string, args ...interface{}) {
logger.Infof(format, args...) logger.Infof(format, args...)
addToBuffer("INFO", fmt.Sprintf(format, args...))
} }
func Warning(args ...interface{}) { func Warning(args ...interface{}) {
logger.Warning(args...) logger.Warning(args...)
addToBuffer("WARNING", fmt.Sprint(args...))
} }
func Warningf(format string, args ...interface{}) { func Warningf(format string, args ...interface{}) {
logger.Warningf(format, args...) logger.Warningf(format, args...)
addToBuffer("WARNING", fmt.Sprintf(format, args...))
} }
func Error(args ...interface{}) { func Error(args ...interface{}) {
logger.Error(args...) logger.Error(args...)
addToBuffer("ERROR", fmt.Sprint(args...))
} }
func Errorf(format string, args ...interface{}) { func Errorf(format string, args ...interface{}) {
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
} }

20
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"
@@ -252,10 +251,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
@@ -282,7 +277,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")
} }
@@ -303,16 +297,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 {
@@ -334,12 +318,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

@@ -3,12 +3,14 @@ package sub
import ( import (
"encoding/base64" "encoding/base64"
"strings" "strings"
"x-ui/web/service"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
type SUBController struct { type SUBController struct {
subService SubService subService SubService
settingService service.SettingService
} }
func NewSUBController(g *gin.RouterGroup) *SUBController { func NewSUBController(g *gin.RouterGroup) *SUBController {
@@ -24,9 +26,11 @@ func (a *SUBController) initRouter(g *gin.RouterGroup) {
} }
func (a *SUBController) subs(c *gin.Context) { func (a *SUBController) subs(c *gin.Context) {
subEncrypt, _ := a.settingService.GetSubEncrypt()
subShowInfo, _ := a.settingService.GetSubShowInfo()
subId := c.Param("subid") subId := c.Param("subid")
host := strings.Split(c.Request.Host, ":")[0] host := strings.Split(c.Request.Host, ":")[0]
subs, headers, err := a.subService.GetSubs(subId, host) subs, headers, err := a.subService.GetSubs(subId, host, subShowInfo)
if err != nil || len(subs) == 0 { if err != nil || len(subs) == 0 {
c.String(400, "Error!") c.String(400, "Error!")
} else { } else {
@@ -40,6 +44,10 @@ func (a *SUBController) subs(c *gin.Context) {
c.Writer.Header().Set("Profile-Update-Interval", headers[1]) c.Writer.Header().Set("Profile-Update-Interval", headers[1])
c.Writer.Header().Set("Profile-Title", headers[2]) c.Writer.Header().Set("Profile-Title", headers[2])
c.String(200, base64.StdEncoding.EncodeToString([]byte(result))) if subEncrypt {
c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
} else {
c.String(200, result)
}
} }
} }

View File

@@ -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"
@@ -16,12 +18,14 @@ import (
type SubService struct { type SubService struct {
address string address string
showInfo bool
inboundService service.InboundService inboundService service.InboundService
settingServics service.SettingService settingServics service.SettingService
} }
func (s *SubService) GetSubs(subId string, host string) ([]string, []string, error) { func (s *SubService) GetSubs(subId string, host string, showInfo bool) ([]string, []string, error) {
s.address = host s.address = host
s.showInfo = showInfo
var result []string var result []string
var headers []string var headers []string
var traffic xray.ClientTraffic var traffic xray.ClientTraffic
@@ -38,6 +42,21 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, []string, err
if clients == nil { if clients == nil {
continue continue
} }
if len(inbound.Listen) > 0 && inbound.Listen[0] == '@' {
fallbackMaster, err := s.getFallbackMaster(inbound.Listen)
if err == nil {
inbound.Listen = fallbackMaster.Listen
inbound.Port = fallbackMaster.Port
var stream map[string]interface{}
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
var masterStream map[string]interface{}
json.Unmarshal([]byte(fallbackMaster.StreamSettings), &masterStream)
stream["security"] = masterStream["security"]
stream["tlsSettings"] = masterStream["tlsSettings"]
modifiedStream, _ := json.MarshalIndent(stream, "", " ")
inbound.StreamSettings = string(modifiedStream)
}
}
for _, client := range clients { 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)
@@ -93,6 +112,19 @@ func (s *SubService) getClientTraffics(traffics []xray.ClientTraffic, email stri
return xray.ClientTraffic{} return xray.ClientTraffic{}
} }
func (s *SubService) getFallbackMaster(dest string) (*model.Inbound, error) {
db := database.GetDB()
var inbound *model.Inbound
err := db.Model(model.Inbound{}).
Where("JSON_TYPE(settings, '$.fallbacks') = 'array'").
Where("EXISTS (SELECT * FROM json_each(settings, '$.fallbacks') WHERE json_extract(value, '$.dest') = ?)", dest).
Find(&inbound).Error
if err != nil {
return nil, err
}
return inbound, nil
}
func (s *SubService) getLink(inbound *model.Inbound, email string) string { func (s *SubService) getLink(inbound *model.Inbound, email string) string {
switch inbound.Protocol { switch inbound.Protocol {
case "vmess": case "vmess":
@@ -111,10 +143,8 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
if inbound.Protocol != model.VMess { if inbound.Protocol != model.VMess {
return "" return ""
} }
remark := fmt.Sprintf("%s-%s", inbound.Remark, email)
obj := map[string]interface{}{ obj := map[string]interface{}{
"v": "2", "v": "2",
"ps": remark,
"add": s.address, "add": s.address,
"port": inbound.Port, "port": inbound.Port,
"type": "none", "type": "none",
@@ -208,13 +238,12 @@ 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 := ""
for index, d := range domains { for index, d := range domains {
domain := d.(map[string]interface{}) domain := d.(map[string]interface{})
obj["ps"] = remark + "-" + domain["remark"].(string) obj["ps"] = s.genRemark(inbound, email, domain["remark"].(string))
obj["add"] = domain["domain"].(string) obj["add"] = domain["domain"].(string)
if index > 0 { if index > 0 {
links += "\n" links += "\n"
@@ -225,6 +254,8 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
return links return links
} }
obj["ps"] = s.genRemark(inbound, email, "")
jsonStr, _ := json.MarshalIndent(obj, "", " ") jsonStr, _ := json.MarshalIndent(obj, "", " ")
return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr) return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
} }
@@ -380,14 +411,13 @@ 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)
if len(domains) > 0 { if len(domains) > 0 {
links := "" links := ""
for index, d := range domains { for index, d := range domains {
domain := d.(map[string]interface{}) domain := d.(map[string]interface{})
url.Fragment = remark + "-" + domain["remark"].(string) url.Fragment = s.genRemark(inbound, email, domain["remark"].(string))
url.Host = domain["domain"].(string) url.Host = fmt.Sprintf("%s:%d", domain["domain"].(string), port)
if index > 0 { if index > 0 {
links += "\n" links += "\n"
} }
@@ -396,7 +426,7 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
return links return links
} }
url.Fragment = remark url.Fragment = s.genRemark(inbound, email, "")
return url.String() return url.String()
} }
@@ -511,7 +541,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)
} }
@@ -531,10 +561,6 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
} }
} }
} }
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
params["flow"] = clients[clientIndex].Flow
}
} }
link := fmt.Sprintf("trojan://%s@%s:%d", password, address, port) link := fmt.Sprintf("trojan://%s@%s:%d", password, address, port)
@@ -549,13 +575,11 @@ 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)
if len(domains) > 0 { if len(domains) > 0 {
links := "" links := ""
for index, d := range domains { for index, d := range domains {
domain := d.(map[string]interface{}) domain := d.(map[string]interface{})
url.Fragment = remark + "-" + domain["remark"].(string) url.Fragment = s.genRemark(inbound, email, domain["remark"].(string))
url.Host = fmt.Sprintf("%s:%d", domain["domain"].(string), port) url.Host = fmt.Sprintf("%s:%d", domain["domain"].(string), port)
if index > 0 { if index > 0 {
links += "\n" links += "\n"
@@ -565,7 +589,7 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
return links return links
} }
url.Fragment = remark url.Fragment = s.genRemark(inbound, email, "")
return url.String() return url.String()
} }
@@ -634,7 +658,10 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
} }
} }
encPart := fmt.Sprintf("%s:%s:%s", method, inboundPassword, clients[clientIndex].Password) 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) link := fmt.Sprintf("ss://%s@%s:%d", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port)
url, _ := url.Parse(link) url, _ := url.Parse(link)
q := url.Query() q := url.Query()
@@ -645,12 +672,55 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
// Set the new query values on the URL // Set the new query values on the URL
url.RawQuery = q.Encode() url.RawQuery = q.Encode()
url.Fragment = s.genRemark(inbound, email, "")
remark := fmt.Sprintf("%s-%s", inbound.Remark, clients[clientIndex].Email)
url.Fragment = remark
return url.String() return url.String()
} }
func (s *SubService) genRemark(inbound *model.Inbound, email string, extra string) string {
var remark []string
if len(email) > 0 {
if len(inbound.Remark) > 0 {
remark = append(remark, inbound.Remark)
}
remark = append(remark, email)
if len(extra) > 0 {
remark = append(remark, extra)
}
} else {
return inbound.Remark
}
if s.showInfo {
statsExist := false
var stats xray.ClientTraffic
for _, clientStat := range inbound.ClientStats {
if clientStat.Email == email {
stats = clientStat
statsExist = true
break
}
}
// Get remained days
if statsExist {
if !stats.Enable {
return fmt.Sprintf("⛔N/A-%s", strings.Join(remark, "-"))
}
if vol := stats.Total - (stats.Up + stats.Down); vol > 0 {
remark = append(remark, fmt.Sprintf("%s%s", common.FormatTraffic(vol), "📊"))
}
now := time.Now().Unix()
switch exp := stats.ExpiryTime / 1000; {
case exp > 0:
remark = append(remark, fmt.Sprintf("%d%s⏳", (exp-now)/86400, "Days"))
case exp < 0:
remark = append(remark, fmt.Sprintf("%d%s⏳", exp/-86400, "Days"))
}
}
}
return strings.Join(remark, "-")
}
func searchKey(data interface{}, key string) (interface{}, bool) { func searchKey(data interface{}, key string) (interface{}, bool) {
switch val := data.(type) { switch val := data.(type) {
case map[string]interface{}: case map[string]interface{}:

View File

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

View File

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

View File

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

View File

@@ -197,7 +197,11 @@ body {
.ant-layout-sider-zero-width-trigger, .ant-layout-sider-zero-width-trigger,
.ant-dropdown-menu-dark,.ant-dropdown-menu-dark .ant-dropdown-menu, .ant-dropdown-menu-dark,.ant-dropdown-menu-dark .ant-dropdown-menu,
.ant-menu-dark.ant-menu-horizontal>.ant-menu-item,.ant-menu-dark.ant-menu-horizontal>.ant-menu-submenu { .ant-menu-dark.ant-menu-horizontal>.ant-menu-item,.ant-menu-dark.ant-menu-horizontal>.ant-menu-submenu {
background:#161b22 background:#1a212a
}
.ant-tabs:not(.ant-card-dark) {
background-color: white;
} }
.ant-card-dark { .ant-card-dark {
@@ -236,6 +240,7 @@ body {
.ant-card-dark .ant-collapse-content, .ant-card-dark .ant-collapse-content,
.ant-card-dark .ant-calendar, .ant-card-dark .ant-calendar,
.ant-card-dark .ant-table-placeholder, .ant-card-dark .ant-table-placeholder,
.ant-card-dark .ant-select-selection__choice,
.ant-card-dark .ant-input-group-addon { .ant-card-dark .ant-input-group-addon {
color: hsla(0,0%,100%,.65); color: hsla(0,0%,100%,.65);
background-color: #262f3d; background-color: #262f3d;

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

@@ -166,6 +166,7 @@ class AllSetting {
constructor(data) { constructor(data) {
this.webListen = ""; this.webListen = "";
this.webDomain = "";
this.webPort = 54321; this.webPort = 54321;
this.webCertFile = ""; this.webCertFile = "";
this.webKeyFile = ""; this.webKeyFile = "";
@@ -178,16 +179,20 @@ 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 = "";
this.xrayTemplateConfig = ""; this.xrayTemplateConfig = "";
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 = "";
this.subUpdates = 0; this.subUpdates = 0;
this.subEncrypt = true;
this.subShowInfo = false;
this.timeLocation = "Asia/Tehran"; this.timeLocation = "Asia/Tehran";

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,16 +16,15 @@ const VmessMethods = {
}; };
const SSMethods = { const SSMethods = {
// AES_256_CFB: 'aes-256-cfb', AES_256_GCM: 'aes-256-gcm',
// AES_128_CFB: 'aes-128-cfb', AES_128_GCM: 'aes-128-gcm',
// CHACHA20: 'chacha20', CHACHA20_POLY1305: 'chacha20-poly1305',
// CHACHA20_IETF: 'chacha20-ietf', CHACHA20_IETF_POLY1305: 'chacha20-ietf-poly1305',
// CHACHA20_POLY1305: 'chacha20-poly1305', XCHACHA20_POLY1305: 'xchacha20-poly1305',
// AES_256_GCM: 'aes-256-gcm', XCHACHA20_IETF_POLY1305: 'xchacha20-ietf-poly1305',
// AES_128_GCM: 'aes-128-gcm',
BLAKE3_AES_128_GCM: '2022-blake3-aes-128-gcm', BLAKE3_AES_128_GCM: '2022-blake3-aes-128-gcm',
BLAKE3_AES_256_GCM: '2022-blake3-aes-256-gcm', BLAKE3_AES_256_GCM: '2022-blake3-aes-256-gcm',
// BLAKE3_CHACHA20_POLY1305: '2022-blake3-chacha20-poly1305', BLAKE3_CHACHA20_POLY1305: '2022-blake3-chacha20-poly1305',
}; };
const TLS_FLOW_CONTROL = { const TLS_FLOW_CONTROL = {
@@ -39,7 +37,7 @@ const TLS_VERSION_OPTION = {
TLS11: "1.1", TLS11: "1.1",
TLS12: "1.2", TLS12: "1.2",
TLS13: "1.3", TLS13: "1.3",
} };
const TLS_CIPHER_OPTION = { const TLS_CIPHER_OPTION = {
RSA_AES_128_CBC: "TLS_RSA_WITH_AES_128_CBC_SHA", RSA_AES_128_CBC: "TLS_RSA_WITH_AES_128_CBC_SHA",
@@ -75,15 +73,16 @@ const UTLS_FINGERPRINT = {
}; };
const ALPN_OPTION = { const ALPN_OPTION = {
HTTP1: "http/1.1",
H2: "h2",
H3: "h3", H3: "h3",
H2: "h2",
HTTP1: "http/1.1",
}; };
const SNIFFING_OPTION = { const SNIFFING_OPTION = {
HTTP: "http", HTTP: "http",
TLS: "tls", TLS: "tls",
QUIC: "quic", QUIC: "quic",
FAKEDNS: "fakedns"
}; };
Object.freeze(Protocols); Object.freeze(Protocols);
@@ -462,9 +461,10 @@ class GrpcStreamSettings extends XrayCommonClass {
class TlsStreamSettings extends XrayCommonClass { class TlsStreamSettings extends XrayCommonClass {
constructor(serverName='', constructor(serverName='',
minVersion = TLS_VERSION_OPTION.TLS10, minVersion = TLS_VERSION_OPTION.TLS12,
maxVersion = TLS_VERSION_OPTION.TLS12, maxVersion = TLS_VERSION_OPTION.TLS13,
cipherSuites = '', cipherSuites = '',
rejectUnknownSni = false,
certificates=[new TlsStreamSettings.Cert()], certificates=[new TlsStreamSettings.Cert()],
alpn=[], alpn=[],
settings=new TlsStreamSettings.Settings()) { settings=new TlsStreamSettings.Settings()) {
@@ -473,6 +473,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;
@@ -501,6 +502,7 @@ class TlsStreamSettings extends XrayCommonClass {
json.minVersion, json.minVersion,
json.maxVersion, json.maxVersion,
json.cipherSuites, json.cipherSuites,
json.rejectUnknownSni,
certs, certs,
json.alpn, json.alpn,
settings, settings,
@@ -513,6 +515,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,
@@ -521,13 +524,14 @@ class TlsStreamSettings extends XrayCommonClass {
} }
TlsStreamSettings.Cert = class extends XrayCommonClass { TlsStreamSettings.Cert = class extends XrayCommonClass {
constructor(useFile=true, certificateFile='', keyFile='', certificate='', key='') { constructor(useFile=true, certificateFile='', keyFile='', certificate='', key='', ocspStapling=3600) {
super(); super();
this.useFile = useFile; this.useFile = useFile;
this.certFile = certificateFile; this.certFile = certificateFile;
this.keyFile = keyFile; this.keyFile = keyFile;
this.cert = certificate instanceof Array ? certificate.join('\n') : certificate; this.cert = certificate instanceof Array ? certificate.join('\n') : certificate;
this.key = key instanceof Array ? key.join('\n') : key; this.key = key instanceof Array ? key.join('\n') : key;
this.ocspStapling = ocspStapling;
} }
static fromJson(json={}) { static fromJson(json={}) {
@@ -535,13 +539,15 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
return new TlsStreamSettings.Cert( return new TlsStreamSettings.Cert(
true, true,
json.certificateFile, json.certificateFile,
json.keyFile, json.keyFile, '', '',
json.ocspStapling,
); );
} else { } else {
return new TlsStreamSettings.Cert( return new TlsStreamSettings.Cert(
false, '', '', false, '', '',
json.certificate.join('\n'), json.certificate.join('\n'),
json.key.join('\n'), json.key.join('\n'),
json.ocspStapling,
); );
} }
} }
@@ -551,11 +557,13 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
return { return {
certificateFile: this.certFile, certificateFile: this.certFile,
keyFile: this.keyFile, keyFile: this.keyFile,
ocspStapling: this.ocspStapling,
}; };
} else { } else {
return { return {
certificate: this.cert.split('\n'), certificate: this.cert.split('\n'),
key: this.key.split('\n'), key: this.key.split('\n'),
ocspStapling: this.ocspStapling,
}; };
} }
} }
@@ -592,7 +600,7 @@ class RealityStreamSettings extends XrayCommonClass {
dest = 'microsoft.com:443', dest = 'microsoft.com:443',
serverNames = 'microsoft.com,www.microsoft.com', serverNames = 'microsoft.com,www.microsoft.com',
privateKey = '', minClient = '', maxClient = '', privateKey = '', minClient = '', maxClient = '',
maxTimediff = 0, shortIds = [], maxTimediff = 0, shortIds = RandomUtil.randomShortId(),
settings= new RealityStreamSettings.Settings()) { settings= new RealityStreamSettings.Settings()) {
super(); super();
this.show = show; this.show = show;
@@ -668,6 +676,35 @@ RealityStreamSettings.Settings = class extends XrayCommonClass {
} }
}; };
class SockoptStreamSettings extends XrayCommonClass {
constructor(acceptProxyProtocol = false, tcpFastOpen = false, mark = 0, tproxy="off") {
super();
this.acceptProxyProtocol = acceptProxyProtocol;
this.tcpFastOpen = tcpFastOpen;
this.mark = mark;
this.tproxy = tproxy;
}
static fromJson(json = {}) {
if (Object.keys(json).length === 0) return undefined;
return new SockoptStreamSettings(
json.acceptProxyProtocol,
json.tcpFastOpen,
json.mark,
json.tproxy,
);
}
toJson() {
return {
acceptProxyProtocol: this.acceptProxyProtocol,
tcpFastOpen: this.tcpFastOpen,
mark: this.mark,
tproxy: this.tproxy,
};
}
}
class StreamSettings extends XrayCommonClass { class StreamSettings extends XrayCommonClass {
constructor(network='tcp', constructor(network='tcp',
security='none', security='none',
@@ -679,6 +716,7 @@ class StreamSettings extends XrayCommonClass {
httpSettings=new HttpStreamSettings(), httpSettings=new HttpStreamSettings(),
quicSettings=new QuicStreamSettings(), quicSettings=new QuicStreamSettings(),
grpcSettings=new GrpcStreamSettings(), grpcSettings=new GrpcStreamSettings(),
sockopt = undefined,
) { ) {
super(); super();
this.network = network; this.network = network;
@@ -691,6 +729,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() {
@@ -717,6 +756,14 @@ class StreamSettings extends XrayCommonClass {
} }
} }
get sockoptSwitch() {
return this.sockopt != undefined;
}
set sockoptSwitch(value) {
this.sockopt = value ? new SockoptStreamSettings() : undefined;
}
static fromJson(json={}) { static fromJson(json={}) {
return new StreamSettings( return new StreamSettings(
@@ -730,6 +777,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),
); );
} }
@@ -746,12 +794,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.sockopt != undefined ? 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;
@@ -761,7 +810,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(
@@ -872,6 +921,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.isReality) { if (this.stream.isTls || this.stream.isReality) {
@@ -897,7 +952,7 @@ class Inbound extends XrayCommonClass {
} else if (this.isWs) { } else if (this.isWs) {
return this.stream.ws.path; return this.stream.ws.path;
} else if (this.isH2) { } else if (this.isH2) {
return this.stream.http.path[0]; return this.stream.http.path;
} }
return null; return null;
} }
@@ -941,7 +996,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:
@@ -1051,7 +1106,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,
@@ -1272,7 +1326,11 @@ class Inbound extends XrayCommonClass {
break; break;
} }
let link = `ss://${safeBase64(settings.method + ':' + settings.password + ':' +settings.shadowsockses[clientIndex].password)}@${address}:${this.port}`; let password = new Array();
if (this.isSS2022) password.push(settings.password);
if (this.isSSMultiUser) password.push(settings.shadowsockses[clientIndex].password);
let link = `ss://${safeBase64(settings.method + ':' + password.join(':'))}@${address}:${this.port}`;
const url = new URL(link); const url = new URL(link);
for (const [key, value] of params) { for (const [key, value] of params) {
url.searchParams.set(key, value) url.searchParams.set(key, value)
@@ -1366,9 +1424,6 @@ class Inbound extends XrayCommonClass {
if (!ObjectUtil.isEmpty(this.stream.reality.settings.spiderX)) { if (!ObjectUtil.isEmpty(this.stream.reality.settings.spiderX)) {
params.set("spx", this.stream.reality.settings.spiderX); params.set("spx", this.stream.reality.settings.spiderX);
} }
if (this.stream.network === 'tcp' && !ObjectUtil.isEmpty(this.settings.trojans[clientIndex].flow)) {
params.set("flow", this.settings.trojans[clientIndex].flow);
}
} }
const link = `trojan://${settings.trojans[clientIndex].password}@${address}:${this.port}`; const link = `trojan://${settings.trojans[clientIndex].password}@${address}:${this.port}`;
@@ -1404,10 +1459,10 @@ class Inbound extends XrayCommonClass {
JSON.parse(this.settings).clients.forEach((client,index) => { JSON.parse(this.settings).clients.forEach((client,index) => {
if(this.tls && !ObjectUtil.isArrEmpty(this.stream.tls.settings.domains)){ if(this.tls && !ObjectUtil.isArrEmpty(this.stream.tls.settings.domains)){
this.stream.tls.settings.domains.forEach((domain) => { this.stream.tls.settings.domains.forEach((domain) => {
link += this.genLink(domain.domain, remark + '-' + client.email + '-' + domain.remark, index) + '\r\n'; link += this.genLink(domain.domain, [remark, client.email, domain.remark].filter(x => x.length > 0).join('-'), index) + '\r\n';
}); });
} else { } else {
link += this.genLink(address, remark + '-' + client.email, index) + '\r\n'; link += this.genLink(address, [remark, client.email].filter(x => x.length > 0).join('-'), index) + '\r\n';
} }
}); });
return link; return link;
@@ -1459,7 +1514,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;
@@ -1473,7 +1527,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;
@@ -1487,11 +1540,9 @@ Inbound.Settings = class extends XrayCommonClass {
Inbound.VmessSettings = class extends Inbound.Settings { Inbound.VmessSettings = class extends Inbound.Settings {
constructor(protocol, constructor(protocol,
vmesses=[new Inbound.VmessSettings.Vmess()], vmesses=[new Inbound.VmessSettings.Vmess()]) {
disableInsecureEncryption=false) {
super(protocol); super(protocol);
this.vmesses = vmesses; this.vmesses = vmesses;
this.disableInsecure = disableInsecureEncryption;
} }
indexOfVmessById(id) { indexOfVmessById(id) {
@@ -1516,22 +1567,19 @@ Inbound.VmessSettings = class extends Inbound.Settings {
return new Inbound.VmessSettings( return new Inbound.VmessSettings(
Protocols.VMESS, Protocols.VMESS,
json.clients.map(client => Inbound.VmessSettings.Vmess.fromJson(client)), json.clients.map(client => Inbound.VmessSettings.Vmess.fromJson(client)),
ObjectUtil.isEmpty(json.disableInsecureEncryption) ? false : json.disableInsecureEncryption,
); );
} }
toJson() { toJson() {
return { return {
clients: Inbound.VmessSettings.toJsonArray(this.vmesses), clients: Inbound.VmessSettings.toJsonArray(this.vmesses),
disableInsecureEncryption: this.disableInsecure,
}; };
} }
}; };
Inbound.VmessSettings.Vmess = class extends XrayCommonClass { Inbound.VmessSettings.Vmess = class extends XrayCommonClass {
constructor(id=RandomUtil.randomUUID(), alterId=0, email=RandomUtil.randomText(), totalGB=0, expiryTime=0, enable=true, tgId='', subId='') { constructor(id=RandomUtil.randomUUID(), email=RandomUtil.randomLowerAndNum(9), 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.totalGB = totalGB; this.totalGB = totalGB;
this.expiryTime = expiryTime; this.expiryTime = expiryTime;
@@ -1543,7 +1591,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.totalGB, json.totalGB,
json.expiryTime, json.expiryTime,
@@ -1618,7 +1665,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(), totalGB=0, expiryTime=0, enable=true, tgId='', subId='') { constructor(id=RandomUtil.randomUUID(), flow='', email=RandomUtil.randomLowerAndNum(9), 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;
@@ -1739,10 +1786,9 @@ 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(), totalGB=0, expiryTime=0, enable=true, tgId='', subId='') { constructor(password=RandomUtil.randomSeq(10), email=RandomUtil.randomLowerAndNum(9), totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16)) {
super(); super();
this.password = password; this.password = password;
this.flow = flow;
this.email = email; this.email = email;
this.totalGB = totalGB; this.totalGB = totalGB;
this.expiryTime = expiryTime; this.expiryTime = expiryTime;
@@ -1754,7 +1800,6 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
toJson() { toJson() {
return { return {
password: this.password, password: this.password,
flow: this.flow,
email: this.email, email: this.email,
totalGB: this.totalGB, totalGB: this.totalGB,
expiryTime: this.expiryTime, expiryTime: this.expiryTime,
@@ -1767,7 +1812,6 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
static fromJson(json = {}) { static fromJson(json = {}) {
return new Inbound.TrojanSettings.Trojan( return new Inbound.TrojanSettings.Trojan(
json.password, json.password,
json.flow,
json.email, json.email,
json.totalGB, json.totalGB,
json.expiryTime, json.expiryTime,
@@ -1878,8 +1922,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(), totalGB=0, expiryTime=0, enable=true, tgId='', subId='') { constructor(method='', password=RandomUtil.randomShadowsocksPassword(), email=RandomUtil.randomLowerAndNum(9), 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.totalGB = totalGB; this.totalGB = totalGB;
@@ -1891,6 +1936,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,
totalGB: this.totalGB, totalGB: this.totalGB,
@@ -1903,6 +1949,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.totalGB, json.totalGB,
@@ -1969,36 +2016,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

@@ -114,3 +114,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

@@ -119,7 +119,7 @@ Date.prototype.formatTime = function () {
}; };
/** /**
* 格式化日期加时间 * Formatting date plus time
* *
* @param split Division between date and time, the default is a space * @param split Division between date and time, the default is a space
*/ */

View File

@@ -75,17 +75,7 @@ class PromiseUtil {
} }
} }
const seq = [ const seq = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
'a', 'b', 'c', 'd', 'e', 'f', 'g',
'h', 'i', 'j', 'k', 'l', 'm', 'n',
'o', 'p', 'q', 'r', 's', 't',
'u', 'v', 'w', 'x', 'y', 'z',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'A', 'B', 'C', 'D', 'E', 'F', 'G',
'H', 'I', 'J', 'K', 'L', 'M', 'N',
'O', 'P', 'Q', 'R', 'S', 'T',
'U', 'V', 'W', 'X', 'Y', 'Z'
];
class RandomUtil { class RandomUtil {
static randomIntRange(min, max) { static randomIntRange(min, max) {
@@ -112,19 +102,6 @@ class RandomUtil {
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(); let d = new Date().getTime();
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
@@ -134,21 +111,22 @@ class RandomUtil {
}); });
} }
static randomText(minLen = 6, varLen = 5) {
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
var string = '';
var len = minLen + Math.floor(Math.random() * varLen);
for (var ii = 0; ii < len; ii++) {
string += chars[Math.floor(Math.random() * chars.length)];
}
return string;
}
static randomShadowsocksPassword() { static randomShadowsocksPassword() {
let array = new Uint8Array(32); let array = new Uint8Array(32);
window.crypto.getRandomValues(array); window.crypto.getRandomValues(array);
return btoa(String.fromCharCode.apply(null, array)); return btoa(String.fromCharCode.apply(null, array));
} }
static randomShortId() {
let shortIds = ['','','',''];
for (var ii = 0; ii < 4; ii++) {
for (var jj = 0; jj < this.randomInt(8); jj++){
let randomNum = this.randomInt(256);
shortIds[ii] += ('0' + randomNum.toString(16)).slice(-2)
}
}
return shortIds;
}
} }
class ObjectUtil { class ObjectUtil {

View File

@@ -1,10 +1,15 @@
package controller package controller
import "github.com/gin-gonic/gin" import (
"x-ui/web/service"
"github.com/gin-gonic/gin"
)
type APIController struct { type APIController struct {
BaseController BaseController
inboundController *InboundController inboundController *InboundController
Tgbot service.Tgbot
} }
func NewAPIController(g *gin.RouterGroup) *APIController { func NewAPIController(g *gin.RouterGroup) *APIController {
@@ -30,6 +35,7 @@ func (a *APIController) 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)
g.GET("/createbackup", a.createBackup)
a.inboundController = NewInboundController(g) a.inboundController = NewInboundController(g)
} }
@@ -37,39 +43,55 @@ func (a *APIController) initRouter(g *gin.RouterGroup) {
func (a *APIController) inbounds(c *gin.Context) { func (a *APIController) inbounds(c *gin.Context) {
a.inboundController.getInbounds(c) a.inboundController.getInbounds(c)
} }
func (a *APIController) inbound(c *gin.Context) { func (a *APIController) inbound(c *gin.Context) {
a.inboundController.getInbound(c) a.inboundController.getInbound(c)
} }
func (a *APIController) getClientTraffics(c *gin.Context) { func (a *APIController) getClientTraffics(c *gin.Context) {
a.inboundController.getClientTraffics(c) a.inboundController.getClientTraffics(c)
} }
func (a *APIController) addInbound(c *gin.Context) { func (a *APIController) addInbound(c *gin.Context) {
a.inboundController.addInbound(c) a.inboundController.addInbound(c)
} }
func (a *APIController) delInbound(c *gin.Context) { func (a *APIController) delInbound(c *gin.Context) {
a.inboundController.delInbound(c) a.inboundController.delInbound(c)
} }
func (a *APIController) updateInbound(c *gin.Context) { func (a *APIController) updateInbound(c *gin.Context) {
a.inboundController.updateInbound(c) a.inboundController.updateInbound(c)
} }
func (a *APIController) addInboundClient(c *gin.Context) { func (a *APIController) addInboundClient(c *gin.Context) {
a.inboundController.addInboundClient(c) a.inboundController.addInboundClient(c)
} }
func (a *APIController) delInboundClient(c *gin.Context) { func (a *APIController) delInboundClient(c *gin.Context) {
a.inboundController.delInboundClient(c) a.inboundController.delInboundClient(c)
} }
func (a *APIController) updateInboundClient(c *gin.Context) { func (a *APIController) updateInboundClient(c *gin.Context) {
a.inboundController.updateInboundClient(c) a.inboundController.updateInboundClient(c)
} }
func (a *APIController) resetClientTraffic(c *gin.Context) { func (a *APIController) resetClientTraffic(c *gin.Context) {
a.inboundController.resetClientTraffic(c) a.inboundController.resetClientTraffic(c)
} }
func (a *APIController) resetAllTraffics(c *gin.Context) { func (a *APIController) resetAllTraffics(c *gin.Context) {
a.inboundController.resetAllTraffics(c) a.inboundController.resetAllTraffics(c)
} }
func (a *APIController) resetAllClientTraffics(c *gin.Context) { func (a *APIController) resetAllClientTraffics(c *gin.Context) {
a.inboundController.resetAllClientTraffics(c) a.inboundController.resetAllClientTraffics(c)
} }
func (a *APIController) delDepletedClients(c *gin.Context) { func (a *APIController) delDepletedClients(c *gin.Context) {
a.inboundController.delDepletedClients(c) a.inboundController.delDepletedClients(c)
} }
func (a *APIController) createBackup(c *gin.Context) {
a.Tgbot.SendBackupToAdmins()
}

View File

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

View File

@@ -58,20 +58,21 @@ func (a *InboundController) getInbounds(c *gin.Context) {
user := session.GetLoginUser(c) user := session.GetLoginUser(c)
inbounds, err := a.inboundService.GetInbounds(user.Id) inbounds, err := a.inboundService.GetInbounds(user.Id)
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.toasts.obtain"), err) jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
return return
} }
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 {
jsonMsg(c, I18n(c, "get"), err) jsonMsg(c, I18nWeb(c, "get"), err)
return return
} }
inbound, err := a.inboundService.GetInbound(id) inbound, err := a.inboundService.GetInbound(id)
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.toasts.obtain"), err) jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
return return
} }
jsonObj(c, inbound, nil) jsonObj(c, inbound, nil)
@@ -90,16 +91,17 @@ func (a *InboundController) addInbound(c *gin.Context) {
inbound := &model.Inbound{} inbound := &model.Inbound{}
err := c.ShouldBind(inbound) err := c.ShouldBind(inbound)
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.create"), err) jsonMsg(c, I18nWeb(c, "pages.inbounds.create"), err)
return return
} }
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)
jsonMsgObj(c, I18n(c, "pages.inbounds.create"), inbound, err) needRestart := false
if err == nil { inbound, needRestart, err = a.inboundService.AddInbound(inbound)
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.create"), inbound, err)
if err == nil && needRestart {
a.xrayService.SetToNeedRestart() a.xrayService.SetToNeedRestart()
} }
} }
@@ -107,12 +109,13 @@ func (a *InboundController) addInbound(c *gin.Context) {
func (a *InboundController) delInbound(c *gin.Context) { func (a *InboundController) delInbound(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "delete"), err) jsonMsg(c, I18nWeb(c, "delete"), err)
return return
} }
err = a.inboundService.DelInbound(id) needRestart := true
jsonMsgObj(c, I18n(c, "delete"), id, err) needRestart, err = a.inboundService.DelInbound(id)
if err == nil { jsonMsgObj(c, I18nWeb(c, "delete"), id, err)
if err == nil && needRestart {
a.xrayService.SetToNeedRestart() a.xrayService.SetToNeedRestart()
} }
} }
@@ -120,7 +123,7 @@ func (a *InboundController) delInbound(c *gin.Context) {
func (a *InboundController) updateInbound(c *gin.Context) { func (a *InboundController) updateInbound(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.update"), err) jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
return return
} }
inbound := &model.Inbound{ inbound := &model.Inbound{
@@ -128,12 +131,13 @@ func (a *InboundController) updateInbound(c *gin.Context) {
} }
err = c.ShouldBind(inbound) err = c.ShouldBind(inbound)
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.update"), err) jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
return return
} }
inbound, err = a.inboundService.UpdateInbound(inbound) needRestart := true
jsonMsgObj(c, I18n(c, "pages.inbounds.update"), inbound, err) inbound, needRestart, err = a.inboundService.UpdateInbound(inbound)
if err == nil { jsonMsgObj(c, I18nWeb(c, "pages.inbounds.update"), inbound, err)
if err == nil && needRestart {
a.xrayService.SetToNeedRestart() a.xrayService.SetToNeedRestart()
} }
} }
@@ -142,17 +146,19 @@ func (a *InboundController) addInboundClient(c *gin.Context) {
data := &model.Inbound{} data := &model.Inbound{}
err := c.ShouldBind(data) err := c.ShouldBind(data)
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.update"), err) jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
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()
} }
} }
@@ -160,18 +166,20 @@ func (a *InboundController) addInboundClient(c *gin.Context) {
func (a *InboundController) delInboundClient(c *gin.Context) { func (a *InboundController) delInboundClient(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.update"), err) jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
return return
} }
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()
} }
} }
@@ -182,17 +190,19 @@ func (a *InboundController) updateInboundClient(c *gin.Context) {
inbound := &model.Inbound{} inbound := &model.Inbound{}
err := c.ShouldBind(inbound) err := c.ShouldBind(inbound)
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.update"), err) jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
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()
} }
} }
@@ -200,18 +210,20 @@ func (a *InboundController) updateInboundClient(c *gin.Context) {
func (a *InboundController) resetClientTraffic(c *gin.Context) { func (a *InboundController) resetClientTraffic(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.update"), err) jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
return return
} }
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()
} }
} }
@@ -221,6 +233,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)
} }
@@ -228,7 +242,7 @@ func (a *InboundController) resetAllTraffics(c *gin.Context) {
func (a *InboundController) resetAllClientTraffics(c *gin.Context) { func (a *InboundController) resetAllClientTraffics(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.update"), err) jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
return return
} }
@@ -236,6 +250,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)
} }
@@ -243,7 +259,7 @@ func (a *InboundController) resetAllClientTraffics(c *gin.Context) {
func (a *InboundController) delDepletedClients(c *gin.Context) { func (a *InboundController) delDepletedClients(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.update"), err) jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
return return
} }
err = a.inboundService.DelDepletedClients(id) err = a.inboundService.DelDepletedClients(id)

View File

@@ -47,26 +47,27 @@ func (a *IndexController) login(c *gin.Context) {
var form LoginForm var form LoginForm
err := c.ShouldBind(&form) err := c.ShouldBind(&form)
if err != nil { if err != nil {
pureJsonMsg(c, false, I18n(c, "pages.login.toasts.invalidFormData")) pureJsonMsg(c, false, I18nWeb(c, "pages.login.toasts.invalidFormData"))
return return
} }
if form.Username == "" { if form.Username == "" {
pureJsonMsg(c, false, I18n(c, "pages.login.toasts.emptyUsername")) pureJsonMsg(c, false, I18nWeb(c, "pages.login.toasts.emptyUsername"))
return return
} }
if form.Password == "" { if form.Password == "" {
pureJsonMsg(c, false, I18n(c, "pages.login.toasts.emptyPassword")) pureJsonMsg(c, false, I18nWeb(c, "pages.login.toasts.emptyPassword"))
return return
} }
user := a.userService.CheckUser(form.Username, form.Password) user := a.userService.CheckUser(form.Username, form.Password)
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 {
a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0)
logger.Infof("wrong username or password: \"%s\" \"%s\"", form.Username, form.Password) logger.Infof("wrong username or password: \"%s\" \"%s\"", form.Username, form.Password)
pureJsonMsg(c, false, I18n(c, "pages.login.toasts.wrongUsernameOrPassword")) a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0)
pureJsonMsg(c, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword"))
return return
} else { } else {
logger.Infof("%s login success,Ip Address:%s\n", form.Username, getRemoteIp(c)) logger.Infof("%s login success ,Ip Address: %s\n", form.Username, getRemoteIp(c))
a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 1) a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 1)
} }
@@ -84,7 +85,7 @@ func (a *IndexController) login(c *gin.Context) {
err = session.SetLoginUser(c, user) err = session.SetLoginUser(c, user)
logger.Info("user", user.Id, "login success") logger.Info("user", user.Id, "login success")
jsonMsg(c, I18n(c, "pages.login.toasts.successLogin"), err) jsonMsg(c, I18nWeb(c, "pages.login.toasts.successLogin"), err)
} }
func (a *IndexController) logout(c *gin.Context) { func (a *IndexController) logout(c *gin.Context) {

View File

@@ -81,7 +81,7 @@ func (a *ServerController) getXrayVersion(c *gin.Context) {
versions, err := a.serverService.GetXrayVersions() versions, err := a.serverService.GetXrayVersions()
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "getVersion"), err) jsonMsg(c, I18nWeb(c, "getVersion"), err)
return return
} }
@@ -94,7 +94,7 @@ func (a *ServerController) getXrayVersion(c *gin.Context) {
func (a *ServerController) installXray(c *gin.Context) { func (a *ServerController) installXray(c *gin.Context) {
version := c.Param("version") version := c.Param("version")
err := a.serverService.UpdateXray(version) err := a.serverService.UpdateXray(version)
jsonMsg(c, I18n(c, "install")+" xray", err) jsonMsg(c, I18nWeb(c, "install")+" xray", err)
} }
func (a *ServerController) stopXrayService(c *gin.Context) { func (a *ServerController) stopXrayService(c *gin.Context) {
@@ -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

@@ -43,84 +43,51 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) {
func (a *SettingController) getAllSetting(c *gin.Context) { func (a *SettingController) getAllSetting(c *gin.Context) {
allSetting, err := a.settingService.GetAllSetting() allSetting, err := a.settingService.GetAllSetting()
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err) jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return return
} }
jsonObj(c, allSetting, nil) jsonObj(c, allSetting, nil)
} }
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, I18n(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() },
"subEncrypt": func() (interface{}, error) { return a.settingService.GetSubEncrypt() },
"subShowInfo": func() (interface{}, error) { return a.settingService.GetSubShowInfo() },
} }
trafficDiff, err := a.settingService.GetTrafficDiff()
if err != nil { result := make(map[string]interface{})
jsonMsg(c, I18n(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, I18n(c, "pages.settings.toasts.getSettings"), err) return
return }
} result[key] = value
defaultKey, err := a.settingService.GetKeyFile()
if err != nil {
jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
return
}
tgBotEnable, err := a.settingService.GetTgbotenabled()
if err != nil {
jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
return
}
subEnable, err := a.settingService.GetSubEnable()
if err != nil {
jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
return
}
subPort, err := a.settingService.GetSubPort()
if err != nil {
jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
return
}
subPath, err := a.settingService.GetSubPath()
if err != nil {
jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
return
}
subDomain, err := a.settingService.GetSubDomain()
if err != nil {
jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
return
}
subKeyFile, err := a.settingService.GetSubKeyFile()
if err != nil {
jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
return
}
subCertFile, err := a.settingService.GetSubCertFile()
if err != nil {
jsonMsg(c, I18n(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)
} }
@@ -128,27 +95,27 @@ func (a *SettingController) updateSetting(c *gin.Context) {
allSetting := &entity.AllSetting{} allSetting := &entity.AllSetting{}
err := c.ShouldBind(allSetting) err := c.ShouldBind(allSetting)
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.settings.toasts.modifySettings"), err) jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
return return
} }
err = a.settingService.UpdateAllSetting(allSetting) err = a.settingService.UpdateAllSetting(allSetting)
jsonMsg(c, I18n(c, "pages.settings.toasts.modifySettings"), err) jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
} }
func (a *SettingController) updateUser(c *gin.Context) { func (a *SettingController) updateUser(c *gin.Context) {
form := &updateUserForm{} form := &updateUserForm{}
err := c.ShouldBind(form) err := c.ShouldBind(form)
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.settings.toasts.modifySettings"), err) jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
return return
} }
user := session.GetLoginUser(c) user := session.GetLoginUser(c)
if user.Username != form.OldUsername || user.Password != form.OldPassword { if user.Username != form.OldUsername || user.Password != form.OldPassword {
jsonMsg(c, I18n(c, "pages.settings.toasts.modifyUser"), errors.New(I18n(c, "pages.settings.toasts.originalUserPassIncorrect"))) jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), errors.New(I18nWeb(c, "pages.settings.toasts.originalUserPassIncorrect")))
return return
} }
if form.NewUsername == "" || form.NewPassword == "" { if form.NewUsername == "" || form.NewPassword == "" {
jsonMsg(c, I18n(c, "pages.settings.toasts.modifyUser"), errors.New(I18n(c, "pages.settings.toasts.userPassMustBeNotEmpty"))) jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), errors.New(I18nWeb(c, "pages.settings.toasts.userPassMustBeNotEmpty")))
return return
} }
err = a.userService.UpdateUser(user.Id, form.NewUsername, form.NewPassword) err = a.userService.UpdateUser(user.Id, form.NewUsername, form.NewPassword)
@@ -157,18 +124,18 @@ func (a *SettingController) updateUser(c *gin.Context) {
user.Password = form.NewPassword user.Password = form.NewPassword
session.SetLoginUser(c, user) session.SetLoginUser(c, user)
} }
jsonMsg(c, I18n(c, "pages.settings.toasts.modifyUser"), err) jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err)
} }
func (a *SettingController) restartPanel(c *gin.Context) { func (a *SettingController) restartPanel(c *gin.Context) {
err := a.panelService.RestartPanel(time.Second * 3) err := a.panelService.RestartPanel(time.Second * 3)
jsonMsg(c, I18n(c, "pages.settings.restartPanel"), err) jsonMsg(c, I18nWeb(c, "pages.settings.restartPanel"), err)
} }
func (a *SettingController) getDefaultXrayConfig(c *gin.Context) { func (a *SettingController) getDefaultXrayConfig(c *gin.Context) {
defaultJsonConfig, err := a.settingService.GetDefaultXrayConfig() defaultJsonConfig, err := a.settingService.GetDefaultXrayConfig()
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err) jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return return
} }
jsonObj(c, defaultJsonConfig, nil) jsonObj(c, defaultJsonConfig, nil)

View File

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

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,7 +41,9 @@ 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"`
XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"` XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"`
TimeLocation string `json:"timeLocation" form:"timeLocation"` TimeLocation string `json:"timeLocation" form:"timeLocation"`
SubEnable bool `json:"subEnable" form:"subEnable"` SubEnable bool `json:"subEnable" form:"subEnable"`
@@ -51,6 +54,8 @@ type AllSetting struct {
SubCertFile string `json:"subCertFile" form:"subCertFile"` SubCertFile string `json:"subCertFile" form:"subCertFile"`
SubKeyFile string `json:"subKeyFile" form:"subKeyFile"` SubKeyFile string `json:"subKeyFile" form:"subKeyFile"`
SubUpdates int `json:"subUpdates" form:"subUpdates"` SubUpdates int `json:"subUpdates" form:"subUpdates"`
SubEncrypt bool `json:"subEncrypt" form:"subEncrypt"`
SubShowInfo bool `json:"subShowInfo" form:"subShowInfo"`
} }
func (s *AllSetting) CheckValid() error { func (s *AllSetting) CheckValid() error {

View File

@@ -35,14 +35,16 @@
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 : '')].filter(Boolean).join('-');
address = this.dbInbound.address; address = this.dbInbound.address;
this.subId = '';
this.qrcodes = []; this.qrcodes = [];
if (this.inbound.tls && !ObjectUtil.isArrEmpty(this.inbound.stream.tls.settings.domains)) { if (this.inbound.tls && !ObjectUtil.isArrEmpty(this.inbound.stream.tls.settings.domains)) {
this.inbound.stream.tls.settings.domains.forEach((domain) => { this.inbound.stream.tls.settings.domains.forEach((domain) => {
remarkText = [remark, domain.remark].filter(Boolean).join('-');
this.qrcodes.push({ this.qrcodes.push({
remark: remark + "-" + domain.remark, remark: remarkText,
link: this.inbound.genLink(domain.domain, remark + "-" + domain.remark, clientIndex) link: this.inbound.genLink(domain.domain, remarkText, clientIndex)
}); });
}); });
} else { } else {
@@ -65,8 +67,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', () => {
@@ -74,29 +76,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+'?name='+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(this.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

@@ -179,8 +179,8 @@
newClient = clientsBulkModal.newClient(clientsBulkModal.dbInbound.protocol); newClient = clientsBulkModal.newClient(clientsBulkModal.dbInbound.protocol);
if(method==4) newClient.email = ""; if(method==4) newClient.email = "";
newClient.email += useNum ? prefix + i.toString() + postfix : prefix + postfix; newClient.email += useNum ? prefix + i.toString() + postfix : prefix + postfix;
newClient.subId = clientsBulkModal.subId; if (clientsBulkModal.subId.length > 0) newClient.subId = clientsBulkModal.subId;
newClient.tgId = clientsBulkModal.tgId; if (clientsBulkModal.tgId.length > 0) newClient.tgId = clientsBulkModal.tgId;
newClient._totalGB = clientsBulkModal.totalGB; newClient._totalGB = clientsBulkModal.totalGB;
newClient._expiryTime = clientsBulkModal.expiryTime; newClient._expiryTime = clientsBulkModal.expiryTime;
if(clientsBulkModal.inbound.canEnableTlsFlow()){ if(clientsBulkModal.inbound.canEnableTlsFlow()){
@@ -210,21 +210,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

@@ -69,7 +69,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;
} }
}, },
@@ -119,15 +119,6 @@
}, },
}, },
methods: { methods: {
getNewEmail(client) {
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
var string = '';
var len = 6 + Math.floor(Math.random() * 5);
for(var ii=0; ii<len; ii++){
string += chars[Math.floor(Math.random() * chars.length)];
}
client.email = string;
},
resetClientTraffic(email,dbInboundId,iconElement) { resetClientTraffic(email,dbInboundId,iconElement) {
this.$confirm({ this.$confirm({
title: '{{ i18n "pages.inbounds.resetTraffic"}}', title: '{{ i18n "pages.inbounds.resetTraffic"}}',

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
{{define "form/shadowsocks"}} {{define "form/shadowsocks"}}
<a-form layout="inline"> <a-form layout="inline">
<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" }}'>
<table width="100%" class="ant-table-tbody"> <table width="100%" class="ant-table-tbody">
@@ -10,7 +11,7 @@
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.emailDesc" }}</span> <span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template> </template>
<a-icon @click="getNewEmail(client)" type="sync"> </a-icon> <a-icon @click="client.email = RandomUtil.randomLowerAndNum(9)" type="sync"> </a-icon>
</a-tooltip> </a-tooltip>
</td> </td>
<td> <td>
@@ -30,7 +31,7 @@
</td> </td>
</tr> </tr>
<tr v-if="client.email && app.subSettings.enable"> <tr v-if="client.email && app.subSettings.enable">
<td>Subscription <a-icon @click="client.subId = RandomUtil.randomText(16,16)" type="sync"></a-icon></td> <td>Subscription <a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"></a-icon></td>
<td> <td>
<a-form-item> <a-form-item>
<a-input v-model.trim="client.subId" style="width: 200px;"></a-input> <a-input v-model.trim="client.subId" style="width: 200px;"></a-input>
@@ -102,26 +103,29 @@
<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>
<table width="100%" class="ant-table-tbody"> <table width="100%" class="ant-table-tbody">
<tr> <tr>
<td>{{ i18n "encryption" }}</td> <td>{{ i18n "encryption" }}</td>
<td> <td>
<a-form-item> <a-form-item>
<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>
</td> </td>
</tr> </tr>
<tr> <tr v-if="inbound.isSS2022">
<td>{{ i18n "password" }} <td>{{ i18n "password" }}
<a-icon @click="inbound.settings.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon> <a-icon @click="inbound.settings.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
</td> </td>

View File

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

View File

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

@@ -1,7 +1,7 @@
{{define "form/vless"}} {{define "form/vless"}}
<a-form layout="inline"> <a-form layout="inline">
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit"> <a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit">
<a-collapse-panel header="{{ i18n "pages.inbounds.client" }}"> <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
<table width="100%" class="ant-table-tbody"> <table width="100%" class="ant-table-tbody">
<tr> <tr>
<td> <td>
@@ -10,7 +10,7 @@
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.emailDesc" }}</span> <span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template> </template>
<a-icon @click="getNewEmail(client)" type="sync"> </a-icon> <a-icon @click="client.email = RandomUtil.randomLowerAndNum(9)" type="sync"> </a-icon>
</a-tooltip> </a-tooltip>
</td> </td>
<td> <td>
@@ -28,7 +28,7 @@
</td> </td>
</tr> </tr>
<tr v-if="inbound.canEnableTlsFlow()"> <tr v-if="inbound.canEnableTlsFlow()">
<td>flow</td> <td>Flow</td>
<td> <td>
<a-form-item> <a-form-item>
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass"> <a-select v-model="inbound.settings.vlesses[index].flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass">
@@ -39,7 +39,7 @@
</td> </td>
</tr> </tr>
<tr v-if="client.email && app.subSettings.enable"> <tr v-if="client.email && app.subSettings.enable">
<td>Subscription <a-icon @click="client.subId = RandomUtil.randomText(16,16)" type="sync"></a-icon></td> <td>Subscription <a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"></a-icon></td>
<td> <td>
<a-form-item> <a-form-item>
<a-input v-model.trim="client.subId" style="width: 200px;"></a-input> <a-input v-model.trim="client.subId" style="width: 200px;"></a-input>
@@ -111,10 +111,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

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
{{define "form/streamTCP"}} {{define "form/streamTCP"}}
<!-- tcp type --> <!-- tcp type -->
<a-form layout="inline"> <a-form layout="inline">
<a-form-item label="acceptProxyProtocol"> <a-form-item label="Accept Proxy Protocol" v-if="inbound.canEnableTls()">
<a-switch v-model="inbound.stream.tcp.acceptProxyProtocol"></a-switch> <a-switch v-model="inbound.stream.tcp.acceptProxyProtocol"></a-switch>
</a-form-item> </a-form-item>
<a-form-item label="http {{ i18n "camouflage" }}"> <a-form-item label="http {{ i18n "camouflage" }}">

View File

@@ -1,8 +1,8 @@
{{define "form/streamWS"}} {{define "form/streamWS"}}
<a-form layout="inline"> <a-form layout="inline">
<table width="100%" class="ant-table-tbody"> <table width="100%" class="ant-table-tbody">
<tr> <tr v-if="inbound.canEnableTls()">
<td>acceptProxyProtocol</td> <td>Accept Proxy Protocol</td>
<td> <td>
<a-form-item> <a-form-item>
<a-switch v-model="inbound.stream.ws.acceptProxyProtocol"></a-switch> <a-switch v-model="inbound.stream.ws.acceptProxyProtocol"></a-switch>

View File

@@ -1,6 +1,7 @@
{{define "form/tlsSettings"}} {{define "form/tlsSettings"}}
<!-- tls enable --> <!-- tls enable -->
<a-form v-if="inbound.canSetTls()" layout="inline"> <a-form v-if="inbound.canSetTls()" layout="inline">
<a-divider dashed style="margin:0;">
<a-form-item label="TLS"> <a-form-item label="TLS">
<a-switch v-model="inbound.tls"> <a-switch v-model="inbound.tls">
</a-switch> </a-switch>
@@ -8,6 +9,7 @@
<a-form-item v-if="inbound.canEnableReality()" label="Reality"> <a-form-item v-if="inbound.canEnableReality()" label="Reality">
<a-switch v-model="inbound.reality"></a-switch> <a-switch v-model="inbound.reality"></a-switch>
</a-form-item> </a-form-item>
</a-divider>
</a-form> </a-form>
<!-- tls settings --> <!-- tls settings -->
@@ -94,12 +96,16 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<td>Alpn</td> <td>ALPN</td>
<td> <td>
<a-form-item> <a-form-item>
<a-checkbox-group v-model="inbound.stream.tls.alpn"> <a-select
<a-checkbox v-for="key,value in ALPN_OPTION" :value="key">[[ value ]]</a-checkbox> mode="multiple"
</a-checkbox-group> style="width: 250px"
:dropdown-class-name="themeSwitcher.darkCardClass"
v-model="inbound.stream.tls.alpn">
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
</a-select>
</a-form-item> </a-form-item>
</td> </td>
</tr> </tr>
@@ -111,6 +117,14 @@
</a-form-item> </a-form-item>
</td> </td>
</tr> </tr>
<tr>
<td>Reject Unknown SNI</td>
<td>
<a-form-item>
<a-switch v-model="inbound.stream.tls.rejectUnknownSni"></a-switch>
</a-form-item>
</td>
</tr>
<template v-for="cert,index in inbound.stream.tls.certs"> <template v-for="cert,index in inbound.stream.tls.certs">
<tr> <tr>
<td colspan="2"> <td colspan="2">
@@ -166,6 +180,14 @@
</td> </td>
</tr> </tr>
</template> </template>
<tr>
<td>ocspStapling</td>
<td>
<a-form-item>
<a-input-number v-model.number="cert.ocspStapling" :min="0"></a-input-number>
</a-form-item>
</td>
</tr>
</template> </template>
</table> </table>
</a-form> </a-form>
@@ -225,7 +247,9 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<td>Short Ids</td> <td>Short Ids
<a-icon @click="inbound.stream.reality.shortIds = RandomUtil.randomShortId().join(',')" type="sync">
</td>
<td> <td>
<a-form-item> <a-form-item>
<a-input v-model.trim="inbound.stream.reality.shortIds" style="width:250px"></a-input> <a-input v-model.trim="inbound.stream.reality.shortIds" style="width:250px"></a-input>

View File

@@ -29,11 +29,25 @@
<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>&infin;</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

@@ -45,87 +45,148 @@
</template> </template>
</table> </table>
</td></tr> </td></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 />
tls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag> tls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
</td>
<td v-else-if="inbound.reality">
reality: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
reality Destination: <a-tag :color="inbound.stream.reality.dest ? 'green' : 'orange'">[[ inbound.stream.reality.dest ]]</a-tag>
</td>
<td v-else>tls: <a-tag color="red">{{ i18n "disabled" }}</a-tag>
</td> </td>
<td v-else-if="inbound.reality">
reality: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
reality Destination: <a-tag :color="inbound.stream.reality.dest ? 'green' : 'orange'">[[ inbound.stream.reality.dest ]]</a-tag>
</td>
<td v-else>tls: <a-tag color="red">{{ i18n "disabled" }}</a-tag></td>
</tr>
</table>
<table v-if="dbInbound.isSS" style="margin-bottom: 10px; width: 100%;">
<tr>
<td>{{ i18n "encryption" }}</td>
<td><a-tag color="green">[[ inbound.settings.method ]]</a-tag></td>
</tr><tr v-if="inbound.isSS2022">
<td>{{ i18n "password" }}</td>
<td><a-tag color="blue">[[ inbound.settings.password ]]</a-tag></td>
</tr><tr>
<td>{{ i18n "pages.inbounds.network" }}</td>
<td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td>
</tr> </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>
<th>{{ i18n "pages.inbounds.totalFlow" }}</th> <td><a-tag color="green">[[ infoModal.clientSettings.email ]]</a-tag></td>
<th>{{ i18n "pages.inbounds.expireDate" }}</th> </tr>
<tr> <tr v-if="infoModal.clientSettings.id">
<td> <td>ID</td>
<a-tag v-if="infoModal.clientStats" color="green"> <td><a-tag color="green">[[ infoModal.clientSettings.id ]]</a-tag></td>
[[ sizeFormat(infoModal.clientStats['up']) ]] / </tr>
[[ sizeFormat(infoModal.clientStats['down']) ]] <tr v-if="infoModal.inbound.canEnableTlsFlow()">
([[ sizeFormat(infoModal.clientStats['up'] + infoModal.clientStats['down']) ]]) <td>Flow</td>
</a-tag> <td><a-tag color="green">[[ infoModal.clientSettings.flow ]]</a-tag></td>
</td> </tr>
<td> <tr v-if="infoModal.clientSettings.password">
<a-tag v-if="infoModal.clientSettings.totalGB > 0" :color="statsColor(infoModal.clientStats)">[[ sizeFormat(infoModal.clientSettings.totalGB) ]]</a-tag> <td>Password</td>
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag> <td><a-tag color="green">[[ infoModal.clientSettings.password ]]</a-tag></td>
</td> </tr>
<td>
<template v-if="infoModal.clientSettings.expiryTime > 0">
<a-tag :color="usageColor(new Date().getTime(), app.expireDiff, infoModal.clientSettings.expiryTime)">
[[ DateUtil.formatMillis(infoModal.clientSettings.expiryTime) ]]
</a-tag>
</template>
<a-tag v-else-if="infoModal.clientSettings.expiryTime < 0" color="cyan">[[ infoModal.clientSettings.expiryTime / -86400000 ]] {{ i18n "pages.client.days" }}</a-tag>
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
</td>
</tr>
</table>
<template v-if="app.subSettings.enable && infoModal.clientSettings.subId">
<a-divider>Subscription link</a-divider>
<a :href="[[ infoModal.subLink ]]" target="_blank">[[ infoModal.subLink ]]</a>
<a-icon id="copy-sub-link" type="snippets" @click="copyToClipboard('copy-sub-link', infoModal.subLink)"></a-icon>
</template>
<template v-if="app.tgBotEnable && infoModal.clientSettings.tgId">
<a-divider>Telegram Username</a-divider>
<a :href="[[ infoModal.tgLink ]]" target="_blank">@[[ infoModal.clientSettings.tgId ]]</a>
<a-icon id="copy-tg-link" type="snippets" @click="copyToClipboard('copy-tg-link', '@' + infoModal.clientSettings.tgId)"></a-icon>
</template>
</template>
<template v-else>
<a-divider></a-divider>
<table v-if="inbound.protocol == Protocols.SHADOWSOCKS" style="margin-bottom: 10px; width: 100%;">
<tr> <tr>
<th>{{ i18n "encryption" }}</th> <td>{{ i18n "status" }}</td>
<th>{{ i18n "password" }}</th> <td>
<th>{{ i18n "pages.inbounds.network" }}</th> <a-tag v-if="isEnable" color="blue">{{ i18n "enabled" }}</a-tag>
</tr><tr> <a-tag v-else color="red">{{ i18n "disabled" }}</a-tag>
<td><a-tag color="green">[[ inbound.settings.method ]]</a-tag></td> <a-tag v-if="!isActive" color="red">{{ i18n "depleted" }}</a-tag>
<td><a-tag color="blue">[[ inbound.settings.password ]]</a-tag></td> </td>
<td><a-tag color="green">[[ inbound.settings.network ]]</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> </tr>
</table> </table>
<table style="margin-bottom: 10px; width: 100%;">
<tr>
<th>{{ i18n "remained" }}</th>
<th>{{ i18n "pages.inbounds.totalFlow" }}</th>
<th>{{ i18n "pages.inbounds.expireDate" }}</th>
</tr>
<tr>
<td>
<a-tag v-if="infoModal.clientStats && infoModal.clientSettings.totalGB > 0" :color="statsColor(infoModal.clientStats)">
[[ sizeFormat(infoModal.clientSettings.totalGB - infoModal.clientStats.up - infoModal.clientStats.down) ]]
</a-tag>
</td>
<td>
<a-tag v-if="infoModal.clientSettings.totalGB > 0" :color="statsColor(infoModal.clientStats)">
[[ sizeFormat(infoModal.clientSettings.totalGB) ]]
</a-tag>
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
</td>
<td>
<template v-if="infoModal.clientSettings.expiryTime > 0">
<a-tag :color="usageColor(new Date().getTime(), app.expireDiff, infoModal.clientSettings.expiryTime)">
[[ DateUtil.formatMillis(infoModal.clientSettings.expiryTime) ]]
</a-tag>
</template>
<a-tag v-else-if="infoModal.clientSettings.expiryTime < 0" color="cyan">[[ infoModal.clientSettings.expiryTime / -86400000 ]] {{ i18n "pages.client.days" }}</a-tag>
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
</td>
</tr>
</table>
<template v-if="app.subSettings.enable && infoModal.clientSettings.subId">
<a-divider>Subscription link</a-divider>
<a-row>
<a-col :span="22"><a :href="[[ infoModal.subLink ]]" target="_blank">[[ infoModal.subLink ]]</a></a-col>
<a-col :span="2">
<a-tooltip title='{{ i18n "copy" }}'>
<button class="ant-btn ant-btn-primary" id="copy-sub-link" @click="copyToClipboard('copy-sub-link', infoModal.subLink)">
<a-icon type="snippets"></a-icon>
</button>
</a-tooltip>
</a-col>
</a-row>
</template>
<template v-if="app.tgBotEnable && infoModal.clientSettings.tgId">
<a-divider>Telegram Username</a-divider>
<a-row>
<a-col :span="22"><a :href="[[ infoModal.tgLink ]]" target="_blank">@[[ infoModal.clientSettings.tgId ]]</a></a-col>
<a-col :span="2">
<a-tooltip title='{{ i18n "copy" }}'>
<button class="ant-btn ant-btn-primary" id="copy-tg-link" @click="copyToClipboard('copy-tg-link', '@' + infoModal.clientSettings.tgId)">
<a-icon type="snippets"></a-icon>
</button>
</a-tooltip>
</a-col>
</a-row>
</template>
<template v-if="dbInbound.hasLink()">
<a-divider>URL</a-divider>
<a-row v-for="(link,index) in infoModal.links">
<a-col :span="22"><a-tag color="cyan">[[ link.remark ]]</a-tag><br />[[ link.link ]]</a-col>
<a-col :span="2" style="text-align: right;">
<a-tooltip title='{{ i18n "copy" }}'>
<button class="ant-btn ant-btn-primary" :id="'copy-url-link-'+index" @click="copyToClipboard('copy-url-link-'+index, link.link)">
<a-icon type="snippets"></a-icon>
</button>
</a-tooltip>
</a-col>
</a-row>
</template>
</template>
<template v-else>
<template v-if="dbInbound.isSS && !inbound.isSSMultiUser">
<a-divider>URL</a-divider>
<a-row v-for="(link,index) in infoModal.links">
<a-col :span="22"><a-tag color="cyan">[[ link.remark ]]</a-tag><br />[[ link.link ]]</a-col>
<a-col :span="2" style="text-align: right;">
<a-tooltip title='{{ i18n "copy" }}'>
<button class="ant-btn ant-btn-primary" :id="'copy-url-link-'+index" @click="copyToClipboard('copy-url-link-'+index, link.link)">
<a-icon type="snippets"></a-icon>
</button>
</a-tooltip>
</a-col>
</a-row>
</template>
<table v-if="inbound.protocol == Protocols.DOKODEMO" style="margin-bottom: 10px; width: 100%;"> <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>
@@ -139,17 +200,19 @@
<td><a-tag color="blue">[[ inbound.settings.followRedirect ]]</a-tag></td> <td><a-tag color="blue">[[ inbound.settings.followRedirect ]]</a-tag></td>
</tr> </tr>
</table> </table>
</table> <table v-if="dbInbound.isSocks" style="margin-bottom: 10px; width: 100%;">
<table v-if="inbound.protocol == Protocols.SOCKS" style="margin-bottom: 10px; width: 100%;">
<tr> <tr>
<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 v-if="inbound.settings.auth == 'password'"> </tr>
<template v-if="inbound.settings.auth == 'password'">
<tr>
<td> </td> <td> </td>
<td>{{ i18n "username" }}</td> <td>{{ i18n "username" }}</td>
<td>{{ i18n "password" }}</td> <td>{{ i18n "password" }}</td>
@@ -158,9 +221,9 @@
<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>
</tr> </tr>
</template>
</table> </table>
</table> <table v-if="dbInbound.isHTTP" style="margin-bottom: 10px; width: 100%;">
<table v-if="inbound.protocol == Protocols.HTTP" style="margin-bottom: 10px; width: 100%;">
<tr> <tr>
<th> </th> <th> </th>
<th>{{ i18n "username" }}</th> <th>{{ i18n "username" }}</th>
@@ -171,19 +234,7 @@
<td><a-tag color="green">[[ account.pass ]]</a-tag></td> <td><a-tag color="green">[[ account.pass ]]</a-tag></td>
</tr> </tr>
</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 = {
@@ -209,14 +260,15 @@
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 : '')].filter(Boolean).join('-');
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)) {
this.inbound.stream.tls.settings.domains.forEach((domain) => { this.inbound.stream.tls.settings.domains.forEach((domain) => {
remarkText = [remark, domain.remark].filter(Boolean).join('-');
this.links.push({ this.links.push({
remark: remark + "-" + domain.remark, remark: remarkText,
link: this.inbound.genLink(domain.domain, remark + "-" + domain.remark, index) link: this.inbound.genLink(domain.domain, remarkText, index)
}); });
}); });
} else { } else {
@@ -239,12 +291,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+'?name='+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 (!this.inModal.inbound.isSS2022) {
this.inModal.inbound.settings.shadowsockses.forEach(client => {
client.method = this.inModal.inbound.settings.method;
})
} else {
this.inModal.inbound.settings.shadowsockses.forEach(client => {
client.method = "";
})
}
} else {
if (this.inModal.inbound.settings.shadowsockses.length > 0){
this.inModal.inbound.settings.shadowsockses = [];
}
}
}, },
setDefaultCertData(index){ setDefaultCertData(index){
inModal.inbound.stream.tls.certs[index].certFile = app.defaultCert; inModal.inbound.stream.tls.certs[index].certFile = app.defaultCert;
@@ -126,15 +139,6 @@
inModal.inbound.stream.reality.privateKey = msg.obj.privateKey; inModal.inbound.stream.reality.privateKey = msg.obj.privateKey;
inModal.inbound.stream.reality.settings.publicKey = msg.obj.publicKey; inModal.inbound.stream.reality.settings.publicKey = msg.obj.publicKey;
}, },
getNewEmail(client) {
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
var string = '';
var len = 6 + Math.floor(Math.random() * 5);
for(var ii=0; ii<len; ii++){
string += chars[Math.floor(Math.random() * chars.length)];
}
client.email = string;
}
}, },
}); });

View File

@@ -11,6 +11,9 @@
.ant-col-sm-24 { .ant-col-sm-24 {
margin-top: 10px; margin-top: 10px;
} }
tr.hideExpandIcon .ant-table-row-expand-icon {
display: none;
}
</style> </style>
<body> <body>
@@ -20,9 +23,13 @@
<a-layout-content> <a-layout-content>
<a-spin :spinning="spinning" :delay="500" tip="loading"> <a-spin :spinning="spinning" :delay="500" tip="loading">
<transition name="list" appear> <transition name="list" appear>
<a-tag v-if="false" color="red" style="margin-bottom: 10px"> <a-alert type="error" v-if="showAlert" style="margin-bottom: 10px"
Please go to the panel settings as soon as possible to modify the username and password, otherwise there may be a risk of leaking account information message='{{ i18n "secAlertTitle" }}'
</a-tag> color="red"
description='{{ i18n "secAlertSsl" }}'
show-icon closable
>
</a-alert>
</transition> </transition>
<transition name="list" appear> <transition name="list" appear>
<a-card hoverable style="margin-bottom: 20px;" :class="themeSwitcher.darkCardClass"> <a-card hoverable style="margin-bottom: 20px;" :class="themeSwitcher.darkCardClass">
@@ -105,8 +112,8 @@
</a-row> </a-row>
</div> </div>
<a-switch v-model="enableFilter" <a-switch v-model="enableFilter"
checked-children="{{ i18n "search" }}" un-checked-children="{{ i18n "filter" }}" checked-children='{{ i18n "search" }}' un-checked-children='{{ i18n "filter" }}'
@change="toggleFilter"> @change="toggleFilter" style="margin-right: 10px;">
</a-switch> </a-switch>
<a-input v-if="!enableFilter" v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus style="max-width: 300px"></a-input> <a-input v-if="!enableFilter" v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus style="max-width: 300px"></a-input>
<a-radio-group v-if="enableFilter" v-model="filterBy" @change="filterInbounds" button-style="solid"> <a-radio-group v-if="enableFilter" v-model="filterBy" @change="filterInbounds" button-style="solid">
@@ -117,8 +124,12 @@
</a-radio-group> </a-radio-group>
<a-table :columns="columns" :row-key="dbInbound => dbInbound.id" <a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
:data-source="searchedInbounds" :data-source="searchedInbounds"
:loading="spinning" :scroll="{ x: 1300 }" :loading="spinning" :scroll="{ x: 1200 }"
:pagination="false" :pagination="false"
:expand-icon-as-cell="false"
:expand-row-by-click="false"
:expand-icon-column-index="0"
:row-class-name="dbInbound => (dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess || (dbInbound.isSS && dbInbound.toInbound().isSSMultiUser) ? '' : 'hideExpandIcon')"
style="margin-top: 20px" style="margin-top: 20px"
@change="() => getDBInbounds()"> @change="() => getDBInbounds()">
<template slot="action" slot-scope="text, dbInbound"> <template slot="action" slot-scope="text, dbInbound">
@@ -129,7 +140,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.isSS && 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"}}
@@ -203,12 +218,27 @@
</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>&infin;</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>
@@ -231,15 +261,17 @@
:columns="innerColumns" :columns="innerColumns"
:data-source="getInboundClients(record)" :data-source="getInboundClients(record)"
:pagination="false" :pagination="false"
style="margin-left: 20px;"
> >
{{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)"
:pagination="false" :pagination="false"
style="margin-left: 20px;"
> >
{{template "client_table"}} {{template "client_table"}}
</a-table> </a-table>
@@ -255,20 +287,20 @@
{{template "component/themeSwitcher" .}} {{template "component/themeSwitcher" .}}
<script> <script>
const columns = [{ const columns = [{
title: "ID",
align: 'right',
dataIndex: "id",
width: 30,
}, {
title: '{{ i18n "pages.inbounds.operate" }}', title: '{{ i18n "pages.inbounds.operate" }}',
align: 'center', align: 'center',
width: 40, width: 30,
scopedSlots: { customRender: 'action' }, scopedSlots: { customRender: 'action' },
}, { }, {
title: '{{ i18n "pages.inbounds.enable" }}', title: '{{ i18n "pages.inbounds.enable" }}',
align: 'center', align: 'center',
width: 40,
scopedSlots: { customRender: 'enable' },
}, {
title: "ID",
align: 'center',
dataIndex: "id",
width: 30, width: 30,
scopedSlots: { customRender: 'enable' },
}, { }, {
title: '{{ i18n "pages.inbounds.remark" }}', title: '{{ i18n "pages.inbounds.remark" }}',
align: 'center', align: 'center',
@@ -287,12 +319,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" }}',
@@ -305,18 +337,18 @@
{ 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: 30, scopedSlots: { customRender: 'enable' } }, { title: '{{ i18n "pages.inbounds.enable" }}', width: 30, 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: 70, scopedSlots: { customRender: 'traffic' } }, { title: '{{ i18n "pages.inbounds.traffic" }}', width: 60, scopedSlots: { customRender: 'traffic' } },
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } }, { title: '{{ i18n "pages.inbounds.expireDate" }}', width: 55, 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: 30, scopedSlots: { customRender: 'enable' } }, { title: '{{ i18n "pages.inbounds.enable" }}', width: 30, 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: 70, 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: 55, scopedSlots: { customRender: 'expiryTime' } },
{ title: 'Password', width: 120, dataIndex: "password" }, { title: '{{ i18n "password" }}', width: 165, dataIndex: "password" },
]; ];
const app = new Vue({ const app = new Vue({
@@ -336,7 +368,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,
@@ -347,7 +379,8 @@
domain: '', domain: '',
tls: false tls: false
}, },
tgBotEnable: false tgBotEnable: false,
showAlert: false,
}, },
methods: { methods: {
loading(spinning = true) { loading(spinning = true) {
@@ -357,6 +390,7 @@
this.refreshing = true; this.refreshing = true;
const msg = await HttpUtil.post('/xui/inbound/list'); const msg = await HttpUtil.post('/xui/inbound/list');
if (!msg.success) { if (!msg.success) {
this.refreshing = false;
return; return;
} }
this.setInbounds(msg.obj); this.setInbounds(msg.obj);
@@ -387,12 +421,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);
} }
} }
@@ -600,7 +638,7 @@
port: RandomUtil.randomIntRange(10000, 60000), port: RandomUtil.randomIntRange(10000, 60000),
protocol: baseInbound.protocol, protocol: baseInbound.protocol,
settings: Inbound.Settings.getSettings(baseInbound.protocol).toString(), settings: Inbound.Settings.getSettings(baseInbound.protocol).toString(),
streamSettings: baseInbound.stream.toString(), streamSettings: baseInbound.stream.toString(),
sniffing: baseInbound.canSniffing() ? baseInbound.sniffing.toString() : '{}', sniffing: baseInbound.canSniffing() ? baseInbound.sniffing.toString() : '{}',
}; };
await this.submit('/xui/inbound/add', data, inModal); await this.submit('/xui/inbound/add', data, inModal);
@@ -688,8 +726,7 @@
}); });
}, },
findIndexOfClient(clients, client) { findIndexOfClient(clients, client) {
firstKey = Object.keys(client)[0]; return clients.findIndex(item => JSON.stringify(item) === JSON.stringify(client));
return clients.findIndex(c => c[firstKey] === client[firstKey]);
}, },
async addClient(clients, dbInboundId) { async addClient(clients, dbInboundId) {
const data = { const data = {
@@ -759,11 +796,32 @@
default: return client.id; default: return client.id;
} }
}, },
checkFallback(dbInbound) {
newDbInbound = new DBInbound(dbInbound);
if (dbInbound.listen.startsWith("@")){
rootInbound = this.inbounds.find((i) =>
i.tls &&
['trojan','vless'].includes(i.protocol) &&
i.settings.fallbacks.find(f => f.dest === dbInbound.listen)
);
if (rootInbound) {
newDbInbound.listen = rootInbound.listen;
newDbInbound.port = rootInbound.port;
newInbound = newDbInbound.toInbound();
newInbound.stream.security = 'tls';
newInbound.stream.tls = rootInbound.stream.tls;
newDbInbound.streamSettings = newInbound.stream.toString();
}
}
return newDbInbound;
},
showQrcode(dbInbound, clientIndex) { showQrcode(dbInbound, clientIndex) {
qrModal.show('{{ i18n "qrCode"}}', dbInbound, clientIndex); newDbInbound = this.checkFallback(dbInbound);
qrModal.show('{{ i18n "qrCode"}}', newDbInbound, clientIndex);
}, },
showInfo(dbInbound, index) { showInfo(dbInbound, index) {
infoModal.show(dbInbound, index); newDbInbound = this.checkFallback(dbInbound);
infoModal.show(newDbInbound, index);
}, },
switchEnable(dbInboundId) { switchEnable(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
@@ -774,6 +832,7 @@
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
inbound = dbInbound.toInbound(); inbound = dbInbound.toInbound();
clients = this.getClients(dbInbound.protocol, inbound.settings); clients = this.getClients(dbInbound.protocol, inbound.settings);
client.enable = !client.enable; // For finding correct index in findIndexOfClient() function
index = this.findIndexOfClient(clients, client); index = this.findIndexOfClient(clients, client);
clients[index].enable = !clients[index].enable; clients[index].enable = !clients[index].enable;
clientId = this.getClientId(dbInbound.protocol, clients[index]); clientId = this.getClientId(dbInbound.protocol, clients[index]);
@@ -864,7 +923,8 @@
}, },
inboundLinks(dbInboundId) { inboundLinks(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
txtModal.show('{{ i18n "pages.inbounds.export"}}', dbInbound.genInboundLinks, dbInbound.remark); newDbInbound = this.checkFallback(dbInbound);
txtModal.show('{{ i18n "pages.inbounds.export"}}', newDbInbound.genInboundLinks, newDbInbound.remark);
}, },
exportAllLinks() { exportAllLinks() {
let copyText = ''; let copyText = '';
@@ -906,6 +966,9 @@
}, 500) }, 500)
}, },
mounted() { mounted() {
if (window.location.protocol !== "https:") {
this.showAlert = true;
}
this.loading(); this.loading();
this.getDefaultSettings(); this.getDefaultSettings();
if (this.isRefreshEnabled) { if (this.isRefreshEnabled) {
@@ -951,4 +1014,4 @@
{{template "clientsModal"}} {{template "clientsModal"}}
{{template "clientsBulkModal"}} {{template "clientsBulkModal"}}
</body> </body>
</html> </html>

View File

@@ -22,6 +22,15 @@
<a-layout id="content-layout" :style="themeSwitcher.bgStyle"> <a-layout id="content-layout" :style="themeSwitcher.bgStyle">
<a-layout-content> <a-layout-content>
<a-spin :spinning="spinning" :delay="200" :tip="loadingTip"/> <a-spin :spinning="spinning" :delay="200" :tip="loadingTip"/>
<transition name="list" appear>
<a-alert type="error" v-if="showAlert" style="margin-bottom: 10px"
message='{{ i18n "secAlertTitle" }}'
color="red"
description='{{ i18n "secAlertSsl" }}'
show-icon closable
>
</a-alert>
</transition>
<transition name="list" appear> <transition name="list" appear>
<a-row> <a-row>
<a-card hoverable :class="themeSwitcher.darkCardClass"> <a-card hoverable :class="themeSwitcher.darkCardClass">
@@ -33,7 +42,7 @@
:stroke-color="status.cpu.color" :stroke-color="status.cpu.color"
:class="themeSwitcher.darkCardClass" :class="themeSwitcher.darkCardClass"
:percent="status.cpu.percent"></a-progress> :percent="status.cpu.percent"></a-progress>
<div>CPU</div> <div>CPU ([[ status.cpuCount ]]core)</div>
</a-col> </a-col>
<a-col :span="12" style="text-align: center"> <a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal" <a-progress type="dashboard" status="normal"
@@ -83,13 +92,16 @@
<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 "pages.index.operationHours" }}: {{ i18n "pages.index.operationHours" }}:
<a-tag color="green">[[ formatSecond(status.uptime) ]]</a-tag> xray
<a-tag color="green">[[ formatSecond(status.appStats.uptime) ]]</a-tag>
os
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
{{ i18n "pages.index.operationHoursDesc" }} {{ i18n "pages.index.operationHoursDesc" }}
</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-tag color="green">[[ formatSecond(status.uptime) ]]</a-tag>
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
@@ -110,7 +122,7 @@
<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>
@@ -122,7 +134,36 @@
</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">
tcp / udp {{ i18n "pages.index.connectionCount" }}: [[ status.tcpCount ]] / [[ status.udpCount ]] {{ i18n "usage"}}:
Memory [[ sizeFormat(status.appStats.mem) ]] -
Threads [[ status.appStats.threads ]]
</a-tooltip>
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass">
Host: [[ status.hostInfo.hostname ]] -
<template v-if="status.hostInfo.ipv4">IPv4:
<a-tooltip>
<template slot="title">
[[ status.hostInfo.ipv4 ]]
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</template>
<template v-if="status.hostInfo.ipv6">IPv6:
<a-tooltip>
<template slot="title">
[[ status.hostInfo.ipv6 ]]
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</template>
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass">
{{ i18n "pages.index.connectionCount" }}: tcp: [[ status.tcpCount ]] udp: [[ status.udpCount ]]
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
{{ i18n "pages.index.connectionCountDesc" }} {{ i18n "pages.index.connectionCountDesc" }}
@@ -211,7 +252,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>
@@ -219,8 +260,22 @@
<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="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;"
@@ -240,7 +295,7 @@
<a-icon type="warning" style="color: inherit; font-size: 20px;"></a-icon> <a-icon type="warning" style="color: inherit; font-size: 20px;"></a-icon>
[[ backupModal.description ]] [[ backupModal.description ]]
</p> </p>
<a-space direction="horizontal" align="center" style="margin-bottom: 10px;"> <a-space direction="horizontal" style="text-align: center" style="margin-bottom: 10px;">
<a-button type="primary" @click="exportDatabase()"> <a-button type="primary" @click="exportDatabase()">
[[ backupModal.exportText ]] [[ backupModal.exportText ]]
</a-button> </a-button>
@@ -292,6 +347,7 @@
class Status { class Status {
constructor(data) { constructor(data) {
this.cpu = new CurTotal(0, 0); this.cpu = new CurTotal(0, 0);
this.cpuCount = 0;
this.disk = new CurTotal(0, 0); this.disk = new CurTotal(0, 0);
this.loads = [0, 0, 0]; this.loads = [0, 0, 0];
this.mem = new CurTotal(0, 0); this.mem = new CurTotal(0, 0);
@@ -301,12 +357,16 @@
this.tcpCount = 0; this.tcpCount = 0;
this.udpCount = 0; this.udpCount = 0;
this.uptime = 0; this.uptime = 0;
this.appUptime = 0;
this.appStats = {threads: 0, mem: 0, uptime: 0};
this.hostInfo = {hostname:"", ipv4: "", ipv6: ""};
this.xray = {state: State.Stop, errorMsg: "", version: "", color: ""}; this.xray = {state: State.Stop, errorMsg: "", version: "", color: ""};
if (data == null) { if (data == null) {
return; return;
} }
this.cpu = new CurTotal(data.cpu, 100); this.cpu = new CurTotal(data.cpu, 100);
this.cpuCount = data.cpuCount;
this.disk = new CurTotal(data.disk.current, data.disk.total); this.disk = new CurTotal(data.disk.current, data.disk.total);
this.loads = data.loads.map(load => toFixed(load, 2)); this.loads = data.loads.map(load => toFixed(load, 2));
this.mem = new CurTotal(data.mem.current, data.mem.total); this.mem = new CurTotal(data.mem.current, data.mem.total);
@@ -316,6 +376,9 @@
this.tcpCount = data.tcpCount; this.tcpCount = data.tcpCount;
this.udpCount = data.udpCount; this.udpCount = data.udpCount;
this.uptime = data.uptime; this.uptime = data.uptime;
this.appUptime = data.appUptime;
this.appStats = data.appStats;
this.hostInfo = data.hostInfo;
this.xray = data.xray; this.xray = data.xray;
switch (this.xray.state) { switch (this.xray.state) {
case State.Running: case State.Running:
@@ -349,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;
@@ -394,6 +458,7 @@
backupModal, backupModal,
spinning: false, spinning: false,
loadingTip: '{{ i18n "loading"}}', loadingTip: '{{ i18n "loading"}}',
showAlert: false,
}, },
methods: { methods: {
loading(spinning, tip = '{{ i18n "loading"}}') { loading(spinning, tip = '{{ i18n "loading"}}') {
@@ -449,14 +514,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);
@@ -512,6 +577,9 @@
}, },
}, },
async mounted() { async mounted() {
if (window.location.protocol !== "https:") {
this.showAlert = true;
}
while (true) { while (true) {
try { try {
await this.getStatus(); await this.getStatus();

View File

@@ -19,6 +19,32 @@
.ant-list-item { .ant-list-item {
display: block; display: block;
} }
.alert-msg {
color: inherit;
font-weight: bold;
font-size: 18px;
padding: 20px 20px;
text-align: center;
}
.alert-msg > i {
color: inherit;
font-size: 24px;
}
.collapse-title {
color: inherit;
font-weight: bold;
font-size: 18px;
padding: 10px 20px;
border-bottom: 2px solid;
}
.collapse-title > i {
color: inherit;
font-size: 24px;
}
</style> </style>
<body> <body>
<a-layout id="app" v-cloak> <a-layout id="app" v-cloak>
@@ -26,21 +52,31 @@
<a-layout id="content-layout" :style="themeSwitcher.bgStyle"> <a-layout id="content-layout" :style="themeSwitcher.bgStyle">
<a-layout-content> <a-layout-content>
<a-spin :spinning="spinning" :delay="500" tip="loading"> <a-spin :spinning="spinning" :delay="500" tip="loading">
<transition name="list" appear>
<a-alert type="error" v-if="showAlert" style="margin-bottom: 10px"
message='{{ i18n "secAlertTitle" }}'
color="red"
description='{{ i18n "secAlertSsl" }}'
show-icon closable
>
</a-alert>
</transition>
<a-space direction="vertical"> <a-space direction="vertical">
<a-space direction="horizontal"> <a-space direction="horizontal">
<a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n "pages.settings.save" }}</a-button> <a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n "pages.settings.save" }}</a-button>
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.settings.restartPanel" }}</a-button> <a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.settings.restartPanel" }}</a-button>
</a-space> </a-space>
<a-tabs default-active-key="1" :class="themeSwitcher.darkCardClass" :style="!themeSwitcher.isDarkTheme? 'background: white':''"> <a-tabs default-active-key="1" :class="themeSwitcher.darkCardClass">
<a-tab-pane key="1" tab='{{ i18n "pages.settings.panelConfig"}}'> <a-tab-pane key="1" tab='{{ i18n "pages.settings.panelConfig"}}'>
<a-row :xs="24" :sm="24" :lg="12"> <a-row :xs="24" :sm="24" :lg="12">
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 20px 20px; text-align: center;"> <h2 class="alert-msg">
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon> <a-icon type="warning"></a-icon>
{{ i18n "pages.settings.infoDesc" }} {{ i18n "pages.settings.infoDesc" }}
</h2> </h2>
</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.number="allSetting.webPort"></setting-list-item> <setting-list-item type="number" title='{{ i18n "pages.settings.panelPort"}}' desc='{{ i18n "pages.settings.panelPortDesc"}}' v-model.number="allSetting.webPort"></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>
@@ -97,8 +133,8 @@
<a-tab-pane key="3" tab='{{ i18n "pages.settings.xrayConfiguration"}}'> <a-tab-pane key="3" tab='{{ i18n "pages.settings.xrayConfiguration"}}'>
<a-row :xs="24" :sm="24" :lg="12"> <a-row :xs="24" :sm="24" :lg="12">
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 20px 20px; text-align: center;"> <h2 class="alert-msg">
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon> <a-icon type="warning"></a-icon>
{{ i18n "pages.settings.infoDesc" }} {{ i18n "pages.settings.infoDesc" }}
</h2> </h2>
</a-row> </a-row>
@@ -108,8 +144,8 @@
<a-collapse> <a-collapse>
<a-collapse-panel header='{{ i18n "pages.settings.templates.generalConfigs"}}'> <a-collapse-panel header='{{ i18n "pages.settings.templates.generalConfigs"}}'>
<a-row :xs="24" :sm="24" :lg="12"> <a-row :xs="24" :sm="24" :lg="12">
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;"> <h2 class="collapse-title">
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon> <a-icon type="warning"></a-icon>
{{ i18n "pages.settings.templates.generalConfigsDesc" }} {{ i18n "pages.settings.templates.generalConfigsDesc" }}
</h2> </h2>
</a-row> </a-row>
@@ -153,8 +189,8 @@
</a-collapse-panel> </a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.blockConfigs"}}'> <a-collapse-panel header='{{ i18n "pages.settings.templates.blockConfigs"}}'>
<a-row :xs="24" :sm="24" :lg="12"> <a-row :xs="24" :sm="24" :lg="12">
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;"> <h2 class="collapse-title">
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon> <a-icon type="warning"></a-icon>
{{ i18n "pages.settings.templates.blockConfigsDesc" }} {{ i18n "pages.settings.templates.blockConfigsDesc" }}
</h2> </h2>
</a-row> </a-row>
@@ -165,8 +201,8 @@
</a-collapse-panel> </a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.blockCountryConfigs"}}'> <a-collapse-panel header='{{ i18n "pages.settings.templates.blockCountryConfigs"}}'>
<a-row :xs="24" :sm="24" :lg="12"> <a-row :xs="24" :sm="24" :lg="12">
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;"> <h2 class="collapse-title">
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon> <a-icon type="warning"></a-icon>
{{ i18n "pages.settings.templates.blockCountryConfigsDesc" }} {{ i18n "pages.settings.templates.blockCountryConfigsDesc" }}
</h2> </h2>
</a-row> </a-row>
@@ -179,8 +215,8 @@
</a-collapse-panel> </a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.directCountryConfigs"}}'> <a-collapse-panel header='{{ i18n "pages.settings.templates.directCountryConfigs"}}'>
<a-row :xs="24" :sm="24" :lg="12"> <a-row :xs="24" :sm="24" :lg="12">
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;"> <h2 class="collapse-title">
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon> <a-icon type="warning"></a-icon>
{{ i18n "pages.settings.templates.directCountryConfigsDesc" }} {{ i18n "pages.settings.templates.directCountryConfigsDesc" }}
</h2> </h2>
</a-row> </a-row>
@@ -193,26 +229,14 @@
</a-collapse-panel> </a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.ipv4Configs"}}'> <a-collapse-panel header='{{ i18n "pages.settings.templates.ipv4Configs"}}'>
<a-row :xs="24" :sm="24" :lg="12"> <a-row :xs="24" :sm="24" :lg="12">
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;"> <h2 class="collapse-title">
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon> <a-icon type="warning"></a-icon>
{{ i18n "pages.settings.templates.ipv4ConfigsDesc" }} {{ i18n "pages.settings.templates.ipv4ConfigsDesc" }}
</h2> </h2>
</a-row> </a-row>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigGoogleIPv4"}}' desc='{{ i18n "pages.settings.templates.xrayConfigGoogleIPv4Desc"}}' v-model="GoogleIPv4Settings"></setting-list-item> <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigGoogleIPv4"}}' desc='{{ i18n "pages.settings.templates.xrayConfigGoogleIPv4Desc"}}' v-model="GoogleIPv4Settings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigNetflixIPv4"}}' desc='{{ i18n "pages.settings.templates.xrayConfigNetflixIPv4Desc"}}' v-model="NetflixIPv4Settings"></setting-list-item> <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigNetflixIPv4"}}' desc='{{ i18n "pages.settings.templates.xrayConfigNetflixIPv4Desc"}}' v-model="NetflixIPv4Settings"></setting-list-item>
</a-collapse-panel> </a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.manualLists"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
{{ i18n "pages.settings.templates.manualListsDesc" }}
</h2>
</a-row>
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualBlockedIPs"}}' v-model="manualBlockedIPs"></setting-list-item>
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualBlockedDomains"}}' v-model="manualBlockedDomains"></setting-list-item>
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualDirectIPs"}}' v-model="manualDirectIPs"></setting-list-item>
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualDirectDomains"}}' v-model="manualDirectDomains"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.resetDefaultConfig"}}'> <a-collapse-panel header='{{ i18n "pages.settings.resetDefaultConfig"}}'>
<a-space direction="horizontal" style="padding: 0 20px"> <a-space direction="horizontal" style="padding: 0 20px">
<a-button type="primary" @click="resetXrayConfigToDefault">{{ i18n "pages.settings.resetDefaultConfig" }}</a-button> <a-button type="primary" @click="resetXrayConfigToDefault">{{ i18n "pages.settings.resetDefaultConfig" }}</a-button>
@@ -220,7 +244,32 @@
</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-2" tab='{{ i18n "pages.settings.templates.manualLists"}}' style="padding-top: 20px;">
<a-row :xs="24" :sm="24" :lg="12">
<h2 class="collapse-title">
<a-icon type="warning"></a-icon>
{{ i18n "pages.settings.templates.manualListsDesc" }}
</h2>
</a-row>
<a-collapse>
<a-collapse-panel header='{{ i18n "pages.settings.templates.manualBlockedIPs"}}'>
<setting-list-item type="textarea" v-model="manualBlockedIPs"></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>
</a-tab-pane>
<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>
@@ -233,7 +282,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>
@@ -241,8 +290,8 @@
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="4" tab='{{ i18n "pages.settings.TGBotSettings"}}'> <a-tab-pane key="4" tab='{{ i18n "pages.settings.TGBotSettings"}}'>
<a-row :xs="24" :sm="24" :lg="12"> <a-row :xs="24" :sm="24" :lg="12">
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 20px 20px; text-align: center;"> <h2 class="alert-msg">
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon> <a-icon type="warning"></a-icon>
{{ i18n "pages.settings.infoDesc" }} {{ i18n "pages.settings.infoDesc" }}
</h2> </h2>
</a-row> </a-row>
@@ -252,22 +301,48 @@
<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-row style="padding: 20px">
<a-col :lg="24" :xl="12">
<a-list-item-meta title="Telegram Bot Language" />
</a-col>
<a-col :lg="24" :xl="12">
<template>
<a-select
ref="selectBotLang"
v-model="allSetting.tgLang"
:dropdown-class-name="themeSwitcher.darkCardClass"
style="width: 100%"
>
<a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs">
<span role="img" aria-label="l.name" v-text="l.icon"></span>
&nbsp;&nbsp;<span v-text="l.name"></span>
</a-select-option>
</a-select>
</template>
</a-col>
</a-row>
</a-list-item>
</a-list> </a-list>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="5" tab='{{ i18n "pages.settings.subSettings" }}'> <a-tab-pane key="5" tab='{{ i18n "pages.settings.subSettings" }}'>
<a-row :xs="24" :sm="24" :lg="12"> <a-row :xs="24" :sm="24" :lg="12">
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 20px 20px; text-align: center;"> <h2 class="alert-msg">
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon> <a-icon type="warning"></a-icon>
{{ i18n "pages.settings.infoDesc" }} {{ i18n "pages.settings.infoDesc" }}
</h2> </h2>
</a-row> </a-row>
<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="switch" title='{{ i18n "pages.settings.subEncrypt"}}' desc='{{ i18n "pages.settings.subEncryptDesc"}}' v-model="allSetting.subEncrypt"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.subShowInfo"}}' desc='{{ i18n "pages.settings.subShowInfoDesc"}}' v-model="allSetting.subShowInfo"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.subListen"}}' desc='{{ i18n "pages.settings.subListenDesc"}}' v-model="allSetting.subListen"></setting-list-item> <setting-list-item type="text" title='{{ i18n "pages.settings.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>
@@ -297,6 +372,7 @@
saveBtnDisable: true, saveBtnDisable: true,
user: {}, user: {},
lang: getLang(), lang: getLang(),
showAlert: false,
ipv4Settings: { ipv4Settings: {
tag: "IPv4", tag: "IPv4",
protocol: "freedom", protocol: "freedom",
@@ -381,7 +457,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() {
@@ -400,7 +476,10 @@
if (msg.success) { if (msg.success) {
this.loading(true); this.loading(true);
await PromiseUtil.sleep(5000); await PromiseUtil.sleep(5000);
window.location.replace(this.allSetting.webBasePath + "xui/settings"); const { webCertFile, webKeyFile, webDomain: host, webPort: port, webBasePath: base } = this.allSetting;
const isTLS = webCertFile !== "" || webKeyFile !== "";
const url = buildURL({ host, port, isTLS, base, path: "xui/settings" });
window.location.replace(url);
} }
}, },
async resetXrayConfigToDefault() { async resetXrayConfigToDefault() {
@@ -482,6 +561,9 @@
} }
}, },
async mounted() { async mounted() {
if (window.location.protocol !== "https:") {
this.showAlert = true;
}
await this.getAllSetting(); await this.getAllSetting();
while (true) { while (true) {
await PromiseUtil.sleep(1000); await PromiseUtil.sleep(1000);
@@ -491,30 +573,30 @@
computed: { computed: {
templateSettings: { templateSettings: {
get: function () { return this.allSetting.xrayTemplateConfig ? JSON.parse(this.allSetting.xrayTemplateConfig) : null; }, get: function () { return this.allSetting.xrayTemplateConfig ? JSON.parse(this.allSetting.xrayTemplateConfig) : null; },
set: function (newValue) { this.allSetting.xrayTemplateConfig = JSON.stringify(newValue, null, 2) }, set: function (newValue) { this.allSetting.xrayTemplateConfig = JSON.stringify(newValue, null, 2); },
}, },
inboundSettings: { inboundSettings: {
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.inbounds, null, 2) : null; }, get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.inbounds, null, 2) : null; },
set: function (newValue) { set: function (newValue) {
newTemplateSettings = this.templateSettings; newTemplateSettings = this.templateSettings;
newTemplateSettings.inbounds = JSON.parse(newValue) newTemplateSettings.inbounds = JSON.parse(newValue);
this.templateSettings = newTemplateSettings this.templateSettings = newTemplateSettings;
}, },
}, },
outboundSettings: { outboundSettings: {
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.outbounds, null, 2) : null; }, get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.outbounds, null, 2) : null; },
set: function (newValue) { set: function (newValue) {
newTemplateSettings = this.templateSettings; newTemplateSettings = this.templateSettings;
newTemplateSettings.outbounds = JSON.parse(newValue) newTemplateSettings.outbounds = JSON.parse(newValue);
this.templateSettings = newTemplateSettings this.templateSettings = newTemplateSettings;
}, },
}, },
routingRuleSettings: { routingRuleSettings: {
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.routing.rules, null, 2) : null; }, get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.routing.rules, null, 2) : null; },
set: function (newValue) { set: function (newValue) {
newTemplateSettings = this.templateSettings; newTemplateSettings = this.templateSettings;
newTemplateSettings.routing.rules = JSON.parse(newValue) newTemplateSettings.routing.rules = JSON.parse(newValue);
this.templateSettings = newTemplateSettings this.templateSettings = newTemplateSettings;
}, },
}, },
freedomStrategy: { freedomStrategy: {
@@ -589,6 +671,15 @@
this.syncRulesWithOutbound("direct", this.directSettings); this.syncRulesWithOutbound("direct", this.directSettings);
} }
}, },
ipv4Domains: {
get: function () {
return this.templateRuleGetter({ outboundTag: "IPv4", property: "domain" });
},
set: function (newValue) {
this.templateRuleSetter({ outboundTag: "IPv4", property: "domain", data: newValue });
this.syncRulesWithOutbound("IPv4", this.ipv4Settings);
}
},
manualBlockedIPs: { manualBlockedIPs: {
get: function () { return JSON.stringify(this.blockedIPs, null, 2); }, get: function () { return JSON.stringify(this.blockedIPs, null, 2); },
set: debounce(function (value) { this.blockedIPs = JSON.parse(value); }, 1000) set: debounce(function (value) { this.blockedIPs = JSON.parse(value); }, 1000)
@@ -605,6 +696,10 @@
get: function () { return JSON.stringify(this.directDomains, null, 2); }, get: function () { return JSON.stringify(this.directDomains, null, 2); },
set: debounce(function (value) { this.directDomains = JSON.parse(value); }, 1000) set: debounce(function (value) { this.directDomains = JSON.parse(value); }, 1000)
}, },
manualIPv4Domains: {
get: function () { return JSON.stringify(this.ipv4Domains, null, 2); },
set: debounce(function (value) { this.ipv4Domains = JSON.parse(value); }, 1000)
},
torrentSettings: { torrentSettings: {
get: function () { get: function () {
return doAllItemsExist(this.settingsData.protocols.bittorrent, this.blockedProtocols); return doAllItemsExist(this.settingsData.protocols.bittorrent, this.blockedProtocols);
@@ -658,40 +753,26 @@
}, },
GoogleIPv4Settings: { GoogleIPv4Settings: {
get: function () { get: function () {
return doAllItemsExist(this.settingsData.domains.google, this.templateRuleGetter({ outboundTag: "IPv4", property: "domain" })); return doAllItemsExist(this.settingsData.domains.google, this.ipv4Domains);
}, },
set: function (newValue) { set: function (newValue) {
oldData = this.templateRuleGetter({ outboundTag: "IPv4", property: "domain" });
if (newValue) { if (newValue) {
oldData = [...oldData, ...this.settingsData.domains.google]; this.ipv4Domains = [...this.ipv4Domains, ...this.settingsData.domains.google];
} else { } else {
oldData = oldData.filter(data => !this.settingsData.domains.google.includes(data)) this.ipv4Domains = this.ipv4Domains.filter(data => !this.settingsData.domains.google.includes(data));
} }
this.templateRuleSetter({
outboundTag: "IPv4",
property: "domain",
data: oldData
});
this.syncRulesWithOutbound("IPv4", this.ipv4Settings);
}, },
}, },
NetflixIPv4Settings: { NetflixIPv4Settings: {
get: function () { get: function () {
return doAllItemsExist(this.settingsData.domains.netflix, this.templateRuleGetter({ outboundTag: "IPv4", property: "domain" })); return doAllItemsExist(this.settingsData.domains.netflix, this.ipv4Domains);
}, },
set: function (newValue) { set: function (newValue) {
oldData = this.templateRuleGetter({ outboundTag: "IPv4", property: "domain" });
if (newValue) { if (newValue) {
oldData = [...oldData, ...this.settingsData.domains.netflix]; this.ipv4Domains = [...this.ipv4Domains, ...this.settingsData.domains.netflix];
} else { } else {
oldData = oldData.filter(data => !this.settingsData.domains.netflix.includes(data)) this.ipv4Domains = this.ipv4Domains.filter(data => !this.settingsData.domains.netflix.includes(data));
} }
this.templateRuleSetter({
outboundTag: "IPv4",
property: "domain",
data: oldData
});
this.syncRulesWithOutbound("IPv4", this.ipv4Settings);
}, },
}, },
IRIpSettings: { IRIpSettings: {

View File

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

View File

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

View File

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

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

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

View File

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

View File

@@ -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,53 +134,30 @@ 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()
err = db.Save(inbound).Error
if err == nil {
for _, client := range clients {
s.AddClientStat(inbound.Id, &client)
}
}
return inbound, err
}
func (s *InboundService) AddInbounds(inbounds []*model.Inbound) error {
for _, inbound := range inbounds {
exist, err := s.checkPortExist(inbound.Port, 0)
if err != nil {
return err
}
if exist {
return common.NewError("Port already exists:", inbound.Port)
}
} }
db := database.GetDB() db := database.GetDB()
tx := db.Begin() tx := db.Begin()
var err error
defer func() { defer func() {
if err == nil { if err == nil {
tx.Commit() tx.Commit()
@@ -188,23 +166,61 @@ func (s *InboundService) AddInbounds(inbounds []*model.Inbound) error {
} }
}() }()
for _, inbound := range inbounds { err = tx.Save(inbound).Error
err = tx.Save(inbound).Error if err == nil {
if err != nil { for _, client := range clients {
return err s.AddClientStat(tx, inbound.Id, &client)
} }
} }
return nil 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) 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
} }
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) {
@@ -217,19 +233,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
@@ -244,40 +268,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{})
@@ -287,30 +389,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 := ""
@@ -337,7 +474,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)
@@ -346,33 +483,46 @@ 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
} }
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 := ""
@@ -396,17 +546,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]
@@ -414,58 +564,10 @@ 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()
if len(clients[0].Email) > 0 {
if len(oldEmail) > 0 {
err = s.UpdateClientStat(oldEmail, &clients[0])
if err != nil {
return err
}
} else {
s.AddClientStat(data.Id, &clients[0])
}
} else {
err = s.DelClientStat(db, oldEmail)
if err != nil {
return err
}
}
return db.Save(oldInbound).Error
}
func (s *InboundService) AddTraffic(traffics []*xray.Traffic) error {
if len(traffics) == 0 {
return nil
}
// Update traffics in a single transaction
err := database.GetDB().Transaction(func(tx *gorm.DB) error {
for _, traffic := range traffics {
if traffic.IsInbound {
update := tx.Model(&model.Inbound{}).Where("tag = ?", traffic.Tag).
Updates(map[string]interface{}{
"up": gorm.Expr("up + ?", traffic.Up),
"down": gorm.Expr("down + ?", traffic.Down),
})
if update.Error != nil {
return update.Error
}
}
}
return nil
})
return err
}
func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err error) {
if len(traffics) == 0 {
return nil
}
db := database.GetDB() db := database.GetDB()
tx := db.Begin() tx := db.Begin()
@@ -477,16 +579,133 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
} }
}() }()
if len(clients[0].Email) > 0 {
if len(oldEmail) > 0 {
err = s.UpdateClientStat(oldEmail, &clients[0])
if err != nil {
return false, err
}
} else {
s.AddClientStat(tx, data.Id, &clients[0])
}
} else {
err = s.DelClientStat(tx, oldEmail)
if err != nil {
return false, err
}
}
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(inboundTraffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) {
var err error
db := database.GetDB()
tx := db.Begin()
defer func() {
if err != nil {
tx.Rollback()
} else {
tx.Commit()
}
}()
err = s.addInboundTraffic(tx, inboundTraffics)
if err != nil {
return err, false
}
err = s.addClientTraffic(tx, clientTraffics)
if err != nil {
return err, false
}
needRestart1, count, err := s.disableInvalidClients(tx)
if err != nil {
logger.Warning("Error in disabling invalid clients:", err)
} else if count > 0 {
logger.Debugf("%v clients disabled", count)
}
needRestart2, count, err := s.disableInvalidInbounds(tx)
if err != nil {
logger.Warning("Error in disabling invalid inbounds:", err)
} else if count > 0 {
logger.Debugf("%v inbounds disabled", count)
}
return nil, (needRestart1 || needRestart2)
}
func (s *InboundService) addInboundTraffic(tx *gorm.DB, traffics []*xray.Traffic) error {
if len(traffics) == 0 {
return nil
}
var err error
for _, traffic := range traffics {
if traffic.IsInbound {
err = tx.Model(&model.Inbound{}).Where("tag = ?", traffic.Tag).
Updates(map[string]interface{}{
"up": gorm.Expr("up + ?", traffic.Up),
"down": gorm.Expr("down + ?", traffic.Down),
}).Error
if err != nil {
return err
}
}
}
return nil
}
func (s *InboundService) addClientTraffic(tx *gorm.DB, traffics []*xray.ClientTraffic) (err error) {
if len(traffics) == 0 {
return nil
}
emails := make([]string, 0, len(traffics)) emails := make([]string, 0, len(traffics))
for _, traffic := range traffics { for _, traffic := range traffics {
emails = append(emails, traffic.Email) emails = append(emails, traffic.Email)
} }
dbClientTraffics := make([]*xray.ClientTraffic, 0, len(traffics)) dbClientTraffics := make([]*xray.ClientTraffic, 0, len(traffics))
err = db.Model(xray.ClientTraffic{}).Where("email IN (?)", emails).Find(&dbClientTraffics).Error err = tx.Model(xray.ClientTraffic{}).Where("email IN (?)", emails).Find(&dbClientTraffics).Error
if err != nil { if err != nil {
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
@@ -562,26 +781,76 @@ 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(tx *gorm.DB) (bool, int64, error) {
db := database.GetDB()
now := time.Now().Unix() * 1000 now := time.Now().Unix() * 1000
result := db.Model(model.Inbound{}). needRestart := false
if p != nil {
var tags []string
err := tx.Table("inbounds").
Select("inbounds.tag").
Where("((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ?", now, true).
Scan(&tags).Error
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 := tx.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(tx *gorm.DB) (bool, int64, error) {
db := database.GetDB()
now := time.Now().Unix() * 1000 now := time.Now().Unix() * 1000
result := db.Model(xray.ClientTraffic{}). needRestart := false
if p != nil {
var results []struct {
Tag string
Email string
}
err := tx.Table("inbounds").
Select("inbounds.tag, client_traffics.email").
Joins("JOIN client_traffics ON inbounds.id = client_traffics.inbound_id").
Where("((client_traffics.total > 0 AND client_traffics.up + client_traffics.down >= client_traffics.total) OR (client_traffics.expiry_time > 0 AND client_traffics.expiry_time <= ?)) AND client_traffics.enable = ?", now, true).
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 := tx.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() {
@@ -596,9 +865,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
@@ -607,7 +874,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
@@ -635,19 +902,65 @@ func (s *InboundService) DelClientStat(tx *gorm.DB, email string) error {
return tx.Where("email = ?", email).Delete(xray.ClientTraffic{}).Error return tx.Where("email = ?", email).Delete(xray.ClientTraffic{}).Error
} }
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 {
@@ -862,10 +1175,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
} }
@@ -909,17 +1231,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,6 +12,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"runtime" "runtime"
"strconv"
"strings" "strings"
"time" "time"
"x-ui/config" "x-ui/config"
@@ -38,9 +39,10 @@ const (
) )
type Status struct { type Status struct {
T time.Time `json:"-"` T time.Time `json:"-"`
Cpu float64 `json:"cpu"` Cpu float64 `json:"cpu"`
Mem struct { CpuCount int `json:"cpuCount"`
Mem struct {
Current uint64 `json:"current"` Current uint64 `json:"current"`
Total uint64 `json:"total"` Total uint64 `json:"total"`
} `json:"mem"` } `json:"mem"`
@@ -69,6 +71,16 @@ type Status struct {
Sent uint64 `json:"sent"` Sent uint64 `json:"sent"`
Recv uint64 `json:"recv"` Recv uint64 `json:"recv"`
} `json:"netTraffic"` } `json:"netTraffic"`
AppStats struct {
Threads uint32 `json:"threads"`
Mem uint64 `json:"mem"`
Uptime uint64 `json:"uptime"`
} `json:"appStats"`
HostInfo struct {
HostName string `json:"hostname"`
Ipv4 string `json:"ipv4"`
Ipv6 string `json:"ipv6"`
} `json:"hostInfo"`
} }
type Release struct { type Release struct {
@@ -175,6 +187,36 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
} }
status.Xray.Version = s.xrayService.GetXrayVersion() status.Xray.Version = s.xrayService.GetXrayVersion()
var rtm runtime.MemStats
runtime.ReadMemStats(&rtm)
status.AppStats.Mem = rtm.Sys
status.AppStats.Threads = uint32(runtime.NumGoroutine())
status.CpuCount = runtime.NumCPU()
if p.IsRunning() {
status.AppStats.Uptime = p.GetUptime()
} else {
status.AppStats.Uptime = 0
}
status.HostInfo.HostName, _ = os.Hostname()
// get ip address
netInterfaces, _ := net.Interfaces()
for i := 0; i < len(netInterfaces); i++ {
if len(netInterfaces[i].Flags) > 2 && netInterfaces[i].Flags[0] == "up" && netInterfaces[i].Flags[1] != "loopback" {
addrs := netInterfaces[i].Addrs
for _, address := range addrs {
if strings.Contains(address.Addr, ".") {
status.HostInfo.Ipv4 += address.Addr + " "
} else if address.Addr[0:6] != "fe80::" {
status.HostInfo.Ipv6 += address.Addr + " "
}
}
}
}
return status return status
} }
@@ -334,45 +376,40 @@ func (s *ServerService) UpdateXray(version string) error {
} }
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": "54321", "webPort": "54321",
"webCertFile": "", "webCertFile": "",
"webKeyFile": "", "webKeyFile": "",
@@ -38,15 +39,19 @@ var defaultValueMap = map[string]string{
"tgBotChatId": "", "tgBotChatId": "",
"tgRunTime": "@daily", "tgRunTime": "@daily",
"tgBotBackup": "false", "tgBotBackup": "false",
"tgBotLoginNotify": "false",
"tgCpu": "0", "tgCpu": "0",
"tgLang": "en-US",
"subEnable": "false", "subEnable": "false",
"subListen": "", "subListen": "",
"subPort": "2096", "subPort": "2096",
"subPath": "sub/", "subPath": "/sub/",
"subDomain": "", "subDomain": "",
"subCertFile": "", "subCertFile": "",
"subKeyFile": "", "subKeyFile": "",
"subUpdates": "12", "subUpdates": "12",
"subEncrypt": "true",
"subShowInfo": "false",
} }
type SettingService struct { type SettingService struct {
@@ -208,6 +213,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")
} }
@@ -244,10 +253,18 @@ 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")
} }
func (s *SettingService) GetTgLang() (string, error) {
return s.getString("tgLang")
}
func (s *SettingService) GetPort() (int, error) { func (s *SettingService) GetPort() (int, error) {
return s.getInt("webPort") return s.getInt("webPort")
} }
@@ -357,6 +374,14 @@ func (s *SettingService) GetSubUpdates() (int, error) {
return s.getInt("subUpdates") return s.getInt("subUpdates")
} }
func (s *SettingService) GetSubEncrypt() (bool, error) {
return s.getBool("subEncrypt")
}
func (s *SettingService) GetSubShowInfo() (bool, error) {
return s.getBool("subShowInfo")
}
func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error { func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error {
if err := allSetting.CheckValid(); err != nil { if err := allSetting.CheckValid(); err != nil {
return err return err

View File

@@ -1,6 +1,7 @@
package service package service
import ( import (
"embed"
"fmt" "fmt"
"net" "net"
"os" "os"
@@ -11,6 +12,7 @@ import (
"x-ui/database/model" "x-ui/database/model"
"x-ui/logger" "x-ui/logger"
"x-ui/util/common" "x-ui/util/common"
"x-ui/web/locale"
"x-ui/xray" "x-ui/xray"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
@@ -19,6 +21,7 @@ import (
var bot *tgbotapi.BotAPI var bot *tgbotapi.BotAPI
var adminIds []int64 var adminIds []int64
var isRunning bool var isRunning bool
var hostname string
type LoginStatus byte type LoginStatus byte
@@ -38,7 +41,17 @@ func (t *Tgbot) NewTgbot() *Tgbot {
return new(Tgbot) return new(Tgbot)
} }
func (t *Tgbot) Start() error { func (t *Tgbot) I18nBot(name string, params ...string) string {
return locale.I18n(locale.Bot, name, params...)
}
func (t *Tgbot) Start(i18nFS embed.FS) error {
err := locale.InitLocalizer(i18nFS, &t.settingService)
if err != nil {
return err
}
t.SetHostname()
tgBottoken, err := t.settingService.GetTgBotToken() tgBottoken, err := t.settingService.GetTgBotToken()
if err != nil || tgBottoken == "" { if err != nil || tgBottoken == "" {
logger.Warning("Get TgBotToken failed:", err) logger.Warning("Get TgBotToken failed:", err)
@@ -51,13 +64,15 @@ func (t *Tgbot) Start() 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 = tgbotapi.NewBotAPI(tgBottoken) bot, err = tgbotapi.NewBotAPI(tgBottoken)
@@ -77,10 +92,20 @@ func (t *Tgbot) Start() error {
return nil return nil
} }
func (t *Tgbot) IsRunnging() bool { func (t *Tgbot) IsRunning() bool {
return isRunning return isRunning
} }
func (t *Tgbot) SetHostname() {
host, err := os.Hostname()
if err != nil {
logger.Error("get hostname error:", err)
hostname = ""
return
}
hostname = host
}
func (t *Tgbot) Stop() { func (t *Tgbot) Stop() {
bot.StopReceivingUpdates() bot.StopReceivingUpdates()
logger.Info("Stop Telegram receiver ...") logger.Info("Stop Telegram receiver ...")
@@ -111,38 +136,52 @@ func (t *Tgbot) OnReceive() {
} }
func (t *Tgbot) answerCommand(message *tgbotapi.Message, chatId int64, isAdmin bool) { func (t *Tgbot) answerCommand(message *tgbotapi.Message, chatId int64, isAdmin bool) {
msg := "" msg, onlyMessage := "", false
command, commandArgs := message.Command(), message.CommandArguments()
// Extract the command from the Message. // Extract the command from the Message.
switch message.Command() { switch command {
case "help": case "help":
msg = "This bot is providing you some specefic data from the server.\n\n Please choose:" msg += t.I18nBot("tgbot.commands.help")
msg += t.I18nBot("tgbot.commands.pleaseChoose")
case "start": case "start":
msg = "Hello <i>" + message.From.FirstName + "</i> 👋" msg += t.I18nBot("tgbot.commands.start", "Firstname=="+message.From.FirstName)
if isAdmin { if isAdmin {
hostname, _ := os.Hostname() msg += t.I18nBot("tgbot.commands.welcome", "Hostname=="+hostname)
msg += "\nWelcome to <b>" + hostname + "</b> management bot"
} }
msg += "\n\nI can do some magics for you, please choose:" msg += "\n\n" + t.I18nBot("tgbot.commands.pleaseChoose")
case "status": case "status":
msg = "bot is ok ✅" onlyMessage = true
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":
if len(message.CommandArguments()) > 1 { onlyMessage = true
if len(commandArgs) > 1 {
if isAdmin { if isAdmin {
t.searchClient(chatId, message.CommandArguments()) t.searchClient(chatId, commandArgs)
} else { } else {
t.searchForClient(chatId, message.CommandArguments()) t.searchForClient(chatId, commandArgs)
} }
} else { } else {
msg = "❗Please provide a text for search!" msg += t.I18nBot("tgbot.commands.usage")
} }
case "inbound": case "inbound":
onlyMessage = true
if isAdmin { if isAdmin {
t.searchInbound(chatId, message.CommandArguments()) t.searchInbound(chatId, commandArgs)
} else { } else {
msg = "❗ Unknown command" msg += t.I18nBot("tgbot.commands.unknown")
} }
default: default:
msg = "❗ Unknown command" msg += t.I18nBot("tgbot.commands.unknown")
}
if onlyMessage {
t.SendMsgToTgbot(chatId, msg)
return
} }
t.SendAnswer(chatId, msg, isAdmin) t.SendAnswer(chatId, msg, isAdmin)
} }
@@ -167,9 +206,9 @@ func (t *Tgbot) asnwerCallback(callbackQuery *tgbotapi.CallbackQuery, isAdmin bo
case "client_traffic": case "client_traffic":
t.getClientUsage(callbackQuery.From.ID, callbackQuery.From.UserName) t.getClientUsage(callbackQuery.From.ID, callbackQuery.From.UserName)
case "client_commands": case "client_commands":
t.SendMsgToTgbot(callbackQuery.From.ID, "To search for statistics, just use folowing command:\r\n \r\n<code>/usage [UID|Password]</code>\r\n \r\nUse UID for vmess/vless and Password for Trojan.") t.SendMsgToTgbot(callbackQuery.From.ID, t.I18nBot("tgbot.commands.helpClientCommands"))
case "commands": case "commands":
t.SendMsgToTgbot(callbackQuery.From.ID, "Search for a client email:\r\n<code>/usage email</code>\r\n \r\nSearch for inbounds (with client stats):\r\n<code>/inbound [remark]</code>") t.SendMsgToTgbot(callbackQuery.From.ID, t.I18nBot("tgbot.commands.helpAdminCommands"))
} }
} }
@@ -183,48 +222,53 @@ func checkAdmin(tgId int64) bool {
} }
func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) { func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) {
var numericKeyboard = tgbotapi.NewInlineKeyboardMarkup( numericKeyboard := tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow( tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("Server Usage", "get_usage"), tgbotapi.NewInlineKeyboardButtonData(t.I18nBot("tgbot.buttons.serverUsage"), "get_usage"),
tgbotapi.NewInlineKeyboardButtonData("Get DB Backup", "get_backup"), tgbotapi.NewInlineKeyboardButtonData(t.I18nBot("tgbot.buttons.dbBackup"), "get_backup"),
), ),
tgbotapi.NewInlineKeyboardRow( tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("Get Inbounds", "inbounds"), tgbotapi.NewInlineKeyboardButtonData(t.I18nBot("tgbot.buttons.getInbounds"), "inbounds"),
tgbotapi.NewInlineKeyboardButtonData("Deplete soon", "deplete_soon"), tgbotapi.NewInlineKeyboardButtonData(t.I18nBot("tgbot.buttons.depleteSoon"), "deplete_soon"),
), ),
tgbotapi.NewInlineKeyboardRow( tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("Commands", "commands"), tgbotapi.NewInlineKeyboardButtonData(t.I18nBot("tgbot.buttons.commands"), "commands"),
), ),
) )
var numericKeyboardClient = tgbotapi.NewInlineKeyboardMarkup( numericKeyboardClient := tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow( tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("Get Usage", "client_traffic"), tgbotapi.NewInlineKeyboardButtonData(t.I18nBot("tgbot.buttons.clientUsage"), "client_traffic"),
tgbotapi.NewInlineKeyboardButtonData("Commands", "client_commands"), tgbotapi.NewInlineKeyboardButtonData(t.I18nBot("tgbot.buttons.commands"), "client_commands"),
), ),
) )
msgConfig := tgbotapi.NewMessage(chatId, msg)
msgConfig.ParseMode = "HTML" var keyboardMarkup tgbotapi.InlineKeyboardMarkup
if isAdmin { if isAdmin {
msgConfig.ReplyMarkup = numericKeyboard keyboardMarkup = numericKeyboard
} else { } else {
msgConfig.ReplyMarkup = numericKeyboardClient keyboardMarkup = numericKeyboardClient
}
_, err := bot.Send(msgConfig)
if err != nil {
logger.Warning("Error sending telegram message :", err)
} }
t.SendMsgToTgbot(chatId, msg, keyboardMarkup)
} }
func (t *Tgbot) SendMsgToTgbot(tgid int64, msg string) { func (t *Tgbot) SendMsgToTgbot(tgid int64, msg string, replyMarkup ...tgbotapi.InlineKeyboardMarkup) {
if !isRunning { if !isRunning {
return return
} }
if msg == "" {
logger.Info("[tgbot] message is empty!")
return
}
var allMessages []string var allMessages []string
limit := 2000 limit := 2000
// paging message if it is big // paging message if it is big
if len(msg) > limit { if len(msg) > limit {
messages := strings.Split(msg, "\r\n \r\n") messages := strings.Split(msg, "\r\n \r\n")
lastIndex := -1 lastIndex := -1
for _, message := range messages { for _, message := range messages {
if (len(allMessages) == 0) || (len(allMessages[lastIndex])+len(message) > limit) { if (len(allMessages) == 0) || (len(allMessages[lastIndex])+len(message) > limit) {
allMessages = append(allMessages, message) allMessages = append(allMessages, message)
@@ -239,6 +283,9 @@ func (t *Tgbot) SendMsgToTgbot(tgid int64, msg string) {
for _, message := range allMessages { for _, message := range allMessages {
info := tgbotapi.NewMessage(tgid, message) info := tgbotapi.NewMessage(tgid, message)
info.ParseMode = "HTML" info.ParseMode = "HTML"
if len(replyMarkup) > 0 {
info.ReplyMarkup = replyMarkup[0]
}
_, err := bot.Send(info) _, err := bot.Send(info)
if err != nil { if err != nil {
logger.Warning("Error sending telegram message :", err) logger.Warning("Error sending telegram message :", err)
@@ -256,37 +303,44 @@ func (t *Tgbot) SendMsgToTgbotAdmins(msg string) {
func (t *Tgbot) SendReport() { func (t *Tgbot) SendReport() {
runTime, err := t.settingService.GetTgbotRuntime() runTime, err := t.settingService.GetTgbotRuntime()
if err == nil && len(runTime) > 0 { if err == nil && len(runTime) > 0 {
t.SendMsgToTgbotAdmins("🕰 Scheduled reports: " + runTime + "\r\nDate-Time: " + time.Now().Format("2006-01-02 15:04:05")) msg := ""
msg += t.I18nBot("tgbot.messages.report", "RunTime=="+runTime)
msg += t.I18nBot("tgbot.messages.datetime", "DateTime=="+time.Now().Format("2006-01-02 15:04:05"))
t.SendMsgToTgbotAdmins(msg)
} }
info := t.getServerUsage() info := t.getServerUsage()
t.SendMsgToTgbotAdmins(info) t.SendMsgToTgbotAdmins(info)
exhausted := t.getExhausted() exhausted := t.getExhausted()
t.SendMsgToTgbotAdmins(exhausted) t.SendMsgToTgbotAdmins(exhausted)
backupEnable, err := t.settingService.GetTgBotBackup() backupEnable, err := t.settingService.GetTgBotBackup()
if err == nil && backupEnable { if err == nil && backupEnable {
for _, adminId := range adminIds { t.SendBackupToAdmins()
t.sendBackup(int64(adminId)) }
} }
func (t *Tgbot) SendBackupToAdmins() {
if !t.IsRunning() {
return
}
for _, adminId := range adminIds {
t.sendBackup(int64(adminId))
} }
} }
func (t *Tgbot) getServerUsage() string { func (t *Tgbot) getServerUsage() string {
var info string info, ipv4, ipv6 := "", "", ""
//get hostname info += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname)
name, err := os.Hostname() info += t.I18nBot("tgbot.messages.version", "Version=="+config.GetVersion())
if err != nil {
logger.Error("get hostname error:", err) // get ip address
name = ""
}
info = fmt.Sprintf("💻 Hostname: %s\r\n", name)
info += fmt.Sprintf("🚀X-UI Version: %s\r\n", config.GetVersion())
//get ip address
var ip string
var ipv6 string
netInterfaces, err := net.Interfaces() netInterfaces, err := net.Interfaces()
if err != nil { if err != nil {
logger.Error("net.Interfaces failed, err:", err.Error()) logger.Error("net.Interfaces failed, err: ", err.Error())
info += "🌐 IP: Unknown\r\n \r\n" info += t.I18nBot("tgbot.messages.ip", "IP=="+t.I18nBot("tgbot.unknown"))
info += " \r\n"
} else { } else {
for i := 0; i < len(netInterfaces); i++ { for i := 0; i < len(netInterfaces); i++ {
if (netInterfaces[i].Flags & net.FlagUp) != 0 { if (netInterfaces[i].Flags & net.FlagUp) != 0 {
@@ -295,7 +349,7 @@ func (t *Tgbot) getServerUsage() string {
for _, address := range addrs { for _, address := range addrs {
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil { if ipnet.IP.To4() != nil {
ip += ipnet.IP.String() + " " ipv4 += ipnet.IP.String() + " "
} else if ipnet.IP.To16() != nil && !ipnet.IP.IsLinkLocalUnicast() { } else if ipnet.IP.To16() != nil && !ipnet.IP.IsLinkLocalUnicast() {
ipv6 += ipnet.IP.String() + " " ipv6 += ipnet.IP.String() + " "
} }
@@ -303,42 +357,50 @@ func (t *Tgbot) getServerUsage() string {
} }
} }
} }
info += fmt.Sprintf("🌐IP: %s\r\n🌐IPv6: %s\r\n", ip, ipv6)
info += t.I18nBot("tgbot.messages.ipv4", "IPv4=="+ipv4)
info += t.I18nBot("tgbot.messages.ipv6", "IPv6=="+ipv6)
} }
// get latest status of server // get latest status of server
t.lastStatus = t.serverService.GetStatus(t.lastStatus) t.lastStatus = t.serverService.GetStatus(t.lastStatus)
info += fmt.Sprintf("🔌Server Uptime: %d days\r\n", int(t.lastStatus.Uptime/86400)) info += t.I18nBot("tgbot.messages.serverUpTime", "UpTime=="+strconv.FormatUint(t.lastStatus.Uptime/86400, 10), "Unit=="+t.I18nBot("tgbot.days"))
info += fmt.Sprintf("📈Server Load: %.1f, %.1f, %.1f\r\n", t.lastStatus.Loads[0], t.lastStatus.Loads[1], t.lastStatus.Loads[2]) info += t.I18nBot("tgbot.messages.serverLoad", "Load1=="+strconv.FormatFloat(t.lastStatus.Loads[0], 'f', 2, 64), "Load2=="+strconv.FormatFloat(t.lastStatus.Loads[1], 'f', 2, 64), "Load3=="+strconv.FormatFloat(t.lastStatus.Loads[2], 'f', 2, 64))
info += fmt.Sprintf("📋Server Memory: %s/%s\r\n", common.FormatTraffic(int64(t.lastStatus.Mem.Current)), common.FormatTraffic(int64(t.lastStatus.Mem.Total))) info += t.I18nBot("tgbot.messages.serverMemory", "Current=="+common.FormatTraffic(int64(t.lastStatus.Mem.Current)), "Total=="+common.FormatTraffic(int64(t.lastStatus.Mem.Total)))
info += fmt.Sprintf("🔹TcpCount: %d\r\n", t.lastStatus.TcpCount) info += t.I18nBot("tgbot.messages.tcpCount", "Count=="+strconv.Itoa(t.lastStatus.TcpCount))
info += fmt.Sprintf("🔸UdpCount: %d\r\n", t.lastStatus.UdpCount) info += t.I18nBot("tgbot.messages.udpCount", "Count=="+strconv.Itoa(t.lastStatus.UdpCount))
info += fmt.Sprintf("🚦Traffic: %s (↑%s,↓%s)\r\n", common.FormatTraffic(int64(t.lastStatus.NetTraffic.Sent+t.lastStatus.NetTraffic.Recv)), common.FormatTraffic(int64(t.lastStatus.NetTraffic.Sent)), common.FormatTraffic(int64(t.lastStatus.NetTraffic.Recv))) info += t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic(int64(t.lastStatus.NetTraffic.Sent+t.lastStatus.NetTraffic.Recv)), "Upload=="+common.FormatTraffic(int64(t.lastStatus.NetTraffic.Sent)), "Download=="+common.FormatTraffic(int64(t.lastStatus.NetTraffic.Recv)))
info += fmt.Sprintf("Xray status: %s", t.lastStatus.Xray.State) info += t.I18nBot("tgbot.messages.xrayStatus", "State=="+fmt.Sprint(t.lastStatus.Xray.State))
return info return info
} }
func (t *Tgbot) UserLoginNotify(username string, ip string, time string, status LoginStatus) { func (t *Tgbot) UserLoginNotify(username string, ip string, time string, status LoginStatus) {
if !t.IsRunning() {
return
}
if username == "" || ip == "" || time == "" { if username == "" || ip == "" || time == "" {
logger.Warning("UserLoginNotify failed,invalid info") logger.Warning("UserLoginNotify failed,invalid info")
return return
} }
var msg string
// Get hostname loginNotifyEnabled, err := t.settingService.GetTgBotLoginNotify()
name, err := os.Hostname() if err != nil || !loginNotifyEnabled {
if err != nil {
logger.Warning("get hostname error:", err)
return return
} }
msg := ""
if status == LoginSuccess { if status == LoginSuccess {
msg = fmt.Sprintf("✅ Successfully logged-in to the panel\r\nHostname:%s\r\n", name) msg += t.I18nBot("tgbot.messages.loginSuccess")
} else if status == LoginFail { } else if status == LoginFail {
msg = fmt.Sprintf("❗ Login to the panel was unsuccessful\r\nHostname:%s\r\n", name) msg += t.I18nBot("tgbot.messages.loginFailed")
} }
msg += fmt.Sprintf("⏰ Time:%s\r\n", time) msg += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname)
msg += fmt.Sprintf("🆔 Username:%s\r\n", username) msg += t.I18nBot("tgbot.messages.username", "Username=="+username)
msg += fmt.Sprintf("🌐 IP:%s\r\n", ip) msg += t.I18nBot("tgbot.messages.ip", "IP=="+ip)
msg += t.I18nBot("tgbot.messages.time", "Time=="+time)
t.SendMsgToTgbotAdmins(msg) t.SendMsgToTgbotAdmins(msg)
} }
@@ -348,17 +410,19 @@ func (t *Tgbot) getInboundUsages() string {
inbouds, err := t.inboundService.GetAllInbounds() inbouds, err := t.inboundService.GetAllInbounds()
if err != nil { if err != nil {
logger.Warning("GetAllInbounds run failed:", err) logger.Warning("GetAllInbounds run failed:", err)
info += "❌ Failed to get inbounds" info += t.I18nBot("tgbot.answers.getInboundsFailed")
} else { } else {
// NOTE:If there no any sessions here,need to notify here // NOTE:If there no any sessions here,need to notify here
// TODO:Sub-node push, automatic conversion format // TODO:Sub-node push, automatic conversion format
for _, inbound := range inbouds { for _, inbound := range inbouds {
info += fmt.Sprintf("📍Inbound:%s\r\nPort:%d\r\n", inbound.Remark, inbound.Port) info += t.I18nBot("tgbot.messages.inbound", "Remark=="+inbound.Remark)
info += fmt.Sprintf("Traffic: %s (↑%s,↓%s)\r\n", common.FormatTraffic((inbound.Up + inbound.Down)), common.FormatTraffic(inbound.Up), common.FormatTraffic(inbound.Down)) info += t.I18nBot("tgbot.messages.port", "Port=="+strconv.Itoa(inbound.Port))
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 += "Expire date: ♾ Unlimited\r\n \r\n" info += t.I18nBot("tgbot.messages.expire", "DateTime=="+t.I18nBot("tgbot.unlimited"))
} else { } else {
info += fmt.Sprintf("Expire date:%s\r\n \r\n", time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05")) info += t.I18nBot("tgbot.messages.expire", "DateTime=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
} }
} }
} }
@@ -367,75 +431,92 @@ func (t *Tgbot) getInboundUsages() string {
func (t *Tgbot) getClientUsage(chatId int64, tgUserName string) { func (t *Tgbot) getClientUsage(chatId int64, tgUserName string) {
if len(tgUserName) == 0 { if len(tgUserName) == 0 {
msg := "Your configuration is not found!\nYou should configure your telegram username and ask Admin to add it to your configuration." msg := t.I18nBot("tgbot.answers.askToAddUser")
t.SendMsgToTgbot(chatId, msg) t.SendMsgToTgbot(chatId, msg)
return return
} }
traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserName) traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserName)
if err != nil { if err != nil {
logger.Warning(err) logger.Warning(err)
msg := "❌ Something went wrong!" msg := t.I18nBot("tgbot.wentWrong")
t.SendMsgToTgbot(chatId, msg) t.SendMsgToTgbot(chatId, msg)
return return
} }
if len(traffics) == 0 { if len(traffics) == 0 {
msg := "Your configuration is not found!\nPlease ask your Admin to use your telegram username in your configuration(s).\n\nYour username: <b>@" + tgUserName + "</b>" msg := t.I18nBot("tgbot.answers.askToAddUserName", "TgUserName=="+tgUserName)
t.SendMsgToTgbot(chatId, msg) t.SendMsgToTgbot(chatId, msg)
return return
} }
for _, traffic := range traffics { for _, traffic := range traffics {
expiryTime := "" expiryTime := ""
if traffic.ExpiryTime == 0 { if traffic.ExpiryTime == 0 {
expiryTime = "♾Unlimited" expiryTime = t.I18nBot("tgbot.unlimited")
} else if traffic.ExpiryTime < 0 { } else if traffic.ExpiryTime < 0 {
expiryTime = fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000) expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
} else { } 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")
} }
total := "" total := ""
if traffic.Total == 0 { if traffic.Total == 0 {
total = "♾Unlimited" total = t.I18nBot("tgbot.unlimited")
} else { } else {
total = common.FormatTraffic((traffic.Total)) total = common.FormatTraffic((traffic.Total))
} }
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)), output := ""
total, expiryTime) 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.upload", "Upload=="+common.FormatTraffic(traffic.Up))
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.expireIn", "Time=="+expiryTime)
t.SendMsgToTgbot(chatId, output) t.SendMsgToTgbot(chatId, output)
} }
t.SendAnswer(chatId, "Please choose:", false) t.SendAnswer(chatId, t.I18nBot("tgbot.commands.pleaseChoose"), false)
} }
func (t *Tgbot) searchClient(chatId int64, email string) { func (t *Tgbot) searchClient(chatId int64, email string) {
traffic, err := t.inboundService.GetClientTrafficByEmail(email) traffic, err := t.inboundService.GetClientTrafficByEmail(email)
if err != nil { if err != nil {
logger.Warning(err) logger.Warning(err)
msg := "❌ Something went wrong!" msg := t.I18nBot("tgbot.wentWrong")
t.SendMsgToTgbot(chatId, msg) t.SendMsgToTgbot(chatId, msg)
return return
} }
if traffic == nil { if traffic == nil {
msg := "No result!" msg := t.I18nBot("tgbot.noResult")
t.SendMsgToTgbot(chatId, msg) t.SendMsgToTgbot(chatId, msg)
return return
} }
expiryTime := "" expiryTime := ""
if traffic.ExpiryTime == 0 { if traffic.ExpiryTime == 0 {
expiryTime = "♾Unlimited" expiryTime = t.I18nBot("tgbot.unlimited")
} else if traffic.ExpiryTime < 0 { } else if traffic.ExpiryTime < 0 {
expiryTime = fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000) expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
} else { } 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")
} }
total := "" total := ""
if traffic.Total == 0 { if traffic.Total == 0 {
total = "♾Unlimited" total = t.I18nBot("tgbot.unlimited")
} else { } else {
total = common.FormatTraffic((traffic.Total)) total = common.FormatTraffic((traffic.Total))
} }
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)), output := ""
total, expiryTime) 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.upload", "Upload=="+common.FormatTraffic(traffic.Up))
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.expireIn", "Time=="+expiryTime)
t.SendMsgToTgbot(chatId, output) t.SendMsgToTgbot(chatId, output)
} }
@@ -443,38 +524,55 @@ func (t *Tgbot) searchInbound(chatId int64, remark string) {
inbouds, err := t.inboundService.SearchInbounds(remark) inbouds, err := t.inboundService.SearchInbounds(remark)
if err != nil { if err != nil {
logger.Warning(err) logger.Warning(err)
msg := "❌ Something went wrong!" msg := t.I18nBot("tgbot.wentWrong")
t.SendMsgToTgbot(chatId, msg) t.SendMsgToTgbot(chatId, msg)
return return
} }
if len(inbouds) == 0 {
msg := t.I18nBot("tgbot.noInbounds")
t.SendMsgToTgbot(chatId, msg)
return
}
for _, inbound := range inbouds { for _, inbound := range inbouds {
info := "" info := ""
info += fmt.Sprintf("📍Inbound:%s\r\nPort:%d\r\n", inbound.Remark, inbound.Port) info += t.I18nBot("tgbot.messages.inbound", "Remark=="+inbound.Remark)
info += fmt.Sprintf("Traffic: %s (↑%s,↓%s)\r\n", common.FormatTraffic((inbound.Up + inbound.Down)), common.FormatTraffic(inbound.Up), common.FormatTraffic(inbound.Down)) info += t.I18nBot("tgbot.messages.port", "Port=="+strconv.Itoa(inbound.Port))
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 += "Expire date: ♾ Unlimited\r\n \r\n" info += t.I18nBot("tgbot.messages.expire", "DateTime=="+t.I18nBot("tgbot.unlimited"))
} else { } else {
info += fmt.Sprintf("Expire date:%s\r\n \r\n", time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05")) info += t.I18nBot("tgbot.messages.expire", "DateTime=="+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 := ""
if traffic.ExpiryTime == 0 { if traffic.ExpiryTime == 0 {
expiryTime = "♾Unlimited" expiryTime = t.I18nBot("tgbot.unlimited")
} else if traffic.ExpiryTime < 0 { } else if traffic.ExpiryTime < 0 {
expiryTime = fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000) expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
} else { } 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")
} }
total := "" total := ""
if traffic.Total == 0 { if traffic.Total == 0 {
total = "♾Unlimited" total = t.I18nBot("tgbot.unlimited")
} else { } else {
total = common.FormatTraffic((traffic.Total)) total = common.FormatTraffic((traffic.Total))
} }
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)), output := ""
total, expiryTime) 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.upload", "Upload=="+common.FormatTraffic(traffic.Up))
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.expireIn", "Time=="+expiryTime)
t.SendMsgToTgbot(chatId, output) t.SendMsgToTgbot(chatId, output)
} }
} }
@@ -484,32 +582,40 @@ func (t *Tgbot) searchForClient(chatId int64, query string) {
traffic, err := t.inboundService.SearchClientTraffic(query) traffic, err := t.inboundService.SearchClientTraffic(query)
if err != nil { if err != nil {
logger.Warning(err) logger.Warning(err)
msg := "❌ Something went wrong!" msg := t.I18nBot("tgbot.wentWrong")
t.SendMsgToTgbot(chatId, msg) t.SendMsgToTgbot(chatId, msg)
return return
} }
if traffic == nil { if traffic == nil {
msg := "No result!" msg := t.I18nBot("tgbot.noResult")
t.SendMsgToTgbot(chatId, msg) t.SendMsgToTgbot(chatId, msg)
return return
} }
expiryTime := "" expiryTime := ""
if traffic.ExpiryTime == 0 { if traffic.ExpiryTime == 0 {
expiryTime = "♾Unlimited" expiryTime = t.I18nBot("tgbot.unlimited")
} else if traffic.ExpiryTime < 0 { } else if traffic.ExpiryTime < 0 {
expiryTime = fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000) expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
} else { } 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")
} }
total := "" total := ""
if traffic.Total == 0 { if traffic.Total == 0 {
total = "♾Unlimited" total = t.I18nBot("tgbot.unlimited")
} else { } else {
total = common.FormatTraffic((traffic.Total)) total = common.FormatTraffic((traffic.Total))
} }
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)), output := ""
total, expiryTime) 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.upload", "Upload=="+common.FormatTraffic(traffic.Up))
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.expireIn", "Time=="+expiryTime)
t.SendMsgToTgbot(chatId, output) t.SendMsgToTgbot(chatId, output)
} }
@@ -521,7 +627,7 @@ func (t *Tgbot) getExhausted() string {
var exhaustedClients []xray.ClientTraffic var exhaustedClients []xray.ClientTraffic
var disabledInbounds []model.Inbound var disabledInbounds []model.Inbound
var disabledClients []xray.ClientTraffic var disabledClients []xray.ClientTraffic
output := ""
TrafficThreshold, err := t.settingService.GetTrafficDiff() TrafficThreshold, err := t.settingService.GetTrafficDiff()
if err == nil && TrafficThreshold > 0 { if err == nil && TrafficThreshold > 0 {
trDiff = int64(TrafficThreshold) * 1073741824 trDiff = int64(TrafficThreshold) * 1073741824
@@ -534,6 +640,7 @@ func (t *Tgbot) getExhausted() string {
if err != nil { if err != nil {
logger.Warning("Unable to load Inbounds", err) logger.Warning("Unable to load Inbounds", err)
} }
for _, inbound := range inbounds { for _, inbound := range inbounds {
if inbound.Enable { if inbound.Enable {
if (inbound.ExpiryTime > 0 && (inbound.ExpiryTime-now < exDiff)) || if (inbound.ExpiryTime > 0 && (inbound.ExpiryTime-now < exDiff)) ||
@@ -556,39 +663,63 @@ func (t *Tgbot) getExhausted() string {
disabledInbounds = append(disabledInbounds, *inbound) disabledInbounds = append(disabledInbounds, *inbound)
} }
} }
output += fmt.Sprintf("Exhausted Inbounds count:\r\n🛑 Disabled: %d\r\n🔜 Deplete soon: %d\r\n \r\n", len(disabledInbounds), len(exhaustedInbounds))
// Inbounds
output := ""
output += t.I18nBot("tgbot.messages.exhaustedCount", "Type=="+t.I18nBot("tgbot.inbounds"))
output += t.I18nBot("tgbot.messages.disabled", "Disabled=="+strconv.Itoa(len(disabledInbounds)))
output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+strconv.Itoa(len(exhaustedInbounds)))
output += "\r\n \r\n"
if len(exhaustedInbounds) > 0 { if len(exhaustedInbounds) > 0 {
output += "Exhausted Inbounds:\r\n" output += t.I18nBot("tgbot.messages.exhaustedMsg", "Type=="+t.I18nBot("tgbot.inbounds"))
for _, inbound := range exhaustedInbounds { for _, inbound := range exhaustedInbounds {
output += fmt.Sprintf("📍Inbound:%s\r\nPort:%d\r\nTraffic: %s (↑%s,↓%s)\r\n", inbound.Remark, inbound.Port, common.FormatTraffic((inbound.Up + inbound.Down)), common.FormatTraffic(inbound.Up), common.FormatTraffic(inbound.Down)) output += t.I18nBot("tgbot.messages.inbound", "Remark=="+inbound.Remark)
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))
if inbound.ExpiryTime == 0 { if inbound.ExpiryTime == 0 {
output += "Expire date: ♾Unlimited\r\n \r\n" output += t.I18nBot("tgbot.messages.expire", "DateTime=="+t.I18nBot("tgbot.unlimited"))
} else { } else {
output += fmt.Sprintf("Expire date:%s\r\n \r\n", time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05")) output += t.I18nBot("tgbot.messages.expire", "DateTime=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
} }
output += "\r\n \r\n"
} }
} }
output += fmt.Sprintf("Exhausted Clients count:\r\n🛑 Exhausted: %d\r\n🔜 Deplete soon: %d\r\n \r\n", len(disabledClients), len(exhaustedClients))
// Clients
output += t.I18nBot("tgbot.messages.exhaustedCount", "Type=="+t.I18nBot("tgbot.clients"))
output += t.I18nBot("tgbot.messages.disabled", "Disabled=="+strconv.Itoa(len(disabledClients)))
output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+strconv.Itoa(len(exhaustedClients)))
output += "\r\n \r\n"
if len(exhaustedClients) > 0 { if len(exhaustedClients) > 0 {
output += "Exhausted Clients:\r\n" output += t.I18nBot("tgbot.messages.exhaustedMsg", "Type=="+t.I18nBot("tgbot.clients"))
for _, traffic := range exhaustedClients { for _, traffic := range exhaustedClients {
expiryTime := "" expiryTime := ""
if traffic.ExpiryTime == 0 { if traffic.ExpiryTime == 0 {
expiryTime = "♾Unlimited" expiryTime = t.I18nBot("tgbot.unlimited")
} else if traffic.ExpiryTime < 0 { } else if traffic.ExpiryTime < 0 {
expiryTime += fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000) expiryTime += fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
} else { } 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")
} }
total := "" total := ""
if traffic.Total == 0 { if traffic.Total == 0 {
total = "♾Unlimited" total = t.I18nBot("tgbot.unlimited")
} else { } else {
total = common.FormatTraffic((traffic.Total)) total = common.FormatTraffic((traffic.Total))
} }
output += fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire date: %s\r\n \r\n",
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)), output += t.I18nBot("tgbot.messages.active", "Enable=="+strconv.FormatBool(traffic.Enable))
total, expiryTime) output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
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.total", "UpDown=="+common.FormatTraffic((traffic.Up+traffic.Down)), "Total=="+total)
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
output += "\r\n \r\n"
} }
} }
@@ -596,14 +727,20 @@ func (t *Tgbot) getExhausted() string {
} }
func (t *Tgbot) sendBackup(chatId int64) { func (t *Tgbot) sendBackup(chatId int64) {
sendingTime := time.Now().Format("2006-01-02 15:04:05") if !t.IsRunning() {
t.SendMsgToTgbot(chatId, "Backup time: "+sendingTime) return
}
output := t.I18nBot("tgbot.messages.backupTime", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
t.SendMsgToTgbot(chatId, output)
file := tgbotapi.FilePath(config.GetDBPath()) file := tgbotapi.FilePath(config.GetDBPath())
msg := tgbotapi.NewDocument(chatId, file) msg := tgbotapi.NewDocument(chatId, file)
_, err := bot.Send(msg) _, err := bot.Send(msg)
if err != nil { if err != nil {
logger.Warning("Error in uploading backup: ", err) logger.Warning("Error in uploading backup: ", err)
} }
file = tgbotapi.FilePath(xray.GetConfigPath()) file = tgbotapi.FilePath(xray.GetConfigPath())
msg = tgbotapi.NewDocument(chatId, file) msg = tgbotapi.NewDocument(chatId, file)
_, err = bot.Send(msg) _, err = bot.Send(msg)

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

@@ -12,7 +12,6 @@
"protocol" = "Protocol" "protocol" = "Protocol"
"search" = "Search" "search" = "Search"
"filter" = "Filter" "filter" = "Filter"
"loading" = "Loading" "loading" = "Loading"
"second" = "Second" "second" = "Second"
"minute" = "Minute" "minute" = "Minute"
@@ -40,7 +39,6 @@
"depleted" = "Depleted" "depleted" = "Depleted"
"depletingSoon" = "Depleting soon" "depletingSoon" = "Depleting soon"
"domainName" = "Domain name" "domainName" = "Domain name"
"additional" = "Alter ID"
"monitor" = "Listen IP" "monitor" = "Listen IP"
"certificate" = "Certificate" "certificate" = "Certificate"
"fail" = " Fail" "fail" = " Fail"
@@ -49,6 +47,9 @@
"install" = "Install" "install" = "Install"
"clients" = "Clients" "clients" = "Clients"
"usage" = "Usage" "usage" = "Usage"
"remained" = "Remained"
"secAlertTitle" = "Security Alert"
"secAlertSsl" = "This connection is not secure; Please refrain from entering sensitive information until TLS is activated for data protection"
[menu] [menu]
"dashboard" = "System Status" "dashboard" = "System Status"
@@ -126,7 +127,6 @@
"network" = "Network" "network" = "Network"
"destinationPort" = "Destination Port" "destinationPort" = "Destination Port"
"targetAddress" = "Target Address" "targetAddress" = "Target Address"
"disableInsecureEncryption" = "Disable Insecure Encryption"
"monitorDesc" = "Leave blank by default" "monitorDesc" = "Leave blank by default"
"meansNoLimit" = "Means No Limit" "meansNoLimit" = "Means No Limit"
"totalFlow" = "Total Flow" "totalFlow" = "Total Flow"
@@ -160,7 +160,7 @@
"email" = "Email" "email" = "Email"
"emailDesc" = "Please provide a unique email address." "emailDesc" = "Please provide a unique email address."
"setDefaultCert" = "Set cert from panel" "setDefaultCert" = "Set cert from panel"
"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]
@@ -212,6 +212,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" = "Port number for serving the panel." "panelPortDesc" = "Port number for serving the panel."
"publicKeyPath" = "Panel Certificate Public Key File Path" "publicKeyPath" = "Panel Certificate Public Key File Path"
@@ -229,11 +231,13 @@
"telegramToken" = "Telegram Token" "telegramToken" = "Telegram Token"
"telegramTokenDesc" = "The Token you have got from @BotFather" "telegramTokenDesc" = "The Token you have got from @BotFather"
"telegramChatId" = "Telegram Admin ChatIDs" "telegramChatId" = "Telegram Admin ChatIDs"
"telegramChatIdDesc" = "Multi chatIDs separated by comma." "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" = "Send database backup file with report notification" "tgNotifyBackupDesc" = "Send 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 time that you can stay login (unit: minute)" "sessionMaxAgeDesc" = "The time that you can stay login (unit: minute)"
"expireTimeDiff" = "Expiration threshold for notification" "expireTimeDiff" = "Expiration threshold for notification"
@@ -261,7 +265,10 @@
"subDomainDesc" = "Leave blank by default to monitor all domains and IPs" "subDomainDesc" = "Leave blank by default to monitor all domains and IPs"
"subUpdates" = "Subscription update intervals" "subUpdates" = "Subscription update intervals"
"subUpdatesDesc" = "Interval hours between updates in client application" "subUpdatesDesc" = "Interval hours between updates in client application"
"subEncrypt" = "Encrypt configs"
"subEncryptDesc" = "Encrypt the returned configs in subscription"
"subShowInfo" = "Show usage info"
"subShowInfoDesc" = "Show remianed traffic and date after config name"
[pages.settings.templates] [pages.settings.templates]
"title" = "Templates" "title" = "Templates"
@@ -332,6 +339,7 @@
"manualBlockedDomains" = "List of Blocked Domains" "manualBlockedDomains" = "List of Blocked Domains"
"manualDirectIPs" = "List of Direct IPs" "manualDirectIPs" = "List of Direct IPs"
"manualDirectDomains" = "List of Direct Domains" "manualDirectDomains" = "List of Direct Domains"
"manualIPv4Domains" = "List of IPv4 Domains"
[pages.settings.toasts] [pages.settings.toasts]
"modifySettings" = "Modify Settings " "modifySettings" = "Modify Settings "
@@ -339,3 +347,74 @@
"modifyUser" = "Modify User " "modifyUser" = "Modify User "
"originalUserPassIncorrect" = "Incorrect original username or password" "originalUserPassIncorrect" = "Incorrect original username or password"
"userPassMustBeNotEmpty" = "New username and new password cannot be empty" "userPassMustBeNotEmpty" = "New username and new password cannot be empty"
[tgbot]
"noResult" = "❗ No result!"
"wentWrong" = "❌ Something went wrong!"
"noInbounds" = "❗ No inbound found!"
"unlimited" = "♾ Unlimited"
"day" = "Day"
"days" = "Days"
"unknown" = "Unknown"
"inbounds" = "Inbounds"
"clients" = "Clients"
[tgbot.commands]
"unknown" = "❗ Unknown command"
"pleaseChoose" = "👇 Please choose:\r\n"
"help" = "🤖 Welcome to this bot! It's designed to offer you specific data from the server, and it allows you to make modifications as needed.\r\n\r\n"
"start" = "👋 Hello <i>{{ .Firstname }}</i>.\r\n"
"welcome" = "🤖 Welcome to <b>{{ .Hostname }}</b> management bot.\r\n"
"status" = "✅ Bot is ok!"
"usage" = "❗ Please provide a text to search!"
"getID" = "🆔 Your ID: <code>{{ .ID }}</code>"
"helpAdminCommands" = "Search for a client email:\r\n<code>/usage [Email]</code>\r\n \r\nSearch for inbounds (with client stats):\r\n<code>/inbound [Remark]</code>"
"helpClientCommands" = "To search for statistics, just use folowing command:\r\n \r\n<code>/usage [UUID|Password]</code>\r\n \r\nUse UUID for vmess/vless and Password for Trojan."
[tgbot.messages]
"cpuThreshold" = "🔴 The CPU usage {{ .Percent }}% is more than threshold {{ .Threshold }}%"
"loginSuccess" = "✅ Successfully logged-in to the panel.\r\n"
"loginFailed" = "❗️ Login to the panel failed.\r\n"
"report" = "🕰 Scheduled Reports: {{ .RunTime }}\r\n"
"datetime" = "⏰ Date-Time: {{ .DateTime }}\r\n"
"hostname" = "💻 Hostname: {{ .Hostname }}\r\n"
"version" = "🚀 X-UI Version: {{ .Version }}\r\n"
"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n"
"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n"
"ip" = "🌐 IP: {{ .IP }}\r\n"
"serverUpTime" = "⏳ Server Uptime: {{ .UpTime }} {{ .Unit }}\r\n"
"serverLoad" = "📈 Server Load: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
"serverMemory" = "📋 Server Memory: {{ .Current }}/{{ .Total }}\r\n"
"tcpCount" = "🔹 TcpCount: {{ .Count }}\r\n"
"udpCount" = "🔸 UdpCount: {{ .Count }}\r\n"
"traffic" = "🚦 Traffic: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
"xrayStatus" = " Xray Status: {{ .State }}\r\n"
"username" = "👤 Username: {{ .Username }}\r\n"
"time" = "⏰ Time: {{ .Time }}\r\n"
"inbound" = "📍 Inbound: {{ .Remark }}\r\n"
"port" = "🔌 Port: {{ .Port }}\r\n"
"expire" = "📅 Expire Date: {{ .DateTime }}\r\n \r\n"
"expireIn" = "📅 Expire In: {{ .Time }}\r\n \r\n"
"active" = "💡 Active: {{ .Enable }}\r\n"
"email" = "📧 Email: {{ .Email }}\r\n"
"upload" = "🔼 Upload↑: {{ .Upload }}\r\n"
"download" = "🔽 Download↓: {{ .Download }}\r\n"
"total" = "🔄 Total: {{ .UpDown }} / {{ .Total }}\r\n"
"exhaustedMsg" = "🚨 Exhausted {{ .Type }}:\r\n"
"exhaustedCount" = "🚨 Exhausted {{ .Type }} count:\r\n"
"disabled" = "🛑 Disabled: {{ .Disabled }}\r\n"
"depleteSoon" = "🔜 Deplete soon: {{ .Deplete }}\r\n \r\n"
"backupTime" = "🗄 Backup Time: {{ .Time }}\r\n"
[tgbot.buttons]
"dbBackup" = "Get DB Backup"
"serverUsage" = "Server Usage"
"getInbounds" = "Get Inbounds"
"depleteSoon" = "Deplete soon"
"clientUsage" = "Get Usage"
"commands" = "Commands"
[tgbot.answers]
"getInboundsFailed" = "❌ Failed to get inbounds"
"askToAddUser" = "Your configuration is not found!\r\nYou should configure your telegram username and ask your Admin to add it to your configuration."
"askToAddUserName" = "Your configuration is not found!\r\nPlease ask your Admin to use your telegram username in your configuration(s).\r\n\r\nYour username: <b>@{{ .TgUserName }}</b>"

View File

@@ -12,7 +12,6 @@
"protocol" = "پروتکل" "protocol" = "پروتکل"
"search" = "جستجو" "search" = "جستجو"
"filter" = "فیلتر" "filter" = "فیلتر"
"loading" = "در حال بروزرسانی.." "loading" = "در حال بروزرسانی.."
"second" = "ثانیه" "second" = "ثانیه"
"minute" = "دقیقه" "minute" = "دقیقه"
@@ -40,7 +39,6 @@
"depleted" = "منقضی" "depleted" = "منقضی"
"depletingSoon" = "در حال انقضا" "depletingSoon" = "در حال انقضا"
"domainName" = "آدرس دامنه" "domainName" = "آدرس دامنه"
"additional" = "آی دی جایگزین"
"monitor" = "آی پی اتصال" "monitor" = "آی پی اتصال"
"certificate" = "گواهی دیجیتال" "certificate" = "گواهی دیجیتال"
"fail" = "خطا" "fail" = "خطا"
@@ -49,6 +47,9 @@
"install" = "نصب" "install" = "نصب"
"clients" = "کاربران" "clients" = "کاربران"
"usage" = "استفاده" "usage" = "استفاده"
"remained" = "باقیمانده"
"secAlertTitle" = "هشدار امنیتی"
"secAlertSsl" = "این اتصال امن نیست؛ لطفا تا زمانی که تی‌ال‌اس برای حفاظت از داده ها فعال نشده است از وارد کردن اطلاعات حساس خودداری کنید"
[menu] [menu]
"dashboard" = "وضعیت سیستم" "dashboard" = "وضعیت سیستم"
@@ -126,7 +127,6 @@
"network" = "شبکه" "network" = "شبکه"
"destinationPort" = "پورت مقصد" "destinationPort" = "پورت مقصد"
"targetAddress" = "آدرس مقصد" "targetAddress" = "آدرس مقصد"
"disableInsecureEncryption" = "غیرفعال سازی رمزگذاری ناامن"
"monitorDesc" = "به طور پیش فرض خالی بگذارید" "monitorDesc" = "به طور پیش فرض خالی بگذارید"
"meansNoLimit" = "یعنی بدون محدودیت" "meansNoLimit" = "یعنی بدون محدودیت"
"totalFlow" = "کل ترافیک" "totalFlow" = "کل ترافیک"
@@ -159,7 +159,7 @@
"email" = "ایمیل" "email" = "ایمیل"
"emailDesc" = "ایمیل باید کاملا منحصر به فرد باشد" "emailDesc" = "ایمیل باید کاملا منحصر به فرد باشد"
"setDefaultCert" = "استفاده از گواهی پنل" "setDefaultCert" = "استفاده از گواهی پنل"
"telegramDesc" = "از آیدی تلگرام بدون @ یا آیدی چت استفاده کنید (می توانید آن را از اینجا دریافت کنید @userinfobot)" "telegramDesc" = "از آیدی تلگرام بدون @ یا آیدی چت استفاده کنید (می توانید آن را از اینجا دریافت کنید @userinfobot یا در ربات دستور '/id' را وارد کنید)"
"subscriptionDesc" = "می توانید ساب لینک خود را در جزئیات پیدا کنید، همچنین می توانید از همین نام برای چندین کانفیگ استفاده کنید" "subscriptionDesc" = "می توانید ساب لینک خود را در جزئیات پیدا کنید، همچنین می توانید از همین نام برای چندین کانفیگ استفاده کنید"
[pages.client] [pages.client]
@@ -211,6 +211,8 @@
"TGBotSettings" = "تنظیمات ربات تلگرام" "TGBotSettings" = "تنظیمات ربات تلگرام"
"panelListeningIP" = "محدودیت آی پی پنل" "panelListeningIP" = "محدودیت آی پی پنل"
"panelListeningIPDesc" = "برای استفاده از تمام آی‌پیها به طور پیش فرض خالی بگذارید" "panelListeningIPDesc" = "برای استفاده از تمام آی‌پیها به طور پیش فرض خالی بگذارید"
"panelListeningDomain" = "محدودیت دامین پنل"
"panelListeningDomainDesc" = "برای استفاده از تمام دامنه‌ها و آی‌پی‌ها به طور پیش فرض خالی بگذارید"
"panelPort" = "پورت پنل" "panelPort" = "پورت پنل"
"panelPortDesc" = "پورت مورد استفاده برای نمایش این پنل" "panelPortDesc" = "پورت مورد استفاده برای نمایش این پنل"
"publicKeyPath" = "مسیر فایل گواهی کلید عمومی پنل" "publicKeyPath" = "مسیر فایل گواهی کلید عمومی پنل"
@@ -228,11 +230,13 @@
"telegramToken" = "توکن تلگرام" "telegramToken" = "توکن تلگرام"
"telegramTokenDesc" = "توکن را باید از مدیر بات های تلگرام دریافت کنید @botfather" "telegramTokenDesc" = "توکن را باید از مدیر بات های تلگرام دریافت کنید @botfather"
"telegramChatId" = "آی دی تلگرام مدیریت" "telegramChatId" = "آی دی تلگرام مدیریت"
"telegramChatIdDesc" = "با استفاده از کاما میتونید چند آی دی را از هم جدا کنید" "telegramChatIdDesc" = "از @userinfobot یا دستور '/id' در ربات برای دریافت شناسه های چت خود استفاده کنید. با استفاده از کاما میتونید چند آی دی را از هم جدا کنید. "
"telegramNotifyTime" = "مدت زمان نوتیفیکیشن ربات تلگرام" "telegramNotifyTime" = "مدت زمان نوتیفیکیشن ربات تلگرام"
"telegramNotifyTimeDesc" = "از فرمت زمان بندی لینوکس استفاده کنید " "telegramNotifyTimeDesc" = "از فرمت زمان بندی لینوکس استفاده کنید "
"tgNotifyBackup" = "پشتیبان گیری از پایگاه داده" "tgNotifyBackup" = "پشتیبان گیری از پایگاه داده"
"tgNotifyBackupDesc" = "ارسال کپی فایل پایگاه داده به همراه گزارش دوره ای" "tgNotifyBackupDesc" = "ارسال کپی فایل پایگاه داده به همراه گزارش دوره ای"
"tgNotifyLogin" = "اعلان ورود"
"tgNotifyLoginDesc" = "نام کاربری، آدرس ای پی، و زمان وقتی که فردی سعی می‌کند به پنل شما وارد شود نمایش میدهد"
"sessionMaxAge" = "بیشینه زمان جلسه وب" "sessionMaxAge" = "بیشینه زمان جلسه وب"
"sessionMaxAgeDesc" = "بیشینه زمانی که میتوانید لاگین بمانید (واحد: دقیقه)" "sessionMaxAgeDesc" = "بیشینه زمانی که میتوانید لاگین بمانید (واحد: دقیقه)"
"expireTimeDiff" = "آستانه زمان باقی مانده" "expireTimeDiff" = "آستانه زمان باقی مانده"
@@ -260,6 +264,10 @@
"subDomainDesc" = "برای نظارت بر همه دامنه ها و آی‌پی ها به طور پیش فرض خالی بگذارید" "subDomainDesc" = "برای نظارت بر همه دامنه ها و آی‌پی ها به طور پیش فرض خالی بگذارید"
"subUpdates" = "فاصله به روز رسانی های سابسکریپشن" "subUpdates" = "فاصله به روز رسانی های سابسکریپشن"
"subUpdatesDesc" = "ساعت های فاصله بین به روز رسانی در برنامه کاربر" "subUpdatesDesc" = "ساعت های فاصله بین به روز رسانی در برنامه کاربر"
"subEncrypt" = "رمزگذاری کانفیگ ها"
"subEncryptDesc" = "رمزگذاری کانفیگ های بازگشتی سابسکریپشن"
"subShowInfo" = "نمایش اطلاعات مصرف"
"subShowInfoDesc" = "ترافیک و زمان باقیمانده را در هر کانفیگ نمایش میدهد"
[pages.settings.templates] [pages.settings.templates]
"title" = "الگوها" "title" = "الگوها"
@@ -330,6 +338,7 @@
"manualBlockedDomains" = "لیست دامنه های مسدود شده" "manualBlockedDomains" = "لیست دامنه های مسدود شده"
"manualDirectIPs" = "لیست آی‌پی های مستقیم" "manualDirectIPs" = "لیست آی‌پی های مستقیم"
"manualDirectDomains" = "لیست دامنه های مستقیم" "manualDirectDomains" = "لیست دامنه های مستقیم"
"manualIPv4Domains" = "لیست دامنه‌های IPv4"
[pages.settings.toasts] [pages.settings.toasts]
"modifySettings" = "ویرایش تنظیمات" "modifySettings" = "ویرایش تنظیمات"
@@ -337,3 +346,74 @@
"modifyUser" = "ویرایش کاربر" "modifyUser" = "ویرایش کاربر"
"originalUserPassIncorrect" = "نام کاربری و رمز عبور فعلی اشتباه می باشد " "originalUserPassIncorrect" = "نام کاربری و رمز عبور فعلی اشتباه می باشد "
"userPassMustBeNotEmpty" = "نام کاربری و رمز عبور جدید نمیتواند خالی باشد " "userPassMustBeNotEmpty" = "نام کاربری و رمز عبور جدید نمیتواند خالی باشد "
[tgbot]
"noResult" = "❗ نتیجه‌ای یافت نشد!"
"wentWrong" = "❌ مشکلی رخ داده است!"
"noInbounds" = "❗ هیچ ورودی یافت نشد!"
"unlimited" = "♾ نامحدود"
"day" = "روز"
"days" = "روزها"
"unknown" = "نامشخص"
"inbounds" = "ورودی‌ها"
"clients" = "کلاینت‌ها"
[tgbot.commands]
"unknown" = "❗ دستور ناشناخته"
"pleaseChoose" = "👇 لطفاً انتخاب کنید:\r\n"
"help" = "🤖 به این ربات خوش آمدید! این ربات برای ارائه داده‌های خاص از سرور طراحی شده است و به شما امکان تغییرات لازم را می‌دهد.\r\n\r\n"
"start" = "👋 سلام <i>{{ .Firstname }}</i>.\r\n"
"welcome" = "🤖 به ربات مدیریت <b>{{ .Hostname }}</b> خوش آمدید.\r\n"
"status" = "✅ ربات در حالت عادی است!"
"usage" = "❗ لطفاً یک متن برای جستجو وارد کنید!"
"getID" = "🆔 شناسه شما: <code>{{ .ID }}</code>"
"helpAdminCommands" = "برای جستجوی ایمیل مشتری:\r\n<code>/usage [ایمیل]</code>\r\n \r\nبرای جستجوی ورودی‌ها (با آمار مشتری):\r\n<code>/inbound [توضیح]</code>"
"helpClientCommands" = "برای جستجوی آمار، فقط از دستور زیر استفاده کنید:\r\n \r\n<code>/usage [UUID|رمز عبور]</code>\r\n \r\nاز UUID برای vmess/vless و از رمز عبور برای Trojan استفاده کنید."
[tgbot.messages]
"cpuThreshold" = "🔴 میزان استفاده از CPU {{ .Percent }}% بیشتر از آستانه {{ .Threshold }}% است."
"loginSuccess" = "✅ با موفقیت به پنل وارد شدید.\r\n"
"loginFailed" = "❗️ ورود به پنل ناموفق بود.\r\n"
"report" = "🕰 گزارشات زمان‌بندی شده: {{ .RunTime }}\r\n"
"datetime" = "⏰ تاریخ-زمان: {{ .DateTime }}\r\n"
"hostname" = "💻 نام میزبان: {{ .Hostname }}\r\n"
"version" = "🚀 نسخه X-UI: {{ .Version }}\r\n"
"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n"
"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n"
"ip" = "🌐 آدرس IP: {{ .IP }}\r\n"
"serverUpTime" = "⏳ زمان کارکرد سرور: {{ .UpTime }} {{ .Unit }}\r\n"
"serverLoad" = "📈 بار سرور: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
"serverMemory" = "📋 حافظه سرور: {{ .Current }}/{{ .Total }}\r\n"
"tcpCount" = "🔹 تعداد ترافیک TCP: {{ .Count }}\r\n"
"udpCount" = "🔸 تعداد ترافیک UDP: {{ .Count }}\r\n"
"traffic" = "🚦 ترافیک: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
"xrayStatus" = " وضعیت Xray: {{ .State }}\r\n"
"username" = "👤 نام کاربری: {{ .Username }}\r\n"
"time" = "⏰ زمان: {{ .Time }}\r\n"
"inbound" = "📍 ورودی: {{ .Remark }}\r\n"
"port" = "🔌 پورت: {{ .Port }}\r\n"
"expire" = "📅 تاریخ انقضا: {{ .DateTime }}\r\n \r\n"
"expireIn" = "📅 باقیمانده از انقضا: {{ .Time }}\r\n \r\n"
"active" = "💡 فعال: {{ .Enable }}\r\n"
"email" = "📧 ایمیل: {{ .Email }}\r\n"
"upload" = "🔼 آپلود↑: {{ .Upload }}\r\n"
"download" = "🔽 دانلود↓: {{ .Download }}\r\n"
"total" = "🔄 کل: {{ .UpDown }} / {{ .Total }}\r\n"
"exhaustedMsg" = "🚨 {{ .Type }} به اتمام رسیده است:\r\n"
"exhaustedCount" = "🚨 تعداد {{ .Type }} به اتمام رسیده:\r\n"
"disabled" = "🛑 غیرفعال: {{ .Disabled }}\r\n"
"depleteSoon" = "🔜 به زودی به پایان خواهد رسید: {{ .Deplete }}\r\n \r\n"
"backupTime" = "🗄 زمان پشتیبان‌گیری: {{ .Time }}\r\n"
[tgbot.buttons]
"dbBackup" = "دریافت پشتیبان پایگاه داده"
"serverUsage" = "استفاده از سرور"
"getInbounds" = "دریافت ورودی‌ها"
"depleteSoon" = "به زودی به پایان خواهد رسید"
"clientUsage" = "دریافت آمار کاربر"
"commands" = "دستورات"
[tgbot.answers]
"getInboundsFailed" = "❌ دریافت ورودی‌ها با خطا مواجه شد."
"askToAddUser" = "پیکربندی شما یافت نشد!\r\nشما باید نام کاربری تلگرام خود را پیکربندی کنید و از مدیر خود درخواست اضافه کردن آن به پیکربندی خود بکنید."
"askToAddUserName" = "پیکربندی شما یافت نشد!\r\nلطفاً از مدیر خود درخواست استفاده از نام کاربری تلگرام خود در پیکربندی (ها) خود را بکنید.\r\n\r\nنام کاربری شما: <b>@{{ .TgUserName }}</b>"

View File

@@ -1,93 +1,94 @@
"username" = "имя пользователя" "username" = "Имя пользователя"
"password" = "пароль" "password" = "Пароль"
"login" = "логин" "login" = "Войти"
"confirm" = "подтвердить" "confirm" = "Подтвердить"
"cancel" = "отмена" "cancel" = "Отмена"
"close" = "закрыть" "close" = "Закрыть"
"copy" = "копировать" "copy" = "Копировать"
"copied" = "скопировано" "copied" = "Скопировано"
"download" = "скачать" "download" = "Скачать"
"remark" = "примечание" "remark" = "Примечание"
"enable" = "включить" "enable" = "Включить"
"protocol" = "протокол" "protocol" = "Протокол"
"search" = "поиск" "search" = "Поиск"
"filter" = "Фильтр" "filter" = "Фильтр"
"loading" = "Загрузка"
"loading" = "загрузка" "second" = "Секунда"
"second" = "секунда" "minute" = "Минута"
"minute" = "минута" "hour" = "Час"
"hour" = "час" "day" = "День"
"day" = "день"
"check" = "просмотр" "check" = "просмотр"
"indefinite" = "бессрочно" "indefinite" = "Бессрочно"
"unlimited" = "безлимитно" "unlimited" = "Безлимитно"
"none" = "пусто" "none" = "Пусто"
"qrCode" = "QR-код" "qrCode" = "QR-код"
"info" = "больше информации" "info" = "Больше информации"
"edit" = "изменить" "edit" = "Изменить"
"delete" = "удалить" "delete" = "Удалить"
"reset" = "обнулить" "reset" = "Обнулить"
"copySuccess" = "скопировано" "copySuccess" = "Успешно скопировано"
"sure" = "да" "sure" = "Да"
"encryption" = "Шифрование" "encryption" = "Шифрование"
"transmission" = "протокол передачи" "transmission" = "Протокол передачи"
"host" = "хост" "host" = "Хост"
"path" = "путь" "path" = "Путь"
"camouflage" = "маскировка" "camouflage" = "Маскировка"
"status" = "статус" "status" = "Статус"
"enabled" = "включено" "enabled" = "Включено"
"disabled" = "отключено" "disabled" = "Отключено"
"depleted" = "исчерпано" "depleted" = "Отключены"
"depletingSoon" = "почти исчерпано" "depletingSoon" = "Почти отключены"
"domainName" = "домен" "domainName" = "Домен"
"additional" = "допольнительно" "monitor" = "Прослушиваемый IP"
"monitor" = "порт IP" "certificate" = "Сертификат"
"certificate" = "сертификат" "fail" = "Неудачно"
"fail" = "неудача" "success" = "Успешно"
"success" = "успешно" "getVersion" = "Узнать версию"
"getVersion" = "узнать версию"
"install" = "установка" "install" = "установка"
"clients" = "клиенты" "clients" = "Клиенты"
"usage" = "использование" "usage" = "Использовано"
"remained" = "Осталось"
"secAlertTitle" = "Предупреждение системы безопасности"
"secAlertSsl" = "Это соединение не защищено. Пожалуйста, воздержитесь от ввода конфиденциальной информации до тех пор, пока не будет активирован TLS для защиты данных"
[menu] [menu]
"dashboard" = "статус системы" "dashboard" = "Статус системы"
"inbounds" = "пользователи" "inbounds" = "Подключения"
"settings" = "настройки" "settings" = "Настройки"
"logout" = "выход" "logout" = "Выйти"
"link" = "другое" "link" = "Другое"
[pages.login] [pages.login]
"title" = "логин" "title" = "Войти"
"loginAgain" = "Время пребывания в сети вышло. Пожалуйста, войдите в систему снова" "loginAgain" = "Время сессии истекло. Пожалуйста, войдите в систему снова"
[pages.login.toasts] [pages.login.toasts]
"invalidFormData" = "Недопустимый формат данных" "invalidFormData" = "Недопустимый формат данных"
"emptyUsername" = "Введите имя пользователя" "emptyUsername" = "Введите имя пользователя"
"emptyPassword" = "Введите пароль" "emptyPassword" = "Введите пароль"
"wrongUsernameOrPassword" = "Неверное имя пользователя или пароль" "wrongUsernameOrPassword" = "Неверное имя пользователя или пароль"
"successLogin" = "успешный вход" "successLogin" = "Успешный вход"
[pages.index] [pages.index]
"title" = "статус системы" "title" = "Статус системы"
"memory" = "память" "memory" = "ОЗУ"
"hard" = "жесткий диск" "hard" = "Место на диске"
"xrayStatus" = "статус Xray" "xrayStatus" = "Статус Xray"
"stopXray" = "стоп" "stopXray" = "Остановка"
"restartXray" = "рестарт Xray" "restartXray" = "Перезапуск Xray"
"xraySwitch" = "переключить версию" "xraySwitch" = "Сменить версию"
"xraySwitchClick" = "Выберите желаемую версию" "xraySwitchClick" = "Выберите желаемую версию"
"xraySwitchClickDesk" = "Выбирайте внимательно, так как старые версии могут быть несовместимы с текущими конфигурациями" "xraySwitchClickDesk" = "Выбирайте внимательно, так как старые версии могут быть несовместимы с текущими конфигурациями"
"operationHours" = "Часы работы" "operationHours" = "Время работы"
"operationHoursDesc" = "Аптайм системы: время системы в сети" "operationHoursDesc" = "Время работы системы: время с момента запуска."
"systemLoad" = "Системная нагрузка" "systemLoad" = "Системная нагрузка"
"connectionCount" = "количество соединений" "connectionCount" = "Количество соединений"
"connectionCountDesc" = "Всего подключений по всем сетям»" "connectionCountDesc" = "Всего подключений по всем сетям»"
"upSpeed" = "Общая скорость upload" "upSpeed" = "Общая скорость отдачи"
"downSpeed" = "Общая скорость download" "downSpeed" = "Общая скорость получения"
"totalSent" = "Общий объем загруженных данных с момента запуска системы" "totalSent" = "Общий объем загруженных данных с момента запуска системы"
"totalReceive" = "Общий объем полученных данных с момента запуска системы" "totalReceive" = "Общий объем полученных данных с момента запуска системы"
"xraySwitchVersionDialog" = "переключить версию Xray" "xraySwitchVersionDialog" = "Переключить версию Xray"
"xraySwitchVersionDialogDesc" = "Вы точно хотите сменить версию Xray?" "xraySwitchVersionDialogDesc" = "Вы точно хотите сменить версию Xray?"
"dontRefresh" = "Установка. Не обновляйте эту страницу" "dontRefresh" = "Установка. Не обновляйте эту страницу"
"logs" = "Логи" "logs" = "Логи"
@@ -99,39 +100,38 @@
"importDatabase" = "Импорт базы данных" "importDatabase" = "Импорт базы данных"
[pages.inbounds] [pages.inbounds]
"title" = "пользователи" "title" = "Подключения"
"totalDownUp" = "Всего входящих/исходящих" "totalDownUp" = "Всего получено/отправлено"
"totalUsage" = "Всего использовано" "totalUsage" = "Всего использовано"
"inboundCount" = "Количество пользователей" "inboundCount" = "Количество подключений"
"operate" = "Меню" "operate" = "Меню"
"enable" = "Включить" "enable" = "Включить"
"remark" = "Примечание" "remark" = "Примечание"
"protocol" = "Протокол" "protocol" = "Протокол"
"port" = "Порт" "port" = "Порт"
"traffic" = "Траффик" "traffic" = "Трафик"
"details" = "Подробнее" "details" = "Подробнее"
"transportConfig" = "Перенести" "transportConfig" = "Перенести"
"expireDate" = "Дата окончания" "expireDate" = "Дата окончания"
"resetTraffic" = "Обнулить траффик" "resetTraffic" = "Обнулить трафик"
"addInbound" = "Добавить пользователя" "addInbound" = "Добавить подключение"
"generalActions" = "Общие действия" "generalActions" = "Общие действия"
"create" = "Создать" "create" = "Создать"
"update" = "Обновить" "update" = "Обновить"
"modifyInbound" = "Изменить данные" "modifyInbound" = "Изменить данные"
"deleteInbound" = "Удалить пользователя" "deleteInbound" = "Удалить подключение"
"deleteInboundContent" = "Подтвердите удаление пользователя?" "deleteInboundContent" = "Вы уверены, что хотите удалить подключение?"
"resetTrafficContent" = "Подтвердите обнуление траффика?" "resetTrafficContent" = "Подтвердите обнуление трафика?"
"copyLink" = "Копировать ключ" "copyLink" = "Копировать ключ"
"address" = "Адрес" "address" = "Адрес"
"network" = "Сеть" "network" = "Сеть"
"destinationPort" = "Порт назначения" "destinationPort" = "Порт назначения"
"targetAddress" = "Целевой адрес" "targetAddress" = "Целевой адрес"
"disableInsecureEncryption" = "Отключить небезопасное шифрование"
"monitorDesc" = "Оставьте пустым по умолчанию" "monitorDesc" = "Оставьте пустым по умолчанию"
"meansNoLimit" = "Значит без ограничений" "meansNoLimit" = "Значит без ограничений"
"totalFlow" = "Общий расход" "totalFlow" = "Общий расход"
"leaveBlankToNeverExpire" = "Оставьте пустым, чтобы никогда не истекать" "leaveBlankToNeverExpire" = "Оставьте пустым, чтобы сделать бессрочно"
"noRecommendKeepDefault" = "Нет требований для сохранения настроек по умолчанию" "noRecommendKeepDefault" = "Нет особых требований для сохранения настроек по умолчанию"
"certificatePath" = "Путь файла сертификата" "certificatePath" = "Путь файла сертификата"
"certificateContent" = "Содержимое файла сертификата" "certificateContent" = "Содержимое файла сертификата"
"publicKeyPath" = "Путь к публичному ключу" "publicKeyPath" = "Путь к публичному ключу"
@@ -142,34 +142,34 @@
"client" = "Клиент" "client" = "Клиент"
"export" = "Поделиться ключом" "export" = "Поделиться ключом"
"clone" = "Клонировать" "clone" = "Клонировать"
"cloneInbound" = "Клонировать пользователя" "cloneInbound" = "Клонировать подключение"
"cloneInboundContent" = "Все настройки этого пользователя, кроме порта, порт прослушки и клиентов, будут клонированы" "cloneInboundContent" = "Все настройки этого подключения, кроме порта, порт прослушки и клиентов, будут клонированы"
"cloneInboundOk" = "Клонировать" "cloneInboundOk" = "Клонировать"
"resetAllTraffic" = "Обнулить весь траффик" "resetAllTraffic" = "Обнулить весь трафик"
"resetAllTrafficTitle" = "Обнуление всего траффика" "resetAllTrafficTitle" = "Обнуление всего трафика"
"resetAllTrafficContent" = "Подтверждаете обнуление всего траффика пользователей?" "resetAllTrafficContent" = "Вы уверены, что хотите сбросить трафик всех подключений?"
"resetInboundClientTraffics" = "Обнулить траффик пользователей" "resetInboundClientTraffics" = "Обнулить трафик клиентов"
"resetInboundClientTrafficTitle" = "Обнуление траффика пользователей" "resetInboundClientTrafficTitle" = "Обнуление трафика клиентов"
"resetInboundClientTrafficContent" = "Вы уверены, что хотите обнулить весь трафик для этих пользователей?" "resetInboundClientTrafficContent" = "Вы уверены, что хотите сбросить весь трафик для клиентов этого подключения?"
"resetAllClientTraffics" = "Обнулить весь траффик пользователей" "resetAllClientTraffics" = "Обнулить весь трафик клиентов"
"resetAllClientTrafficTitle" = "Обнуление всего траффика пользователей" "resetAllClientTrafficTitle" = "Обнуление всего трафика клиентов"
"resetAllClientTrafficContent" = "Подтверждаете обнуление всего траффика пользователей?" "resetAllClientTrafficContent" = "Вы уверены, что хотите сбросить трафик для всех клиентов?"
"delDepletedClients" = "Удалить отключенных пользователей" "delDepletedClients" = "Удалить отключенных клиентов"
"delDepletedClientsTitle" = "Удаление отключенных пользователей" "delDepletedClientsTitle" = "Удаление отключенных клиентов"
"delDepletedClientsContent" = "Подтверждаете удаление отключенных пользователей?" "delDepletedClientsContent" = "Вы уверены, что хотите удалить всех отключенных клиентов?"
"email" = "Email" "email" = "Email"
"emailDesc" = "Пожалуйста, укажите уникальный Email" "emailDesc" = "Пожалуйста, укажите уникальный Email"
"setDefaultCert" = "Установить сертификат с панели" "setDefaultCert" = "Установить сертификат с панели"
"telegramDesc" = "используйте Telegram ID (вы можете получить его у @userinfobot)" "telegramDesc" = "Используйте идентификатор Telegram без символа @ или идентификатора чата (можно получить его здесь @userinfobot или использовать команду '/id' в боте)"
"subscriptionDesc" = "вы можете найти свою ссылку подписки в разделе «Подробнее», также вы можете использовать одно и то же имя для нескольких конфигов" "subscriptionDesc" = "вы можете найти свою ссылку подписки в разделе «Подробнее», также вы можете использовать одно и то же имя для нескольких конфигов"
[pages.client] [pages.client]
"add" = "Добавить пользователя" "add" = "Добавить клиента"
"edit" = "Редактировать пользователя" "edit" = "Редактировать клиента"
"submitAdd" = "Добавить пользователя" "submitAdd" = "Добавить клиента"
"submitEdit" = "Сохранить изменения" "submitEdit" = "Сохранить изменения"
"clientCount" = "Количество пользователей" "clientCount" = "Количество клиентов"
"bulk" = "Добавить несколько" "bulk" = "Добавить несколько клиентов"
"method" = "Метод" "method" = "Метод"
"first" = "Первый" "first" = "Первый"
"last" = "Последний" "last" = "Последний"
@@ -183,18 +183,18 @@
"obtain" = "Получить" "obtain" = "Получить"
[pages.inbounds.stream.general] [pages.inbounds.stream.general]
"requestHeader" = "Требуется заголовок" "requestHeader" = "Заголовок запроса"
"name" = "Имя" "name" = "Имя"
"value" = "Значение" "value" = "Значение"
[pages.inbounds.stream.tcp] [pages.inbounds.stream.tcp]
"requestVersion" = "Требуется версия" "requestVersion" = "Версия запроса"
"requestMethod" = "Требуется метод" "requestMethod" = "Метод запроса"
"requestPath" = "Требуется путь" "requestPath" = "Петь запроса"
"responseVersion" = "Указать версию" "responseVersion" = "Версия ответа"
"responseStatus" = "Указать статус" "responseStatus" = "Статус ответа"
"responseStatusDescription" = "Указать примечание статуса" "responseStatusDescription" = "Описание статуса ответа"
"responseHeader" = "Указать заголовок" "responseHeader" = "Заголовок ответа"
[pages.inbounds.stream.quic] [pages.inbounds.stream.quic]
"encryption" = "Шифрование" "encryption" = "Шифрование"
@@ -202,107 +202,115 @@
[pages.settings] [pages.settings]
"title" = "Настройки" "title" = "Настройки"
"save" = "Сохранить" "save" = "Сохранить"
"infoDesc" = "Каждое изменение здесь необходимо сохранить и перезапустить панель, чтобы оно вступило в силу" "infoDesc" = "Все внесенные здесь изменения должны быть сохранены. Чтобы изменения вступили в силу, перезапустите панель."
"restartPanel" = "Рестарт панели" "restartPanel" = "Перезапуск панели"
"restartPanelDesc" = "Подтвердите рестарт панели? ОК для рестарта панели через 3 сек. Если вы не можете пользоваться панелью после рестарта, пожалуйста, посмотрите лог панели на сервере" "restartPanelDesc" = "Вы уверены, что хотите перезапустить панель? Нажмите OK для перезапуска через 3 секунды. Если после перезапуска не удается получить доступ к панели, просмотрите информацию журнала панели на сервере."
"resetDefaultConfig" = "Сбросить всё по-умолчанию" "resetDefaultConfig" = "Сбросить всё по-умолчанию"
"panelConfig" = "Настройки панели" "panelConfig" = "Настройки панели"
"userSettings" = "Настройки безопасности" "userSettings" = "Настройки безопасности"
"xrayConfiguration" = "Конфигурация Xray" "xrayConfiguration" = "Конфигурация Xray"
"TGBotSettings" = "Настройки Телеграм-бота" "TGBotSettings" = "Настройки Телеграм-бота"
"panelListeningIP" = "IP-порт панели" "panelListeningIP" = "IP-адрес прослушивания панели"
"panelListeningIPDesc" = "Оставьте пустым для работы с любого IP. Перезагрузите панель для применения настроек" "panelListeningIPDesc" = "Оставьте пустым, чтобы прослушивать все IP-адреса."
"panelListeningDomain" = "Домен прослушивания панели"
"panelListeningDomainDesc" = "Оставьте пустым, чтобы прослушивать все домены и IP-адреса"
"panelPort" = "Порт панели" "panelPort" = "Порт панели"
"panelPortDesc" = "Перезагрузите панель для применения настроек" "panelPortDesc" = "Номер порта для доступа к панели"
"publicKeyPath" = "Путь к файлу публичного ключа сертификата панели" "publicKeyPath" = "Путь к файлу публичного ключа сертификата панели"
"publicKeyPathDesc" = "Введите полный путь, начинающийся с «/». Перезагрузите панель для применения настроек" "publicKeyPathDesc" = "Введите полный путь, начинающийся с «/»."
"privateKeyPath" = "Путь к файлу приватного ключа сертификата панели" "privateKeyPath" = "Путь к файлу приватного ключа сертификата панели"
"privateKeyPathDesc" = "Введите полный путь, начинающийся с «/». Перезагрузите панель для применения настроек" "privateKeyPathDesc" = "Введите полный путь, начинающийся с «/»."
"panelUrlPath" = "Корневой путь URL-адреса панели" "panelUrlPath" = "Корневой путь URL-адреса панели"
"panelUrlPathDesc" = "Должен начинаться с «/» и заканчиваться на «/». Перезагрузите панель для применения настроек" "panelUrlPathDesc" = "Должен начинаться с «/» и заканчиваться на «/»."
"oldUsername" = "Имя пользователя сейчас" "oldUsername" = "Текущее имя пользователя"
"currentPassword" = "Пароль сейчас" "currentPassword" = "Текущий пароль"
"newUsername" = "Новое имя пользователя" "newUsername" = "Новое имя пользователя"
"newPassword" = "Новый пароль" "newPassword" = "Новый пароль"
"telegramBotEnable" = "Включить Телеграм-бота" "telegramBotEnable" = "Включить Телеграм-бота"
"telegramBotEnableDesc" = "Перезагрузите панель для применения настроек" "telegramBotEnableDesc" = "Ваш telegram-бот будет взаимодействовать с панелью"
"telegramToken" = "Токен Телеграм-бота" "telegramToken" = "Токен Телеграм-бота"
"telegramTokenDesc" = "Перезагрузите панель для применения настроек" "telegramTokenDesc" = "Токен, который вы получили от @BotFather"
"telegramChatId" = "Телеграм-ID админа бота" "telegramChatId" = "Телеграм-ID админа бота"
"telegramChatIdDesc" = "Если несколько Телеграм-ID, разделить запятой. Используйте @userinfobot, чтобы получить Телеграм-ID. Перезагрузите панель для применения настроек" "telegramChatIdDesc" = "Несколько идентификаторов чата, разделенных запятой. Используйте @userinfobot или команду '/id' в боте для получения идентификаторов чата."
"telegramNotifyTime" = "Частота уведомлений телеграм-бота" "telegramNotifyTime" = "Частота уведомлений телеграм-бота"
"telegramNotifyTimeDesc" = "Используйте формат Crontab. Перезагрузите панель для применения настроек" "telegramNotifyTimeDesc" = "Используйте временной формат Crontab."
"tgNotifyBackup" = "Резервное копирование базы данных" "tgNotifyBackup" = "Резервное копирование базы данных"
"tgNotifyBackupDesc" = "Включать файл резервной копии базы данных с уведомлением об отчете. Перезагрузите панель для применения настроек" "tgNotifyBackupDesc" = "Включение отправки файла резервной копии базы данных с уведомлением об отчете"
"tgNotifyLogin" = "Уведомление о входе"
"tgNotifyLoginDesc" = "Отображает имя пользователя, IP-адрес и время, когда кто-то пытается войти в вашу панель."
"sessionMaxAge" = "Продолжительность сессии" "sessionMaxAge" = "Продолжительность сессии"
"sessionMaxAgeDesc" = "Продолжительность сессии в системе (значение: минута)" "sessionMaxAgeDesc" = "Продолжительность сессии в системе (единица измерения: минута)"
"expireTimeDiff" = "Порог истечения срока сессии для уведомления" "expireTimeDiff" = "Порог истечения срока сессии для уведомления"
"expireTimeDiffDesc" = "Получение уведомления об истечении срока действия сессии до достижения порогового значения (значение: день)" "expireTimeDiffDesc" = "Получение уведомления об истечении срока действия сессии до достижения порогового значения (единица измерения: день)"
"trafficDiff" = "Порог траффика для уведомления" "trafficDiff" = "Порог трафика для уведомления"
"trafficDiffDesc" = "Получение уведомления об исчерпании трафика до достижения порога (значение: ГБ)" "trafficDiffDesc" = "Получение уведомления об исчерпании трафика до достижения порога (единица измерения: ГБ)"
"tgNotifyCpu" = "Порог нагрузки на ЦП для уведомления" "tgNotifyCpu" = "Порог нагрузки на ЦП для уведомления"
"tgNotifyCpuDesc" = "Получение уведомления, если нагрузка на ЦП превышает этот порог (значение:%)" "tgNotifyCpuDesc" = "Получение уведомления, если нагрузка на ЦП превышает этот порог (единица измерения:%)"
"timeZone" = "Временная зона" "timeZone" = "Часовой пояс"
"timeZoneDesc" = "Запланированные задачи выполняются в соответствии со временем в этом часовом поясе. Перезагрузите панель для применения настроек" "timeZoneDesc" = "Запланированные задания выполняются в соответствии со временем в данном часовом поясе."
"subSettings" = "Подписка" "subSettings" = "Подписка"
"subEnable" = "Включить службу" "subEnable" = "Включить службу"
"subEnableDesc" = "Функция подписки с отдельной конфигурацией" "subEnableDesc" = "Функция подписки с отдельной конфигурацией"
"subListen" = "Прослушивание IP" "subListen" = "Прослушиваемый IP"
"subListenDesc" = "Оставьте пустым по умолчанию, чтобы отслеживать все IP-адреса" "subListenDesc" = "Оставьте пустым, чтобы прослушивать все IP-адреса"
"subPort" = "Порт подписки" "subPort" = "Порт подписки"
"subPortDesc" = "Номер порта для обслуживания службы подписки не должен использоваться на сервере" "subPortDesc" = "Номер порта для прослушивания службы подписки не должен использоваться на сервере"
"subCertPath" = "Путь к файлу открытого ключа сертификата подписки" "subCertPath" = "Путь к файлу открытого ключа сертификата подписки"
"subCertPathDesc" = "Введите абсолютный путь, начинающийся с '/'" "subCertPathDesc" = "Введите абсолютный путь, начинающийся с '/'"
"subKeyPath" = "Путь к файлу закрытого ключа сертификата подписки" "subKeyPath" = "Путь к файлу закрытого ключа сертификата подписки"
"subKeyPathDesc" = "Введите абсолютный путь, начинающийся с '/'" "subKeyPathDesc" = "Введите абсолютный путь, начинающийся с '/'"
"subPath" = "Корневой путь URL-адреса подписки" "subPath" = "Корневой путь URL-адреса подписки"
"subPathDesc" = "Должен начинаться с '/' и заканчиваться на '/'" "subPathDesc" = "Должен начинаться с '/' и заканчиваться на '/'"
"subDomain" = "Домен прослушивания" "subDomain" = "Домен для прослушивания"
"subDomainDesc" = "Оставьте пустым по умолчанию, чтобы отслеживать все домены и IP-адреса" "subDomainDesc" = "Оставьте пустым, чтобы прослушивать все домены и IP-адреса"
"subUpdates" = "Интервалы обновления подписки" "subUpdates" = "Интервалы обновления подписки"
"subUpdatesDesc" = "Часовой интервал между обновлениями в клиентском приложении" "subUpdatesDesc" = "Часовой интервал между обновлениями в клиентском приложении"
"subEncrypt" = "Шифрование конфигураций"
"subEncryptDesc" = "Шифрование возвращаемых конфигураций в подписке"
"subShowInfo" = "Показать информацию об использовании"
"subShowInfoDesc" = "Показывать восстановленный трафик и дату после имени конфигурации"
[pages.settings.templates] [pages.settings.templates]
"title" = "Шаблоны" "title" = "Шаблоны"
"basicTemplate" = "Базовые шаблоны" "basicTemplate" = "Базовые шаблоны"
"advancedTemplate" = "Расширенные шаблоны" "advancedTemplate" = "Расширенные шаблоны"
"completeTemplate" = "Конфигурация шаблона" "completeTemplate" = "Итоговый шаблон"
"generalConfigs" = "Основные настройки" "generalConfigs" = "Основные настройки"
"generalConfigsDesc" = "Эти параметры не позволят пользователям подключаться к определенным протоколам и веб-сайтам" "generalConfigsDesc" = "Общие настройки"
"blockConfigs" = "Блокировка конфигураций" "blockConfigs" = "Блокирующие конфигурации"
"blockConfigsDesc" = "Эти параметры не позволят пользователям подключаться к определенным протоколам и веб-сайтам." "blockConfigsDesc" = "Эти параметры не позволят пользователям подключаться к определенным протоколам и веб-сайтам."
"blockCountryConfigs" = "Заблокировать конфигурации страны" "blockCountryConfigs" = "Конфигурация блокировки стран"
"blockCountryConfigsDesc" = "Эти параметры не позволят пользователям подключаться к доменам определенной страны." "blockCountryConfigsDesc" = "Эти параметры не позволят пользователям подключаться к доменам определенной страны."
"directCountryConfigs" = "Прямые настройки страны" "directCountryConfigs" = "Прямые настройки стран"
"directCountryConfigsDesc" = "Эти параметры будут подключать пользователей напрямую к доменам определенной страны." "directCountryConfigsDesc" = "Эти параметры будут подключать пользователей напрямую к доменам определенной страны."
"ipv4Configs" = "Настройки IPv4 " "ipv4Configs" = "Настройки IPv4"
"ipv4ConfigsDesc" = "Эти параметры будут маршрутизироваться к целевым доменам только через IPv4" "ipv4ConfigsDesc" = "Эти параметры будут маршрутизироваться к целевым доменам только через IPv4"
"xrayConfigTemplate" = "Шаблон конфигурации Xray" "xrayConfigTemplate" = "Шаблон конфигурации Xray"
"xrayConfigTemplateDesc" = "Создание файла конфигурации Xray на основе этого шаблона. Перезагрузите панель для применения настроек" "xrayConfigTemplateDesc" = "Создание файла конфигурации Xray на основе этого шаблона."
"xrayConfigFreedomStrategy" = "Настроить стратегию протокола Freedom" "xrayConfigFreedomStrategy" = "Настроить стратегию протокола Freedom"
"xrayConfigFreedomStrategyDesc" = "Установить стратегию вывода сети в протоколе Freedom" "xrayConfigFreedomStrategyDesc" = "Установить стратегию вывода сети в протоколе Freedom"
"xrayConfigRoutingStrategy" = "Настроить доменную стратегию маршрутизации" "xrayConfigRoutingStrategy" = "Настроить доменную стратегию маршрутизации"
"xrayConfigRoutingStrategyDesc" = "Установить общую стратегию маршрутизации разрешения DNS" "xrayConfigRoutingStrategyDesc" = "Установить общую стратегию маршрутизации разрешения DNS"
"xrayConfigTorrent" = "Запретить использование BitTorrent" "xrayConfigTorrent" = "Запретить использование BitTorrent"
"xrayConfigTorrentDesc" = "Измените конфигурацию, чтобы пользователи не использовали BitTorrent. Перезагрузите панель для применения настроек" "xrayConfigTorrentDesc" = "Измените конфигурацию, чтобы пользователи не использовали BitTorrent."
"xrayConfigPrivateIp" = "Запрет частных диапазонов IP-адресов для подключения" "xrayConfigPrivateIp" = "Запрет частных диапазонов IP-адресов для подключения"
"xrayConfigPrivateIpDesc" = "Измените конфигурацию, чтобы избежать подключения к диапазонам частных IP-адресов. Перезагрузите панель для применения настроек" "xrayConfigPrivateIpDesc" = "Измените конфигурацию, чтобы избежать подключения к диапазонам частных IP-адресов."
"xrayConfigAds" = "Бокировка рекламы" "xrayConfigAds" = локировка рекламы"
"xrayConfigAdsDesc" = "Измените конфигурацию, чтобы заблокировать рекламу. Перезагрузите панель для применения настроек" "xrayConfigAdsDesc" = "Измените конфигурацию, чтобы заблокировать рекламу."
"xrayConfigFamily" = "Включить семейную конфигурацию" "xrayConfigFamily" = "Включить семейную конфигурацию"
"xrayConfigFamilyDesc" = "Избегайте подключения к небезопасным веб-сайтам для всей семьи" "xrayConfigFamilyDesc" = "Избегать подключения к небезопасным веб-сайтам для всей семьи"
"xrayConfigIRIp" = "Отключить подключение к диапазонам IP-адресов Ирана" "xrayConfigIRIp" = "Отключить подключение к диапазонам IP-адресов Ирана"
"xrayConfigIRIpDesc" = "Измените конфигурацию, чтобы отключить подключение к диапазонам IP-адресов Ирана. Перезагрузите панель для применения настроек" "xrayConfigIRIpDesc" = "Измените конфигурацию, чтобы отключить подключение к диапазонам IP-адресов Ирана."
"xrayConfigIRDomain" = "Отключить подключение к доменам Ирана" "xrayConfigIRDomain" = "Отключить подключение к доменам Ирана"
"xrayConfigIRDomainDesc" = "Измените конфигурацию, чтобы отключить подключение к доменам Ирана. Перезагрузите панель для применения настроек" "xrayConfigIRDomainDesc" = "Измените конфигурацию, чтобы отключить подключение к доменам Ирана."
"xrayConfigChinaIp" = "Отключить подключение к диапазонам IP-адресов Китая" "xrayConfigChinaIp" = "Отключить подключение к диапазонам IP-адресов Китая"
"xrayConfigChinaIpDesc" = "Измените конфигурацию, чтобы отключить подключение к диапазонам IP-адресов Китая. Перезагрузите панель для применения настроек" "xrayConfigChinaIpDesc" = "Измените конфигурацию, чтобы отключить подключение к диапазонам IP-адресов Китая."
"xrayConfigChinaDomain" = "Отключить подключение к доменам Китая" "xrayConfigChinaDomain" = "Отключить подключение к доменам Китая"
"xrayConfigChinaDomainDesc" = "Измените конфигурацию, чтобы отключить подключение к доменам Китая. Перезагрузите панель для применения настроек" "xrayConfigChinaDomainDesc" = "Измените конфигурацию, чтобы отключить подключение к доменам Китая."
"xrayConfigRussiaIp" = "Отключить подключение к диапазонам IP-адресов России" "xrayConfigRussiaIp" = "Отключить подключение к диапазонам IP-адресов России"
"xrayConfigRussiaIpDesc" = "Измените конфигурацию, чтобы отключить соединения с диапазонами IP-адресов России. Перезагрузите панель для применения настроек" "xrayConfigRussiaIpDesc" = "Измените конфигурацию, чтобы отключить соединения с диапазонами IP-адресов России."
"xrayConfigRussiaDomain" = "Отключить подключение к доменам России" "xrayConfigRussiaDomain" = "Отключить подключение к доменам России"
"xrayConfigRussiaDomainDesc" = "Измените конфигурацию, чтобы избежать подключения к доменам России. Перезагрузите панель для применения настроек" "xrayConfigRussiaDomainDesc" = "Измените конфигурацию, чтобы избежать подключения к доменам России."
"xrayConfigDirectIRIp" = "Прямое подключение к диапазонам IP-адресов Ирана" "xrayConfigDirectIRIp" = "Прямое подключение к диапазонам IP-адресов Ирана"
"xrayConfigDirectIRIpDesc" = "Измените шаблон конфигурации для прямого подключения к диапазонам IP-адресов Ирана" "xrayConfigDirectIRIpDesc" = "Измените шаблон конфигурации для прямого подключения к диапазонам IP-адресов Ирана"
"xrayConfigDirectIRDomain" = "Прямое подключение к доменам Ирана" "xrayConfigDirectIRDomain" = "Прямое подключение к доменам Ирана"
@@ -316,21 +324,22 @@
"xrayConfigDirectRussiaDomain" = "Прямое подключение к доменам России" "xrayConfigDirectRussiaDomain" = "Прямое подключение к доменам России"
"xrayConfigDirectRussiaDomainDesc" = "Изменить шаблон конфигурации для прямого подключения к доменам России" "xrayConfigDirectRussiaDomainDesc" = "Изменить шаблон конфигурации для прямого подключения к доменам России"
"xrayConfigGoogleIPv4" = "Использовать IPv4 для Google" "xrayConfigGoogleIPv4" = "Использовать IPv4 для Google"
"xrayConfigGoogleIPv4Desc" = "Применить маршрутизацию Google для подключения к IPv4. Перезагрузите панель для применения настроек" "xrayConfigGoogleIPv4Desc" = "Применить маршрутизацию Google для подключения к IPv4."
"xrayConfigNetflixIPv4" = "Использовать IPv4 для Netflix" "xrayConfigNetflixIPv4" = "Использовать IPv4 для Netflix"
"xrayConfigNetflixIPv4Desc" = "Применить маршрутизацию Netflix для подключения к IPv4. Перезагрузите панель для применения настроек" "xrayConfigNetflixIPv4Desc" = "Применить маршрутизацию Netflix для подключения к IPv4."
"xrayConfigInbounds" = "Конфигурация подключений" "xrayConfigInbounds" = "Конфигурация подключений"
"xrayConfigInboundsDesc" = "Изменение шаблона конфигурации, для подключения определенных пользователей. Перезагрузите панель для применения настроек" "xrayConfigInboundsDesc" = "Изменение шаблона конфигурации, для подключения определенных пользователей."
"xrayConfigOutbounds" = "Конфигурация исходящих" "xrayConfigOutbounds" = "Конфигурация исходящих"
"xrayConfigOutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить исходящие пути для этого сервера. Перезагрузите панель для применения настроек" "xrayConfigOutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить исходящие пути для этого сервера."
"xrayConfigRoutings" = "Настройка правил маршрутизации" "xrayConfigRoutings" = "Настройка правил маршрутизации"
"xrayConfigRoutingsDesc" = "Изменение шаблона конфигурации, для определения правил маршрутизации для этого сервера. Перезагрузите панель для применения настроек" "xrayConfigRoutingsDesc" = "Изменение шаблона конфигурации, для определения правил маршрутизации для этого сервера."
"manualLists" = "ручные списки" "manualLists" = "Пользовательские списки"
"manualListsDesc" = "Пожалуйста, используйте формат массива JSON" "manualListsDesc" = "Пожалуйста, используйте формат массива JSON"
"manualBlockedIPs" = "Список заблокированных IP-адресов" "manualBlockedIPs" = "Список заблокированных IP-адресов"
"manualBlockedDomains" = "Список заблокированных доменов" "manualBlockedDomains" = "Список заблокированных доменов"
"manualDirectIPs" = "Список прямых IP-адресов" "manualDirectIPs" = "Список прямых IP-адресов"
"manualDirectDomains" = "Список прямых доменов" "manualDirectDomains" = "Список прямых доменов"
"manualIPv4Domains" = "Список доменов IPv4"
[pages.settings.toasts] [pages.settings.toasts]
"modifySettings" = "Изменение настроек" "modifySettings" = "Изменение настроек"
@@ -338,3 +347,74 @@
"modifyUser" = "Изменение пользователя " "modifyUser" = "Изменение пользователя "
"originalUserPassIncorrect" = "Неверное имя пользователя или пароль" "originalUserPassIncorrect" = "Неверное имя пользователя или пароль"
"userPassMustBeNotEmpty" = "Новое имя пользователя и новый пароль должны быть заполнены" "userPassMustBeNotEmpty" = "Новое имя пользователя и новый пароль должны быть заполнены"
[tgbot]
"noResult" = "❗ Нет результатов!"
"wentWrong" = "❌ Что-то пошло не так!"
"noInbounds" = "❗ Подключений не найдено!"
"unlimited" = "♾ Неограниченно"
"day" = "День"
"days" = "Дней"
"unknown" = "Неизвестно"
"inbounds" = "Подключения"
"clients" = "Клиенты"
[tgbot.commands]
"unknown" = "❗ Неизвестная команда"
"pleaseChoose" = "👇 Пожалуйста, выберите:\r\n"
"help" = "🤖 Добро пожаловать в этого бота! Он предназначен для предоставления вам конкретных данных с сервера и позволяет вносить необходимые изменения.\r\n\r\n"
"start" = "👋 Привет, <i>{{ .Firstname }}</i>.\r\n"
"welcome" = "🤖 Добро пожаловать в бота управления <b>{{ .Hostname }}</b>.\r\n"
"status" = "✅ Бот работает нормально!"
"usage" = "❗ Пожалуйста, укажите текст для поиска!"
"getID" = "🆔 Ваш ID: <code>{{ .ID }}</code>"
"helpAdminCommands" = "Поиск по электронной почте клиента:\r\n<code>/usage [Email]</code>\r\n \r\nПоиск входящих соединений (со статистикой клиента):\r\n<code>/inbound [Remark]</code>"
"helpClientCommands" = "Для получения статистики используйте следующую команду:\r\n \r\n<code>/usage [UUID|Password]</code>\r\n \r\nИспользуйте UUID для vmess/vless и пароль для Trojan."
[tgbot.messages]
"cpuThreshold" = "🔴 Загрузка процессора составляет {{ .Percent }}%, что превышает пороговое значение {{ .Threshold }}%"
"loginSuccess" = "✅ Успешный вход в панель.\r\n"
"loginFailed" = "❗️ Ошибка входа в панель.\r\n"
"report" = "🕰 Запланированные отчеты: {{ .RunTime }}\r\n"
"datetime" = "⏰ Дата и время: {{ .DateTime }}\r\n"
"hostname" = "💻 Имя хоста: {{ .Hostname }}\r\n"
"version" = "🚀 Версия X-UI: {{ .Version }}\r\n"
"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n"
"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n"
"ip" = "🌐 IP: {{ .IP }}\r\n"
"serverUpTime" = "⏳ Время работы сервера: {{ .UpTime }} {{ .Unit }}\r\n"
"serverLoad" = "📈 Загрузка сервера: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
"serverMemory" = "📋 Память сервера: {{ .Current }}/{{ .Total }}\r\n"
"tcpCount" = "🔹 Количество TCP-соединений: {{ .Count }}\r\n"
"udpCount" = "🔸 Количество UDP-соединений: {{ .Count }}\r\n"
"traffic" = "🚦 Трафик: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
"xrayStatus" = " Состояние Xray: {{ .State }}\r\n"
"username" = "👤 Имя пользователя: {{ .Username }}\r\n"
"time" = "⏰ Время: {{ .Time }}\r\n"
"inbound" = "📍 Входящий поток: {{ .Remark }}\r\n"
"port" = "🔌 Порт: {{ .Port }}\r\n"
"expire" = "📅 Дата окончания: {{ .DateTime }}\r\n \r\n"
"expireIn" = "📅 Окончание через: {{ .Time }}\r\n \r\n"
"active" = "💡 Активен: {{ .Enable }}\r\n"
"email" = "📧 Email: {{ .Email }}\r\n"
"upload" = "🔼 Загрузка↑: {{ .Upload }}\r\n"
"download" = "🔽 Скачивание↓: {{ .Download }}\r\n"
"total" = "🔄 Всего: {{ .UpDown }} / {{ .Total }}\r\n"
"exhaustedMsg" = "🚨 Истекли {{ .Type }}:\r\n"
"exhaustedCount" = "🚨 Количество истекших {{ .Type }}:\r\n"
"disabled" = "🛑 Отключено: {{ .Disabled }}\r\n"
"depleteSoon" = "🔜 Скоро отключатся: {{ .Deplete }}\r\n \r\n"
"backupTime" = "🗄 Время резервного копирования: {{ .Time }}\r\n"
[tgbot.buttons]
"dbBackup" = "Получить резервную копию базы данных"
"serverUsage" = "Использование сервера"
"getInbounds" = "Получить список подключений"
"depleteSoon" = "Скоро отключатся"
"clientUsage" = "Получить статистику"
"commands" = "Команды"
[tgbot.answers]
"getInboundsFailed" = "❌ Не удалось получить подключения."
"askToAddUser" = "Конфигурация не найдена!\r\nВы должны настроить свое имя пользователя Telegram и попросить вашего администратора добавить его в вашу конфигурацию."
"askToAddUserName" = "Конфигурация не найдена!\r\nПожалуйста, попросите вашего администратора использовать ваше имя пользователя Telegram в вашей конфигурации(ях).\r\n\r\nВаше имя пользователя: <b>@{{ .TgUserName }}</b>"

View File

@@ -12,7 +12,6 @@
"protocol" = "协议" "protocol" = "协议"
"search" = "搜尋" "search" = "搜尋"
"filter" = "过滤器" "filter" = "过滤器"
"loading" = "加载中" "loading" = "加载中"
"second" = "秒" "second" = "秒"
"minute" = "分钟" "minute" = "分钟"
@@ -40,7 +39,6 @@
"depleted" = "耗尽" "depleted" = "耗尽"
"depletingSoon" = "即将耗尽" "depletingSoon" = "即将耗尽"
"domainName" = "域名" "domainName" = "域名"
"additional" = "额外 ID"
"monitor" = "监听" "monitor" = "监听"
"certificate" = "证书" "certificate" = "证书"
"fail" = "失败" "fail" = "失败"
@@ -49,6 +47,9 @@
"install" = "安装" "install" = "安装"
"clients" = "客户端" "clients" = "客户端"
"usage" = "用法" "usage" = "用法"
"remained" = "仍然存在"
"secAlertTitle" = "安全警报"
"secAlertSsl" = "此连接不安全;在激活 TLS 进行数据保护之前,请勿输入敏感信息"
[menu] [menu]
"dashboard" = "系统状态" "dashboard" = "系统状态"
@@ -126,7 +127,6 @@
"network" = "网络" "network" = "网络"
"destinationPort" = "目标端口" "destinationPort" = "目标端口"
"targetAddress" = "目标地址" "targetAddress" = "目标地址"
"disableInsecureEncryption" = "禁用不安全加密"
"monitorDesc" = "默认留空即可" "monitorDesc" = "默认留空即可"
"meansNoLimit" = "表示不限制" "meansNoLimit" = "表示不限制"
"totalFlow" = "总流量" "totalFlow" = "总流量"
@@ -160,7 +160,7 @@
"email" = "电子邮件" "email" = "电子邮件"
"emailDesc" = "电子邮件必须完全唯" "emailDesc" = "电子邮件必须完全唯"
"setDefaultCert" = "从面板设置证书" "setDefaultCert" = "从面板设置证书"
"telegramDesc" = "使用不带@的电报 ID 或聊天 ID可以在此处获取 @userinfobot" "telegramDesc" = "使用 Telegram ID不包含 @ 符号或聊天 ID可以在 @userinfobot 处获取,或在机器人中使用'/id'命令"
"subscriptionDesc" = "您可以在详细信息上找到您的子链接,也可以对多个配置使用相同的名称" "subscriptionDesc" = "您可以在详细信息上找到您的子链接,也可以对多个配置使用相同的名称"
[pages.client] [pages.client]
@@ -212,6 +212,8 @@
"TGBotSettings" = "TG提醒相关设置" "TGBotSettings" = "TG提醒相关设置"
"panelListeningIP" = "面板监听 IP" "panelListeningIP" = "面板监听 IP"
"panelListeningIPDesc" = "默认留空监听所有 IP" "panelListeningIPDesc" = "默认留空监听所有 IP"
"panelListeningDomain" = "面板监听域名"
"panelListeningDomainDesc" = "默认情况下留空以监视所有域名和 IP 地址"
"panelPort" = "面板监听端口" "panelPort" = "面板监听端口"
"panelPortDesc" = "重启面板生效" "panelPortDesc" = "重启面板生效"
"publicKeyPath" = "面板证书公钥文件路径" "publicKeyPath" = "面板证书公钥文件路径"
@@ -229,11 +231,13 @@
"telegramToken" = "电报机器人TOKEN" "telegramToken" = "电报机器人TOKEN"
"telegramTokenDesc" = "重启面板生效" "telegramTokenDesc" = "重启面板生效"
"telegramChatId" = "以逗号分隔的多个 chatID" "telegramChatId" = "以逗号分隔的多个 chatID"
"telegramChatIdDesc" = "重启面板生效" "telegramChatIdDesc" = "多个聊天 ID 用逗号分隔。使用 @userinfobot 或在机器人中使用'/id'命令获取您的聊天 ID。"
"telegramNotifyTime" = "电报机器人通知时间" "telegramNotifyTime" = "电报机器人通知时间"
"telegramNotifyTimeDesc" = "采用Crontab定时格式" "telegramNotifyTimeDesc" = "采用Crontab定时格式"
"tgNotifyBackup" = "数据库备份" "tgNotifyBackup" = "数据库备份"
"tgNotifyBackupDesc" = "正在发送数据库备份文件和报告通知" "tgNotifyBackupDesc" = "正在发送数据库备份文件和报告通知"
"tgNotifyLogin" = "登录通知"
"tgNotifyLoginDesc" = "当有人试图登录您的面板时显示用户名、IP 地址和时间"
"sessionMaxAge" = "会话最大年龄" "sessionMaxAge" = "会话最大年龄"
"sessionMaxAgeDesc" = "您可以保持登录状态的时间(单位:分钟)" "sessionMaxAgeDesc" = "您可以保持登录状态的时间(单位:分钟)"
"expireTimeDiff" = "耗尽时间阈值" "expireTimeDiff" = "耗尽时间阈值"
@@ -261,6 +265,10 @@
"subDomainDesc" = "留空默认监控所有域名和IP" "subDomainDesc" = "留空默认监控所有域名和IP"
"subUpdates" = "订阅更新间隔" "subUpdates" = "订阅更新间隔"
"subUpdatesDesc" = "客户端应用程序更新之间的间隔时间" "subUpdatesDesc" = "客户端应用程序更新之间的间隔时间"
"subEncrypt" = "加密配置"
"subEncryptDesc" = "在订阅中加密返回的配置"
"subShowInfo" = "显示使用信息"
"subShowInfoDesc" = "在配置名称后显示剩余流量和日期"
[pages.settings.templates] [pages.settings.templates]
"title" = "模板" "title" = "模板"
@@ -331,6 +339,7 @@
"manualBlockedDomains" = "被阻止的域列表" "manualBlockedDomains" = "被阻止的域列表"
"manualDirectIPs" = "直接 IP 列表" "manualDirectIPs" = "直接 IP 列表"
"manualDirectDomains" = "直接域列表" "manualDirectDomains" = "直接域列表"
"manualIPv4Domains" = "IPv4 域名列表"
[pages.settings.toasts] [pages.settings.toasts]
"modifySettings" = "修改设置" "modifySettings" = "修改设置"
@@ -338,3 +347,74 @@
"modifyUser" = "修改用户" "modifyUser" = "修改用户"
"originalUserPassIncorrect" = "原用户名或原密码错误" "originalUserPassIncorrect" = "原用户名或原密码错误"
"userPassMustBeNotEmpty" = "新用户名和新密码不能为空" "userPassMustBeNotEmpty" = "新用户名和新密码不能为空"
[tgbot]
"noResult" = "❗ 没有结果!"
"wentWrong" = "❌ 出了点问题!"
"noInbounds" = "❗ 没有找到入站连接!"
"unlimited" = "♾ 无限制"
"day" = "天"
"days" = "天"
"unknown" = "未知"
"inbounds" = "入站连接"
"clients" = "客户端"
[tgbot.commands]
"unknown" = "❗ 未知命令"
"pleaseChoose" = "👇 请选择:\r\n"
"help" = "🤖 欢迎使用本机器人!它旨在为您提供来自服务器的特定数据,并允许您进行必要的修改。\r\n\r\n"
"start" = "👋 你好,<i>{{ .Firstname }}</i>。\r\n"
"welcome" = "🤖 欢迎来到<b>{{ .Hostname }}</b>管理机器人。\r\n"
"status" = "✅ 机器人正常运行!"
"usage" = "❗ 请输入要搜索的文本!"
"getID" = "🆔 您的ID为<code>{{ .ID }}</code>"
"helpAdminCommands" = "搜索客户端邮箱:\r\n<code>/usage [Email]</code>\r\n \r\n搜索入站连接包含客户端统计信息\r\n<code>/inbound [Remark]</code>"
"helpClientCommands" = "要搜索统计信息,请使用以下命令:\r\n \r\n<code>/usage [UUID|Password]</code>\r\n \r\n对于vmess/vless请使用UUID对于Trojan请使用密码。"
[tgbot.messages]
"cpuThreshold" = "🔴 CPU 使用率为 {{ .Percent }}%,超过阈值 {{ .Threshold }}%"
"loginSuccess" = "✅ 成功登录到面板。\r\n"
"loginFailed" = "❗️ 面板登录失败。\r\n"
"report" = "🕰 定时报告:{{ .RunTime }}\r\n"
"datetime" = "⏰ 日期时间:{{ .DateTime }}\r\n"
"hostname" = "💻 主机名:{{ .Hostname }}\r\n"
"version" = "🚀 X-UI 版本:{{ .Version }}\r\n"
"ipv6" = "🌐 IPv6{{ .IPv6 }}\r\n"
"ipv4" = "🌐 IPv4{{ .IPv4 }}\r\n"
"ip" = "🌐 IP{{ .IP }}\r\n"
"serverUpTime" = "⏳ 服务器运行时间:{{ .UpTime }} {{ .Unit }}\r\n"
"serverLoad" = "📈 服务器负载:{{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
"serverMemory" = "📋 服务器内存:{{ .Current }}/{{ .Total }}\r\n"
"tcpCount" = "🔹 TCP 连接数:{{ .Count }}\r\n"
"udpCount" = "🔸 UDP 连接数:{{ .Count }}\r\n"
"traffic" = "🚦 流量:{{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
"xrayStatus" = " Xray 状态:{{ .State }}\r\n"
"username" = "👤 用户名:{{ .Username }}\r\n"
"time" = "⏰ 时间:{{ .Time }}\r\n"
"inbound" = "📍 入站:{{ .Remark }}\r\n"
"port" = "🔌 端口:{{ .Port }}\r\n"
"expire" = "📅 过期日期:{{ .DateTime }}\r\n \r\n"
"expireIn" = "📅 剩余时间:{{ .Time }}\r\n \r\n"
"active" = "💡 激活:{{ .Enable }}\r\n"
"email" = "📧 邮箱:{{ .Email }}\r\n"
"upload" = "🔼 上传↑:{{ .Upload }}\r\n"
"download" = "🔽 下载↓:{{ .Download }}\r\n"
"total" = "🔄 总计:{{ .UpDown }} / {{ .Total }}\r\n"
"exhaustedMsg" = "🚨 耗尽的{{ .Type }}\r\n"
"exhaustedCount" = "🚨 耗尽的{{ .Type }}数量:\r\n"
"disabled" = "🛑 禁用:{{ .Disabled }}\r\n"
"depleteSoon" = "🔜 即将耗尽:{{ .Deplete }}\r\n \r\n"
"backupTime" = "🗄 备份时间:{{ .Time }}\r\n"
[tgbot.buttons]
"dbBackup" = "获取数据库备份"
"serverUsage" = "服务器使用情况"
"getInbounds" = "获取入站信息"
"depleteSoon" = "即将耗尽"
"clientUsage" = "获取使用情况"
"commands" = "命令"
[tgbot.answers]
"getInboundsFailed" = "❌ 获取入站信息失败。"
"askToAddUser" = "找不到您的配置!\r\n您应该配置您的 Telegram 用户名,并要求管理员将其添加到您的配置中。"
"askToAddUserName" = "找不到您的配置!\r\n请要求您的管理员在您的配置中使用您的 Telegram 用户名。\r\n\r\n您的用户名<b>@{{ .TgUserName }}</b>"

View File

@@ -18,16 +18,15 @@ import (
"x-ui/util/common" "x-ui/util/common"
"x-ui/web/controller" "x-ui/web/controller"
"x-ui/web/job" "x-ui/web/job"
"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" "github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie" "github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/pelletier/go-toml/v2"
"github.com/robfig/cron/v3" "github.com/robfig/cron/v3"
"golang.org/x/text/language"
) )
//go:embed assets/* //go:embed assets/*
@@ -157,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
@@ -179,13 +187,23 @@ func (s *Server) initRouter() (*gin.Engine, error) {
c.Header("Cache-Control", "max-age=31536000") c.Header("Cache-Control", "max-age=31536000")
} }
}) })
err = s.initI18n(engine)
// init i18n
err = locale.InitLocalizer(i18nFS, &s.settingService)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Apply locale middleware for i18n
i18nWebFunc := func(key string, params ...string) string {
return locale.I18n(locale.Web, key, params...)
}
engine.FuncMap["i18n"] = i18nWebFunc
engine.Use(locale.LocalizerMiddleware())
// set static files and template
if config.IsDebug() { if config.IsDebug() {
// for develop // for development
files, err := s.getHtmlFiles() files, err := s.getHtmlFiles()
if err != nil { if err != nil {
return nil, err return nil, err
@@ -193,12 +211,12 @@ func (s *Server) initRouter() (*gin.Engine, error) {
engine.LoadHTMLFiles(files...) engine.LoadHTMLFiles(files...)
engine.StaticFS(basePath+"assets", http.FS(os.DirFS("web/assets"))) engine.StaticFS(basePath+"assets", http.FS(os.DirFS("web/assets")))
} else { } else {
// for prod // for production
t, err := s.getHtmlTemplate(engine.FuncMap) template, err := s.getHtmlTemplate(engine.FuncMap)
if err != nil { if err != nil {
return nil, err return nil, err
} }
engine.SetHTMLTemplate(t) engine.SetHTMLTemplate(template)
engine.StaticFS(basePath+"assets", http.FS(&wrapAssetsFS{FS: assetsFS})) engine.StaticFS(basePath+"assets", http.FS(&wrapAssetsFS{FS: assetsFS}))
} }
@@ -212,85 +230,6 @@ func (s *Server) initRouter() (*gin.Engine, error) {
return engine, nil return engine, nil
} }
func (s *Server) initI18n(engine *gin.Engine) error {
bundle := i18n.NewBundle(language.SimplifiedChinese)
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
err := fs.WalkDir(i18nFS, "translation", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
data, err := i18nFS.ReadFile(path)
if err != nil {
return err
}
_, err = bundle.ParseMessageFileBytes(data, path)
return err
})
if err != nil {
return err
}
findI18nParamNames := func(key string) []string {
names := make([]string, 0)
keyLen := len(key)
for i := 0; i < keyLen-1; i++ {
if key[i:i+2] == "{{" {
j := i + 2
isFind := false
for ; j < keyLen-1; j++ {
if key[j:j+2] == "}}" {
isFind = true
break
}
}
if isFind {
names = append(names, key[i+3:j])
}
}
}
return names
}
var localizer *i18n.Localizer
I18n := func(key string, params ...string) (string, error) {
names := findI18nParamNames(key)
if len(names) != len(params) {
return "", common.NewError("find names:", names, "---------- params:", params, "---------- num not equal")
}
templateData := map[string]interface{}{}
for i := range names {
templateData[names[i]] = params[i]
}
return localizer.Localize(&i18n.LocalizeConfig{
MessageID: key,
TemplateData: templateData,
})
}
engine.FuncMap["i18n"] = I18n
engine.Use(func(c *gin.Context) {
var lang string
if cookie, err := c.Request.Cookie("lang"); err == nil {
lang = cookie.Value
} else {
lang = c.GetHeader("Accept-Language")
}
localizer = i18n.NewLocalizer(bundle, lang)
c.Set("localizer", localizer)
c.Set("I18n", I18n)
c.Next()
})
return nil
}
func (s *Server) startTask() { func (s *Server) startTask() {
err := s.xrayService.RestartXray(true) err := s.xrayService.RestartXray(true)
if err != nil { if err != nil {
@@ -305,16 +244,13 @@ func (s *Server) startTask() {
s.cron.AddJob("@every 10s", job.NewXrayTrafficJob()) s.cron.AddJob("@every 10s", job.NewXrayTrafficJob())
}() }()
// Check the inbound traffic every 30 seconds that the traffic exceeds and expires
s.cron.AddJob("@every 30s", job.NewCheckInboundJob())
// Make a traffic condition every day, 8:30 // 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()
if (err == nil) && (isTgbotenabled) { if (err == nil) && (isTgbotenabled) {
runtime, err := s.settingService.GetTgbotRuntime() runtime, err := s.settingService.GetTgbotRuntime()
if err != nil || runtime == "" { if err != nil || runtime == "" {
logger.Errorf("Add NewStatsNotifyJob error[%s],Runtime[%s] invalid,wil run default", err, runtime) logger.Errorf("Add NewStatsNotifyJob error[%s], Runtime[%s] invalid, will run default", err, runtime)
runtime = "@daily" runtime = "@daily"
} }
logger.Infof("Tg notify enabled,run at %s", runtime) logger.Infof("Tg notify enabled,run at %s", runtime)
@@ -329,7 +265,6 @@ func (s *Server) startTask() {
if (err == nil) && (cpuThreshold > 0) { if (err == nil) && (cpuThreshold > 0) {
s.cron.AddJob("@every 10s", job.NewCheckCpuJob()) s.cron.AddJob("@every 10s", job.NewCheckCpuJob())
} }
} else { } else {
s.cron.Remove(entry) s.cron.Remove(entry)
} }
@@ -409,7 +344,7 @@ func (s *Server) Start() (err error) {
isTgbotenabled, err := s.settingService.GetTgbotenabled() isTgbotenabled, err := s.settingService.GetTgbotenabled()
if (err == nil) && (isTgbotenabled) { if (err == nil) && (isTgbotenabled) {
tgBot := s.tgbotService.NewTgbot() tgBot := s.tgbotService.NewTgbot()
tgBot.Start() tgBot.Start(i18nFS)
} }
return nil return nil
@@ -421,7 +356,7 @@ func (s *Server) Stop() error {
if s.cron != nil { if s.cron != nil {
s.cron.Stop() s.cron.Stop()
} }
if s.tgbotService.IsRunnging() { if s.tgbotService.IsRunning() {
s.tgbotService.Stop() s.tgbotService.Stop()
} }
var err1 error var err1 error

161
x-ui.sh
View File

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

235
xray/api.go Normal file
View File

@@ -0,0 +1,235 @@
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"
)
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.WithInsecure())
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,28 +3,22 @@ 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"
"syscall"
"time" "time"
"x-ui/config" "x-ui/config"
"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"
) )
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)
} }
@@ -65,16 +59,18 @@ type process struct {
version string version string
apiPort int apiPort int
config *Config config *Config
lines *queue.Queue lines *queue.Queue
exitErr error exitErr error
startTime time.Time
} }
func newProcess(config *Config) *process { func newProcess(config *Config) *process {
return &process{ return &process{
version: "Unknown", version: "Unknown",
config: config, config: config,
lines: queue.New(100), lines: queue.New(100),
startTime: time.Now(),
} }
} }
@@ -118,6 +114,10 @@ func (p *Process) GetConfig() *Config {
return p.config return p.config
} }
func (p *Process) GetUptime() uint64 {
return uint64(time.Since(p.startTime).Seconds())
}
func (p *process) refreshAPIPort() { func (p *process) refreshAPIPort() {
for _, inbound := range p.config.InboundConfigs { for _, inbound := range p.config.InboundConfigs {
if inbound.Tag == "api" { if inbound.Tag == "api" {
@@ -228,87 +228,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.WithInsecure())
if err != nil {
return nil, nil, err
}
defer conn.Close()
client := statsservice.NewStatsServiceClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
request := &statsservice.QueryStatsRequest{
Reset_: reset,
}
resp, err := client.QueryStats(ctx, request)
if err != nil {
return nil, nil, err
}
tagTrafficMap := map[string]*Traffic{}
emailTrafficMap := map[string]*ClientTraffic{}
clientTraffics := make([]*ClientTraffic, 0)
traffics := make([]*Traffic, 0)
for _, stat := range resp.GetStat() {
matchs := trafficRegex.FindStringSubmatch(stat.Name)
if len(matchs) < 3 {
matchs := ClientTrafficRegex.FindStringSubmatch(stat.Name)
if len(matchs) < 3 {
continue
} else {
isUser := matchs[1] == "user"
email := matchs[2]
isDown := matchs[3] == "downlink"
if !isUser {
continue
}
traffic, ok := emailTrafficMap[email]
if !ok {
traffic = &ClientTraffic{
Email: email,
}
emailTrafficMap[email] = traffic
clientTraffics = append(clientTraffics, traffic)
}
if isDown {
traffic.Down = stat.Value
} else {
traffic.Up = stat.Value
}
}
continue
}
isInbound := matchs[1] == "inbound"
tag := matchs[2]
isDown := matchs[3] == "downlink"
if tag == "api" {
continue
}
traffic, ok := tagTrafficMap[tag]
if !ok {
traffic = &Traffic{
IsInbound: isInbound,
Tag: tag,
}
tagTrafficMap[tag] = traffic
traffics = append(traffics, traffic)
}
if isDown {
traffic.Down = stat.Value
} else {
traffic.Up = stat.Value
}
}
return traffics, clientTraffics, nil
} }